update
This commit is contained in:
@@ -2,21 +2,28 @@
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\EmailNotificationTemplate;
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Mail\Concerns\RequiresDbTemplate;
|
||||
use App\Models\Invoice;
|
||||
use App\Services\EmailNotificationTemplateRenderer;
|
||||
use App\Services\InvoicePaymentService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Attachment;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 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 Mailable
|
||||
{
|
||||
use RequiresDbTemplate;
|
||||
|
||||
public function __construct(
|
||||
public readonly Invoice $invoice,
|
||||
public readonly Collection $invoiceAttachments,
|
||||
@@ -31,50 +38,15 @@ class InvoiceNotificationMail extends Mailable
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
$tpl = $this->activeTemplate();
|
||||
$fallbackSubject = $this->type === 'renewal'
|
||||
? "Thông báo gia hạn cước #{$this->invoice->invoice_number} - {$this->companyName}"
|
||||
: "Thông báo hóa đơn #{$this->invoice->invoice_number} - {$this->companyName}";
|
||||
|
||||
$subject = $tpl
|
||||
? $this->resolveSubject($tpl->subject)
|
||||
: $fallbackSubject;
|
||||
|
||||
return new Envelope(subject: $subject);
|
||||
return new Envelope(
|
||||
subject: $this->renderFromTemplate('invoice_notification', $this->vars())['subject'],
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
$tpl = $this->activeTemplate();
|
||||
|
||||
if ($tpl) {
|
||||
try {
|
||||
$renderer = app(EmailNotificationTemplateRenderer::class);
|
||||
$html = $renderer->render(
|
||||
$tpl,
|
||||
$this->invoice->load(['order.orderItems.product', 'customer', 'attachments']),
|
||||
$this->companyName
|
||||
);
|
||||
return new Content(htmlString: $html);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('InvoiceNotificationMail: renderer failed, falling back to blade', [
|
||||
'template_id' => $tpl->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return new Content(
|
||||
view: 'emails.invoice-notification',
|
||||
with: [
|
||||
'invoice' => $this->invoice,
|
||||
'attachments' => $this->invoiceAttachments,
|
||||
'customerName' => $this->customerName,
|
||||
'bodyMessage' => $this->message,
|
||||
'companyName' => $this->companyName,
|
||||
'type' => $this->type,
|
||||
'paymentUrl' => $this->paymentUrl,
|
||||
]
|
||||
htmlString: $this->renderFromTemplate('invoice_notification', $this->vars())['html'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,19 +62,120 @@ class InvoiceNotificationMail extends Mailable
|
||||
return [];
|
||||
}
|
||||
|
||||
private function activeTemplate(): ?EmailNotificationTemplate
|
||||
private function vars(): array
|
||||
{
|
||||
return EmailNotificationTemplate::where('type', 'invoice_notification')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
$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' => $this->attachmentsBlock(),
|
||||
'note_block' => $this->noteBlock(),
|
||||
];
|
||||
}
|
||||
|
||||
private function resolveSubject(string $subject): string
|
||||
private function formatDate(mixed $value): string
|
||||
{
|
||||
return str_replace(
|
||||
['{{invoice_number}}', '{{company_name}}'],
|
||||
[$this->invoice->invoice_number ?? '', $this->companyName],
|
||||
$subject
|
||||
);
|
||||
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 '<tr><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Đã thanh toán:</td><td style="padding: 7px 11px;"><strong style="color: #059669;">'
|
||||
. number_format($paid, 0, ',', '.') . ' VNĐ</strong></td></tr>'
|
||||
. '<tr bgcolor="#FFF1E6"><td style="padding: 7px 11px; border-right: 1px solid #e8e8e8;">Còn thiếu:</td><td style="padding: 7px 11px;"><strong style="color: #dc2626;">'
|
||||
. number_format($outstanding, 0, ',', '.') . ' VNĐ</strong></td></tr>';
|
||||
}
|
||||
|
||||
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 attachmentsBlock(): string
|
||||
{
|
||||
if ($this->invoiceAttachments->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$items = $this->invoiceAttachments
|
||||
->map(fn($file) => '<li style="margin-bottom: 6px;"><a href="'
|
||||
. htmlspecialchars((string) $file->url, ENT_QUOTES, 'UTF-8')
|
||||
. '" target="_blank" rel="noopener" style="color: #f16723; text-decoration: none;">'
|
||||
. htmlspecialchars((string) $file->filename, ENT_QUOTES, 'UTF-8')
|
||||
. '</a></li>')
|
||||
->implode('');
|
||||
|
||||
return '<div style="margin: 20px 0;"><p style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600; color: #374151;">File đính kèm</p>'
|
||||
. '<ul style="padding-left: 18px; margin: 0; font-size: 14px;">' . $items . '</ul></div>';
|
||||
}
|
||||
|
||||
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: #626262;">'
|
||||
. '<strong style="color: #374151;">Ghi chú:</strong> '
|
||||
. htmlspecialchars((string) $this->invoice->note, ENT_QUOTES, 'UTF-8')
|
||||
. '</td></tr></tbody></table>';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user