feat(billing): renewals, coupons, VAT numbering, billing cycles and invoice payment tracking

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-19 16:37:56 +07:00
co-authored by Claude Sonnet 5.5
parent ea9a53f742
commit 25f23d541e
134 changed files with 4926 additions and 185 deletions
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Mail;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Service;
use App\Services\InvoicePaymentService;
use App\Support\MailGlobals;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class AutoRenewFailedMail extends TemplateMailable
{
use Queueable, SerializesModels;
public const SCENARIO = 'auto_renew_failed';
public function __construct(
public readonly Customer $customer,
public readonly Service $service,
public readonly Invoice $invoice,
) {}
protected function scenario(): string
{
return self::SCENARIO;
}
protected function variables(): array
{
$serverName = (string) $this->service->server_name;
$frontend = MailGlobals::frontendUrl();
return [
'client_name' => $this->customer->fullname ?? '',
'product_name' => $this->service->product?->name ?? 'Dịch vụ',
'server_name_suffix' => $serverName !== '' ? ' (' . e($serverName) . ')' : '',
'expires_at' => $this->service->expires_at?->format('d/m/Y') ?? 'N/A',
'invoice_number' => $this->invoice->invoice_number,
'outstanding' => number_format(InvoicePaymentService::outstanding($this->invoice), 0, ',', '.'),
'balance_formatted' => number_format((int) $this->customer->total, 0, ',', '.'),
'topup_url' => $frontend . '/nap-tien',
'invoice_url' => $frontend . $this->invoice->clientPath(),
];
}
}
@@ -3,8 +3,10 @@
namespace App\Mail\Customer;
use App\Mail\TemplateMailable;
use App\Services\InvoicePdfService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Queue\SerializesModels;
class DomainOrderReceivedNotification extends TemplateMailable implements ShouldQueue
@@ -15,6 +17,15 @@ class DomainOrderReceivedNotification extends TemplateMailable implements Should
protected array $data,
) {}
public function attachments(): array
{
$invoice = app(InvoicePdfService::class)->attachment($this->data['invoice_id'] ?? null);
return $invoice
? [Attachment::fromData(fn () => $invoice['pdf'], $invoice['file_name'])->withMime('application/pdf')]
: [];
}
protected function scenario(): string
{
return 'domain_order_received';
+25 -2
View File
@@ -6,17 +6,26 @@ use App\Mail\TemplateMailable;
use App\Support\EmailTemplateShell;
use App\Support\MailGlobals;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Queue\SerializesModels;
class HostingAccountCreated extends TemplateMailable
{
use Queueable, SerializesModels;
public function __construct(public array $data) {}
public function __construct(
public array $data,
public readonly ?string $invoicePdf = null,
public readonly ?string $invoicePdfFileName = null,
) {}
public function attachments(): array
{
return [];
if ($this->invoicePdf === null || $this->invoicePdfFileName === null) {
return [];
}
return [Attachment::fromData(fn () => $this->invoicePdf, $this->invoicePdfFileName)->withMime('application/pdf')];
}
protected function scenario(): string
@@ -47,6 +56,8 @@ class HostingAccountCreated extends TemplateMailable
'full_name' => $data['customer_name'] ?? 'Quý khách',
'domain_name' => $data['domain_name'] ?? '',
'hosting_plan' => $data['hosting_plan'] ?? '',
'server_name' => $data['server_name'] ?? '',
'recipient_label' => $this->recipientLabel($data),
'server_host_row' => $row('Máy chủ / FTP Host', $data['server_host'] ?? null),
'ftp_username_row' => $row('Tài khoản FTP', $data['ftp_username'] ?? null),
'ftp_password_row' => $row('Mật khẩu FTP', $data['ftp_password'] ?? null),
@@ -56,4 +67,16 @@ class HostingAccountCreated extends TemplateMailable
'plesk_block' => $pleskBlock,
];
}
private function recipientLabel(array $data): string
{
foreach (['customer_name', 'customer_company'] as $key) {
$value = trim((string) ($data[$key] ?? ''));
if ($value !== '') {
return $value;
}
}
return 'Công ty';
}
}
+3
View File
@@ -5,6 +5,7 @@ namespace App\Mail;
use App\Enums\Invoice\InvoiceStatus;
use App\Models\Invoice;
use App\Services\InvoicePaymentService;
use App\Support\InvoiceLookup;
use App\Support\MailGlobals;
use App\Support\EmailTemplateShell;
use Carbon\Carbon;
@@ -91,6 +92,8 @@ 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(),
'lookup_row' => InvoiceLookup::emailRow($this->invoice),
'lookup_note' => InvoiceLookup::emailNote($this->invoice),
'attachments_block' => '',
'note_block' => $this->noteBlock(),
];
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
class ServiceRenewedMail extends TemplateMailable
{
use Queueable, SerializesModels;
public function __construct(
private readonly string $scenarioKey,
private readonly array $data,
) {}
protected function scenario(): string
{
return $this->scenarioKey;
}
protected function variables(): array
{
return $this->data;
}
}