Files
xin-procurement/tests/Feature/BillPaymentTest.php
T
2026-09-04 20:37:34 +08:00

412 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Models\BillModel;
use App\Models\CustomerLevelModel;
use App\Models\PaymentModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use Modules\SystemTool\Models\SysFileModel;
use Modules\SystemTool\Models\SysSiteConfigGroupModel;
use Modules\SystemTool\Models\SysSiteConfigItemsModel;
use Modules\SystemTool\Services\SysSiteConfigService;
/**
* 账单链路与支付:订单状态随业务链自动推进(采购单完成→配送中、生成账单→已完成);
* 账单支付后累加门店总采购金额(只统计商品金额,手动收款与支付审核双入口)
*/
class BillPaymentTest extends ProcurementTestCase
{
/**
* 完整业务链造账单:下单 → 接单 → 生成采购单 → 完成采购单 → 生成账单
* (商品 5.00 × 4 = 商品金额 20.00,配送费/筐/托盘均 0)
*
* @return array{0: StoreModel, 1: StoreOrderModel, 2: BillModel}
*/
private function makeBillViaChain(): array
{
$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])
->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, '未传售后金额不影响总金额');
}
/** 生成账单:售后金额为正数时加收计入总金额 */
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']);
}
}