paymentCycle($cycleDays)->create(); } /** 造一张账单(默认 2026-08-10 出账,总额 140.00,未支付) */ private function makeBill(StoreModel $store, array $attributes = []): BillModel { $seq = ++self::$billSeq; return BillModel::create(array_merge([ 'bill_no' => 'ZD' . str_pad((string) $seq, 12, '0', STR_PAD_LEFT), 'purchase_id' => PurchaseOrderModel::factory()->create()->id, 'store_id' => $store->id, 'bill_date' => '2026-08-10', 'product_amount' => '100.00', 'delivery_fee' => '10.00', 'box_num' => 2, 'tray_num' => 1, 'box_price' => '5.00', 'tray_price' => '20.00', 'added_amount' => '30.00', 'total_amount' => '140.00', 'status' => BillModel::STATUS_UNPAID, ], $attributes)); } /** 支付进度推导与应结算日期(账单日期 + 门店回款周期) */ public function test_bill_list_derives_pay_state_and_settlement_date(): void { $store = $this->makeStore(3); $this->actingAsMiniStore($store); $unpaid = $this->makeBill($store); $reviewing = $this->makeBill($store, ['payment_id' => 99]); $paid = $this->makeBill($store, ['status' => BillModel::STATUS_PAID, 'paid_at' => '2026-08-12 10:00:00']); $rows = collect($this->getJson('/mini/bill')->assertOk()->json('data.data'))->keyBy('id'); $unpaidRow = $rows[$unpaid->id]; $this->assertSame(BillModel::PAY_STATE_UNPAID, $unpaidRow['pay_state']); $this->assertSame('待支付', $unpaidRow['pay_state_name']); $this->assertTrue($unpaidRow['can_pay']); $this->assertSame('2026-08-13', $unpaidRow['settlement_date'], '账单日期 2026-08-10 + 回款周期 3 天'); $this->assertArrayNotHasKey('operator_id', $unpaidRow, '小程序端不输出后台操作人字段'); $this->assertArrayNotHasKey('paid_operator_id', $unpaidRow); $reviewingRow = $rows[$reviewing->id]; $this->assertSame(BillModel::PAY_STATE_REVIEWING, $reviewingRow['pay_state']); $this->assertSame('审核中', $reviewingRow['pay_state_name']); $this->assertFalse($reviewingRow['can_pay']); $paidRow = $rows[$paid->id]; $this->assertSame(BillModel::PAY_STATE_PAID, $paidRow['pay_state']); $this->assertSame('已支付', $paidRow['pay_state_name']); $this->assertFalse($paidRow['can_pay']); $this->assertSame('2026-08-12 10:00:00', $paidRow['paid_at']); } /** 待支付汇总:未支付笔数/金额(含审核中),不受列表筛选影响 */ public function test_bill_list_summary_aggregates_unpaid(): void { $store = $this->makeStore(); $this->actingAsMiniStore($store); $this->makeBill($store, ['total_amount' => '100.00']); $this->makeBill($store, ['total_amount' => '50.50', 'payment_id' => 99]); $this->makeBill($store, ['total_amount' => '200.00', 'status' => BillModel::STATUS_PAID]); $response = $this->getJson('/mini/bill?status=1')->assertOk(); $this->assertSame(1, $response->json('data.total'), 'status 筛选只影响列表'); $this->assertSame(2, $response->json('data.summary.unpaid_count')); $this->assertSame('150.50', $response->json('data.summary.unpaid_amount')); } /** 可付款筛选:仅未支付且未锁定到支付记录的账单(合并付款选择页) */ public function test_bill_list_payable_filter(): void { $store = $this->makeStore(); $this->actingAsMiniStore($store); $payable = $this->makeBill($store); $this->makeBill($store, ['payment_id' => 99]); $this->makeBill($store, ['status' => BillModel::STATUS_PAID]); $rows = $this->getJson('/mini/bill?payable=1')->assertOk()->json('data.data'); $this->assertCount(1, $rows); $this->assertSame($payable->id, $rows[0]['id']); } /** 账单日期区间筛选 */ public function test_bill_list_filters_by_bill_date_range(): void { $store = $this->makeStore(); $this->actingAsMiniStore($store); $this->makeBill($store, ['bill_date' => '2026-08-01']); $this->makeBill($store, ['bill_date' => '2026-08-05']); $this->makeBill($store, ['bill_date' => '2026-08-10']); $this->getJson('/mini/bill?start_date=2026-08-03')->assertJsonPath('data.total', 2); $this->getJson('/mini/bill?start_date=2026-08-03&end_date=2026-08-06')->assertJsonPath('data.total', 1); $this->getJson('/mini/bill?end_date=2026-08-06')->assertJsonPath('data.total', 2); } /** 门店数据隔离:只能查看本店账单 */ public function test_bill_data_isolated_between_stores(): void { $storeA = $this->makeStore(); $billOfA = $this->makeBill($storeA); $storeB = $this->makeStore(); $this->actingAsMiniStore($storeB); $this->getJson('/mini/bill')->assertJsonPath('data.total', 0); $this->getJson("/mini/bill/{$billOfA->id}")->assertJsonPath('success', false); } /** 详情:格式化账单 + 合并商品明细 + 关联订单 */ public function test_bill_detail_outputs_bill_items_and_orders(): void { $store = $this->makeStore(0); $this->actingAsMiniStore($store); $file = SysFileModel::create([ 'group_id' => 1, 'disk' => 'local', 'channel' => 10, 'file_type' => 10, 'file_name' => '白菜.jpg', 'file_path' => 'product/baicai.jpg', 'file_size' => 1024, 'file_ext' => 'jpg', 'uploader_id' => 1, ]); $bill = $this->makeBill($store, ['after_sale' => '-3.00']); $product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]); $order = StoreOrderModel::factory()->create([ 'store_id' => $store->id, 'purchase_id' => $bill->purchase_id, 'bill_id' => $bill->id, 'total_amount' => '100.00', ]); StoreOrderItemModel::factory()->create([ 'order_id' => $order->id, 'purchase_id' => $bill->purchase_id, 'bill_id' => $bill->id, 'store_id' => $store->id, 'product_id' => $product->id, 'product_name' => '白菜', 'product_spec' => '30斤/筐', 'unit' => '筐', 'price' => '5.00', 'quantity' => 20, 'amount' => '100.00', 'image_ids' => (string) $file->id, ]); $data = $this->getJson("/mini/bill/{$bill->id}")->assertOk()->json('data'); $this->assertSame($bill->bill_no, $data['bill']['bill_no']); $this->assertSame('-3.00', $data['bill']['after_sale'], '售后金额输出给门店(计入总金额构成)'); $this->assertSame('待支付', $data['bill']['pay_state_name']); $this->assertSame('2026-08-10', $data['bill']['settlement_date'], '回款周期 0 天,出账当天应结算'); $this->assertSame($bill->purchase->purchase_no, $data['bill']['purchase']['purchase_no']); $this->assertCount(1, $data['items']); $this->assertSame('白菜', $data['items'][0]['product_name']); $this->assertSame('30斤/筐', $data['items'][0]['product_spec'], '明细附规格'); $this->assertSame('筐', $data['items'][0]['unit'], '明细附单位'); $this->assertSame('5.00', $data['items'][0]['price'], '合并明细单价 = Σ金额 ÷ Σ数量'); $this->assertSame(20, $data['items'][0]['quantity']); $this->assertSame('100.00', $data['items'][0]['amount']); $this->assertStringContainsString('baicai.jpg', (string) $data['items'][0]['image'], '明细附首图URL'); $this->assertArrayNotHasKey('image_ids', $data['items'][0], 'image_ids 解析后不输出'); $this->assertCount(1, $data['orders']); $this->assertSame($order->order_no, $data['orders'][0]['order_no']); } }