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
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Jobs\Hosting;
use App\Models\Invoice;
use App\Models\OrderItem;
use App\Models\ServicePlan;
use App\Services\Plesk\PleskHostingService;
@@ -103,6 +104,7 @@ class ProvisionHostingAccountJob implements ShouldQueue
'dns_manager_server_id' => $server->id,
'domain_name' => $domainName,
'hosting_plan' => $hostingPlan,
'invoice_id' => Invoice::where('order_id', $this->item->order_id)->where('invoice_type', 1)->orderBy('id')->value('id'),
]);
if ($result['error']) {
@@ -3,6 +3,7 @@
namespace App\Jobs\Mail\Customer;
use App\Mail\Customer\HostingAccountCreated;
use App\Services\InvoicePdfService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -34,8 +35,10 @@ class HostingAccountCreatedJob implements ShouldQueue
}
try {
$invoice = app(InvoicePdfService::class)->attachment($this->data['invoice_id'] ?? null);
Mail::to($this->data['customer_email'])
->send(new HostingAccountCreated($this->data));
->send(new HostingAccountCreated($this->data, $invoice['pdf'] ?? null, $invoice['file_name'] ?? null));
} catch (\Throwable $e) {
Log::error('Không thể gửi email thông báo khởi tạo hosting: ' . $e->getMessage(), [
'customer_email' => $this->data['customer_email'] ?? null,
@@ -4,7 +4,6 @@ namespace App\Jobs\Mail;
use App\Http\Services\Telegram;
use App\Mail\Customer\VmCredentials;
use App\Models\Invoice;
use App\Services\InvoicePdfService;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
@@ -33,28 +32,9 @@ class SendInformationVirtualMachine implements ShouldQueue
public function handle(): void
{
try {
// Generate PDF inside the job — never pass binary blobs through queue payload
$invoice = null;
$invoicePdf = null;
$invoicePdfFileName = null;
if ($this->invoiceId) {
try {
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
->find($this->invoiceId);
if ($invoice) {
$pdfService = app(InvoicePdfService::class);
$invoicePdf = $pdfService->generate($invoice);
$invoicePdfFileName = $pdfService->generateFileName($invoice);
}
} catch (\Throwable $e) {
Log::warning('SendInformationVirtualMachine: PDF generation failed, sending without attachment', [
'invoice_id' => $this->invoiceId,
'error' => $e->getMessage(),
]);
}
}
$attachment = app(InvoicePdfService::class)->attachment($this->invoiceId);
$invoicePdf = $attachment['pdf'] ?? null;
$invoicePdfFileName = $attachment['file_name'] ?? null;
$email = new VmCredentials($this->data, $invoicePdf, $invoicePdfFileName);
+21
View File
@@ -5,6 +5,7 @@ namespace App\Jobs\Mail;
use App\Mail\GenericTemplateMail;
use App\Mail\InvoiceNotificationMail;
use App\Models\Invoice;
use App\Models\InvoiceAttachment;
use App\Models\Mail as MailLog;
use App\Services\Email\EmailTemplateService;
use App\Services\InvoicePdfService;
@@ -31,6 +32,8 @@ class SendInvoiceMailJob implements ShouldQueue
public readonly ?string $subject,
public readonly ?string $html,
public readonly ?int $sentBy,
public readonly bool $attachVat = false,
public readonly array $contractIds = [],
) {}
public function handle(InvoicePdfService $pdfService, EmailTemplateService $templates): void
@@ -47,6 +50,7 @@ class SendInvoiceMailJob implements ShouldQueue
$mail = $this->html !== null
? $this->editedMail($templates, $pdf, $pdfFileName)
: InvoiceNotificationMail::forInvoice($invoice, $this->type, $this->message, $pdf, $pdfFileName);
$this->attachExtras($mail, $invoice, $pdfService);
$sent = Mail::to($invoice->customer->email)->send($mail);
$this->log($templates, $invoice, $mail, $sent === null ? 'failed' : 'sent');
} catch (Throwable $e) {
@@ -64,6 +68,23 @@ class SendInvoiceMailJob implements ShouldQueue
}
}
private function attachExtras(Mailable $mail, Invoice $invoice, InvoicePdfService $pdfService): void
{
if ($this->attachVat && $invoice->vat_number) {
$mail->attachData($pdfService->generate($invoice), $pdfService->vatFileName($invoice), ['mime' => 'application/pdf']);
}
$contracts = InvoiceAttachment::query()
->where('invoice_id', $invoice->id)
->where('type', InvoiceAttachment::TYPE_CONTRACT)
->whereIn('id', $this->contractIds)
->get();
foreach ($contracts as $contract) {
$mail->attachFromStorageDisk($contract->disk, $contract->path, $contract->filename, ['mime' => $contract->mime_type]);
}
}
private function editedMail(EmailTemplateService $templates, string $pdf, string $pdfFileName): GenericTemplateMail
{
return (new GenericTemplateMail(