numerify('#######'); $keyword = 'KW'.Str::random(5); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->asSmsSignal([$sender], $keyword)->create([ 'is_active' => true, 'daily_limit_target' => 5, ]); // Pre-existing orphan: B3 supplier_project keyed under sender alone (legacy buildUniqueKey). $orphanId88001 = DB::connection('pgsql_supplier')->table('supplier_projects')->insertGetId([ 'platform' => 'B3', 'signal_type' => 'sms', 'unique_key' => $sender, // orphan key (no '+keyword') 'subject_code' => null, 'supplier_external_id' => '88001', 'current_limit' => 5, 'current_workdays' => json_encode([1, 2, 3, 4, 5]), 'current_regions' => null, 'sync_status' => 'ok', 'last_synced_at' => now(), ]); DB::connection('pgsql_supplier')->table('project_supplier_links')->insert([ 'project_id' => $project->id, 'supplier_project_id' => $orphanId88001, 'platform' => 'B3', ]); $exitCode = $this->artisan('supplier:rekey-orphans')->run(); expect($exitCode)->toBe(0); // Orphan now has unified key. $sp = SupplierProject::on('pgsql_supplier')->where('supplier_external_id', '88001')->first(); expect($sp)->not->toBeNull(); expect($sp->unique_key)->toBe($sender.'+'.$keyword); }); it('R-17 migrate: orphan SMS row WITH sibling at sender+keyword → dispatch DeleteSupplierProjectJob for orphan', function (): void { Queue::fake(); $sender = '7923'.fake()->numerify('#######'); $keyword = 'KW'.Str::random(5); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->asSmsSignal([$sender], $keyword)->create([ 'is_active' => true, 'daily_limit_target' => 5, ]); // Sibling B2 row at unified key. $siblingId = DB::connection('pgsql_supplier')->table('supplier_projects')->insertGetId([ 'platform' => 'B2', 'signal_type' => 'sms', 'unique_key' => $sender.'+'.$keyword, 'subject_code' => null, 'supplier_external_id' => '88002', 'current_limit' => 5, 'current_workdays' => json_encode([1, 2, 3, 4, 5]), 'current_regions' => null, 'sync_status' => 'ok', 'last_synced_at' => now(), ]); DB::connection('pgsql_supplier')->table('project_supplier_links')->insert([ 'project_id' => $project->id, 'supplier_project_id' => $siblingId, 'platform' => 'B2', ]); // Orphan B3 row under sender alone. $orphanId = DB::connection('pgsql_supplier')->table('supplier_projects')->insertGetId([ 'platform' => 'B3', 'signal_type' => 'sms', 'unique_key' => $sender, // orphan 'subject_code' => null, 'supplier_external_id' => '88003', 'current_limit' => 5, 'current_workdays' => json_encode([1, 2, 3, 4, 5]), 'current_regions' => null, 'sync_status' => 'ok', 'last_synced_at' => now(), ]); DB::connection('pgsql_supplier')->table('project_supplier_links')->insert([ 'project_id' => $project->id, 'supplier_project_id' => $orphanId, 'platform' => 'B3', ]); $exitCode = $this->artisan('supplier:rekey-orphans')->run(); expect($exitCode)->toBe(0); Queue::assertPushed(DeleteSupplierProjectJob::class, function ($job) use ($orphanId) { return in_array($orphanId, $job->supplierProjectIds, true); }); }); it('R-17 migrate: --dry-run reports orphans without modifying anything', function (): void { Queue::fake(); $sender = '7933'.fake()->numerify('#######'); $keyword = 'KW'.Str::random(5); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->asSmsSignal([$sender], $keyword)->create([ 'is_active' => true, 'daily_limit_target' => 5, ]); $dryOrphanId = DB::connection('pgsql_supplier')->table('supplier_projects')->insertGetId([ 'platform' => 'B3', 'signal_type' => 'sms', 'unique_key' => $sender, // orphan 'subject_code' => null, 'supplier_external_id' => '88004', 'current_limit' => 5, 'current_workdays' => json_encode([1, 2, 3, 4, 5]), 'current_regions' => null, 'sync_status' => 'ok', 'last_synced_at' => now(), ]); DB::connection('pgsql_supplier')->table('project_supplier_links')->insert([ 'project_id' => $project->id, 'supplier_project_id' => $dryOrphanId, 'platform' => 'B3', ]); $exitCode = $this->artisan('supplier:rekey-orphans', ['--dry-run' => true])->run(); expect($exitCode)->toBe(0); // Unchanged. $sp = SupplierProject::on('pgsql_supplier')->where('supplier_external_id', '88004')->first(); expect($sp->unique_key)->toBe($sender); Queue::assertNothingPushed(); });