, 2: SupplierModel} */ private function buildAllocatedChain(): array { $level = CustomerLevelModel::factory()->create(); $supplier = SupplierModel::factory()->create(); $product = ProductModel::factory()->create([ 'status' => ProductModel::STATUS_ON, 'supplier_id' => $supplier->id, ]); ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '10.00']); $stores = []; foreach (['2.00', '3.00'] as $qty) { $store = StoreModel::factory()->create(['level_id' => $level->id]); $stores[] = $store; $this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create()); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]]) ->assertJsonPath('success', true); } $this->actingAsSysUser(); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); $purchase = PurchaseOrderModel::first(); $item = $purchase->items->first(); $this->putJson("/purchase/order/item/{$item->id}", [ 'price' => '10.00', 'quantity' => $item->quantity, 'weight' => 0, ])->assertJsonPath('success', true); $this->postJson("/purchase/order/{$purchase->id}/allocate")->assertJsonPath('success', true); return [$purchase->fresh(), $stores, $supplier]; } private function createRecon(array $extra = []): int { $response = $this->postJson('/recon/list', array_merge([ 'title' => '测试对账', 'period_start' => now()->toDateString(), 'period_end' => now()->toDateString(), ], $extra)); $response->assertJsonPath('success', true); return (int) $response->json('data.id'); } /** 构建明细:publish=订货金额,actual=分摊金额,diff=publish-actual,头汇总回写 */ public function test_build_creates_reconciliation_items(): void { [, $stores] = $this->buildAllocatedChain(); $this->actingAsSysUser(); $reconId = $this->createRecon(); $this->postJson("/recon/list/{$reconId}/build")->assertJsonPath('success', true); $items = ReconciliationItemModel::where('recon_id', $reconId)->get(); $this->assertCount(2, $items, '两门店分摊 → 两条对账明细'); $byStore = $items->keyBy('store_id'); $this->assertSame('20.00', (string) $byStore[$stores[0]->id]->publish_amount, '订货金额 2×10'); $this->assertSame('20.00', (string) $byStore[$stores[0]->id]->actual_amount, '分摊金额'); $this->assertSame('0.00', (string) $byStore[$stores[0]->id]->diff_amount); $recon = ReconciliationModel::find($reconId); $this->assertSame('50.00', (string) $recon->publish_amount); $this->assertSame('50.00', (string) $recon->actual_amount); $this->assertSame('0.00', (string) $recon->diff_amount); $this->assertSame(ReconciliationModel::STATUS_WORKING, $recon->status); } /** 供应商筛选:仅拉取该供应商的采购数据 */ public function test_build_filters_by_supplier(): void { [, , $supplier] = $this->buildAllocatedChain(); $this->actingAsSysUser(); // 无关供应商 → 无数据报错 $other = SupplierModel::factory()->create(); $reconId = $this->createRecon(['supplier_id' => $other->id]); $this->postJson("/recon/list/{$reconId}/build")->assertJsonPath('success', false); // 正确供应商 → 构建成功 $reconId2 = $this->createRecon(['supplier_id' => $supplier->id]); $this->postJson("/recon/list/{$reconId2}/build")->assertJsonPath('success', true); $this->assertSame(2, ReconciliationItemModel::where('recon_id', $reconId2)->count()); } /** D4 修改明细:自动重算本行 diff 与对账单头汇总 */ public function test_update_item_recalculates_diff_and_header(): void { $this->buildAllocatedChain(); $this->actingAsSysUser(); $reconId = $this->createRecon(); $this->postJson("/recon/list/{$reconId}/build"); $item = ReconciliationItemModel::where('recon_id', $reconId)->orderBy('id')->first(); $this->putJson("/recon/item/{$item->id}", ['actual_amount' => '25.00']) ->assertJsonPath('success', true); $item = $item->fresh(); $this->assertSame('-5.00', (string) $item->diff_amount, '20.00 - 25.00'); $recon = ReconciliationModel::find($reconId); $this->assertSame('55.00', (string) $recon->actual_amount, '25 + 30'); $this->assertSame('-5.00', (string) $recon->diff_amount, '50 - 55'); } /** D8 对账状态标记翻转 */ public function test_toggle_reconciled_flag(): void { $this->buildAllocatedChain(); $this->actingAsSysUser(); $reconId = $this->createRecon(); $this->postJson("/recon/list/{$reconId}/build"); $item = ReconciliationItemModel::where('recon_id', $reconId)->first(); $this->assertSame(0, $item->is_reconciled); $this->putJson("/recon/item/{$item->id}/toggle")->assertJsonPath('success', true); $this->assertSame(1, $item->fresh()->is_reconciled); $this->putJson("/recon/item/{$item->id}/toggle")->assertJsonPath('success', true); $this->assertSame(0, $item->fresh()->is_reconciled); } /** D9 结算:按门店生成结算表,对账单转为已结算且不可重复结算 */ public function test_settle_creates_settlements_per_store(): void { [, $stores] = $this->buildAllocatedChain(); $this->actingAsSysUser(); $reconId = $this->createRecon(); $this->postJson("/recon/list/{$reconId}/build"); $this->postJson("/recon/list/{$reconId}/settle")->assertJsonPath('success', true); $settlements = SettlementModel::where('recon_id', $reconId)->get(); $this->assertCount(2, $settlements, '按门店各生成一张结算表'); $byStore = $settlements->keyBy('store_id'); $this->assertSame('20.00', (string) $byStore[$stores[0]->id]->total_amount); $this->assertSame('20.00', (string) $byStore[$stores[0]->id]->actual_amount); $this->assertStringStartsWith('JS', $byStore[$stores[0]->id]->settlement_no); $this->assertSame(ReconciliationModel::STATUS_SETTLED, ReconciliationModel::find($reconId)->status); // 已结算不可重复结算 $this->postJson("/recon/list/{$reconId}/settle")->assertJsonPath('success', false); } }