instance('config', new Repository(['app' => ['platform_timezone' => 'Asia/Shanghai']])); $db = new Manager($app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']); $app->instance('db', $db->getDatabaseManager()); Facade::clearResolvedInstances(); Facade::setFacadeApplication($app); $app->instance('request', Request::create('/?month=2026-09&state=all')); $app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Illuminate\Routing\ResponseFactory( Mockery::mock(\Illuminate\Contracts\View\Factory::class), Mockery::mock(\Illuminate\Routing\Redirector::class) )); } protected function tearDown(): void { Mockery::close(); parent::tearDown(); } private function row(array $patch = []): array { return array_replace(['source_id' => 1, 'trade_no' => '123456789012345678', 'express_no' => 'BF001', 'express_name1' => '补发快递', 'ship_time' => '2026-09-15', 'barcode' => 'GBGJ-SJ038024N1410V02-10,GBGJ-SJ038024N1410V02-2', 'freight' => '12.35'], $patch); } public function test_four_province_spellings_deduct_full_freight_once(): void { foreach (['山东', '山东省', '河北', '河北省'] as $province) { $result = (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name' => '成品', 'supplier_name' => '供应商甲'], ['name' => '成品', 'supplier_name' => '供应商甲']], [['linkerprovience' => $province]]); $this->assertSame('GBGJ-SJ038024N1410V02', $result['normalized_barcode']); $this->assertSame('ready', $result['state']); $this->assertSame(1235, $result['deduction_cents']); } } public function test_external_courier_always_excluded_and_keeps_supplier_details(): void { foreach (['express_name1', 'express_name2'] as $field) { foreach (['12.35', null, '0'] as $freight) { $row = (new SupplierAccessoryFreightMatcher)->match($this->row([$field => ' 外采快递 ', 'freight' => $freight]), [['name'=>'成品','supplier_name'=>'甲']], [['linkerprovience'=>'河北省']]); $this->assertSame('excluded', $row['state']); $this->assertSame(0, $row['deduction_cents']); $this->assertSame('甲', $row['supplier_name']); $this->assertSame('外采快递不扣配件运费', $row['reason']); } } $row = (new SupplierAccessoryFreightMatcher)->match($this->row(['express_name1'=>'外采快递']), [], []); $this->assertSame('excluded', $row['state']); $this->assertSame(0, $row['deduction_cents']); $normal = (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name'=>'成品','supplier_name'=>'甲']], [['linkerprovience'=>'河北省']]); $this->assertSame(1235, $normal['deduction_cents']); } public function test_missing_or_ambiguous_product_supplier_province_never_deducts(): void { $product = [['name' => '成品', 'supplier_name' => '甲']]; foreach ([ [[], []], [[['name' => '成品', 'supplier_name' => '']], []], [$product, []], [[...$product, ['name' => '成品', 'supplier_name' => '乙']], [['linkerprovience' => '山东']]], [$product, [['linkerprovience' => '']]], [$product, [['linkerprovience' => '山东'], ['linkerprovience' => '河北']]], [$product, [['linkerprovience' => '山东'], ['linkerprovience' => '']]] ] as [$products, $suppliers]) { $result = (new SupplierAccessoryFreightMatcher)->match($this->row(), $products, $suppliers); $this->assertSame('pending', $result['state']); $this->assertSame(0, $result['deduction_cents']); } } public function test_other_provinces_zero_invalid_and_equivalent_duplicate_provinces(): void { $matcher = new SupplierAccessoryFreightMatcher; $products = [['name' => '成品', 'supplier_name' => '甲']]; foreach (['江苏省', '山东济南', '河北省石家庄市'] as $province) { $result = $matcher->match($this->row(), $products, [['linkerprovience' => $province]]); $this->assertSame('excluded', $result['state']); $this->assertSame(0, $result['deduction_cents']); } foreach ([null, '', '-5', 'abc'] as $amount) { $result = $matcher->match($this->row(['freight' => $amount]), $products, [['linkerprovience' => '河北']]); $this->assertSame('pending', $result['state']); $this->assertSame(0, $result['deduction_cents']); } $result = $matcher->match($this->row(['freight' => '0']), $products, [['linkerprovience' => '河北']]); $this->assertSame('excluded', $result['state']); $result = $matcher->match($this->row(['freight' => '0.10']), $products, [['linkerprovience' => ' 山东 '], ['linkerprovience' => '山东省']]); $this->assertSame('ready', $result['state']); $this->assertSame(10, $result['deduction_cents']); } public function test_repository_uses_product_supplier_not_shipping_supplier_and_ignores_combinations(): void { $connection = app('db')->connection(); $connection->statement('CREATE TABLE products (sku_id TEXT, sku_type TEXT, name TEXT, supplier_name TEXT)'); $connection->statement('CREATE TABLE suppliers (scmname TEXT, linkerprovience TEXT)'); $connection->table('products')->insert([ ['sku_id' => 'GBGJ-SJ038024N1410V02', 'sku_type' => 'normal', 'name' => '成品', 'supplier_name' => '商品供应商'], ['sku_id' => 'GBGJ-SJ038024N1410V02', 'sku_type' => 'combine', 'name' => '组合', 'supplier_name' => '其他供应商'], ]); $connection->table('suppliers')->insert(['scmname' => '商品供应商', 'linkerprovience' => '河北省']); $registry = Mockery::mock(BusinessMirrorDatasetRegistry::class); foreach ([ProductCatalogMirrorRepository::DATASET_KEY => 'products', SupplierManagementMirrorRepository::DATASET => 'suppliers'] as $key => $table) { $dataset = Mockery::mock(BusinessMirrorDataset::class); $dataset->shouldReceive('targetTable')->andReturn($table); $registry->shouldReceive('get')->with($key)->andReturn($dataset); } $mirror = Mockery::mock(GenericBusinessMirrorRepository::class); $mirror->shouldReceive('ready')->andReturnTrue(); $mirror->shouldReceive('connection')->andReturn($connection); $shipping = Mockery::mock(ShippingOrderRepository::class); $shipping->shouldReceive('accessoryFreightBatches')->once()->with('2026-09-01', '2026-10-01')->andReturn((function () { yield [$this->row(['supplier_name' => '错误的历史供应商'])]; })()); $repo = new SupplierAccessoryFreightRepository($shipping, $registry, $mirror, new SupplierAccessoryFreightMatcher); $rows = iterator_to_array($repo->rows('2026-09')); $this->assertCount(1, $rows); $this->assertSame('商品供应商', $rows[0]['supplier_name']); $this->assertSame(1235, $rows[0]['deduction_cents']); } private function data(): SupplierAccessoryFreightPageData { $repository = Mockery::mock(SupplierAccessoryFreightRepository::class); $repository->shouldReceive('rows')->with('2026-09')->andReturnUsing(function () { $matcher = new SupplierAccessoryFreightMatcher; for ($i = 0; $i < 105; $i++) yield $matcher->match($this->row(['source_id' => $i, 'freight' => '0.10']), [['name' => '=危险商品', 'supplier_name' => '山东供应商']], [['linkerprovience' => '山东']]); yield $matcher->match($this->row(['source_id' => 200]), [], []); }); return new SupplierAccessoryFreightPageData($repository); } public function test_summary_pagination_and_filters_use_full_month_without_multiplying_amount(): void { app()->instance('request', Request::create('/?month=2026-09&state=all&page=2')); $page = $this->data()->page(); $this->assertNull($page['freightError']); $this->assertCount(50, $page['freightRows']); $this->assertSame(106, $page['freightRows']->total()); $this->assertSame(1050, $page['freightSummary']['amount']); app()->instance('request', Request::create('/?month=2026-09&state=ready&supplier_name=山东&page=3')); $page = $this->data()->page(); $this->assertCount(5, $page['freightRows']); $this->assertSame(105, $page['freightRows']->total()); $this->assertSame(0, $page['freightSummary']['pending']); $this->assertSame(1050, $page['freightSummary']['amount']); } public function test_csv_matches_filters_preserves_long_ids_and_escapes_formulas(): void { app()->instance('request', Request::create('/?month=2026-09&state=pending')); $controller = new \App\Http\Controllers\Business\SupplierAccessoryFreight; $response = $controller->export($this->data()); ob_start(); $response->sendContent(); $csv = ob_get_clean(); $this->assertStringStartsWith("\xEF\xBB\xBF", $csv); $this->assertSame(2, substr_count($csv, "\n")); $this->assertStringContainsString("'123456789012345678", $csv); $this->assertStringContainsString('待核实', $csv); app()->instance('request', Request::create('/?month=2026-09&state=ready')); $response = $controller->export($this->data()); ob_start(); $response->sendContent(); $csv = ob_get_clean(); $this->assertStringContainsString("'=危险商品", $csv); app()->instance('request', Request::create('/?month=2026-09&state=ready&columns=deduction,supplier_name')); $response = $controller->export($this->data()); ob_start(); $response->sendContent(); $csv = ob_get_clean(); $lines = explode("\n", trim(substr($csv, 3))); $this->assertSame(['业务月份', '发货记录ID', '仓库', '扣款金额(元)', '供应商'], str_getcsv($lines[0])); $this->assertCount(5, str_getcsv($lines[1])); } public function test_filters_accept_province_alias_and_reject_invalid_month_state_and_columns(): void { app()->instance('request', Request::create('/?month=2026-09&state=all&province=山东省')); $this->assertSame('山东', $this->data()->filters()['province']); foreach (['month=2026-13', 'month[]=2026-09', 'month=1999-01', 'month=2026-09&state=unknown', 'month=2026-09&supplier_name[]=x', 'month=2026-09&columns=', 'month=2026-09&columns=state,state', 'month=2026-09&columns=password'] as $query) { app()->instance('request', Request::create('/?' . $query)); try { $this->data()->filters(); $this->data()->exportColumns(); $this->fail('应拒绝无效条件'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) { $this->assertSame(422, $exception->getStatusCode()); } } } public function test_data_error_clears_partial_rows_and_totals(): void { $repository = Mockery::mock(SupplierAccessoryFreightRepository::class); $repository->shouldReceive('rows')->andReturnUsing(function () { yield (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name'=>'商品','supplier_name'=>'甲']], [['linkerprovience'=>'河北']]); throw new \RuntimeException('读取失败'); }); $handler = Mockery::mock(\Illuminate\Contracts\Debug\ExceptionHandler::class); $handler->shouldReceive('report')->once(); app()->instance(\Illuminate\Contracts\Debug\ExceptionHandler::class, $handler); $page = (new SupplierAccessoryFreightPageData($repository))->page(); $this->assertNotNull($page['freightError']); $this->assertSame(0, $page['freightRows']->total()); $this->assertSame(0, $page['freightSummary']['amount']); } }