Files
xin-procurement/tests/Feature/AllocationTest.php
T
2026-07-23 20:41:25 +08:00

145 lines
5.9 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\PurchaseAllocationModel;
use App\Models\PurchaseOrderItemModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\UserModel;
/**
* D3 采购金额分摊:金额守恒(含尾差修正)、按订货比例、幂等重跑
*/
class AllocationTest extends ProcurementTestCase
{
/**
* 构造可分摊的采购单:各门店下单 → 生成采购单 → 录入实际金额
*
* @param array<int, string> $quantities 各门店订货量
* @return array{0: PurchaseOrderModel, 1: PurchaseOrderItemModel}
*/
private function buildAllocatablePurchase(array $quantities, string $actualPrice = '10.00'): array
{
$level = CustomerLevelModel::factory()->create();
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON]);
ProductPriceModel::factory()->create(['product_id' => $product->id, 'level_id' => $level->id, 'price' => '10.00']);
foreach ($quantities as $qty) {
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => $qty]]])
->assertJsonPath('success', true);
}
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
$item = $purchase->items->first();
// 录入实际单价(weight=0 → amount = quantity × price
$this->putJson("/purchase/order/item/{$item->id}", [
'price' => $actualPrice,
'quantity' => $item->quantity,
'weight' => 0,
])->assertJsonPath('success', true);
return [$purchase->fresh(), $item->fresh()];
}
/** 金额守恒:Σallocation.amount === item.amount,尾差由最后一行承担 */
public function test_allocation_conserves_amount_with_tail_correction(): void
{
[$purchase, $item] = $this->buildAllocatablePurchase(['1', '1', '1']);
// 把实际金额调成不可被 3 整除的 100.00quantity=10 × price=10
$this->actingAsSysUser();
$this->putJson("/purchase/order/item/{$item->id}", [
'price' => '10.00',
'quantity' => '10.00',
'weight' => 0,
])->assertJsonPath('success', true);
$item = $item->fresh();
$this->assertSame('100.00', (string) $item->amount);
$this->postJson("/purchase/order/{$purchase->id}/allocate")
->assertOk()
->assertJsonPath('success', true);
$allocations = PurchaseAllocationModel::where('purchase_item_id', $item->id)
->orderBy('id')
->get();
$this->assertCount(3, $allocations);
$sum = $allocations->reduce(
static fn (string $carry, $a): string => bcadd($carry, (string) $a->amount, 2),
'0'
);
$this->assertSame('100.00', $sum, '分摊总额必须守恒');
// 三等分尾差修正:33.33 / 33.33 / 33.34
$this->assertSame('33.33', (string) $allocations[0]->amount);
$this->assertSame('33.33', (string) $allocations[1]->amount);
$this->assertSame('33.34', (string) $allocations[2]->amount);
}
/** 按订货数量比例分摊 */
public function test_allocation_follows_order_ratio(): void
{
[$purchase, $item] = $this->buildAllocatablePurchase(['1', '3']);
// quantity=4,实际金额 4×10=40 → 1:3 → 10.00 / 30.00
$this->actingAsSysUser();
$this->postJson("/purchase/order/{$purchase->id}/allocate")->assertJsonPath('success', true);
$amounts = PurchaseAllocationModel::where('purchase_item_id', $item->id)
->pluck('amount')
->map(static fn ($v) => (string) $v)
->sort()
->values()
->all();
$this->assertSame(['10.00', '30.00'], $amounts);
}
/** 幂等:重复分摊先删旧记录再重建,数量不变 */
public function test_allocation_is_idempotent(): void
{
[$purchase] = $this->buildAllocatablePurchase(['2', '3']);
$this->actingAsSysUser();
$this->postJson("/purchase/order/{$purchase->id}/allocate")->assertJsonPath('success', true);
$first = PurchaseAllocationModel::count();
$this->assertGreaterThan(0, $first);
$this->postJson("/purchase/order/{$purchase->id}/allocate")->assertJsonPath('success', true);
$this->assertSame($first, PurchaseAllocationModel::count(), '重复分摊不应产生重复记录');
}
/** 未录入实际金额时拒绝分摊 */
public function test_allocation_rejected_without_actual_amount(): 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' => '10.00']);
$this->actingAsMiniUser(UserModel::factory()->forStore($store->id)->create());
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 1]]]);
$this->actingAsSysUser();
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()]);
$purchase = PurchaseOrderModel::first();
// 模拟未录入实际金额
$purchase->items->first()->update(['amount' => 0, 'price' => 0]);
$this->postJson("/purchase/order/{$purchase->id}/allocate")->assertJsonPath('success', false);
$this->assertSame(0, PurchaseAllocationModel::count());
}
}