feat: Complete checkout overhaul with Stripe PaymentIntent integration and abandoned cart recovery
This commit is contained in:
150
app/Console/Commands/SendAbandonedCheckoutReminders.php
Normal file
150
app/Console/Commands/SendAbandonedCheckoutReminders.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?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) {
|
||||
Mail::to($checkout->user)->queue(
|
||||
new AbandonedCheckout(
|
||||
$checkout->user,
|
||||
$checkout->package,
|
||||
$stage,
|
||||
$resumeUrl
|
||||
)
|
||||
);
|
||||
|
||||
$checkout->updateReminderStage($stage);
|
||||
$totalSent++;
|
||||
} else {
|
||||
$this->line(" 📧 Would send {$stage} reminder to: {$checkout->email} for package: {$checkout->package->name}");
|
||||
}
|
||||
|
||||
$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', $checkout->package_id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user