Files
fotospiel-app/app/Console/Commands/SendAbandonedCheckoutReminders.php
Codex Agent fa33e7cbcf Fix Event & EventType resource issues and apply formatting
- Fix EventType deletion error handling (constraint violations)
- Fix Event update error (package_id column missing)
- Fix Event Type dropdown options (JSON display issue)
- Fix EventPackagesRelationManager query error
- Add missing translations for deletion errors
- Apply Pint formatting
2026-01-21 10:34:06 +01:00

159 lines
5.3 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Mail\AbandonedCheckout;
use App\Models\AbandonedCheckout as AbandonedCheckoutModel;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Throwable;
class SendAbandonedCheckoutReminders extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'checkout:send-reminders {--dry-run : Show what would be sent without actually sending}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Send reminder emails for abandoned checkouts';
/**
* Execute the console command.
*/
public function handle()
{
$isDryRun = $this->option('dry-run');
if ($isDryRun) {
$this->info('🔍 DRY RUN MODE - No emails will be sent');
}
$this->info('🚀 Starting abandoned checkout reminder process...');
// Reminder-Stufen definieren: [Stufe, Stunden, Beschreibung]
$reminderStages = [
['1h', 1, '1 hour reminders'],
['24h', 24, '24 hour reminders'],
['1w', 168, '1 week reminders'], // 7 * 24 = 168 Stunden
];
$totalProcessed = 0;
$totalSent = 0;
foreach ($reminderStages as [$stage, $hours, $description]) {
$this->info("📧 Processing {$description}...");
$checkouts = AbandonedCheckoutModel::readyForReminder($stage, $hours)
->with(['user', 'package'])
->get();
$this->info(" Found {$checkouts->count()} checkouts ready for {$stage} reminder");
foreach ($checkouts as $checkout) {
try {
if ($this->shouldSendReminder($checkout, $stage)) {
$resumeUrl = $this->generateResumeUrl($checkout);
if (! $isDryRun) {
$mailLocale = $checkout->user->preferred_locale ?? config('app.locale');
Mail::to($checkout->user)
->locale($mailLocale)
->queue(
new AbandonedCheckout(
$checkout->user,
$checkout->package,
$stage,
$resumeUrl
)
);
$checkout->updateReminderStage($stage);
$totalSent++;
} else {
$packageName = $checkout->package->getNameForLocale($checkout->user->preferred_locale ?? config('app.locale'));
$this->line(" 📧 Would send {$stage} reminder to: {$checkout->email} for package: {$packageName}");
}
$totalProcessed++;
}
} catch (Throwable $e) {
Log::error("Failed to send {$stage} reminder for checkout {$checkout->id}: ".$e->getMessage());
$this->error(" ❌ Failed to process checkout {$checkout->id}: ".$e->getMessage());
}
}
}
// Cleanup: Alte Checkouts löschen (älter als 30 Tage)
$oldCheckouts = AbandonedCheckoutModel::where('abandoned_at', '<', now()->subDays(30))
->where('converted', false)
->count();
if ($oldCheckouts > 0) {
if (! $isDryRun) {
AbandonedCheckoutModel::where('abandoned_at', '<', now()->subDays(30))
->where('converted', false)
->delete();
$this->info("🧹 Cleaned up {$oldCheckouts} old abandoned checkouts");
} else {
$this->info("🧹 Would clean up {$oldCheckouts} old abandoned checkouts");
}
}
$this->info('✅ Reminder process completed!');
$this->info(" Processed: {$totalProcessed} checkouts");
if (! $isDryRun) {
$this->info(" Sent: {$totalSent} reminder emails");
} else {
$this->info(" Would send: {$totalSent} reminder emails");
}
return Command::SUCCESS;
}
/**
* Prüft ob ein Reminder versendet werden sollte
*/
private function shouldSendReminder(AbandonedCheckoutModel $checkout, string $stage): bool
{
// Verfällt der Checkout bald? Dann kein Reminder mehr
if ($checkout->isExpired()) {
return false;
}
// User existiert noch?
if (! $checkout->user) {
return false;
}
// Package existiert noch?
if (! $checkout->package) {
return false;
}
return true;
}
/**
* Generiert die URL zum Wiederaufnehmen des Checkouts
*/
private function generateResumeUrl(AbandonedCheckoutModel $checkout): string
{
// Für jetzt: Einfache Package-URL
// Später: Persönliche Resume-Token URLs
return route('buy.packages', [
'locale' => config('app.locale', 'de'),
'packageId' => $checkout->package_id,
]);
}
}