139 lines
7.2 KiB
PHP
139 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository as Repository;
|
|
use Modules\EcommerceFinance\Services\SupplierBookkeepingValuation;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class SupplierBookkeepingValuationTest extends TestCase
|
|
{
|
|
private function row(array $patch = []): array
|
|
{
|
|
return array_replace(['started_at' => '2026-10-15 10:00:00', 'bookkeeping_type' => '补偿款项', 'amount' => '100.00',
|
|
'original_amount' => '100.00', 'deduction_status' => '扣款', 'deduction_basis' => '供应商正常记账',
|
|
'remark' => '原备注', 'tracking_no' => 'EXP-1', 'online_order_no' => 'ORDER-1', 'product_code' => 'SKU-A'], $patch);
|
|
}
|
|
|
|
private function shipment(array $patch = []): array
|
|
{
|
|
return array_replace(['id' => 1, 'source_id' => 10, 'express_no' => 'EXP-1', 'trade_no' => 'ORDER-1',
|
|
'barcode' => 'SKU-A', 'qty' => '2', 'freight' => '8.50', 'ship_time' => '2026-05-01 00:00:00'], $patch);
|
|
}
|
|
|
|
private function service(array $shipments, array $costs = ['SKU-A' => '20.00']): SupplierBookkeepingValuation
|
|
{
|
|
$repository = $this->createMock(Repository::class);
|
|
$repository->method('shipmentsFor')->willReturn($shipments);
|
|
$repository->method('costsFor')->willReturn($costs);
|
|
return new SupplierBookkeepingValuation($repository);
|
|
}
|
|
|
|
public function test_caps_by_matched_shipped_quantity_times_cost_plus_actual_freight(): void
|
|
{
|
|
$row = $this->service([$this->shipment()])->apply([$this->row()])[0];
|
|
$this->assertSame('48.50', $row['amount']);
|
|
$this->assertSame('100.00', $row['original_amount']);
|
|
$this->assertSame('40.00', $row['matched_cost']);
|
|
$this->assertSame('8.50', $row['matched_freight']);
|
|
$this->assertSame('2', $row['matched_quantity']);
|
|
$this->assertSame('2026-05 至 2026-10', $row['valuation_window']);
|
|
$this->assertStringStartsWith("原备注\n", $row['remark']);
|
|
$this->assertSame('扣款', $row['deduction_status']);
|
|
}
|
|
|
|
public function test_keeps_lower_or_equal_bookkeeping_amount_and_rounds_ceiling_once(): void
|
|
{
|
|
$service = $this->service([$this->shipment()]);
|
|
foreach (['40.00', '48.50'] as $amount) {
|
|
$row = $service->apply([$this->row(['amount' => $amount])])[0];
|
|
$this->assertSame($amount, $row['amount']);
|
|
}
|
|
$rounded = $this->service([$this->shipment(['freight' => '8.505'])])->apply([$this->row()])[0];
|
|
$this->assertSame('48.51', $rounded['amount']);
|
|
}
|
|
|
|
public function test_multiple_goods_share_parcel_freight_once_and_all_matched_goods_are_costed(): void
|
|
{
|
|
$row = $this->service([
|
|
$this->shipment(),
|
|
$this->shipment(['id' => 2, 'barcode' => 'SKU-B', 'qty' => 3, 'freight' => null]),
|
|
], ['SKU-A' => '20.00', 'SKU-B' => '5.00'])->apply([$this->row(['product_code' => "SKU-A\nSKU-B"])])[0];
|
|
$this->assertSame('63.50', $row['amount']);
|
|
$this->assertSame('55.00', $row['matched_cost']);
|
|
$this->assertSame('8.50', $row['matched_freight']);
|
|
$this->assertSame('5', $row['matched_quantity']);
|
|
}
|
|
|
|
public function test_freight_loss_uses_actual_freight_even_above_original_without_requiring_cost(): void
|
|
{
|
|
$row = $this->service([$this->shipment(['freight' => '80.00'])], [])
|
|
->apply([$this->row(['bookkeeping_type' => '运费损耗', 'amount' => '30.00', 'product_code' => ''])])[0];
|
|
$this->assertSame('80.00', $row['amount']);
|
|
$this->assertStringContainsString('运费损耗按原单实际运费记账', $row['remark']);
|
|
$originalZero = $this->service([$this->shipment(['freight' => '8.50'])], [])
|
|
->apply([$this->row(['bookkeeping_type' => '运费损耗', 'amount' => '0.00'])])[0];
|
|
$this->assertSame('8.50', $originalZero['amount']);
|
|
$this->assertSame('扣款', $originalZero['deduction_status']);
|
|
$zero = $this->service([$this->shipment(['freight' => '0.00'])], [])
|
|
->apply([$this->row(['bookkeeping_type' => '运费损耗'])])[0];
|
|
$this->assertSame('0.00', $zero['amount']);
|
|
$this->assertSame('不扣款', $zero['deduction_status']);
|
|
}
|
|
|
|
public function test_exact_tracking_takes_precedence_and_missing_tracking_does_not_borrow_another_parcel(): void
|
|
{
|
|
$shipments = [$this->shipment(), $this->shipment(['id' => 2, 'source_id' => 11, 'express_no' => 'EXP-2', 'freight' => '20.00'])];
|
|
$service = $this->service($shipments);
|
|
$this->assertSame('48.50', $service->apply([$this->row()])[0]['amount']);
|
|
$missing = $service->apply([$this->row(['tracking_no' => 'MISSING'])])[0];
|
|
$this->assertSame('待确认', $missing['deduction_status']);
|
|
$this->assertSame('100.00', $missing['amount']);
|
|
$byOrder = $service->apply([$this->row(['tracking_no' => ''])])[0];
|
|
$this->assertSame('80.00', $byOrder['matched_cost']);
|
|
$this->assertSame('28.50', $byOrder['matched_freight']);
|
|
$this->assertSame('100.00', $byOrder['amount']);
|
|
}
|
|
|
|
public function test_window_excludes_april_and_november_and_crosses_years_correctly(): void
|
|
{
|
|
foreach (['2026-04-30 23:59:59', '2026-11-01 00:00:00'] as $date) {
|
|
$row = $this->service([$this->shipment(['ship_time' => $date])])->apply([$this->row()])[0];
|
|
$this->assertSame('待确认', $row['deduction_status']);
|
|
}
|
|
$this->assertSame(['2025-09-01 00:00:00', '2026-03-01 00:00:00'], Repository::window(['started_at' => '2026-02-28']));
|
|
}
|
|
|
|
public function test_missing_cost_quantity_freight_or_goods_stays_pending_instead_of_silently_zeroing(): void
|
|
{
|
|
foreach ([
|
|
[[$this->shipment()], []], [[$this->shipment()], ['SKU-A' => '0']],
|
|
[[$this->shipment(['qty' => null])], ['SKU-A' => '20']],
|
|
[[$this->shipment(['freight' => null])], ['SKU-A' => '20']],
|
|
[[$this->shipment(['freight' => '-1'])], ['SKU-A' => '20']],
|
|
[[$this->shipment(['barcode' => 'SKU-B'])], ['SKU-A' => '20']],
|
|
] as [$shipments, $costs]) {
|
|
$row = $this->service($shipments, $costs)->apply([$this->row()])[0];
|
|
$this->assertSame('待确认', $row['deduction_status']);
|
|
$this->assertSame('100.00', $row['amount']);
|
|
}
|
|
}
|
|
|
|
public function test_supplier_or_warehouse_exclusions_and_small_zeros_are_never_overridden(): void
|
|
{
|
|
$repository = $this->createMock(Repository::class);
|
|
$repository->expects($this->never())->method('shipmentsFor');
|
|
$service = new SupplierBookkeepingValuation($repository);
|
|
$rows = [$this->row(['deduction_status' => '不扣款']), $this->row(['deduction_status' => '待确认']),
|
|
$this->row(['deduction_status' => '不扣款', 'amount' => '0.00'])];
|
|
$this->assertSame($rows, $service->apply($rows));
|
|
}
|
|
|
|
public function test_partial_multi_tracking_match_is_pending_and_cannot_deduct_only_one_package(): void
|
|
{
|
|
$row = $this->service([$this->shipment()])->apply([$this->row(['tracking_no' => "EXP-1\nEXP-2"])])[0];
|
|
$this->assertSame('待确认', $row['deduction_status']);
|
|
$this->assertSame('100.00', $row['amount']);
|
|
}
|
|
}
|