CaiWuXiTong/tests/Feature/Business/SupplierBookkeepingPageTest.php

289 lines
20 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\Business;
use App\Models\Auth\Permission;
use Modules\EcommerceFinance\Repositories\SupplierBookkeepingWorkOrderRepository;
use Tests\Feature\FeatureTestCase;
class SupplierBookkeepingPageTest extends FeatureTestCase
{
private function row(array $patch = []): array
{
$row = array_fill_keys(array_keys(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::COLUMNS), '');
return array_replace($row, [
'started_at' => '2026-09-05 10:00:00', 'work_order_no' => 'GD614939067', 'initiator' => '梦得',
'bookkeeping_reason' => '厂家记账/孔位错误', 'reason_level_1' => '厂家记账', 'reason_level_2' => '孔位错误',
'bookkeeping_type' => '补偿款项', 'amount' => '382.15', 'actual_paid_amount' => '399.41',
'online_order_no' => '5127787476014026744', 'shop_name' => '淘宝-亿家达家居', 'status' => '待处理',
'supplier' => '彩虹', 'product_code' => "DNZ0134101N24V01\nDNZ0134102N24V01",
'attachment' => 'https://jgd-static.example/a.png', 'work_order_type' => '记账',
], $patch);
}
private function mockRepository(array $rows, string $code = 'ready', bool $realValuation = false): void
{
$repository = \Mockery::mock(SupplierBookkeepingWorkOrderRepository::class);
$repository->shouldReceive('syncStatus')->andReturn(['code' => $code, 'last_succeeded_at' => '2026-10-18 21:00:00']);
$repository->shouldReceive('rows')->andReturn($rows);
$this->app->instance(SupplierBookkeepingWorkOrderRepository::class, $repository);
if (!$realValuation) {
$valuation = \Mockery::mock(\Modules\EcommerceFinance\Services\SupplierBookkeepingValuation::class);
$valuation->shouldReceive('apply')->andReturnUsing(static fn ($rows) => $rows);
$this->app->instance(\Modules\EcommerceFinance\Services\SupplierBookkeepingValuation::class, $valuation);
}
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
}
private function url(array $query = []): string
{
return route('business.pages.show', [
'company_id' => $this->company->id, 'section' => 'supplier-payables', 'page' => 'manufacturer-bookkeeping',
]) . ($query ? '?' . http_build_query($query) : '');
}
public function test_page_renders_rows_totals_and_filters(): void
{
$this->mockRepository([
$this->row(),
$this->row(['work_order_no' => 'GD-WAREHOUSE', 'bookkeeping_reason' => '仓库记账/退货运费', 'reason_level_1' => '仓库记账', 'amount' => '10.05', 'bookkeeping_type' => '运费损耗']),
]);
$this->loginAs()->get($this->url())
->assertOk()
->assertSee('厂家记账')
->assertSee('GD614939067')
->assertSee('GD-WAREHOUSE')
->assertSee('392.20')
->assertSee('运费损耗')
->assertSee('附件1');
$this->loginAs()->get($this->url(['reason' => '仓库记账']))
->assertOk()
->assertSee('GD-WAREHOUSE')
->assertDontSee('GD614939067')
->assertSee('10.05');
$this->withExceptionHandling()->loginAs()->get($this->url(['reason' => 'DROP TABLE']))->assertStatus(422);
}
public function test_page_explains_pending_initial_sync(): void
{
$this->mockRepository([], 'initializing');
$this->loginAs()->get($this->url())
->assertOk()
->assertSee('尚未完成首次同步')
->assertSee('当前为空不代表没有记账工单');
}
public function test_export_uses_same_filters_and_selected_columns(): void
{
$this->mockRepository([$this->row(), $this->row(['work_order_no' => 'GD-2', 'status' => '已完成'])]);
$response = $this->loginAs()->get(route('business.supplier-bookkeeping.export', [
'company_id' => $this->company->id, 'status' => '待处理', 'columns' => 'work_order_no,online_order_no,amount',
]));
$response->assertOk();
$csv = $response->streamedContent();
$this->assertStringContainsString("工单编号,线上订单号,记账金额", $csv);
$this->assertStringContainsString("GD614939067,'5127787476014026744,382.15", $csv);
$this->assertStringNotContainsString('GD-2', $csv);
}
public function test_seeded_configs_drive_deduction_judgement_totals_and_filter(): void
{
$this->mockRepository([
$this->row(),
$this->row(['work_order_no' => 'GD-NORMAL', 'supplier' => '鑫呈', 'amount' => '100.00']),
$this->row(['work_order_no' => 'GD-NONE', 'supplier' => '尊享', 'amount' => '50.00']),
$this->row(['work_order_no' => 'GD-DROP-OTHER', 'supplier' => '曹县洪耀', 'amount' => '30.00']),
$this->row(['work_order_no' => 'GD-DROP-WRONG', 'supplier' => '曹县洪耀', 'amount' => '20.50', 'actual_paid_amount' => '40.00', 'reason_level_1' => '仓库记账',
'reason_level_2' => '仓库发错货(面单照片,实物照片,条形码照片)', 'bookkeeping_reason' => '仓库记账/仓库发错货(面单照片,实物照片,条形码照片)', 'shipping_warehouse' => '曹县洪耀代发仓']),
$this->row(['work_order_no' => 'GD-MAIN-WRONG', 'supplier' => '曹县洪耀', 'amount' => '8.00', 'reason_level_1' => '仓库记账',
'reason_level_2' => '仓库发错货(面单照片,实物照片,条形码照片)', 'bookkeeping_reason' => '仓库记账/仓库发错货(面单照片,实物照片,条形码照片)', 'shipping_warehouse' => '常州主仓']),
]);
$this->loginAs()->get($this->url())
->assertOk()
->assertSee('厂家扣款判定')
->assertSee('仅代发仓发错货记账')
->assertSee('502.65')
->assertSee('供应商不在配置表,按正常记账扣款');
$this->loginAs()->get($this->url(['deduction' => '扣款']))
->assertOk()
->assertSee('GD-NORMAL')
->assertSee('GD-DROP-WRONG')
->assertSee('GD614939067')
->assertDontSee('GD-NONE')
->assertDontSee('GD-DROP-OTHER')
->assertDontSee('GD-MAIN-WRONG');
$this->loginAs()->get($this->url(['deduction' => '不扣款']))->assertOk()->assertSee('GD-NONE')->assertDontSee('GD614939067');
$csv = $this->loginAs()->get(route('business.supplier-bookkeeping.export', [
'company_id' => $this->company->id, 'deduction' => '扣款', 'columns' => 'work_order_no,deduction_status,deduction_basis',
]))->assertOk()->streamedContent();
$this->assertStringContainsString('工单编号,厂家扣款判定,判定说明', $csv);
$this->assertStringContainsString('GD-DROP-WRONG,扣款,代发仓发错货,记厂家扣款', $csv);
$this->assertStringNotContainsString('GD-NONE', $csv);
$this->withExceptionHandling()->loginAs()->get($this->url(['deduction' => 'x']))->assertStatus(422);
}
public function test_small_compensation_zeroes_page_totals_and_exports_with_original_amount_and_note(): void
{
$this->mockRepository([
$this->row(['work_order_no' => 'GD-SMALL', 'amount' => '30.00', 'buyer_paid_amount' => '100', 'remark' => '原备注']),
$this->row(['work_order_no' => 'GD-EQUAL-RATIO', 'amount' => '40.00', 'buyer_paid_amount' => '100', 'actual_paid_amount' => '100.00']),
$this->row(['work_order_no' => 'GD-EQUAL-LIMIT', 'amount' => '50.00', 'buyer_paid_amount' => '200']),
]);
$response = $this->loginAs()->get($this->url())->assertOk();
$data = app(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::class)->page();
$summary = $data['bookkeepingSummary'];
$this->assertSame(9000, $summary['amount_cents']);
$this->assertSame(9000, $summary['deduction_cents']);
$this->assertSame(2, $summary['deduction_count']);
$row = $data['bookkeepingRows']->getCollection()->firstWhere('work_order_no', 'GD-SMALL');
$this->assertSame('0.00', $row['amount']);
$this->assertSame('30.00', $row['original_amount']);
$this->assertSame("原备注\n小于50且小于0.4,记账改0", $row['remark']);
$response->assertSee('原记账金额')->assertSee('小于50且小于0.4,记账改0');
$this->get($this->url(['deduction' => '扣款']))->assertOk()->assertDontSee('GD-SMALL')->assertSee('GD-EQUAL-RATIO');
$this->get($this->url(['deduction' => '不扣款']))->assertOk()->assertSee('GD-SMALL')->assertDontSee('GD-EQUAL-RATIO');
$csv = $this->get(route('business.supplier-bookkeeping.export', [
'company_id' => $this->company->id, 'columns' => 'work_order_no,amount,original_amount,remark',
]))->assertOk()->streamedContent();
$this->assertStringContainsString('GD-SMALL,0.00,30.00,', $csv);
$this->assertStringContainsString('小于50且小于0.4,记账改0', $csv);
}
public function test_real_valuation_keeps_page_totals_deduction_filter_and_export_consistent(): void
{
$this->mockRepository([
$this->row(['work_order_no' => 'GD-CAPPED', 'amount' => '100.00', 'product_code' => 'SKU-A', 'tracking_no' => 'EXP-1']),
$this->row(['work_order_no' => 'GD-FREIGHT', 'bookkeeping_type' => '运费损耗', 'amount' => '20.00', 'tracking_no' => 'EXP-1']),
$this->row(['work_order_no' => 'GD-PENDING', 'amount' => '100.00', 'tracking_no' => 'MISSING']),
$this->row(['work_order_no' => 'GD-ROUND-TWO', 'amount' => '30.00', 'buyer_paid_amount' => '50.00', 'actual_paid_amount' => '100.00']),
], 'ready', true);
$repository = \Mockery::mock(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class);
$repository->shouldReceive('readyMonths')->andReturn(['2026-04', '2026-05', '2026-06', '2026-07', '2026-08', '2026-09', '2026-10']);
$repository->shouldReceive('shipmentsFor')->andReturn([[
'id' => 1, 'source_id' => 10, 'express_no' => 'EXP-1', 'trade_no' => '5127787476014026744',
'barcode' => 'SKU-A', 'qty' => '2', 'freight' => '8.50', 'ship_time' => '2026-08-01 00:00:00',
]]);
$repository->shouldReceive('costsFor')->andReturn(['SKU-A' => '20.00']);
$this->app->instance(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class, $repository);
$this->loginAs()->get($this->url())->assertOk()->assertSee('GD-CAPPED')->assertSee('GD-PENDING');
$data = app(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::class)->page();
$this->assertSame(5700, $data['bookkeepingSummary']['deduction_cents']);
$this->assertSame(2, $data['bookkeepingSummary']['deduction_count']);
$this->assertSame(1, $data['bookkeepingSummary']['review']);
$this->get($this->url(['deduction' => '扣款']))->assertOk()->assertSee('GD-CAPPED')->assertSee('GD-FREIGHT')
->assertDontSee('GD-PENDING')->assertDontSee('GD-ROUND-TWO');
$this->get($this->url(['deduction' => '待确认']))->assertOk()->assertSee('GD-PENDING')->assertDontSee('GD-CAPPED');
$csv = $this->get(route('business.supplier-bookkeeping.export', [
'company_id' => $this->company->id, 'deduction' => '扣款',
'columns' => 'work_order_no,amount,original_amount,matched_cost,matched_freight,matched_quantity',
]))->assertOk()->streamedContent();
$this->assertStringContainsString('GD-CAPPED,48.50,100.00,40.00,8.50,2', $csv);
$this->assertStringContainsString('GD-FREIGHT,8.50,20.00,,8.50,', $csv);
$this->assertStringNotContainsString('GD-PENDING', $csv);
$this->assertStringNotContainsString('GD-ROUND-TWO', $csv);
}
public function test_second_round_zeroes_by_order_paid_when_buyer_paid_ratio_does_not_hit(): void
{
$this->mockRepository([
$this->row(['work_order_no' => 'GD-ROUND-TWO', 'amount' => '30.00', 'buyer_paid_amount' => '50.00', 'actual_paid_amount' => '100.00']),
]);
$this->loginAs()->get($this->url())->assertOk()->assertSee('GD-ROUND-TWO')->assertSee('小于50且小于0.4,记账改0');
$data = app(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::class)->page();
$this->assertSame(0, $data['bookkeepingSummary']['deduction_cents']);
$this->assertSame(0, $data['bookkeepingSummary']['amount_cents']);
$this->get($this->url(['deduction' => '扣款']))->assertOk()->assertDontSee('GD-ROUND-TWO');
$csv = $this->get(route('business.supplier-bookkeeping.export', [
'company_id' => $this->company->id, 'deduction' => '不扣款',
'columns' => 'work_order_no,amount,original_amount,remark',
]))->assertOk()->streamedContent();
$this->assertStringContainsString('GD-ROUND-TWO,0.00,30.00,', $csv);
}
public function test_estimated_freight_matches_page_totals_filters_and_export_with_cost_basis(): void
{
$this->mockRepository([
$this->row(['work_order_no' => 'GD-ESTIMATE', 'bookkeeping_type' => '运费损耗', 'amount' => '100.00', 'product_code' => 'SKU-A', 'tracking_no' => 'EXP-ZERO']),
$this->row(['work_order_no' => 'GD-ACTUAL', 'bookkeeping_type' => '运费损耗', 'amount' => '100.00', 'product_code' => 'SKU-A', 'tracking_no' => 'EXP-ACTUAL']),
$this->row(['work_order_no' => 'GD-NO-COST', 'bookkeeping_type' => '运费损耗', 'amount' => '100.00', 'product_code' => 'SKU-MISSING', 'tracking_no' => 'EXP-MISSING']),
$this->row(['work_order_no' => 'GD-SKIP', 'bookkeeping_type' => '运费损耗', 'amount' => '100.00', 'supplier' => '尊享', 'product_code' => 'SKU-A', 'tracking_no' => 'EXP-ZERO']),
], 'ready', true);
$repository = \Mockery::mock(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class);
$repository->shouldReceive('readyMonths')->andReturn(['2026-04', '2026-05', '2026-06', '2026-07', '2026-08', '2026-09']);
$shipments = [];
foreach (['EXP-ZERO' => ['SKU-A', '0'], 'EXP-ACTUAL' => ['SKU-A', '8.50'], 'EXP-MISSING' => ['SKU-MISSING', null]] as $tracking => [$sku, $freight]) {
$shipments[] = ['id' => $tracking, 'source_id' => $tracking, 'express_no' => $tracking, 'trade_no' => '5127787476014026744',
'barcode' => $sku, 'qty' => '2', 'freight' => $freight, 'ship_time' => '2026-08-01 00:00:00'];
}
$repository->shouldReceive('shipmentsFor')->andReturn($shipments);
$repository->shouldReceive('costsFor')->andReturn(['SKU-A' => '20.00']);
$this->app->instance(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class, $repository);
$this->loginAs()->get($this->url())->assertOk()->assertSee('成本合计×0.2替代运费')->assertSee('核算运费')->assertSee('包裹运费为空或0');
$data = app(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::class)->page();
$this->assertSame(1650, $data['bookkeepingSummary']['deduction_cents']);
$this->assertSame(2, $data['bookkeepingSummary']['deduction_count']);
$this->assertSame(1, $data['bookkeepingSummary']['review']);
$this->get($this->url(['deduction' => '扣款']))->assertOk()->assertSee('GD-ESTIMATE')->assertSee('GD-ACTUAL')->assertDontSee('GD-NO-COST')->assertDontSee('GD-SKIP');
$csv = $this->get(route('business.supplier-bookkeeping.export', [
'company_id' => $this->company->id, 'deduction' => '扣款',
'columns' => 'work_order_no,amount,original_amount,matched_cost,matched_freight,matched_quantity,remark',
]))->assertOk()->streamedContent();
$this->assertStringContainsString('GD-ESTIMATE,8.00,100.00,40.00,8.00,2,', $csv);
$this->assertStringContainsString('成本合计×0.2替代运费', $csv);
$this->assertStringNotContainsString('GD-NO-COST', $csv);
$this->assertStringNotContainsString('GD-SKIP', $csv);
}
public function test_missing_supplier_is_resolved_before_rules_costing_filters_and_export(): void
{
$this->mockRepository([
$this->row(['work_order_no' => 'GD-RESOLVED', 'supplier' => '', 'product_code' => '运营组合套装TEST', 'tracking_no' => 'EXP-1', 'amount' => '100.00']),
$this->row(['work_order_no' => 'GD-SKIP', 'supplier' => '', 'product_code' => 'ZH-SKU-C-1-SKU-D-1', 'amount' => '100.00']),
$this->row(['work_order_no' => 'GD-MIXED', 'supplier' => '', 'product_code' => 'ZH-SKU-A-1-SKU-C-1', 'amount' => '100.00']),
$this->row(['work_order_no' => 'GD-MISSING', 'supplier' => '', 'product_code' => 'ZH-SKU-A-1-NOTFOUND-1', 'amount' => '100.00']),
$this->row(['work_order_no' => 'GD-SMALL', 'supplier' => '', 'product_code' => 'SKU-A', 'amount' => '30.00', 'actual_paid_amount' => '100.00']),
], 'ready', true);
$catalog = \Mockery::mock(\Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository::class);
$catalog->shouldReceive('normalProductsBySkuCodes')->andReturnUsing(static function ($codes) {
$products = [];
foreach (['SKU-A' => '新厂', 'SKU-B' => '新厂', 'SKU-C' => '尊享', 'SKU-D' => '尊享'] as $code => $supplier) {
if (in_array($code, $codes, true)) $products[] = (object) ['autoid' => 1, 'sku_id' => $code, 'supplier_name' => $supplier];
}
return $products;
});
$this->app->instance(\Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository::class, $catalog);
$shipping = \Mockery::mock(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class);
$shipments = [];
foreach (['SKU-A', 'SKU-B'] as $sku) $shipments[] = ['id' => $sku, 'source_id' => 'parcel', 'express_no' => 'EXP-1', 'trade_no' => '5127787476014026744',
'tracking_numbers' => ['EXP-1'], 'order_numbers' => ['5127787476014026744'], 'barcode' => $sku, 'qty' => '1', 'freight' => '8.50', 'ship_time' => '2026-08-01 00:00:00'];
$shipping->shouldReceive('shipmentsFor')->andReturn($shipments);
$shipping->shouldReceive('readyMonths')->andReturn(['2026-04', '2026-05', '2026-06', '2026-07', '2026-08', '2026-09']);
$shipping->shouldReceive('costsFor')->with(['SKU-A', 'SKU-B'])->andReturn(['SKU-A' => '20.00', 'SKU-B' => '30.00']);
$this->app->instance(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class, $shipping);
$this->loginAs()->get($this->url())->assertOk()->assertSee('实际商品编码')->assertSee('供应商匹配说明')->assertSee('SKU-B→新厂');
$data = app(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::class)->page();
$this->assertSame(5850, $data['bookkeepingSummary']['deduction_cents']);
$this->assertSame(1, $data['bookkeepingSummary']['deduction_count']);
$this->assertSame(2, $data['bookkeepingSummary']['review']);
$this->get($this->url(['product_code' => 'SKU-B', 'supplier' => '新厂', 'deduction' => '扣款']))->assertOk()->assertSee('GD-RESOLVED')->assertDontSee('GD-MIXED');
$csv = $this->get(route('business.supplier-bookkeeping.export', ['company_id' => $this->company->id, 'deduction' => '扣款',
'columns' => 'work_order_no,amount,resolved_product_code,supplier,supplier_match_basis']))->assertOk()->streamedContent();
$this->assertStringContainsString('GD-RESOLVED,58.50,', $csv);
$this->assertStringContainsString('SKU-B→新厂', $csv);
$this->assertStringNotContainsString('GD-MIXED', $csv);
}
}