} */ 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'], '订货金额 = Σ明细 amount(5 × 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.00(10 上浮 0%),B 店 15.00(10 上浮 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+32(4 包 × 成本 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()); } /** 采购单列表「应付商品金额」列 = Σ 已生成门店账单的商品金额(未生成账单为 null) */ public function test_list_includes_bill_product_amount(): void { [$purchase, , $stores] = $this->buildPurchase(); $this->actingAsSysUser(); // 未生成账单 → null $this->getJson('/purchase/order') ->assertJsonPath('data.data.0.bill_product_amount', null); // 完成采购单并生成两店账单(各店商品金额 = 数量 × 等级价 10.00) $purchase->update(['status' => PurchaseOrderModel::STATUS_COMPLETED]); $this->postJson("/purchase/order/{$purchase->id}/bill", [ 'stores' => [ ['store_id' => $stores[0]->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0], ['store_id' => $stores[1]->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0], ], ])->assertJsonPath('success', true); // 列表回显:2×10.00 + 3×10.00 = 50.00 $this->getJson('/purchase/order') ->assertJsonPath('data.data.0.bill_product_amount', '50.00'); } }