option('mailing-id'); $hours = $this->option('hours') ?: timebank_config('bulk_mail.abandon_after_hours', 72); $dryRun = $this->option('dry-run'); $force = $this->option('force'); $this->info("Looking for failed mailings" . ($mailingId ? " (ID: {$mailingId})" : " from the last {$hours} hours") . "..."); // Build query for mailings with failures $query = Mailing::where('status', 'sent') ->where('failed_count', '>', 0); if ($mailingId) { $query->where('id', $mailingId); } else { $query->where('sent_at', '>=', now()->subHours($hours)); } $failedMailings = $query->get(); if ($failedMailings->isEmpty()) { $this->info('No failed mailings found to retry.'); return 0; } $this->info("Found {$failedMailings->count()} mailings with failures:"); foreach ($failedMailings as $mailing) { $this->line("- Mailing #{$mailing->id}: {$mailing->title}"); $this->line(" Failed: {$mailing->failed_count}, Sent: {$mailing->sent_count}, Total: {$mailing->recipients_count}"); $this->line(" Sent at: {$mailing->sent_at}"); } if ($dryRun) { $this->info("\n[DRY RUN] Would retry the above mailings. Use --force to actually retry."); return 0; } if (!$force && !$this->confirm('Do you want to retry these failed mailings?')) { $this->info('Operation cancelled.'); return 0; } $totalRetried = 0; foreach ($failedMailings as $mailing) { $this->info("\nRetrying mailing #{$mailing->id}: {$mailing->title}"); $retriedCount = $this->retryFailedMailing($mailing, $force); $totalRetried += $retriedCount; if ($retriedCount > 0) { $this->info("Queued {$retriedCount} retry jobs for mailing #{$mailing->id}"); } else { $this->warn("No recipients to retry for mailing #{$mailing->id}"); } } $this->info("\nCompleted! Total retry jobs queued: {$totalRetried}"); return 0; } /** * Retry a specific failed mailing */ protected function retryFailedMailing(Mailing $mailing, bool $force = false): int { // Check if mailing is still within automatic retry window $abandonAfterHours = timebank_config('bulk_mail.abandon_after_hours', 72); $retryWindowExpired = $mailing->sent_at->addHours($abandonAfterHours)->isPast(); if (!$force && !$retryWindowExpired) { $this->warn("Mailing #{$mailing->id} is still within automatic retry window. Use --force to override."); return 0; } // Get all recipients and group by locale $recipientsByLocale = $this->getRecipientsGroupedByLocale($mailing); if (empty($recipientsByLocale)) { return 0; } $jobsQueued = 0; // Dispatch retry jobs for each locale foreach ($recipientsByLocale as $locale => $recipients) { if (!empty($recipients)) { $contentBlocks = $mailing->getContentBlocksForLocale($locale); SendBulkMailJob::dispatch($mailing, $locale, $contentBlocks, collect($recipients)) ->onQueue('emails'); $jobsQueued++; } } // Reset failure count to allow fresh tracking if ($jobsQueued > 0) { $mailing->update([ 'failed_count' => 0, 'status' => 'sending' // Reset to sending status ]); } return $jobsQueued; } /** * Get recipients grouped by locale for retry * Note: This is a simplified approach - in a production system you might want to track * individual recipient failures more precisely */ protected function getRecipientsGroupedByLocale(Mailing $mailing): array { // Get all potential recipients again $allRecipients = $mailing->getRecipientsQuery()->get(); if ($allRecipients->isEmpty()) { return []; } // Group by language preference $recipientsByLocale = []; foreach ($allRecipients as $recipient) { $locale = $recipient->lang_preference ?? timebank_config('base_language', 'en'); // Only include locales that have content blocks $availableLocales = $mailing->getAvailablePostLocales(); if (in_array($locale, $availableLocales)) { $recipientsByLocale[$locale][] = $recipient; } else { // Check if fallback is enabled if (timebank_config('bulk_mail.use_fallback_locale', true)) { $fallbackLocale = timebank_config('bulk_mail.fallback_locale', 'en'); if (in_array($fallbackLocale, $availableLocales)) { $recipientsByLocale[$fallbackLocale][] = $recipient; } } // If fallback is disabled or fallback locale not available, skip this recipient } } return $recipientsByLocale; } }