option('dry-run'); if ($isDryRun) { $this->warn('DRY RUN — no changes will be made.'); $this->newLine(); } $failedJobs = DB::table('failed_jobs') ->where('queue', 'emails') ->get(); if ($failedJobs->isEmpty()) { $this->info('No failed email jobs found.'); return self::SUCCESS; } $retried = 0; $forgotten = 0; $skipped = 0; $progressBar = $this->output->createProgressBar($failedJobs->count()); $progressBar->start(); foreach ($failedJobs as $job) { $payload = json_decode($job->payload, true); $displayName = $payload['displayName'] ?? ''; $command = $payload['data']['command'] ?? ''; $leadId = $this->extractLeadId($command); if ($leadId === null) { $skipped++; $progressBar->advance(); continue; } $lead = UserLead::find($leadId); $action = $this->determineAction($lead, $displayName); if ($action === 'retry') { $retried++; if (! $isDryRun) { $this->callSilently('queue:retry', ['id' => [$job->uuid]]); } } else { $forgotten++; if (! $isDryRun) { $this->callSilently('queue:forget', ['id' => [$job->uuid]]); } } $progressBar->advance(); } $progressBar->finish(); $this->newLine(2); $this->table( ['Action', 'Count'], [ ['retried'.($isDryRun ? ' (dry run)' : ''), $retried], ['forgotten'.($isDryRun ? ' (dry run)' : ''), $forgotten], ['skipped (no lead ID found)', $skipped], ] ); return self::SUCCESS; } /** * Extract the UserLead UUID from a serialized job payload command string. * * Handles both queued mailables (id stored as a single string) and * queued notifications (id stored as a single-element array). */ private function extractLeadId(string $command): ?string { // Mail jobs store the lead as a single string ID if (preg_match('/App\\\\Models\\\\UserLead";s:2:"id";s:\d+:"([0-9a-f-]{36})"/', $command, $matches)) { return $matches[1]; } // Notification jobs store notifiables as an array of IDs if (preg_match('/App\\\\Models\\\\UserLead";s:2:"id";a:\d+:\{i:0;s:\d+:"([0-9a-f-]{36})"/', $command, $matches)) { return $matches[1]; } return null; } /** * Decide whether to retry or forget a failed job based on lead state. * * Rules: * - Lead deleted → forget (DDoS cleanup or similar) * - Lead verified → retry (Resend rate limit was the issue) * - Lead unverified + VerifyUserLeadEmailNotification → retry (still needs the verification email) * - Lead unverified + anything else → forget (waitlist emails to unverified leads are meaningless) */ private function determineAction(?UserLead $lead, string $displayName): string { if ($lead === null) { return 'forget'; } if ($lead->hasVerifiedEmail()) { return 'retry'; } if (str_contains($displayName, 'VerifyUserLeadEmailNotification')) { return 'retry'; } return 'forget'; } }