Files
portal/app/tests/Feature/Supplier/SupplierRekeyOrphansCommandTest.php
T

167 lines
6.3 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use App\Jobs\Supplier\DeleteSupplierProjectJob;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
// ---------------------------------------------------------------------------
// Stage 4 / Task 4.2 — R-17 migration (spec §4.4.1): one-time artisan command
// to clean up orphan supplier_projects rows created by the now-removed
// buildUniqueKey divergence.
//
// Before R-17 fix: SMS projects with keyword produced two diverging unique_keys:
// B2 row: unique_key='sender+keyword'
// B3 row: unique_key='sender' (no keyword) — ORPHAN after unification
//
// After fix all platforms use unique_key='sender+keyword'. Existing orphans
// (B3 rows keyed under sender alone) need migration:
// - no sibling at 'sender+keyword' for same tenant → UPDATE row's unique_key
// - has sibling → mark for deletion (dispatch DeleteSupplierProjectJob, which
// also removes the donor from supplier portal + cascades pivot cleanup)
// ---------------------------------------------------------------------------
it('R-17 migrate: orphan SMS row with no sibling → UPDATE unique_key to sender+keyword', function (): void {
$sender = '7913'.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,
]);
// 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();
});