125 lines
4.2 KiB
PHP
125 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Bank;
|
|
use App\Models\Organization;
|
|
use App\Models\User;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Keeps the incomplete_at timestamp in step with the live completeness rule.
|
|
*
|
|
* ProfileTrait::hasIncompleteProfile() answers "is this profile incomplete
|
|
* now?" from its fields and relations. That is enough to label a profile page,
|
|
* but it leaves no history — the statistics page could only ever show a flat
|
|
* present-day count. This command records the state so it can be charted over
|
|
* time, mirroring profiles:mark-inactive.
|
|
*
|
|
* Both directions matter: a profile that gets filled in has its timestamp
|
|
* cleared, so the figure tracks the current population rather than a
|
|
* ratchet that only ever climbs.
|
|
*/
|
|
class MarkIncompleteProfiles extends Command
|
|
{
|
|
protected $signature = 'profiles:mark-incomplete
|
|
{--dry-run : Report what would change without writing}';
|
|
|
|
protected $description = 'Record which profiles are incomplete, so the state can be reported over time';
|
|
|
|
/** Banks carry the column for consistency but are not part of the metric. */
|
|
private const TYPES = [User::class, Organization::class];
|
|
|
|
protected string $logFile;
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
$this->logFile = storage_path('logs/mark-incomplete-profiles.log');
|
|
}
|
|
|
|
public function handle(): int
|
|
{
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
|
|
$config = timebank_config('profile_incomplete');
|
|
|
|
if (empty($config)) {
|
|
$this->warn('No profile_incomplete configuration; nothing to do.');
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$relations = array_values(array_filter(
|
|
$config['check_relations'] ?? [],
|
|
fn($relation) => is_string($relation) && $relation !== ''
|
|
));
|
|
|
|
$marked = 0;
|
|
$cleared = 0;
|
|
|
|
foreach (self::TYPES as $type) {
|
|
if (!method_exists($type, 'hasIncompleteProfile')) {
|
|
continue;
|
|
}
|
|
|
|
// Only relations the type actually defines — a white-label config
|
|
// may name one that does not apply to every profile type.
|
|
$usable = array_values(array_filter(
|
|
$relations,
|
|
fn($relation) => method_exists($type, $relation)
|
|
));
|
|
|
|
$type::query()
|
|
->notRemoved()
|
|
->with($usable)
|
|
->chunkById(500, function ($profiles) use (&$marked, &$cleared, $dryRun) {
|
|
foreach ($profiles as $profile) {
|
|
$isIncomplete = $profile->hasIncompleteProfile($profile);
|
|
$wasIncomplete = !empty($profile->incomplete_at);
|
|
|
|
if ($isIncomplete === $wasIncomplete) {
|
|
continue;
|
|
}
|
|
|
|
if ($isIncomplete) {
|
|
$marked++;
|
|
} else {
|
|
$cleared++;
|
|
}
|
|
|
|
if (!$dryRun) {
|
|
// Keep the original timestamp if it is already set:
|
|
// this records when the profile became incomplete,
|
|
// not when the command last looked at it.
|
|
$profile->incomplete_at = $isIncomplete ? now() : null;
|
|
$profile->saveQuietly();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
$summary = $dryRun
|
|
? "Dry run: {$marked} would be marked incomplete, {$cleared} would be cleared"
|
|
: "Marked {$marked} profiles incomplete, cleared {$cleared}";
|
|
|
|
$this->info($summary);
|
|
$this->logMessage($summary);
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function logMessage(string $message): void
|
|
{
|
|
try {
|
|
file_put_contents(
|
|
$this->logFile,
|
|
'[' . now()->toDateTimeString() . '] ' . $message . PHP_EOL,
|
|
FILE_APPEND
|
|
);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('MarkIncompleteProfiles: could not write log file', ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
}
|