option('dry-run'); $offsets = (array) config('billing.reminder_offsets', []); if ($offsets === []) { $this->warn('invoices:send-reminders | billing.reminder_offsets rỗng — không có mốc nào để nhắc.'); return Command::SUCCESS; } // Chỉ quét hoá đơn nằm trong cửa sổ nhắc [mốc sớm nhất … mốc muộn nhất], để không // kéo cả bảng và không đụng vào hoá đơn cũ đã qua mốc cuối (vd. dữ liệu import từ WHMCS). $earliest = min($offsets); $latest = InvoiceReminderService::reminderCutoff($offsets); $invoices = Invoice::query() ->with('customer') ->whereNotNull('due_date') ->whereIn('status', InvoicePaymentService::PAYABLE_STATUSES) ->whereDate('due_date', '<=', now()->subDays($earliest)->toDateString()) ->whereDate('due_date', '>=', now()->subDays($latest)->toDateString()) ->get(); $this->info("invoices:send-reminders | Xét {$invoices->count()} hoá đơn."); $sent = 0; foreach ($invoices as $invoice) { $dueStep = $reminders->dueStep($invoice); if ($dueStep <= (int) $invoice->reminder_step) { continue; } if ($dryRun) { $this->line(sprintf( ' [dry-run] Hoá đơn #%s hạn %s — mốc %d/%d', $invoice->invoice_number, $invoice->due_date->format('Y-m-d'), $dueStep, count($offsets), )); continue; } try { if ($reminders->remind($invoice)) { $sent++; $this->line(" [sent] Hoá đơn #{$invoice->invoice_number} → {$invoice->customer?->email}"); } } catch (\Throwable $e) { Log::error('invoices:send-reminders error', [ 'invoice_id' => $invoice->id, 'error' => $e->getMessage(), ]); $this->error(" [error] Hoá đơn #{$invoice->invoice_number}: " . $e->getMessage()); } } $this->info("invoices:send-reminders | Đã gửi {$sent} email nhắc."); return Command::SUCCESS; } }