- add rotating admin refresh tokens and password gates\n- expand ticket galleries, exports, and client resources\n- improve IP allocation auditing and dashboard data\n- enforce encrypted client access tokens on public routes
172 lines
7.1 KiB
PHP
172 lines
7.1 KiB
PHP
<?php
|
|
|
|
namespace App\Mail;
|
|
|
|
use App\Enums\Invoice\InvoiceStatus;
|
|
use App\Models\Invoice;
|
|
use App\Services\InvoicePaymentService;
|
|
use App\Support\MailGlobals;
|
|
use App\Support\EmailTemplateShell;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Mail\Mailables\Attachment;
|
|
|
|
/**
|
|
* KHÔNG implement ShouldQueue — gọi từ trong queued job, gửi đồng bộ.
|
|
*
|
|
* Render từ tbl.template (scenario `invoice_notification`) như mọi mailable khác. Trước đây
|
|
* đi qua bảng email_notification_templates riêng và rơi về blade khi không có template —
|
|
* hai hệ template song song cho cùng một email, admin sửa một nơi thì nơi kia vẫn nguyên.
|
|
*/
|
|
class InvoiceNotificationMail extends TemplateMailable
|
|
{
|
|
public const SCENARIO = 'invoice_notification';
|
|
|
|
public function __construct(
|
|
public readonly Invoice $invoice,
|
|
public readonly string $customerName,
|
|
public readonly string $message,
|
|
public readonly string $companyName,
|
|
public readonly string $type = 'invoice',
|
|
public readonly ?string $invoicePdf = null,
|
|
public readonly ?string $invoicePdfFileName = null,
|
|
public readonly ?string $paymentUrl = null,
|
|
) {}
|
|
|
|
public static function forInvoice(
|
|
Invoice $invoice,
|
|
string $type = 'invoice',
|
|
?string $message = null,
|
|
?string $invoicePdf = null,
|
|
?string $invoicePdfFileName = null,
|
|
): self {
|
|
$message ??= $type === 'renewal'
|
|
? "Dịch vụ của quý khách sắp đến hạn gia hạn. Vui lòng kiểm tra hóa đơn #{$invoice->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(),
|
|
'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 => '<strong style="color: #059669;">Đã thanh toán</strong>',
|
|
InvoiceStatus::UNPAID->value => '<strong style="color: #dc2626;">Chưa thanh toán</strong>',
|
|
InvoiceStatus::OVERDUE->value => '<strong style="color: #b45309;">Quá hạn</strong>',
|
|
InvoiceStatus::REFUNDED->value => '<strong style="color: #7c3aed;">Đã hoàn tiền</strong>',
|
|
default => '<strong style="color: #9ca3af;">Đã hủy</strong>',
|
|
};
|
|
}
|
|
|
|
/** 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', '<span style="color: #059669;">' . number_format($paid, 0, ',', '.') . ' VNĐ</span>')
|
|
. EmailTemplateShell::row('Còn thiếu', '<span style="color: #dc2626;">' . number_format($outstanding, 0, ',', '.') . ' VNĐ</span>');
|
|
}
|
|
|
|
private function payButton(): string
|
|
{
|
|
if (empty($this->paymentUrl)) {
|
|
return '';
|
|
}
|
|
|
|
$url = htmlspecialchars($this->paymentUrl, ENT_QUOTES, 'UTF-8');
|
|
|
|
return '<table cellspacing="0" cellpadding="0" style="margin: 24px auto 8px auto;"><tbody><tr><td align="center" style="background-color: #f16723; padding: 12px 24px; border-radius: 4px;">'
|
|
. '<a href="' . $url . '" target="_blank" rel="noopener" style="text-decoration: none; font-size: 14px; font-weight: 600; color: #ffffff;">THANH TOÁN HÓA ĐƠN</a>'
|
|
. '</td></tr></tbody></table>'
|
|
. '<p style="margin: 0 0 18px 0; word-break: break-all; font-size: 13px; color: #6b7280;">Hoặc sao chép liên kết thanh toán: <a href="' . $url . '" style="color: #f16723;">' . $url . '</a></p>';
|
|
}
|
|
|
|
private function noteBlock(): string
|
|
{
|
|
if (empty($this->invoice->note)) {
|
|
return '';
|
|
}
|
|
|
|
return '<table style="width: 100%; border-collapse: collapse; margin-top: 16px;" role="presentation" cellspacing="0" cellpadding="0"><tbody><tr>'
|
|
. '<td style="padding: 14px 16px; border-left: 3px solid #f16723; background-color: #f8fafc; font-size: 13px; line-height: 20px; color: #1a1a1a;">'
|
|
. '<strong style="color: #374151;">Ghi chú:</strong> '
|
|
. htmlspecialchars((string) $this->invoice->note, ENT_QUOTES, 'UTF-8')
|
|
. '</td></tr></tbody></table>';
|
|
}
|
|
}
|