[ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ], 'upstream_mirrors.connection' => 'business_mirror', ]); DB::purge('business_mirror'); $schema = Schema::connection('business_mirror'); $schema->create('kuka_posting_base_rows', function (Blueprint $table): void { $table->bigIncrements('id'); $table->unsignedBigInteger('company_id'); $table->string('period', 7); $table->longText('source_values'); }); $schema->create('kuka_posting_quotation_rows', function (Blueprint $table): void { $table->bigIncrements('id'); $table->unsignedBigInteger('company_id'); $table->string('material_number', 191); $table->string('record_status', 64)->nullable(); }); $schema->create('kuka_posting_material_map_rows', function (Blueprint $table): void { $table->bigIncrements('id'); $table->unsignedBigInteger('company_id'); $table->string('item_number', 191); $table->string('material_number', 191)->nullable(); $table->string('match_status', 24); $table->string('match_source', 24)->nullable(); $table->unsignedInteger('candidate_count')->default(0); $table->longText('candidate_materials')->nullable(); $table->timestamp('synced_at'); }); } public function test_only_a_unique_active_quotation_match_is_selected(): void { $connection = DB::connection('business_mirror'); foreach (['A', 'B', 'C', 'D', 'E'] as $itemNumber) { $connection->table('kuka_posting_base_rows')->insert([ 'company_id' => 1, 'period' => '2026-10', 'source_values' => json_encode(['货号' => 'ITEM-'.$itemNumber], JSON_THROW_ON_ERROR), ]); } foreach (['Q-A', 'Q-B', 'Q-C1', 'Q-C2', 'Q-E'] as $materialNumber) { $connection->table('kuka_posting_quotation_rows')->insert([ 'company_id' => 1, 'material_number' => $materialNumber, 'record_status' => '生效', ]); } $query = Mockery::mock(UpstreamReadOnlyQuery::class); $query->shouldReceive('select') ->twice() ->andReturnUsing(static function (string $sql): array { if (str_contains($sql, 'BarcodePrintImportRecord')) { return [ ['item_number' => 'ITEM-A', 'material_number' => 'Q-A'], ['item_number' => 'ITEM-B', 'material_number' => 'Q-B'], ['item_number' => 'ITEM-C', 'material_number' => 'Q-C-OLD'], ['item_number' => 'ITEM-E', 'material_number' => 'Q-E-OLD'], ]; } return [ ['item_number' => 'ITEM-A', 'material_number' => 'Q-A'], ['item_number' => 'ITEM-B', 'material_number' => 'Q-B-ARCHIVE'], ['item_number' => 'ITEM-C', 'material_number' => 'Q-C1'], ['item_number' => 'ITEM-C', 'material_number' => 'Q-C2'], ['item_number' => 'ITEM-E', 'material_number' => 'Q-E'], ]; }); $factory = Mockery::mock(UpstreamReadOnlyQueryFactory::class); $factory->shouldReceive('forSourceWithTimeout') ->once() ->with(KukaPostingMaterialMapSynchronizer::SOURCE_KEY, 15) ->andReturn($query); $result = (new KukaPostingMaterialMapSynchronizer(app('db'), $factory)) ->sync(1, '2026-10'); $this->assertSame([ 'dry_run' => false, 'items' => 5, 'matched' => 3, 'unmatched' => 1, 'ambiguous' => 1, 'direct' => 2, 'archive' => 1, ], $result); $rows = $connection->table('kuka_posting_material_map_rows') ->orderBy('item_number') ->get() ->keyBy('item_number'); $this->assertSame('Q-A', $rows['ITEM-A']->material_number); $this->assertSame('barcode_material_no', $rows['ITEM-A']->match_source); $this->assertSame('Q-B', $rows['ITEM-B']->material_number); $this->assertSame('Q-E', $rows['ITEM-E']->material_number); $this->assertSame('archive_barcode', $rows['ITEM-E']->match_source); $this->assertSame('ambiguous', $rows['ITEM-C']->match_status); $this->assertNull($rows['ITEM-C']->material_number); $this->assertSame('unmatched', $rows['ITEM-D']->match_status); } }