first version
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<?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.00(quantity=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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user