275 lines
12 KiB
PHP
275 lines
12 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature;
|
||
|
||
use App\Models\CustomerLevelModel;
|
||
use App\Models\ProductModel;
|
||
use App\Models\PurchaseOrderModel;
|
||
use App\Models\StoreModel;
|
||
use App\Models\StoreOrderItemModel;
|
||
use App\Models\StoreOrderModel;
|
||
use App\Models\SupplierModel;
|
||
|
||
/**
|
||
* 门店订单明细(商品快照):
|
||
* 下单快照完整商品档案、成本价防泄漏、采购单内门店单品增删改重算汇总、按商品名称搜索订单
|
||
* (门店即用户:小程序端登录主体为 StoreModel)
|
||
*/
|
||
class StoreOrderItemTest extends ProcurementTestCase
|
||
{
|
||
/**
|
||
* 造门店 + 上架商品(指定成本价);
|
||
* 等级上浮比例按 price/costPrice 反算(售价 = 成本价 × (100 + percent) / 100)
|
||
*
|
||
* @return array{0: StoreModel, 1: ProductModel}
|
||
*/
|
||
private function makeStoreWithProduct(string $price = '5.00', string $costPrice = '4.00'): array
|
||
{
|
||
$percent = round(((float) $price / (float) $costPrice - 1) * 100, 2);
|
||
$level = CustomerLevelModel::factory()->create(['percent' => $percent]);
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'cost_price' => $costPrice,
|
||
]);
|
||
|
||
return [$store, $product];
|
||
}
|
||
|
||
/** 以小程序身份下一单并返回订单(可指定初始状态) */
|
||
private function placeOrder(ProductModel $product, StoreModel $store, int $quantity = 2, int $status = StoreOrderModel::STATUS_PENDING): StoreOrderModel
|
||
{
|
||
$this->actingAsMiniStore($store);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $quantity]]])
|
||
->assertOk()->assertJsonPath('success', true);
|
||
|
||
$order = StoreOrderModel::where('store_id', $store->id)->latest('id')->first();
|
||
$this->assertNotNull($order);
|
||
if ($status !== StoreOrderModel::STATUS_PENDING) {
|
||
$order->update(['status' => $status]);
|
||
}
|
||
|
||
return $order;
|
||
}
|
||
|
||
/** 下单 → 接单 → 生成采购单,返回采购单 */
|
||
private function generatePurchase(): PurchaseOrderModel
|
||
{
|
||
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
|
||
$this->actingAsSysUser();
|
||
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
|
||
->assertJsonPath('success', true);
|
||
|
||
return PurchaseOrderModel::first();
|
||
}
|
||
|
||
/** 下单时明细快照完整商品档案(分类/供应商/单位/图片/图文/保质期/成本价) */
|
||
public function test_place_order_snapshots_full_product_info(): void
|
||
{
|
||
$supplier = SupplierModel::factory()->create();
|
||
// 上浮 37.5%:成本 4.00 → 售价 5.50
|
||
$level = CustomerLevelModel::factory()->create(['percent' => 37.5]);
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create([
|
||
'status' => ProductModel::STATUS_ON,
|
||
'supplier_id' => $supplier->id,
|
||
'category_id' => 7,
|
||
'unit' => '箱',
|
||
'image_ids' => '1,2',
|
||
'content' => '图文详情',
|
||
'shelf_life' => 30,
|
||
'cost_price' => '4.00',
|
||
]);
|
||
|
||
$this->actingAsMiniStore($store);
|
||
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 3]]])
|
||
->assertOk()->assertJsonPath('success', true);
|
||
|
||
$item = StoreOrderModel::where('store_id', $store->id)->first()->items->first();
|
||
$this->assertSame($supplier->id, $item->supplier_id, '快照供应商');
|
||
$this->assertSame(7, $item->category_id, '快照分类');
|
||
$this->assertSame('箱', $item->unit, '快照单位');
|
||
$this->assertSame('1,2', implode(',', $item->image_ids), '快照图片');
|
||
$this->assertSame('图文详情', $item->content, '快照图文详情');
|
||
$this->assertSame(30, $item->shelf_life, '快照保质期');
|
||
$this->assertSame('4.00', (string) $item->cost_price, '快照成本价');
|
||
$this->assertSame('5.50', (string) $item->price, '快照等级单价');
|
||
}
|
||
|
||
/** 小程序订单详情不泄漏成本价 */
|
||
public function test_mini_detail_hides_cost_price(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
|
||
$order = $this->placeOrder($product, $store);
|
||
|
||
$this->actingAsMiniStore($store);
|
||
$this->getJson("/mini/order/{$order->id}")
|
||
->assertOk()
|
||
->assertJsonPath('success', true)
|
||
->assertJsonMissingPath('data.items.0.cost_price');
|
||
}
|
||
|
||
/** 后台订单详情:成本价可见、附供应商名与单位 */
|
||
public function test_admin_detail_shows_cost_price_and_supplier(): void
|
||
{
|
||
$supplier = SupplierModel::factory()->create();
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00', '4.00');
|
||
$product->update(['supplier_id' => $supplier->id]);
|
||
$order = $this->placeOrder($product, $store);
|
||
|
||
$this->actingAsSysUser();
|
||
$this->getJson("/order/store/{$order->id}")
|
||
->assertOk()
|
||
->assertJsonPath('success', true)
|
||
->assertJsonPath('data.items.0.cost_price', '4.00')
|
||
->assertJsonPath('data.items.0.unit', $product->unit)
|
||
->assertJsonPath('data.items.0.supplier.name', $supplier->name);
|
||
}
|
||
|
||
/** 采购单内修改门店单品:重算单品金额与订单/采购单汇总 */
|
||
public function test_update_store_item_recalculates_totals(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$order = $this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||
$item = $order->items->first();
|
||
$purchase = $this->generatePurchase();
|
||
|
||
$this->actingAsSysUser();
|
||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||
'price' => '6.00',
|
||
'quantity' => 5,
|
||
'weight' => '2.5',
|
||
])->assertOk()->assertJsonPath('success', true);
|
||
|
||
$item->refresh();
|
||
$this->assertSame('6.00', (string) $item->price);
|
||
$this->assertSame(5, $item->quantity);
|
||
$this->assertSame('2.500', (string) $item->weight);
|
||
$this->assertSame('30.00', (string) $item->amount, '6.00 × 5');
|
||
|
||
$order->refresh();
|
||
$this->assertSame(5, $order->total_quantity, '订货总量 = 明细合计');
|
||
$this->assertSame('2.500', (string) $order->total_weight, '总重量 = 明细合计');
|
||
$this->assertSame('30.00', (string) $order->total_amount, '订单总金额 = 明细金额合计');
|
||
|
||
$purchase->refresh();
|
||
$this->assertSame('5.00', (string) $purchase->total_quantity);
|
||
$this->assertSame('2.500', (string) $purchase->total_weight);
|
||
$this->assertSame('20.00', (string) $purchase->estimate_amount, '5 包 × 成本 4.00');
|
||
}
|
||
|
||
/** 采购单已完成:门店单品增删改均拒绝,明细不变 */
|
||
public function test_store_item_edits_rejected_when_purchase_completed(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$order = $this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||
$purchase = $this->generatePurchase();
|
||
|
||
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
|
||
$this->actingAsSysUser();
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||
'quantity' => 9,
|
||
])->assertOk()->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
|
||
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
|
||
'product_id' => $product->id,
|
||
'quantity' => 1,
|
||
])->assertJsonPath('success', false);
|
||
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}")
|
||
->assertJsonPath('success', false);
|
||
|
||
$item = $order->items->first();
|
||
$this->assertSame(1, $item->refresh()->quantity, '被拒绝后明细不变');
|
||
$this->assertSame('5.00', (string) $order->refresh()->total_amount, '被拒绝后总金额不变');
|
||
}
|
||
|
||
/** 修改门店单品参数校验:负单价/负数量被拦截 */
|
||
public function test_update_store_item_validates_negative_values(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||
$purchase = $this->generatePurchase();
|
||
|
||
$this->actingAsSysUser();
|
||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||
'quantity' => 1,
|
||
'price' => -1,
|
||
])->assertOk()
|
||
->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '单价不能小于 0');
|
||
|
||
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
|
||
'quantity' => -1,
|
||
])->assertOk()
|
||
->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '采购数量不能小于 0');
|
||
}
|
||
|
||
/** 新增单品:单价按门店等级上浮比例换算(成本 20 上浮 30% = 26.00) */
|
||
public function test_add_store_item_uses_level_percent_price(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create(['percent' => 30]);
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '10.00']);
|
||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '20.00']);
|
||
|
||
$this->placeOrder($product, $store, 2, StoreOrderModel::STATUS_SUMMARIZED);
|
||
$purchase = $this->generatePurchase();
|
||
|
||
$this->actingAsSysUser();
|
||
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
|
||
'product_id' => $extra->id,
|
||
'quantity' => 2,
|
||
])->assertOk()->assertJsonPath('success', true);
|
||
|
||
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
|
||
$this->assertNotNull($item);
|
||
$this->assertSame('26.00', (string) $item->price, '20 元上浮 30% = 26.00');
|
||
$this->assertSame('52.00', (string) $item->amount, '26.00 × 2');
|
||
$this->assertSame('20.00', (string) $item->cost_price, '快照成本价');
|
||
$this->assertSame($extra->name, $item->product_name, '快照品名取自商品档案');
|
||
}
|
||
|
||
/** 新增单品:商品已删除时拒绝 */
|
||
public function test_add_store_item_rejected_when_product_deleted(): void
|
||
{
|
||
[$store, $product] = $this->makeStoreWithProduct('5.00');
|
||
$this->placeOrder($product, $store, 1, StoreOrderModel::STATUS_SUMMARIZED);
|
||
$purchase = $this->generatePurchase();
|
||
|
||
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '9.00']);
|
||
$extra->delete(); // 软删除
|
||
|
||
$this->actingAsSysUser();
|
||
$this->postJson("/purchase/order/{$purchase->id}/store/{$store->id}/item", [
|
||
'product_id' => $extra->id,
|
||
'quantity' => 1,
|
||
])->assertOk()
|
||
->assertJsonPath('success', false)
|
||
->assertJsonPath('msg', '商品不存在或已被删除,无法添加');
|
||
}
|
||
|
||
/** 订单列表按包含的商品名称搜索 */
|
||
public function test_order_list_search_by_product_name(): void
|
||
{
|
||
$level = CustomerLevelModel::factory()->create();
|
||
$store = StoreModel::factory()->create(['level_id' => $level->id]);
|
||
|
||
$cabbage = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '大白菜A', 'cost_price' => '5.00']);
|
||
$potato = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'name' => '土豆B', 'cost_price' => '5.00']);
|
||
$this->placeOrder($cabbage, $store);
|
||
$this->placeOrder($potato, $store);
|
||
|
||
$this->actingAsSysUser();
|
||
$response = $this->getJson('/order/store?product_name=' . urlencode('白菜'));
|
||
$response->assertOk()->assertJsonPath('success', true);
|
||
$this->assertSame(1, $response->json('data.total'), '仅命中包含「白菜」的订单');
|
||
$this->assertSame('大白菜A', $response->json('data.data.0.items.0.product_name'));
|
||
|
||
// 无匹配关键字 → 空列表
|
||
$this->getJson('/order/store?product_name=' . urlencode('不存在的商品'))
|
||
->assertOk()->assertJsonPath('data.total', 0);
|
||
}
|
||
}
|