'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); } public function test_bundle_and_replacement_freight_agree_on_page_totals_filters_and_export(): void { $this->mockRepository([ $this->row(['work_order_no' => 'GD-BUNDLE', 'bookkeeping_type' => '记账全款', 'amount' => '100.00', 'product_code' => "SKU-A\nSKU-B", 'tracking_no' => 'EXP-BUNDLE']), $this->row(['work_order_no' => 'GD-COMPENSATION', 'amount' => '100.00', 'product_code' => 'SKU-A', 'tracking_no' => 'EXP-SINGLE']), $this->row(['work_order_no' => 'GD-NO-SHIPMENT', 'amount' => '100.00', 'product_code' => "SKU-A\nSKU-B", 'tracking_no' => 'EXP-MISSING']), ], 'ready', true); $shipping = \Mockery::mock(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class); $shipping->shouldReceive('readyMonths')->andReturn(['2026-04', '2026-05', '2026-06', '2026-07', '2026-08', '2026-09']); $shipping->shouldReceive('shipmentsFor')->andReturn([ ['id' => 1, 'source_id' => 1, 'express_no' => 'EXP-BUNDLE', 'trade_no' => '5127787476014026744', 'barcode' => 'SKU-A,SKU-B', 'qty' => '9', 'freight' => '0', 'ship_time' => '2026-08-01 00:00:00'], ['id' => 2, 'source_id' => 2, 'express_no' => 'EXP-SINGLE', 'trade_no' => '5127787476014026744', 'barcode' => 'SKU-A', 'qty' => '2', 'freight' => null, 'ship_time' => '2026-08-01 00:00:00'], ]); $shipping->shouldReceive('costsFor')->andReturn(['SKU-A' => '20.00', 'SKU-B' => '5.00']); $this->app->instance(\Modules\EcommerceFinance\Repositories\SupplierBookkeepingValuationRepository::class, $shipping); $this->loginAs()->get($this->url())->assertOk() ->assertSee('组合装按各条码1件计') ->assertSee('原单运费缺失或为0,按商品成本×0.2替代运费'); $data = app(\Modules\EcommerceFinance\View\SupplierBookkeepingPageData::class)->page(); $this->assertSame(7800, $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-BUNDLE')->assertSee('GD-COMPENSATION')->assertDontSee('GD-NO-SHIPMENT'); $this->get($this->url(['deduction' => '待确认']))->assertOk()->assertSee('GD-NO-SHIPMENT')->assertDontSee('GD-BUNDLE'); $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,deduction_basis', ]))->assertOk()->streamedContent(); $this->assertStringContainsString('GD-BUNDLE,30.00,100.00,25.00,5.00,2,', $csv); $this->assertStringContainsString('GD-COMPENSATION,48.00,100.00,40.00,8.00,2,', $csv); $this->assertStringContainsString('组合装按各条码1件计', $csv); $this->assertStringContainsString('原单运费缺失或为0,按商品成本×0.2替代运费', $csv); $this->assertStringNotContainsString('GD-NO-SHIPMENT', $csv); } }