150 lines
5.0 KiB
PHP
150 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Bank;
|
|
use App\Models\Locations\Location;
|
|
use App\Models\Organization;
|
|
use App\Models\User;
|
|
use Illuminate\Console\Command;
|
|
|
|
class BackfillBankClientsByLocation extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'bank-clients:backfill-by-location {--dry-run : Preview the assignments without writing to the database}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Link Users/Organizations to a local (level 2) Bank via bank_clients, matched by shared city_id on their Location record';
|
|
|
|
/**
|
|
* The console command help text, shown by `php artisan help bank-clients:backfill-by-location`.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $help = <<<'HELP'
|
|
Populates the bank_clients pivot table, which bank-manager features (the
|
|
scoped "Manage Profiles" view, and the transaction reversal feature) use to
|
|
determine which Users/Organizations belong to their bank.
|
|
|
|
bank_clients is never populated by normal registration, so on any
|
|
environment where these features are newly enabled, most or all profiles
|
|
will have no bank client link yet, and a bank-manager's scoped views will
|
|
appear empty even for profiles that clearly belong to that bank's city.
|
|
|
|
WHAT IT DOES
|
|
For every level-2 Bank that has a Location with a city_id set, this finds
|
|
every User/Organization whose own Location has a matching city_id, and
|
|
links them via a "local" bank_clients row -- unless that profile already
|
|
has one, in which case it is skipped.
|
|
|
|
Only Users and Organizations are ever linked as clients. Banks, Admins,
|
|
and banks at level 0 (central) or level 1 (system/bridge) are never
|
|
targets or clients.
|
|
|
|
SAFETY
|
|
Idempotent -- safe to run repeatedly. Already-linked profiles are always
|
|
skipped, never duplicated or reassigned. Never deletes or modifies an
|
|
existing bank_clients row. Profiles whose city does not match any bank
|
|
are simply left unlinked.
|
|
|
|
EXAMPLES
|
|
Preview only, no database writes:
|
|
php artisan bank-clients:backfill-by-location --dry-run
|
|
|
|
Apply for real:
|
|
php artisan bank-clients:backfill-by-location
|
|
|
|
PRODUCTION USAGE
|
|
Always run with --dry-run first and check that every bank you expect to
|
|
receive clients appears in the printed "Bank #... -> city_id ..." list.
|
|
A missing bank usually means its Location has no city_id set -- fix that
|
|
on the bank's profile first, then re-run the dry-run before applying.
|
|
|
|
See references/BANK_CLIENTS_BACKFILL_COMMAND.md for full documentation.
|
|
HELP;
|
|
|
|
public function handle(): int
|
|
{
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
|
|
$banks = Bank::where('level', 2)->get()->filter(function (Bank $bank) {
|
|
return (bool) $bank->locations()->whereNotNull('city_id')->exists();
|
|
});
|
|
|
|
if ($banks->isEmpty()) {
|
|
$this->error('No level-2 banks with a city_id location found. Nothing to do.');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$bankCityMap = [];
|
|
foreach ($banks as $bank) {
|
|
$cityId = $bank->locations()->whereNotNull('city_id')->value('city_id');
|
|
$bankCityMap[$cityId] = $bank;
|
|
$this->line("Bank #{$bank->id} \"{$bank->name}\" -> city_id {$cityId}");
|
|
}
|
|
|
|
$cityIds = array_keys($bankCityMap);
|
|
|
|
$this->newLine();
|
|
$this->info($dryRun ? 'DRY RUN — no changes will be written.' : 'Writing bank_clients rows.');
|
|
$this->newLine();
|
|
|
|
$totalLinked = 0;
|
|
$totalSkippedAlreadyLinked = 0;
|
|
|
|
foreach (['user' => User::class, 'organization' => Organization::class] as $label => $modelClass) {
|
|
$matches = Location::where('locatable_type', $modelClass)
|
|
->whereIn('city_id', $cityIds)
|
|
->get(['locatable_id', 'city_id']);
|
|
|
|
$this->info(ucfirst($label) . "s matched by city: {$matches->count()}");
|
|
|
|
$bar = $this->output->createProgressBar($matches->count());
|
|
$bar->start();
|
|
|
|
foreach ($matches as $location) {
|
|
$bank = $bankCityMap[$location->city_id];
|
|
$client = $modelClass::find($location->locatable_id);
|
|
|
|
if (!$client) {
|
|
$bar->advance();
|
|
continue;
|
|
}
|
|
|
|
$alreadyLinked = $client->banksClient()->exists();
|
|
if ($alreadyLinked) {
|
|
$totalSkippedAlreadyLinked++;
|
|
$bar->advance();
|
|
continue;
|
|
}
|
|
|
|
if (!$dryRun) {
|
|
$client->attachBankClient($bank, 'local');
|
|
}
|
|
$totalLinked++;
|
|
$bar->advance();
|
|
}
|
|
|
|
$bar->finish();
|
|
$this->newLine(2);
|
|
}
|
|
|
|
$this->info("Linked: {$totalLinked}");
|
|
$this->info("Skipped (already linked): {$totalSkippedAlreadyLinked}");
|
|
|
|
if ($dryRun) {
|
|
$this->comment('Run without --dry-run to apply these changes.');
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|