hasTable(Importer::MONTHS)) { (require base_path('database/migrations/2026_11_07_100000_create_supplier_bookkeeping_shipping_mirror.php'))->up(); } } protected function tearDown(): void { foreach ($this->files as $file) @unlink($file); parent::tearDown(); } private function input(int $count = 2, array $patch = []): array { $file = tempnam(sys_get_temp_dir(), 'sb-shipping-'); $manifest = $file . '.json'; $this->files = array_merge($this->files, [$file, $manifest]); $out = fopen('compress.zlib://' . $file, 'wb'); fputcsv($out, Importer::HEADERS); for ($i = 2; $i < $count+2; $i++) { fputcsv($out, [$i, 'EXP-'.$i, 'OLD-'.$i, 'ORDER-'.$i.',MERGED-'.$i, '', 'SKU-A', '2', $i === 2 ? '' : '8.50', '2026/10/1 1:02:03', '', '']); } fclose($out); $meta = array_replace(['schema_version' => 1, 'source_month' => '2026-10', 'source_filename' => 'source.xlsx', 'source_sha256' => str_repeat('a',64), 'data_sha256' => hash_file('sha256', $file), 'row_count' => $count, 'freight_column' => '运费'], $patch); file_put_contents($manifest, json_encode($meta, JSON_THROW_ON_ERROR)); return [$file, $manifest]; } public function test_streams_exact_references_preserves_missing_freight_and_is_idempotent(): void { [$file, $manifest] = $this->input(); $importer = app(Importer::class); $this->assertSame('validated', $importer->import($file,$manifest,true)['status']); $db = DB::connection('business_mirror'); $this->assertSame(0, $db->table(Importer::MONTHS)->count()); $this->assertSame('succeeded', $importer->import($file,$manifest)['status']); $this->assertSame(2, $db->table(Importer::ROWS)->count()); $this->assertSame(8, $db->table(Importer::REFS)->count()); $this->assertSame('', $db->table(Importer::ROWS)->where('source_row',2)->value('freight')); $this->assertSame('2026-10-01 01:02:03', $db->table(Importer::ROWS)->where('source_row',2)->value('ship_time')); $this->assertSame('unchanged', $importer->import($file,$manifest)['status']); $this->assertSame(2, $db->table(Importer::ROWS)->count()); } public function test_failed_replacement_retains_active_generation_and_resumes_last_committed_batch(): void { $importer = app(Importer::class); $importer->import(...$this->input()); $db = DB::connection('business_mirror'); $active = $db->table(Importer::MONTHS)->value('active_generation'); [$file,$manifest] = $this->input(501); $inserts = 0; $interrupted = false; $db->listen(function ($event) use (&$inserts, &$interrupted): void { if (str_starts_with($event->sql, 'insert into ') && str_contains($event->sql, Importer::ROWS) && ++$inserts === 2 && !$interrupted) { $interrupted = true; throw new \RuntimeException('simulated interruption'); } }); try { $importer->import($file,$manifest); $this->fail('Expected interruption'); } catch (\RuntimeException $e) { $this->assertSame('simulated interruption', $e->getMessage()); } $state = $db->table(Importer::MONTHS)->first(); $this->assertSame($active, $state->active_generation); $this->assertSame(500, (int) $state->checkpoint); $result = $importer->import($file,$manifest); $this->assertSame(500, $result['resumed_from']); $state = $db->table(Importer::MONTHS)->first(); $this->assertNotSame($active, $state->active_generation); $this->assertSame(501, $db->table(Importer::ROWS)->where('generation',$state->active_generation)->count()); } public function test_indexes_clean_references_while_retaining_original_source_values(): void { [$file, $manifest] = $this->input(1); $out = fopen('compress.zlib://' . $file, 'wb'); fputcsv($out, Importer::HEADERS); fputcsv($out, [2, " '@0001234567890123456789 ", '@OLD-1’', " '@ORDER-1,@ORDER-2’", '', 'SKU-A+B', '2', '8.50', '2026/10/1 1:02:03', '', '']); fclose($out); $meta = json_decode(file_get_contents($manifest), true, 512, JSON_THROW_ON_ERROR); $meta['data_sha256'] = hash_file('sha256', $file); file_put_contents($manifest, json_encode($meta, JSON_THROW_ON_ERROR)); $this->assertSame('succeeded', app(Importer::class)->import($file, $manifest)['status']); $db = DB::connection('business_mirror'); $this->assertSame(['0001234567890123456789', 'OLD-1'], $db->table(Importer::REFS)->where('kind', 'tracking')->orderBy('reference')->pluck('reference')->all()); $this->assertSame(['ORDER-1', 'ORDER-2'], $db->table(Importer::REFS)->where('kind', 'order')->orderBy('reference')->pluck('reference')->all()); $this->assertSame(" '@0001234567890123456789 ", $db->table(Importer::ROWS)->value('express_no')); $this->assertSame('SKU-A+B', $db->table(Importer::ROWS)->value('barcode')); } public function test_rejects_wrong_column_hash_month_and_incomplete_row_count(): void { foreach ([['freight_column' => '结算运费'], ['data_sha256' => str_repeat('b',64)], ['source_month' => '2026-09'], ['row_count' => 3]] as $patch) { try { app(Importer::class)->import(...$this->input(2,$patch)); $this->fail('Expected invalid source'); } catch (\RuntimeException $e) { $this->assertNotSame('', $e->getMessage()); } } $this->assertNull(DB::connection('business_mirror')->table(Importer::MONTHS)->value('active_generation')); } }