105 lines
5.2 KiB
PHP
105 lines
5.2 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Business\ThirdPartyWarehouseProductWeightRepository;
|
||
use App\Business\ThirdPartyWarehouseStorageFeeCalculator;
|
||
use App\Exports\ThirdPartyWarehouseStorageSettlementExport;
|
||
use App\Models\Business\ThirdPartyWarehouseQuote;
|
||
use Mockery;
|
||
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
/**
|
||
* 三方仓仓储费核算引擎测试。
|
||
*
|
||
* 真实验收基准(南昌樵舍仓, 2026-08, 单价 20/0.85/23/3/10.5, 用生产镜像实测):
|
||
* 入库管理费 1538.20 / 出库费用 6099.60 / 当天仓储费用 8376.48 / 合计 16014.28
|
||
* 下方用小型 fixture 验证三条公式口径。
|
||
*/
|
||
class ThirdPartyWarehouseStorageFeeCalculatorTest extends FeatureTestCase
|
||
{
|
||
public function test_calculates_inbound_outbound_and_storage_components(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
ThirdPartyWarehouseQuote::query()->create([
|
||
'company_id' => (int) company_id(),
|
||
'warehouse_name' => '南昌樵舍仓',
|
||
'inbound_rate_per_ton' => 20,
|
||
'outbound_rate_per_order' => 0.85,
|
||
'pieces_per_pallet' => 23,
|
||
'pallet_area_sqm' => 3,
|
||
'storage_rate_per_sqm_month' => 10.5,
|
||
'effective_at' => '2026-08-01',
|
||
'status' => ThirdPartyWarehouseQuote::STATUS_ACTIVE,
|
||
]);
|
||
|
||
$rows = [
|
||
['date' => '2026-08-01', 'sku_code' => 'A', 'owner' => '甲', 'product_name' => '商品A', 'supplier' => '', 'category' => '', 'unit' => '个', 'opening_qty' => 100, 'closing_qty' => 100, 'purchase_in' => 0, 'sales_out' => 0, 'other_out' => 0],
|
||
['date' => '2026-08-01', 'sku_code' => 'B', 'owner' => '甲', 'product_name' => '商品B', 'supplier' => '', 'category' => '', 'unit' => '个', 'opening_qty' => 0, 'closing_qty' => 50, 'purchase_in' => 50, 'sales_out' => 0, 'other_out' => 0],
|
||
['date' => '2026-08-02', 'sku_code' => 'A', 'owner' => '甲', 'product_name' => '商品A', 'supplier' => '', 'category' => '', 'unit' => '个', 'opening_qty' => 100, 'closing_qty' => 90, 'purchase_in' => 0, 'sales_out' => -10, 'other_out' => 0],
|
||
];
|
||
|
||
$mirror = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||
$mirror->shouldReceive('exportRows')->andReturnUsing(static function () use ($rows): \Generator {
|
||
foreach ($rows as $row) {
|
||
yield $row;
|
||
}
|
||
});
|
||
$this->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $mirror);
|
||
|
||
$weights = Mockery::mock(ThirdPartyWarehouseProductWeightRepository::class);
|
||
$weights->shouldReceive('weightsBySkuCodes')->andReturn(['A' => 23.0, 'B' => 46.0]);
|
||
$this->instance(ThirdPartyWarehouseProductWeightRepository::class, $weights);
|
||
|
||
$result = app(ThirdPartyWarehouseStorageFeeCalculator::class)
|
||
->calculate(15045364, '南昌樵舍仓', '2026-08');
|
||
$summary = $result['summary'];
|
||
|
||
// 入库:B 采购进仓 50 × 46kg/1000 = 2.3 吨 × 20 = 46.00
|
||
$this->assertSame(46.0, (float) $summary['inbound_fee']);
|
||
// 出库:|−10| = 10 件 × 0.85 = 8.50
|
||
$this->assertSame(10.0, (float) $summary['outbound_qty']);
|
||
$this->assertSame(8.5, (float) $summary['outbound_fee']);
|
||
// 仓储:日结余 08-01=150、08-02=90;当月 31 天
|
||
$expectedStorage = round(150 / 23 * 3 * 10.5 / 31 + 90 / 23 * 3 * 10.5 / 31, 2);
|
||
$this->assertSame($expectedStorage, (float) $summary['storage_fee']);
|
||
$this->assertSame(round(46.0 + 8.5 + $expectedStorage, 2), (float) $summary['total_fee']);
|
||
$this->assertCount(2, $result['daily']);
|
||
$this->assertCount(2, $result['per_sku']);
|
||
$this->assertCount(3, $result['daily_sku']);
|
||
|
||
$firstDay = collect($result['daily_sku'])->where('date', '2026-08-01');
|
||
$carriedStock = $firstDay->firstWhere('sku_code', 'A');
|
||
$this->assertSame(100.0, (float) $carriedStock['opening']);
|
||
$this->assertSame(0.0, (float) $carriedStock['net_movement']);
|
||
$this->assertSame(100.0, (float) $carriedStock['closing']);
|
||
$this->assertEqualsWithDelta(100 / 23 * 3 * 10.5 / 31, $carriedStock['storage_fee'], 0.000001);
|
||
$this->assertEqualsWithDelta(
|
||
$result['daily'][0]['storage_fee'],
|
||
$firstDay->sum('storage_fee'),
|
||
0.0001,
|
||
);
|
||
|
||
$sheets = (new ThirdPartyWarehouseStorageSettlementExport($result))->sheets();
|
||
$this->assertCount(4, $sheets);
|
||
$this->assertSame('每日商品仓储费', $sheets[3]->title());
|
||
$this->assertSame('A', $sheets[3]->array()[0][2]);
|
||
}
|
||
|
||
public function test_requires_effective_quote(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$mirror = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||
$this->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $mirror);
|
||
$weights = Mockery::mock(ThirdPartyWarehouseProductWeightRepository::class);
|
||
$this->instance(ThirdPartyWarehouseProductWeightRepository::class, $weights);
|
||
|
||
$this->expectException(\Symfony\Component\HttpKernel\Exception\HttpException::class);
|
||
app(ThirdPartyWarehouseStorageFeeCalculator::class)
|
||
->calculate(15045364, '南昌樵舍仓', '2026-08');
|
||
}
|
||
}
|