create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); $this->actingAsMiniStore($store); $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_generate_bill_saves_after_sale_and_remark(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); $this->actingAsMiniStore($store); $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]); $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, 'after_sale' => -5.5, 'remark' => '白菜烂叶 2 斤已协商', ]], ])->assertJsonPath('success', true); $bill = BillModel::where('store_id', $store->id)->first(); $this->assertSame('-5.50', (string) $bill->after_sale); $this->assertSame('白菜烂叶 2 斤已协商', $bill->remark); $this->assertSame('14.50', (string) $bill->total_amount, '总金额 = 商品 20.00 + 售后金额 -5.50'); // 不传售后金额时按 0 计入,备注默认空字符串 $store2 = StoreModel::factory()->create(['level_id' => $level->id]); $this->actingAsMiniStore($store2); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]) ->assertJsonPath('success', true); $order2 = StoreOrderModel::where('store_id', $store2->id)->first(); $this->actingAsSysUser(); $this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); $purchase2 = PurchaseOrderModel::latest('id')->first(); $this->putJson("/purchase/order/{$purchase2->id}/finish")->assertJsonPath('success', true); $this->postJson("/purchase/order/{$purchase2->id}/bill", [ 'stores' => [['store_id' => $store2->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0]], ])->assertJsonPath('success', true); $bill2 = BillModel::where('store_id', $store2->id)->first(); $this->assertSame('0.00', (string) $bill2->after_sale); $this->assertSame('', (string) $bill2->remark); $this->assertSame('5.00', (string) $bill2->total_amount, '未传售后金额不影响总金额'); } /** 售后金额按客户等级上浮折算:输入 15、上浮 1% → 实收 15.15;billPrepare 回显等级(负数同比例放大) */ public function test_generate_bill_after_sale_marked_up_by_level(): void { $level = CustomerLevelModel::factory()->create(['percent' => 1]); $store = StoreModel::factory()->create(['level_id' => $level->id]); $level2 = CustomerLevelModel::factory()->create(['percent' => 2]); $store2 = StoreModel::factory()->create(['level_id' => $level2->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); // 两店各下一单:店1 等级价 5.05 × 4 = 20.20;店2 等级价 5.10 × 2 = 10.20 foreach ([[$store, 4], [$store2, 2]] as [$s, $qty]) { $this->actingAsMiniStore($s); $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->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true); // billPrepare 回显客户等级与上浮比例(弹窗预览用) $prepare = $this->getJson("/purchase/order/{$purchase->id}/bill/prepare") ->assertJsonPath('success', true) ->json('data.stores'); $prepareByStore = collect($prepare)->keyBy('store_id'); $this->assertSame($level->name, $prepareByStore[$store->id]['level_name']); $this->assertSame('1.00', (string) $prepareByStore[$store->id]['level_percent']); $this->assertSame($level2->name, $prepareByStore[$store2->id]['level_name']); $this->assertSame('2.00', (string) $prepareByStore[$store2->id]['level_percent']); // 店1 售后 15 × 101% = 15.15;店2 售后 -10 × 102% = -10.20 $this->postJson("/purchase/order/{$purchase->id}/bill", [ 'stores' => [ ['store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0, 'after_sale' => 15], ['store_id' => $store2->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0, 'after_sale' => -10], ], ])->assertJsonPath('success', true); $bill = BillModel::where('store_id', $store->id)->first(); $this->assertSame('15.15', (string) $bill->after_sale, '15 × 101%'); $this->assertSame('35.35', (string) $bill->total_amount, '商品 20.20 + 售后 15.15'); $bill2 = BillModel::where('store_id', $store2->id)->first(); $this->assertSame('-10.20', (string) $bill2->after_sale, '-10 × 102%(负数同比例放大)'); $this->assertSame('0.00', (string) $bill2->total_amount, '商品 10.20 + 售后 -10.20'); } /** 生成账单:售后金额为正数时加收计入总金额 */ public function test_generate_bill_positive_after_sale_added_to_total(): void { [$store, , $bill] = $this->makeBillViaChain(); $this->assertSame('20.00', (string) $bill->total_amount, '基线:无售后金额时总额=商品金额'); // 重新走一条链:售后金额 +8.00,配送费 2.00 → 总额 20 + 2 + 8 = 30.00 $level = CustomerLevelModel::factory()->create(); $store2 = StoreModel::factory()->create(['level_id' => $level->id]); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']); $this->actingAsMiniStore($store2); $this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]]) ->assertJsonPath('success', true); $order2 = StoreOrderModel::where('store_id', $store2->id)->first(); $this->actingAsSysUser(); $this->putJson("/order/store/{$order2->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED]); $this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]) ->assertJsonPath('success', true); $purchase2 = PurchaseOrderModel::latest('id')->first(); $this->putJson("/purchase/order/{$purchase2->id}/finish")->assertJsonPath('success', true); $this->postJson("/purchase/order/{$purchase2->id}/bill", [ 'stores' => [[ 'store_id' => $store2->id, 'delivery_fee' => 2, 'box_num' => 0, 'tray_num' => 0, 'after_sale' => 8, ]], ])->assertJsonPath('success', true); $bill2 = BillModel::where('store_id', $store2->id)->first(); $this->assertSame('8.00', (string) $bill2->after_sale); $this->assertSame('30.00', (string) $bill2->total_amount, '总金额 = 商品 20.00 + 配送费 2.00 + 售后金额 8.00'); } /** 生成账单:售后金额非数值 → 拒绝 */ public function test_generate_bill_rejects_non_numeric_after_sale(): void { $level = CustomerLevelModel::factory()->create(); $store = StoreModel::factory()->create(['level_id' => $level->id]); $this->actingAsSysUser(); $purchase = PurchaseOrderModel::factory()->create(['status' => PurchaseOrderModel::STATUS_COMPLETED]); $this->postJson("/purchase/order/{$purchase->id}/bill", [ 'stores' => [[ 'store_id' => $store->id, 'delivery_fee' => 0, 'box_num' => 0, 'tray_num' => 0, 'after_sale' => '白菜烂叶 2 斤已协商', ]], ])->assertJsonPath('success', false); } /** 订单状态随业务链自动推进:接单→采购中→配送中→(生成账单)已完成 */ 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, 'cost_price' => '5.00']); $this->actingAsMiniStore($store); $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(); $payment = PaymentModel::create([ 'payment_no' => 'ZF202608140001', 'store_id' => $store->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(); $payment = PaymentModel::create([ 'payment_no' => 'ZF202608140002', 'store_id' => $store->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(); $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, '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], '凭证顺序保持提交顺序'); } /** 支付配置接口:返回支付方式开关(未配置时默认全部启用,配置后按勾选项返回) */ public function test_payment_config_returns_pay_methods(): void { $store = StoreModel::factory()->create(); $this->actingAsMiniStore($store); // 未配置时默认全部启用 $this->getJson('/mini/payment/config') ->assertOk() ->assertJsonPath('success', true) ->assertJsonPath('data.pay_methods', ['wechat_qrcode', 'alipay_qrcode', 'bank', 'online']); // 后台勾选部分支付方式 $group = SysSiteConfigGroupModel::create(['title' => '支付配置', 'key' => 'pay', 'remark' => '']); SysSiteConfigItemsModel::create([ 'group_id' => $group->id, 'key' => 'pay_methods', 'title' => '支付方式开关', 'describe' => '', 'values' => '["bank","online"]', 'type' => 'Checkbox', 'options' => '', 'sort' => 0, ]); SysSiteConfigService::refreshSiteConfig(); $this->getJson('/mini/payment/config') ->assertOk() ->assertJsonPath('data.pay_methods', ['bank', 'online']); } }