33b3ac06f2
One-time cleanup of orphan SMS supplier_projects rows created by the now-removed
buildUniqueKey divergence (B3 used sender alone; B2 sender+keyword).
Logic per orphan (sms unique_key without '+', owning project has sms_keyword):
- no sibling at sender+keyword for same tenant → UPDATE row's unique_key
- has sibling → dispatch DeleteSupplierProjectJob (cleans up at portal +
cascades pivot deletion + local row removal)
Discovers orphans via pivot project_supplier_links join (primary path post-Plan-1
pivot rollout). --dry-run flag previews without mutation.
Usage on prod after Stage 4 deploy:
ssh ubuntu@liderra.ru 'cd /var/www/liderra/app && sudo -u www-data php artisan supplier:rekey-orphans --dry-run'
# review output
ssh ubuntu@liderra.ru 'cd /var/www/liderra/app && sudo -u www-data php artisan supplier:rekey-orphans'
3 Pest tests: no-sibling UPDATE path, sibling DELETE-dispatch path, dry-run no-op.
Stage 4 §4.4.1 migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
129 lines
4.7 KiB
PHP
129 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Jobs\Supplier\DeleteSupplierProjectJob;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* One-time migration: clean up orphan supplier_projects rows created by the
|
|
* now-removed buildUniqueKey($p, $platform) divergence for SMS+keyword projects.
|
|
*
|
|
* Before R-17 unification (Stage 4 §4.4.1) SMS+keyword projects had two diverging
|
|
* supplier_projects keys per group:
|
|
* B2: unique_key = sender+keyword
|
|
* B3: unique_key = sender (without keyword) — ORPHAN after unification
|
|
*
|
|
* This command finds orphan B3 rows (sms, no '+' in unique_key, owning project has
|
|
* sms_keyword) and either UPDATEs them to sender+keyword (no sibling) or marks them
|
|
* for deletion via DeleteSupplierProjectJob (sibling at sender+keyword already exists).
|
|
*
|
|
* Usage:
|
|
* php artisan supplier:rekey-orphans --dry-run # preview
|
|
* php artisan supplier:rekey-orphans # apply
|
|
*
|
|
* Spec §4.4.1.
|
|
*/
|
|
final class SupplierRekeyOrphansCommand extends Command
|
|
{
|
|
protected $signature = 'supplier:rekey-orphans {--dry-run : Preview without modifying anything}';
|
|
|
|
protected $description = 'One-time R-17 cleanup of orphan SMS supplier_projects keyed under sender alone';
|
|
|
|
public function handle(): int
|
|
{
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
|
|
// Find candidate orphans: sms supplier_projects whose unique_key has no '+'
|
|
// and whose tenant has an SMS project with sms_keyword set matching this sender.
|
|
$orphans = DB::connection('pgsql_supplier')
|
|
->table('supplier_projects as sp')
|
|
->join('project_supplier_links as psl', 'psl.supplier_project_id', '=', 'sp.id')
|
|
->join('projects as p', 'p.id', '=', 'psl.project_id')
|
|
->where('sp.signal_type', 'sms')
|
|
->where('sp.unique_key', 'NOT LIKE', '%+%')
|
|
->whereNotNull('p.sms_keyword')
|
|
->where('p.sms_keyword', '!=', '')
|
|
->select([
|
|
'sp.id as sp_id',
|
|
'sp.unique_key as sender',
|
|
'sp.platform',
|
|
'p.tenant_id',
|
|
'p.sms_keyword as keyword',
|
|
])
|
|
->get();
|
|
|
|
if ($orphans->isEmpty()) {
|
|
$this->info('No orphan SMS supplier_projects found. Nothing to migrate.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->info(sprintf('Found %d orphan SMS supplier_projects row(s).', $orphans->count()));
|
|
|
|
$updated = 0;
|
|
$dispatched = 0;
|
|
$toDelete = [];
|
|
|
|
foreach ($orphans as $o) {
|
|
$sender = (string) $o->sender;
|
|
$keyword = (string) $o->keyword;
|
|
$newKey = $sender.'+'.$keyword;
|
|
|
|
// Sibling check: another supplier_project for same tenant/keyword combo already
|
|
// exists at the unified key? Look across pivot to the same tenant scope.
|
|
$siblingExists = DB::connection('pgsql_supplier')
|
|
->table('supplier_projects as sp2')
|
|
->join('project_supplier_links as psl2', 'psl2.supplier_project_id', '=', 'sp2.id')
|
|
->join('projects as p2', 'p2.id', '=', 'psl2.project_id')
|
|
->where('sp2.signal_type', 'sms')
|
|
->where('sp2.unique_key', $newKey)
|
|
->where('p2.tenant_id', $o->tenant_id)
|
|
->where('sp2.id', '!=', $o->sp_id)
|
|
->exists();
|
|
|
|
if ($siblingExists) {
|
|
$toDelete[] = (int) $o->sp_id;
|
|
$this->line(sprintf(
|
|
' orphan #%d (%s sender=%s) → DELETE (sibling at %s exists for tenant %d)',
|
|
$o->sp_id, $o->platform, $sender, $newKey, $o->tenant_id
|
|
));
|
|
|
|
continue;
|
|
}
|
|
|
|
$this->line(sprintf(
|
|
' orphan #%d (%s sender=%s) → UPDATE unique_key=%s',
|
|
$o->sp_id, $o->platform, $sender, $newKey
|
|
));
|
|
|
|
if (! $dryRun) {
|
|
DB::connection('pgsql_supplier')
|
|
->table('supplier_projects')
|
|
->where('id', $o->sp_id)
|
|
->update(['unique_key' => $newKey, 'updated_at' => now()]);
|
|
$updated++;
|
|
}
|
|
}
|
|
|
|
if (! $dryRun && $toDelete !== []) {
|
|
DeleteSupplierProjectJob::dispatch($toDelete);
|
|
$dispatched = count($toDelete);
|
|
}
|
|
|
|
if ($dryRun) {
|
|
$this->warn('--dry-run: no changes made.');
|
|
} else {
|
|
$this->info(sprintf(
|
|
'Migration complete: %d row(s) updated, %d row(s) queued for deletion.',
|
|
$updated, $dispatched
|
|
));
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|