182 lines
6.1 KiB
PHP
182 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Jobs\SendBulkMailJob;
|
|
use App\Models\Mailing;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class RetryFailedMailings extends Command
|
|
{
|
|
/**
|
|
* The name and signature of the console command.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $signature = 'mailings:retry-failed
|
|
{--mailing-id= : Specific mailing ID to retry}
|
|
{--hours= : Retry mailings failed within this many hours (default from config)}
|
|
{--dry-run : Show what would be retried without actually retrying}
|
|
{--force : Force retry even if within normal retry window}';
|
|
|
|
/**
|
|
* The console command description.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $description = 'Retry failed email mailings that are outside their normal retry window';
|
|
|
|
/**
|
|
* Execute the console command.
|
|
*/
|
|
public function handle()
|
|
{
|
|
$mailingId = $this->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;
|
|
}
|
|
}
|