Files
xin-procurement/tests/Feature/PurchaseEditTest.php
T
2026-09-07 22:53:50 +08:00

745 lines
34 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\PurchaseItemCheckModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderItemModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
/**
* C4 采购单数据修改:详情矩阵(商品行 × 门店列)、门店单元格下钻编辑、行级修改同步商品档案、
* 门店购买详情单品增删改,修改后重算订单/采购单汇总
* (金额口径:订单=数量×单价;采购单预估成本=Σ数量×每包成本价)
*/
class PurchaseEditTest extends ProcurementTestCase
{
/**
* 造采购链路:门店A 2 件 + 门店B 3 件(同一商品,等级上浮 0% → 售价=成本价 10.00,包规 10斤/箱),
* 接单后生成采购单
*
* @return array{0: PurchaseOrderModel, 1: ProductModel, 2: array<int, StoreModel>}
*/
private function buildPurchase(): array
{
$level = CustomerLevelModel::factory()->create();
$storeA = StoreModel::factory()->create(['level_id' => $level->id]);
$storeB = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => 10,
'spec' => '10斤/箱',
'unit' => '斤',
]);
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
return [PurchaseOrderModel::first(), $product, [$storeA, $storeB]];
}
/** 详情返回「商品行 × 门店列」矩阵:cells 按门店聚合数量 */
public function test_detail_returns_store_matrix(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$response = $this->getJson("/purchase/order/{$purchase->id}")
->assertOk()
->assertJsonPath('success', true);
$data = $response->json('data');
$this->assertCount(2, $data['stores']);
$this->assertCount(1, $data['items']);
$row = $data['items'][0];
$this->assertSame($product->id, $row['product_id']);
$this->assertSame('10斤/箱', $row['product_spec']);
$this->assertSame('斤', $row['unit']);
$this->assertSame('10.00', (string) $row['cost_price']);
$this->assertSame('50.00', (string) $row['amount'], '订货金额 = Σ明细 amount5 × 10.00');
$this->assertSame(5, $row['quantity'], '2+3');
$cells = $row['cells'];
$this->assertSame(2, $cells[$stores[0]->id]);
$this->assertSame(3, $cells[$stores[1]->id]);
}
/** 门店单元格数量/单价修改 → 明细金额重算(数量×单价),订单与采购单汇总同步 */
public function test_update_cell_syncs_order_item_and_totals(): void
{
[$purchase, , $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10])
->assertJsonPath('success', true);
$item = $item->fresh();
$this->assertSame(5, $item->quantity);
$this->assertSame('50.00', (string) $item->amount, '5 × 订货单价 10.00');
$order = StoreOrderModel::find($item->order_id);
$this->assertSame(5, $order->total_quantity);
$this->assertSame('50.00', (string) $order->total_amount);
$purchase = $purchase->fresh();
$this->assertSame('8.00', (string) $purchase->total_quantity, '5+3');
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00');
}
/** 行级成本修改 → 同步该商品全部订货明细与商品档案,采购单预估成本重算 */
public function test_update_row_cost_syncs_all_order_items(): void
{
[$purchase, $product] = $this->buildPurchase();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => $product->name,
'supplier_id' => SupplierModel::factory()->create()->id,
'product_spec' => $product->spec,
'unit' => $product->unit,
'cost_price' => 30,
])->assertJsonPath('success', true)
->assertJsonPath('data.count', 2);
$this->assertSame(2, StoreOrderItemModel::where('cost_price', '30.00')->count());
$this->assertSame('30.00', (string) $product->fresh()->cost_price, '商品档案成本价应同步更新');
$purchase = $purchase->fresh();
$this->assertSame('150.00', (string) $purchase->estimate_amount, '5 包 × 每包成本 30.00');
}
/** 行级属性修改不影响称重(重量为手动录入,仅作参考) */
public function test_update_row_does_not_touch_manual_weight(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$supplier = SupplierModel::factory()->create();
$this->actingAsSysUser();
// 先录入一笔手动称重
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 2, 'price' => 10, 'weight' => 4.5])
->assertJsonPath('success', true);
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => $product->name,
'supplier_id' => $supplier->id,
'product_spec' => $product->spec,
'unit' => $product->unit,
'cost_price' => 30,
])->assertJsonPath('success', true);
$this->assertSame('4.500', (string) $item->fresh()->weight, '重量保持手动录入值,不受行修改影响');
}
/** 行级属性修改:品名/供应商/包规/单位/成本一键同步该商品全部订货明细与商品档案 */
public function test_update_row_syncs_product_attributes(): void
{
[$purchase, $product] = $this->buildPurchase();
$supplier = SupplierModel::factory()->create();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => '优选土豆',
'supplier_id' => $supplier->id,
'product_spec' => '5斤/袋',
'unit' => '袋',
'cost_price' => 25,
])->assertJsonPath('success', true)
->assertJsonPath('data.count', 2);
$items = StoreOrderItemModel::all();
$this->assertCount(2, $items);
foreach ($items as $item) {
$this->assertSame('优选土豆', $item->product_name);
$this->assertSame($supplier->id, $item->supplier_id);
$this->assertSame('5斤/袋', $item->product_spec);
$this->assertSame('袋', $item->unit);
$this->assertSame('25.00', (string) $item->cost_price);
}
// 商品档案同步保存(商品列表可见)
$product = $product->fresh();
$this->assertSame('优选土豆', $product->name);
$this->assertSame($supplier->id, $product->supplier_id);
$this->assertSame('5斤/袋', $product->spec);
$this->assertSame('袋', $product->unit);
$this->assertSame('25.00', (string) $product->cost_price);
$this->assertSame('125.00', (string) $purchase->fresh()->estimate_amount, '5 包 × 每包成本 25.00');
}
/** 行级修改:商品已软删除时明细照常更新、跳过档案同步 */
public function test_update_row_skips_catalog_sync_when_product_deleted(): void
{
[$purchase, $product] = $this->buildPurchase();
$supplier = SupplierModel::factory()->create();
$this->actingAsSysUser();
$product->delete(); // 软删除
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => '已删商品新名',
'supplier_id' => $supplier->id,
'product_spec' => '5斤/袋',
'unit' => '袋',
'cost_price' => 25,
])->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('msg', '已同步 2 条订货明细;商品已删除,档案未同步;门店单价已按等级上浮比例重算');
$this->assertSame('已删商品新名', StoreOrderItemModel::first()->product_name, '订货明细照常更新');
$this->assertNotSame('已删商品新名', $product->fresh()->name, '商品档案不同步');
}
/** 行级修改:必填字段校验(空 payload 拒绝) */
public function test_update_row_requires_at_least_one_field(): void
{
[$purchase, $product] = $this->buildPurchase();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [])
->assertJsonPath('success', false);
}
/** 单元格数量校验:负数拒绝 */
public function test_update_cell_rejects_negative_quantity(): void
{
[$purchase, , $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => -1, 'price' => 10])
->assertJsonPath('success', false);
}
/** 单元格下钻:返回该门店该商品的全部订货明细(订单号/状态/快照),其他门店无明细 */
public function test_cell_detail_returns_items_with_order_info(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$response = $this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[0]->id}")
->assertOk()
->assertJsonPath('success', true);
$data = $response->json('data');
$this->assertSame($stores[0]->name, $data['store']['name']);
$this->assertSame($product->name, $data['product']['name']);
$this->assertCount(1, $data['items']);
$item = $data['items'][0];
$this->assertArrayHasKey('order_no', $item);
$this->assertSame(2, $item['quantity']);
$this->assertSame('20.00', (string) $item['amount']);
$this->assertSame(StoreOrderModel::STATUS_DELIVERING, $item['order_status'], '生成采购单后源订单为采购中');
// 其他门店 → 另一条明细
$this->getJson("/purchase/order/{$purchase->id}/cell?product_id={$product->id}&store_id={$stores[1]->id}")
->assertOk()
->assertJsonPath('data.items.0.quantity', 3);
}
/** 单元格下钻参数校验:缺少商品/门店参数拒绝 */
public function test_cell_detail_rejects_missing_params(): void
{
[$purchase] = $this->buildPurchase();
$this->actingAsSysUser();
$this->getJson("/purchase/order/{$purchase->id}/cell")
->assertJsonPath('success', false);
}
/** 单元格称重修改 → 明细/订货单/采购单重量与金额汇总级联重算 */
public function test_update_cell_weight_cascades_totals(): void
{
[$purchase, , $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10, 'weight' => 4.5])
->assertJsonPath('success', true);
$item = $item->fresh();
$this->assertSame('4.500', (string) $item->weight);
$this->assertSame('50.00', (string) $item->amount, '5 × 订货单价 10.00');
$order = StoreOrderModel::find($item->order_id);
$this->assertSame('4.500', (string) $order->total_weight);
$this->assertSame('50.00', (string) $order->total_amount);
$purchase = $purchase->fresh();
$this->assertSame('7.500', (string) $purchase->total_weight, '4.500 手动称重 + B 店参考重量 3.000');
$this->assertSame('80.00', (string) $purchase->estimate_amount, '8 包 × 每包成本 10.00(称重仅参考,不参与金额)');
}
/** 行级成本修改 → 明细单价按各门店等级上浮比例重算,订单与采购单汇总级联 */
public function test_update_row_cost_change_reprices_items_by_store_level(): void
{
// 门店A 上浮 0%(售价=成本),门店B 上浮 50%
$levelA = CustomerLevelModel::factory()->create(['percent' => 0]);
$levelB = CustomerLevelModel::factory()->create(['percent' => 50]);
$storeA = StoreModel::factory()->create(['level_id' => $levelA->id]);
$storeB = StoreModel::factory()->create(['level_id' => $levelB->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
foreach ([[$storeA, 2], [$storeB, 3]] as [$store, $qty]) {
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
// 下单时快照:A 店 10.0010 上浮 0%),B 店 15.0010 上浮 50%
$itemA = StoreOrderItemModel::where('store_id', $storeA->id)->first();
$itemB = StoreOrderItemModel::where('store_id', $storeB->id)->first();
$this->assertSame('10.00', (string) $itemA->price);
$this->assertSame('15.00', (string) $itemB->price);
// 行修改成本 10 → 20:单价按各店等级上浮比例重算
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => $product->name,
'supplier_id' => SupplierModel::factory()->create()->id,
'product_spec' => $product->spec,
'unit' => $product->unit,
'cost_price' => 20,
])->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('msg', '已同步 2 条订货明细与商品档案;门店单价已按等级上浮比例重算');
$itemA->refresh();
$this->assertSame('20.00', (string) $itemA->cost_price);
$this->assertSame('20.00', (string) $itemA->price, 'A 店:20 上浮 0% = 20.00');
$this->assertSame('40.00', (string) $itemA->amount, '20.00 × 2');
$itemB->refresh();
$this->assertSame('30.00', (string) $itemB->price, 'B 店:20 上浮 50% = 30.00');
$this->assertSame('90.00', (string) $itemB->amount, '30.00 × 3');
$this->assertSame('40.00', (string) StoreOrderModel::where('store_id', $storeA->id)->first()->total_amount);
$this->assertSame('90.00', (string) StoreOrderModel::where('store_id', $storeB->id)->first()->total_amount);
$this->assertSame('100.00', (string) $purchase->fresh()->estimate_amount, '5 包 × 新成本 20.00');
}
/** 行修改未改成本 → 明细单价保持不变(不覆盖单元格的手工改价) */
public function test_update_row_without_cost_change_keeps_item_price(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$this->actingAsSysUser();
// 先通过单元格手工改价 10 → 12
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 2, 'price' => 12])
->assertJsonPath('success', true);
// 行修改只改品名(成本不变)
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => '只改品名',
'supplier_id' => SupplierModel::factory()->create()->id,
'product_spec' => $product->spec,
'unit' => $product->unit,
'cost_price' => 10,
])->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('msg', '已同步 2 条订货明细与商品档案');
$this->assertSame('12.00', (string) $item->fresh()->price, '成本未变时保留手工改价');
}
/** 门店购买详情-新增单品:挂靠最新一笔订单,汇总级联重算;重复添加拒绝 */
public function test_store_item_add_creates_item_and_recalculates(): void
{
[$purchase, , $stores] = $this->buildPurchase();
$extra = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 8]);
$this->actingAsSysUser();
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
'product_id' => $extra->id,
'quantity' => 4,
'weight' => 1.5,
])->assertOk()->assertJsonPath('success', true);
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
$this->assertNotNull($item);
$this->assertSame($stores[0]->id, $item->store_id);
$this->assertSame($purchase->id, $item->purchase_id);
$this->assertSame('8.00', (string) $item->price, '等级上浮 0% → 单价=成本价');
$this->assertSame('32.00', (string) $item->amount, '8.00 × 4');
$this->assertSame('1.500', (string) $item->weight);
$order = StoreOrderModel::find($item->order_id);
$this->assertSame(6, $order->total_quantity, '2+4');
$this->assertSame('52.00', (string) $order->total_amount, '20+32');
$purchase = $purchase->fresh();
$this->assertSame('9.00', (string) $purchase->total_quantity, '2+3+4');
$this->assertSame('82.00', (string) $purchase->estimate_amount, '50+324 包 × 成本 8.00');
$this->assertSame('6.500', (string) $purchase->total_weight, '原有参考重量 2+3 斤 + 新增 1.500');
// 重复添加同一商品 → 拒绝
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
'product_id' => $extra->id,
'quantity' => 1,
])->assertJsonPath('success', false)
->assertJsonPath('msg', '该商品已在此门店采购明细中,请直接修改数量');
}
/** 门店购买详情-新增单品未传称重:按订货量 × 规格预填参考重量并级联汇总 */
public function test_store_item_add_defaults_reference_weight(): void
{
[$purchase, , $stores] = $this->buildPurchase();
$extra = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => 8,
'spec' => '25斤/袋',
'unit' => '袋',
]);
$this->actingAsSysUser();
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
'product_id' => $extra->id,
'quantity' => 2,
])->assertOk()->assertJsonPath('success', true);
$item = StoreOrderItemModel::where('product_id', $extra->id)->first();
$this->assertSame('50.000', (string) $item->weight, '2 袋 × 25斤/袋');
$this->assertSame('55.000', (string) $purchase->fresh()->total_weight, '原有参考重量 5.000 + 新增 50.000');
}
/** 门店购买详情-修改单品:同商品多笔订单明细合并到最早一条,涉及订单逐一重算 */
public function test_store_item_update_merges_multi_order_rows(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => 10]);
// 同一门店两笔订单各订 2 件(同商品)→ 同一采购单
$this->actingAsMiniStore($store);
foreach ([2, 2] as $qty) {
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
$this->assertSame(2, StoreOrderItemModel::count(), '两笔订单各一条明细');
$this->putJson("/purchase/order/{$purchase->id}/store/{$store->id}/item/{$product->id}", [
'quantity' => 10,
'price' => 12,
'weight' => 5,
])->assertJsonPath('success', true);
$this->assertSame(1, StoreOrderItemModel::count(), '多笔明细合并为一条');
$item = StoreOrderItemModel::first();
$this->assertSame(10, $item->quantity);
$this->assertSame('12.00', (string) $item->price);
$this->assertSame('120.00', (string) $item->amount);
$this->assertSame('5.000', (string) $item->weight);
// 保留行所在订单重算;被合并订单清零
$survivedOrder = StoreOrderModel::find($item->order_id);
$this->assertSame(10, $survivedOrder->total_quantity);
$this->assertSame('120.00', (string) $survivedOrder->total_amount);
$mergedOrder = StoreOrderModel::where('id', '<>', $item->order_id)->first();
$this->assertSame(0, $mergedOrder->total_quantity);
$this->assertSame('0.00', (string) $mergedOrder->total_amount);
$purchase = $purchase->fresh();
$this->assertSame('10.00', (string) $purchase->total_quantity);
$this->assertSame('100.00', (string) $purchase->estimate_amount, '10 包 × 成本 10.00');
$this->assertSame('5.000', (string) $purchase->total_weight);
}
/** 门店购买详情-移除单品:明细删除,订单与采购单汇总级联重算 */
public function test_store_item_remove_deletes_and_recalculates(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}")
->assertOk()->assertJsonPath('success', true);
$this->assertSame(0, StoreOrderItemModel::where('store_id', $stores[0]->id)->count());
$this->assertSame(1, StoreOrderItemModel::count(), '门店B 明细不受影响');
$order = StoreOrderModel::where('store_id', $stores[0]->id)->first();
$this->assertSame(0, $order->total_quantity);
$this->assertSame('0.00', (string) $order->total_amount);
$purchase = $purchase->fresh();
$this->assertSame('3.00', (string) $purchase->total_quantity, '仅剩门店B 3 件');
$this->assertSame('30.00', (string) $purchase->estimate_amount);
}
/** 门店购买详情:按商品聚合该门店采购汇总(数量/包规/单位/单价/预计金额),仅含该门店明细 */
public function test_store_summary_returns_aggregated_items(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$this->actingAsSysUser();
$response = $this->getJson("/purchase/order/{$purchase->id}/store?store_id={$stores[0]->id}")
->assertOk()
->assertJsonPath('success', true);
$data = $response->json('data');
$this->assertSame($stores[0]->name, $data['store']['name']);
$this->assertCount(1, $data['items']);
$row = $data['items'][0];
$this->assertSame($product->id, $row['product_id']);
$this->assertSame('10斤/箱', $row['product_spec']);
$this->assertSame('斤', $row['unit']);
$this->assertSame('10.00', (string) $row['price']);
$this->assertSame(2, $row['quantity']);
$this->assertSame('20.00', (string) $row['amount']);
// 另一门店只见自身明细
$this->getJson("/purchase/order/{$purchase->id}/store?store_id={$stores[1]->id}")
->assertOk()
->assertJsonPath('data.items.0.quantity', 3)
->assertJsonPath('data.items.0.amount', '30.00');
}
/** 门店购买详情:同一商品多笔订单不同单价 → 数量合计、金额求和、单价为加权平均 */
public function test_store_summary_aggregates_multiple_orders_with_weighted_price(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => 10,
'spec' => '10斤/箱',
'unit' => '斤',
]);
$this->actingAsMiniStore($store);
foreach ([2, 3] as $qty) {
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_SUMMARIZED]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
// 第二笔明细单价改为 20.00,构造同商品双单价:2×10 + 3×20
$secondItem = StoreOrderItemModel::orderBy('id')->skip(1)->first();
$this->putJson("/purchase/order/cell/{$secondItem->id}", ['quantity' => 3, 'price' => 20])
->assertJsonPath('success', true);
$this->getJson("/purchase/order/{$purchase->id}/store?store_id={$store->id}")
->assertOk()
->assertJsonPath('data.items.0.quantity', 5)
->assertJsonPath('data.items.0.price', '16.00', '(20+60) ÷ 5 加权平均')
->assertJsonPath('data.items.0.amount', '80.00');
}
/** 门店购买详情参数校验:缺少门店参数 / 采购单不存在均拒绝 */
public function test_store_summary_rejects_missing_or_invalid_params(): void
{
[$purchase] = $this->buildPurchase();
$this->actingAsSysUser();
$this->getJson("/purchase/order/{$purchase->id}/store")
->assertJsonPath('success', false)
->assertJsonPath('msg', '缺少门店参数');
$this->getJson('/purchase/order/99999/store?store_id=1')
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单不存在');
}
/** 采购单已完成:单元格编辑、行修改、门店单品增删改均拒绝,明细不变 */
public function test_edits_rejected_when_purchase_completed(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$item = StoreOrderItemModel::where('store_id', $stores[0]->id)->first();
$supplier = SupplierModel::factory()->create();
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
$this->putJson("/purchase/order/cell/{$item->id}", ['quantity' => 5, 'price' => 10])
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
$this->putJson("/purchase/order/{$purchase->id}/row/{$product->id}", [
'product_name' => '改名',
'supplier_id' => $supplier->id,
'product_spec' => '5斤/袋',
'unit' => '袋',
'cost_price' => 30,
])->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单已完成,不允许修改明细');
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
'product_id' => $product->id,
'quantity' => 1,
])->assertJsonPath('success', false);
$this->putJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}", [
'quantity' => 5,
])->assertJsonPath('success', false);
$this->deleteJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item/{$product->id}")
->assertJsonPath('success', false);
$this->assertSame(2, $item->fresh()->quantity, '被拒绝后明细不变');
$this->assertSame('10.00', (string) $item->fresh()->cost_price);
}
/** 单品「已对账」标记:切换入库持久化,详情接口回显,再次切换取消 */
public function test_item_check_toggle_persists_and_echoes_in_detail(): void
{
[$purchase, $product] = $this->buildPurchase();
$this->actingAsSysUser();
// 初始无标记
$this->getJson("/purchase/order/{$purchase->id}")
->assertJsonPath('success', true)
->assertJsonPath('data.checked_product_ids', []);
// 标记 → 入库
$this->putJson("/purchase/order/{$purchase->id}/check/{$product->id}")
->assertJsonPath('success', true)
->assertJsonPath('data.checked', true);
$this->assertDatabaseHas('purchase_item_check', [
'purchase_id' => $purchase->id,
'product_id' => $product->id,
]);
// 详情回显
$this->getJson("/purchase/order/{$purchase->id}")
->assertJsonPath('data.checked_product_ids.0', $product->id);
// 再次切换 → 取消标记,记录删除
$this->putJson("/purchase/order/{$purchase->id}/check/{$product->id}")
->assertJsonPath('success', true)
->assertJsonPath('data.checked', false);
$this->assertDatabaseMissing('purchase_item_check', [
'purchase_id' => $purchase->id,
'product_id' => $product->id,
]);
}
/** 对账标记与采购单状态无关:已完成采购单仍可切换标记 */
public function test_item_check_allowed_when_purchase_completed(): void
{
[$purchase, $product] = $this->buildPurchase();
$purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/check/{$product->id}")
->assertJsonPath('success', true)
->assertJsonPath('data.checked', true);
}
/** 对账标记校验:采购单不存在 / 采购单内无此商品均报错,不入库 */
public function test_item_check_toggle_validates_purchase_and_product(): void
{
[$purchase] = $this->buildPurchase();
$this->actingAsSysUser();
$this->putJson('/purchase/order/99999/check/1')
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单不存在');
$this->putJson("/purchase/order/{$purchase->id}/check/99999")
->assertJsonPath('success', false)
->assertJsonPath('msg', '该采购单下无此商品的订货明细');
$this->assertSame(0, PurchaseItemCheckModel::count());
}
/** 批量标记(全选):一次标记多个商品,重复标记幂等,批量取消仅清指定商品 */
public function test_item_check_batch_marks_and_clears(): void
{
[$purchase, $product, $stores] = $this->buildPurchase();
$this->actingAsSysUser();
// 采购单内再加一个商品(通过新增单品端点挂靠门店订单)
$product2 = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'cost_price' => 5,
'spec' => '1斤',
'unit' => '斤',
]);
$this->postJson("/purchase/order/{$purchase->id}/store/{$stores[0]->id}/item", [
'product_id' => $product2->id,
'quantity' => 1,
])->assertJsonPath('success', true);
// 批量标记两个商品
$this->putJson("/purchase/order/{$purchase->id}/check", [
'product_ids' => [$product->id, $product2->id],
'checked' => true,
])->assertJsonPath('success', true)
->assertJsonPath('data.count', 2);
$this->assertSame(2, PurchaseItemCheckModel::where('purchase_id', $purchase->id)->count());
// 重复批量标记幂等(不产生重复行)
$this->putJson("/purchase/order/{$purchase->id}/check", [
'product_ids' => [$product->id, $product2->id],
'checked' => true,
])->assertJsonPath('success', true);
$this->assertSame(2, PurchaseItemCheckModel::where('purchase_id', $purchase->id)->count());
// 详情回显两个标记
$ids = $this->getJson("/purchase/order/{$purchase->id}")->json('data.checked_product_ids');
$this->assertEqualsCanonicalizing([$product->id, $product2->id], $ids);
// 批量取消其中一个
$this->putJson("/purchase/order/{$purchase->id}/check", [
'product_ids' => [$product->id],
'checked' => false,
])->assertJsonPath('success', true);
$this->assertDatabaseMissing('purchase_item_check', [
'purchase_id' => $purchase->id,
'product_id' => $product->id,
]);
$this->assertDatabaseHas('purchase_item_check', [
'purchase_id' => $purchase->id,
'product_id' => $product2->id,
]);
}
/** 批量标记校验:空列表/采购单不存在报错;无效商品静默忽略 */
public function test_item_check_batch_validates_and_ignores_unknown_products(): void
{
[$purchase] = $this->buildPurchase();
$this->actingAsSysUser();
$this->putJson("/purchase/order/{$purchase->id}/check", ['product_ids' => [], 'checked' => true])
->assertJsonPath('success', false);
$this->putJson('/purchase/order/99999/check', ['product_ids' => [1], 'checked' => true])
->assertJsonPath('success', false)
->assertJsonPath('msg', '采购单不存在');
// 采购单内无此商品 → 静默忽略不入库
$this->putJson("/purchase/order/{$purchase->id}/check", ['product_ids' => [99999], 'checked' => true])
->assertJsonPath('success', true)
->assertJsonPath('data.count', 0);
$this->assertSame(0, PurchaseItemCheckModel::count());
}
}