- 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
159 lines
5.3 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|