sendAdvanceReminders($today); $urgentCount = $this->sendExpiryReminders($today); $this->info("Sent {$advanceCount} advance reminder(s) and {$urgentCount} expiry reminder(s)."); return self::SUCCESS; } /** * Part 1: for each user, notify about certificates expiring exactly on * their personal "months before" threshold. */ private function sendAdvanceReminders(Carbon $today): int { $sent = 0; User::query() ->whereHas('certificates') ->with('certificates') ->chunkById(100, function ($users) use ($today, &$sent) { foreach ($users as $user) { $targetDate = $today->copy()->addMonths($user->reminder_months_before); $certificates = $user->certificates() ->whereDate('expires_at', $targetDate) ->tap($this->notRemindedToday($today)) ->get(); foreach ($certificates as $certificate) { $user->notify(new CertificateExpiringNotification($certificate)); $certificate->forceFill(['last_reminded_at' => now()])->save(); $sent++; } } }); return $sent; } /** * Part 2: notify about certificates expiring today (across all users). */ private function sendExpiryReminders(Carbon $today): int { $sent = 0; Certificate::query() ->whereDate('expires_at', $today) ->tap($this->notRemindedToday($today)) ->with('user') ->chunkById(100, function ($certificates) use (&$sent) { foreach ($certificates as $certificate) { $certificate->user?->notify(new CertificateExpiredNotification($certificate)); $certificate->forceFill(['last_reminded_at' => now()])->save(); $sent++; } }); return $sent; } /** * Skip certificates already reminded today, so a re-run (or a missed day * followed by a catch-up run) never double-sends. */ private function notRemindedToday(Carbon $today): callable { return function (Builder $query) use ($today) { $query->where(function (Builder $query) use ($today) { $query->whereNull('last_reminded_at') ->orWhereDate('last_reminded_at', '<', $today); }); }; } }