Files
timebank-cc-public/app/Console/Commands/BackfillBankClientsByLocation.php
T

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;
}
}