create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); ProductPriceModel::factory()->create([ 'product_id' => $product->id, 'level_id' => $level->id, 'price' => '5.00', ]); $user = UserModel::factory()->forStore($store->id)->create(); $this->actingAsMiniUser($user); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]]) ->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $this->actingAsSysUser(); // 接单 $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]) ->assertJsonPath('success', true); // 生成采购单 → 采购中 $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); $purchase = PurchaseOrderModel::first(); // 完成采购单 → 配送中 $this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true); // 生成账单 → 已完成 $this->postJson("/purchase/order/{$purchase->id}/bill", [ 'stores' => [['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]], ])->assertJsonPath('success', true); return [$store, $order->fresh(), BillModel::where('store_id', $store->id)->first()]; } /** 造一张指定商品金额的未支付账单(总额=商品金额) */ private function makeBill(StoreModel $store, string $productAmount, array $attributes = []): BillModel { return BillModel::create(array_merge([ 'bill_no' => 'ZD' . random_int(100000000000, 999999999999), 'purchase_id' => PurchaseOrderModel::factory()->create()->id, 'store_id' => $store->id, 'bill_date' => '2026-08-10', 'product_amount' => $productAmount, 'delivery_fee' => '0.00', 'box_num' => 0, 'tray_num' => 0, 'box_price' => '0.00', 'tray_price' => '0.00', 'added_amount' => '0.00', 'total_amount' => $productAmount, 'status' => BillModel::STATUS_UNPAID, ], $attributes)); } /** 订单状态随业务链自动推进:接单→采购中→配送中→(生成账单)已完成 */ public function test_order_status_progresses_via_business_chain(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '5.00']); $user = UserModel::factory()->forStore($store->id)->create(); $this->actingAsMiniUser($user); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]]) ->assertJsonPath('success', true); $order = StoreOrderModel::where('store_id', $store->id)->first(); $this->assertSame(StoreOrderModel::STATUS_PENDING, $order->status); $this->actingAsSysUser(); // 接单 → 已接单 $this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]) ->assertJsonPath('success', true); $this->assertSame(StoreOrderModel::STATUS_SUMMARIZED, $order->fresh()->status); // 生成采购单 → 采购中 $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); $purchase = PurchaseOrderModel::first(); $order->refresh(); $this->assertSame(StoreOrderModel::STATUS_DELIVERING, $order->status); $this->assertSame($purchase->id, $order->purchase_id); // 完成采购单 → 配送中 $this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true); $this->assertSame(StoreOrderModel::STATUS_DISTRIBUTION, $order->fresh()->status); // 生成账单 → 已完成 + 回写 bill_id $this->postJson("/purchase/order/{$purchase->id}/bill", [ 'stores' => [['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]], ])->assertJsonPath('success', true); $order->refresh(); $bill = BillModel::where('store_id', $store->id)->first(); $this->assertNotNull($bill); $this->assertSame(StoreOrderModel::STATUS_COMPLETED, $order->status); $this->assertSame($bill->id, $order->bill_id); $this->assertSame('20.00', (string) $bill->product_amount, '5.00 × 4'); } /** 后台手动确认收款:累加门店总采购金额(只统计商品金额),重复收款拒绝不重复累加 */ public function test_manual_pay_accumulates_store_total_purchase_amount(): void { [$store, , $bill] = $this->makeBillViaChain(); $this->actingAsSysUser(); $this->putJson("/recon/bill/{$bill->id}/pay", ['pay_remark' => '线下现金']) ->assertJsonPath('success', true); $store->refresh(); $this->assertSame('20.00', (string) $store->total_purchase_amount, '只累加商品金额 20.00'); // 第二笔账单收款后继续累加 $bill2 = $this->makeBill($store, '100.00'); $this->putJson("/recon/bill/{$bill2->id}/pay")->assertJsonPath('success', true); $this->assertSame('120.00', (string) $store->fresh()->total_purchase_amount); // 重复收款拒绝,金额不重复累加 $this->putJson("/recon/bill/{$bill->id}/pay")->assertJsonPath('success', false); $this->assertSame('120.00', (string) $store->fresh()->total_purchase_amount); } /** 支付审核通过:关联账单置已支付并累加门店总采购金额 */ public function test_payment_audit_pass_accumulates_store_total_purchase_amount(): void { $store = StoreModel::factory()->create(); $user = UserModel::factory()->forStore($store->id)->create(); $payment = PaymentModel::create([ 'payment_no' => 'ZF202608140001', 'store_id' => $store->id, 'user_id' => $user->id, 'amount' => '150.00', 'pay_method' => PaymentModel::METHOD_BANK, 'voucher_ids' => '', 'status' => PaymentModel::STATUS_PENDING, ]); $bill1 = $this->makeBill($store, '100.00', ['payment_id' => $payment->id]); $bill2 = $this->makeBill($store, '50.00', ['payment_id' => $payment->id]); $this->actingAsSysUser(); $this->putJson("/recon/payment/{$payment->id}/audit", ['result' => 'pass']) ->assertJsonPath('success', true); $this->assertSame(BillModel::STATUS_PAID, $bill1->fresh()->status); $this->assertSame(BillModel::STATUS_PAID, $bill2->fresh()->status); $this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount); // 重复审核拒绝,不重复累加 $this->putJson("/recon/payment/{$payment->id}/audit", ['result' => 'pass']) ->assertJsonPath('success', false); $this->assertSame('150.00', (string) $store->fresh()->total_purchase_amount); } /** 审核拒绝:账单释放回待支付,不累加门店总采购金额 */ public function test_payment_audit_reject_does_not_accumulate(): void { $store = StoreModel::factory()->create(); $user = UserModel::factory()->forStore($store->id)->create(); $payment = PaymentModel::create([ 'payment_no' => 'ZF202608140002', 'store_id' => $store->id, 'user_id' => $user->id, 'amount' => '100.00', 'pay_method' => PaymentModel::METHOD_WECHAT, 'voucher_ids' => '', 'status' => PaymentModel::STATUS_PENDING, ]); $bill = $this->makeBill($store, '100.00', ['payment_id' => $payment->id]); $this->actingAsSysUser(); $this->putJson("/recon/payment/{$payment->id}/audit", [ 'result' => 'reject', 'audit_remark' => '凭证不清晰', ])->assertJsonPath('success', true); $bill->refresh(); $this->assertSame(BillModel::STATUS_UNPAID, $bill->status); $this->assertSame(0, $bill->payment_id, '账单释放可重新付款'); $this->assertSame('0.00', (string) $store->fresh()->total_purchase_amount); } /** 后台门店订单:列表显示关联账单与支付进度,详情附带完整账单 */ public function test_admin_order_list_and_detail_show_bill_info(): void { [$store, $order, $bill] = $this->makeBillViaChain(); $this->actingAsSysUser(); // 列表:账单摘要 + 支付进度(待支付) $rows = $this->getJson('/order/store')->assertOk()->json('data.data'); $row = collect($rows)->firstWhere('id', $order->id); $this->assertNotNull($row['bill'], '已出账订单列表应附账单'); $this->assertSame($bill->bill_no, $row['bill']['bill_no']); $this->assertSame('20.00', $row['bill']['total_amount']); $this->assertSame(BillModel::PAY_STATE_UNPAID, $row['bill']['pay_state']); $this->assertSame('待支付', $row['bill']['pay_state_name']); // 详情:完整账单字段 + 支付进度 $detail = $this->getJson("/order/store/{$order->id}")->assertOk()->json('data'); $this->assertSame($bill->bill_no, $detail['bill']['bill_no']); $this->assertSame('20.00', $detail['bill']['product_amount']); $this->assertSame(BillModel::PAY_STATE_UNPAID, $detail['bill']['pay_state']); // 收款后列表支付进度变为已支付 $this->putJson("/recon/bill/{$bill->id}/pay")->assertJsonPath('success', true); $rows = $this->getJson('/order/store')->assertOk()->json('data.data'); $row = collect($rows)->firstWhere('id', $order->id); $this->assertSame(BillModel::PAY_STATE_PAID, $row['bill']['pay_state']); $this->assertSame('已支付', $row['bill']['pay_state_name']); } /** 支付记录详情:凭证图片解析 preview_url(回归:preview_url 为访问器不能 pluck) */ public function test_payment_detail_returns_voucher_urls(): void { $store = StoreModel::factory()->create(); $user = UserModel::factory()->forStore($store->id)->create(); $makeFile = static function (string $path): int { return (int) SysFileModel::create([ 'group_id' => 4, 'disk' => 'local', 'channel' => 20, 'file_type' => 10, 'file_name' => basename($path), 'file_path' => $path, 'file_size' => 1024, 'file_ext' => 'jpg', 'uploader_id' => 1, ])->id; }; $fileA = $makeFile('voucher/a.jpg'); $fileB = $makeFile('voucher/b.jpg'); $payment = PaymentModel::create([ 'payment_no' => 'ZF202608140003', 'store_id' => $store->id, 'user_id' => $user->id, 'amount' => '100.00', 'pay_method' => PaymentModel::METHOD_BANK, 'voucher_ids' => [$fileA, $fileB], 'status' => PaymentModel::STATUS_PENDING, ]); $this->actingAsSysUser(); $data = $this->getJson("/recon/payment/{$payment->id}")->assertOk()->json('data'); $this->assertCount(2, $data['payment']['voucher_urls']); $this->assertStringContainsString('voucher/a.jpg', $data['payment']['voucher_urls'][0]); $this->assertStringContainsString('voucher/b.jpg', $data['payment']['voucher_urls'][1], '凭证顺序保持提交顺序'); } }