> /dev/null 2>&1 * * Oder für stündliche Ausführung: * 0 * * * * cd /path/to/project && php artisan payments:reminders >> /dev/null 2>&1 * * Logs werden automatisch in storage/logs/laravel.log geschrieben */ class PaymentsReminders extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'payments:reminders'; protected $description = 'Run Payments Reminders'; private $timeStart; private $dev = false; private $paymentReminderService; private $stats = [ 'total_processed' => 0, 'reminders_sent' => 0, 'errors' => 0, 'skipped' => 0, ]; public function __construct(PaymentReminderService $paymentReminderService) { parent::__construct(); $this->paymentReminderService = $paymentReminderService; } /** * Execute the console command. * * @return int */ public function handle() { \Log::info('Starting PaymentsReminders Command', ['timestamp' => now()]); $this->info('RUN Command Payments Reminders: '.date('d.m.Y H:i')); $this->timeStart = microtime(true); try { $this->functionReminder(); $executionTime = round(microtime(true) - $this->timeStart, 2); $this->info("\n=== PAYMENT REMINDERS ABGESCHLOSSEN ==="); $this->info("Ausführungszeit: {$executionTime} Sekunden"); $this->info('Statistiken:'); $this->info(" - Gesamt verarbeitet: {$this->stats['total_processed']}"); $this->info(" - Erinnerungen gesendet: {$this->stats['reminders_sent']}"); $this->info(" - Fehler: {$this->stats['errors']}"); $this->info(" - Übersprungen: {$this->stats['skipped']}"); \Log::info('PaymentsReminders Command completed successfully', [ 'execution_time' => $executionTime, 'stats' => $this->stats, ]); return 0; } catch (\Exception $e) { \Log::error('PaymentsReminders Command failed', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); $this->error('Command failed: '.$e->getMessage()); return 1; } } /** * Hauptfunktion für die Verarbeitung der Zahlungserinnerungen */ private function functionReminder() { $this->info('=== STARTE PAYMENT REMINDERS ==='); // Hole alle aktiven PaymentReminder und gruppiere sie nach clearingtype $payment_reminders = PaymentReminder::where('active', true)->get(); $this->info('Gefundene aktive PaymentReminder: '.$payment_reminders->count()); if ($payment_reminders->isEmpty()) { $this->warn('Keine aktiven PaymentReminder gefunden!'); return; } // Finde für jeden clearingtype das kleinste Intervall (in Tagen) $intervals = $this->paymentReminderService->getActiveIntervals(); $this->info('Gefundene clearingtypes mit kleinsten Intervallen:'); foreach ($intervals as $clearingtype => $interval) { $this->line(" - {$clearingtype}: {$interval} Tage"); } // Verarbeite jeden clearingtype mit seinem kleinsten Intervall foreach ($intervals as $clearingtype => $interval) { $this->info("\n--- Verarbeite clearingtype: {$clearingtype} mit Intervall: {$interval} Tage ---"); $date = Carbon::now()->subDays($interval); $this->line('Suche Zahlungen vor: '.$date->format('d.m.Y H:i:s')); // Hole nur die neueste ShoppingPayment pro shopping_order_id $shopping_payments = $this->paymentReminderService->getOpenPaymentsForClearingType($clearingtype, $interval); $this->info("Gefundene offene Zahlungen für {$clearingtype}: ".$shopping_payments->count()); if ($shopping_payments->isEmpty()) { $this->line("Keine Zahlungen für {$clearingtype} gefunden."); continue; } // Verarbeite jede Zahlung $this->line('--- START processPayment VERARBEITUNG'); foreach ($shopping_payments as $shopping_payment) { $this->processPayment($shopping_payment, $clearingtype); } } } /** * Verarbeitet eine einzelne Zahlung und sendet ggf. eine Erinnerung */ private function processPayment($shopping_payment, $clearingtype) { $this->stats['total_processed']++; try { $this->line("Verarbeite Order ID: {$shopping_payment->shopping_order_id}, Created: {$shopping_payment->created_at->format('d.m.Y H:i:s')}, Amount: {$shopping_payment->amount}, Reminder: {$shopping_payment->reminder}"); // Prüfe ob eine Erinnerung gesendet werden soll if ($this->shouldSendReminder($shopping_payment, $clearingtype)) { $this->sendReminderForPayment($shopping_payment); } else { $this->line('Übersprungen - Keine Erinnerung fällig'); $this->stats['skipped']++; } } catch (\Exception $e) { $this->error("Fehler bei Order ID {$shopping_payment->shopping_order_id}: ".$e->getMessage()); $this->stats['errors']++; \Log::error('Error processing payment reminder', [ 'order_id' => $shopping_payment->shopping_order_id, 'payment_id' => $shopping_payment->id, 'error' => $e->getMessage(), ]); } } /** * Prüft ob eine Erinnerung für diese Zahlung gesendet werden soll * * Logik: * - Erste Erinnerung: Nach X Tagen ab Bestelldatum * - Weitere Erinnerungen: Nach Y Tagen ab letzter Erinnerung */ private function shouldSendReminder($shopping_payment, $clearingtype) { // Hole alle aktiven Erinnerungen für diesen Clearingtype $payment_reminders = PaymentReminder::where('active', true) ->where('clearingtype', $clearingtype) ->orderBy('interval', 'asc') // von kein nach gross ->get(); if ($payment_reminders->isEmpty()) { $this->line('shouldSendReminder - keine PaymentReminders'); return false; } // Wenn alle Erinnerungen bereits gesendet wurden if ($shopping_payment->reminder >= $payment_reminders->count()) { $this->line('shouldSendReminder - alle Erinnerungen wurden bereits gesendet'); return false; } $next_reminder = isset($payment_reminders[$shopping_payment->reminder]) ? $payment_reminders[$shopping_payment->reminder] : null; if ($next_reminder == null) { $next_reminder = isset($payment_reminders[0]) ? $payment_reminders[0] : null; if ($next_reminder == null) { $this->line('shouldSendReminder - keine Erinnerung gefunden'); return false; } } $this->line("shouldSendReminder - nächste Erinnerung: {$next_reminder->interval} Tage"); // Wenn noch keine Erinnerung gesendet wurde, prüfe das erste Intervall if ($shopping_payment->reminder == null || $shopping_payment->reminder == 0) { $daysSinceOrder = $shopping_payment->created_at->diffInDays(now()); $this->line("shouldSendReminder - no reminder send first intervall - Tage seit Bestellung: {$daysSinceOrder} : Next intervall: {$next_reminder->interval}"); return $daysSinceOrder >= $next_reminder->interval; } // Hole die nächste Erinnerung // Wenn bereits Erinnerungen gesendet wurden, prüfe das nächste Intervall if ($shopping_payment->reminder_date) { $current_reminder = $payment_reminders[$shopping_payment->reminder - 1]; $this->line("shouldSendReminder - letzte Erinnerung: {$current_reminder->interval} Tage"); $interval_difference = $next_reminder->interval - $current_reminder->interval; $next_reminder_date = Carbon::parse($shopping_payment->reminder_date)->addDays($interval_difference); $this->line("shouldSendReminder - next reminder date: {$next_reminder_date->format('d.m.Y H:i:s')}"); return now()->gte($next_reminder_date); } return false; } /** * Sendet eine Erinnerung für eine spezifische Zahlung */ private function sendReminderForPayment($shopping_payment) { try { $this->line('Sende Erinnerung...'); $result = $this->paymentReminderService->sendReminder($shopping_payment); if ($result) { $this->line('Erinnerung erfolgreich gesendet'); $this->stats['reminders_sent']++; // Log für Cron-Job \Log::info('Payment reminder sent via cron', [ 'order_id' => $shopping_payment->shopping_order_id, 'payment_id' => $shopping_payment->id, 'reminder_count' => $shopping_payment->reminder + 1, 'email' => $shopping_payment->shopping_order->shopping_user->billing_email ?? 'N/A', ]); } else { $this->line('Keine Erinnerung gesendet (keine weitere Erinnerung verfügbar)'); $this->stats['skipped']++; } } catch (\Exception $e) { $this->error('Fehler beim Senden der Erinnerung: '.$e->getMessage()); $this->stats['errors']++; \Log::error('Error sending payment reminder', [ 'order_id' => $shopping_payment->shopping_order_id, 'payment_id' => $shopping_payment->id, 'error' => $e->getMessage(), ]); } } }