Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

@@ -0,0 +1,124 @@
<?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()]);
}
}
}