order_num} được đính kèm và hoàn tất thanh toán để dịch vụ không bị gián đoạn." : "Vui lòng xem thông tin hóa đơn #{$invoice->order_num} của bạn."; return new self( $invoice, $invoice->customer->fullname, $message, setting('general.company_name') ?: config('app.name'), $type, $invoicePdf, $invoicePdfFileName, MailGlobals::frontendUrl() . $invoice->clientPath(), ); } public function attachments(): array { if ($this->invoicePdf !== null && $this->invoicePdfFileName !== null) { return [ Attachment::fromData(fn() => $this->invoicePdf, $this->invoicePdfFileName) ->withMime('application/pdf'), ]; } return []; } protected function scenario(): string { return self::SCENARIO; } protected function variables(): array { $isRenewal = $this->type === 'renewal'; return [ 'title' => $isRenewal ? 'Thông báo gia hạn cước' : 'Thông báo hóa đơn', 'company_name' => $this->companyName, 'customer_name' => $this->customerName, 'body_message' => $this->message, 'invoice_number' => $this->invoice->invoice_number ?? $this->invoice->order?->order_num ?? '', 'created_date' => $this->formatDate($this->invoice->date ?? $this->invoice->created_at), 'due_date' => $this->formatDate($this->invoice->due_date), 'total' => number_format((int) $this->invoice->total, 0, ',', '.'), 'status_label' => $this->statusLabel(), 'outstanding_row' => $this->outstandingRow(), 'intro_note' => $isRenewal ? 'File PDF hóa đơn gia hạn đã được đính kèm trong email này. Vui lòng hoàn tất thanh toán trước hạn để dịch vụ không bị gián đoạn.' : 'File PDF hóa đơn đã được đính kèm trong email này để Quý khách tiện lưu trữ và đối chiếu.', 'pay_button' => $this->payButton(), 'lookup_row' => InvoiceLookup::emailRow($this->invoice), 'lookup_note' => InvoiceLookup::emailNote($this->invoice), 'attachments_block' => '', 'note_block' => $this->noteBlock(), ]; } private function formatDate(mixed $value): string { if (empty($value)) { return '—'; } if ($value instanceof \Carbon\CarbonInterface) { return $value->format('d/m/Y'); } if (is_numeric($value)) { return date('d/m/Y', (int) $value); } try { return Carbon::parse($value)->format('d/m/Y'); } catch (\Throwable) { return '—'; } } private function statusLabel(): string { return match ((int) $this->invoice->status) { InvoiceStatus::PAID->value => 'Đã thanh toán', InvoiceStatus::UNPAID->value => 'Chưa thanh toán', InvoiceStatus::OVERDUE->value => 'Quá hạn', InvoiceStatus::REFUNDED->value => 'Đã hoàn tiền', default => 'Đã hủy', }; } /** Chỉ hiện khi khách đã trả được một phần nhưng chưa đủ. */ private function outstandingRow(): string { $paid = (int) $this->invoice->amount_paid; $outstanding = InvoicePaymentService::outstanding($this->invoice); if ($paid <= 0 || $outstanding <= 0) { return ''; } return EmailTemplateShell::row('Đã thanh toán', '' . number_format($paid, 0, ',', '.') . ' VNĐ') . EmailTemplateShell::row('Còn thiếu', '' . number_format($outstanding, 0, ',', '.') . ' VNĐ'); } private function payButton(): string { if (empty($this->paymentUrl)) { return ''; } $url = htmlspecialchars($this->paymentUrl, ENT_QUOTES, 'UTF-8'); return '
| ' . 'THANH TOÁN HÓA ĐƠN' . ' |
Hoặc sao chép liên kết thanh toán: ' . $url . '
'; } private function noteBlock(): string { if (empty($this->invoice->note)) { return ''; } return '| ' . 'Ghi chú: ' . htmlspecialchars((string) $this->invoice->note, ENT_QUOTES, 'UTF-8') . ' |