feat: khởi tạo/huỷ domain Plesk qua job, gửi mail hoá đơn qua queue, đính kèm triển khai

- ProvisionDnsDomainJob/SuspendDnsDomainJob: tạo domain admin lên Plesk, huỷ = suspend (is_created=3)
- Khách xem zone DNS được gán ở /dich-vu/domain, quản lý bản ghi A/AAAA/CNAME/TXT/MX/SRV
- send-mail hoá đơn đẩy SendInvoiceMailJob, preview + sửa nội dung, ghi mail.order_id
- invoice_attachments.type (payment_proof/deployment)
- CacheService xoá theo SCAN pattern, Redis không persistent mặc định
- Backfill transactions.public_code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-11 09:56:18 +07:00
co-authored by Claude Opus 5.5
parent 30898ea46f
commit 94614d9cb3
55 changed files with 1190 additions and 236 deletions
+28 -22
View File
@@ -5,9 +5,9 @@ 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\Database\Eloquent\Collection;
use Illuminate\Mail\Mailables\Attachment;
/**
@@ -19,9 +19,10 @@ use Illuminate\Mail\Mailables\Attachment;
*/
class InvoiceNotificationMail extends TemplateMailable
{
public const SCENARIO = 'invoice_notification';
public function __construct(
public readonly Invoice $invoice,
public readonly Collection $invoiceAttachments,
public readonly string $customerName,
public readonly string $message,
public readonly string $companyName,
@@ -31,6 +32,29 @@ class InvoiceNotificationMail extends TemplateMailable
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) {
@@ -45,7 +69,7 @@ class InvoiceNotificationMail extends TemplateMailable
protected function scenario(): string
{
return 'invoice_notification';
return self::SCENARIO;
}
protected function variables(): array
@@ -67,7 +91,7 @@ class InvoiceNotificationMail extends TemplateMailable
? '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(),
'attachments_block' => '',
'note_block' => $this->noteBlock(),
];
}
@@ -132,24 +156,6 @@ class InvoiceNotificationMail extends TemplateMailable
. '<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)) {