Files
xin-procurement/tests/Feature/ReconciliationTest.php
T
2026-08-12 14:38:20 +08:00

192 lines
8.2 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\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\ProductPriceModel;
use App\Models\ReconciliationItemModel;
use App\Models\ReconciliationModel;
use App\Models\SettlementModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use App\Models\SupplierModel;
use App\Models\UserModel;
/**
* 财务对账:明细构建(已完成订单直读,品类/供应商筛选)、D4 修改后差额与头汇总重算、D8 标记、D9 结算
* publish = 订货金额;actual = 采购成本(称重>0 ? 称重×单价 : 数量×单价,单价 = 成本/包规)
*/
class ReconciliationTest extends ProcurementTestCase
{
/**
* 构造已完成订单链路:2 门店下单(2/3 件,等级价 10.00;成本 8,包规 1斤 → 单价 8.00),订单置为已完成
* 预期:publish 20/30actual 16/24diff 4/6
*
* @return array{0: array<int, StoreModel>, 1: SupplierModel}
*/
private function buildCompletedOrders(): array
{
$level = CustomerLevelModel::factory()->create();
$supplier = SupplierModel::factory()->create();
$product = ProductModel::factory()->create([
'status' => ProductModel::STATUS_ON,
'supplier_id' => $supplier->id,
'cost_price' => 8,
'spec' => '1斤',
]);
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '10.00']);
$stores = [];
foreach ([2, 3] 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);
}
// 订单完成(对账数据源为已完成订单的订货明细)
StoreOrderModel::query()->update(['status' => StoreOrderModel::STATUS_COMPLETED]);
return [$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->buildCompletedOrders();
$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('16.00', (string) $byStore[$stores[0]->id]->actual_amount, '采购成本 2×8');
$this->assertSame('4.00', (string) $byStore[$stores[0]->id]->diff_amount);
$this->assertSame('30.00', (string) $byStore[$stores[1]->id]->publish_amount);
$this->assertSame('24.00', (string) $byStore[$stores[1]->id]->actual_amount);
$recon = ReconciliationModel::find($reconId);
$this->assertSame('50.00', (string) $recon->publish_amount);
$this->assertSame('40.00', (string) $recon->actual_amount);
$this->assertSame('10.00', (string) $recon->diff_amount);
$this->assertSame(ReconciliationModel::STATUS_WORKING, $recon->status);
}
/** 未完成订单不计入对账 */
public function test_build_excludes_unfinished_orders(): void
{
[$stores] = $this->buildCompletedOrders();
// 第二家门店订单回退为配送中 → 仅第一家进入对账
StoreOrderModel::where('store_id', $stores[1]->id)
->update(['status' => StoreOrderModel::STATUS_DISTRIBUTION]);
$this->actingAsSysUser();
$reconId = $this->createRecon();
$this->postJson("/recon/list/{$reconId}/build")->assertJsonPath('success', true);
$items = ReconciliationItemModel::where('recon_id', $reconId)->get();
$this->assertCount(1, $items);
$this->assertSame($stores[0]->id, $items->first()->store_id);
}
/** 供应商筛选:仅拉取该供应商的订单数据 */
public function test_build_filters_by_supplier(): void
{
[, $supplier] = $this->buildCompletedOrders();
$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->buildCompletedOrders();
$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('49.00', (string) $recon->actual_amount, '25 + 24');
$this->assertSame('1.00', (string) $recon->diff_amount, '50 - 49');
}
/** D8 对账状态标记翻转 */
public function test_toggle_reconciled_flag(): void
{
$this->buildCompletedOrders();
$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->buildCompletedOrders();
$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('16.00', (string) $byStore[$stores[0]->id]->actual_amount);
$this->assertSame('4.00', (string) $byStore[$stores[0]->id]->diff_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);
}
}