feat: nhắc gia hạn QR theo dịch vụ, failover server hosting, CCCD admin, log cấp phát dịch vụ

- Mail campaign: mẫu nhắc gia hạn thanh toán QR, chọn dịch vụ + chu kỳ từng dòng (renewal_services)
- Hosting: nhiều server Plesk cho một gói (service_plan_servers), chuyển server khi server chết
- Admin xem/sửa giấy tờ định danh khách, tuỳ chọn gia hạn cho khách, nhật ký cấp phát dịch vụ
- Đánh dấu mẫu email dịch vụ là non-marketing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
baodev0
2026-10-30 18:36:41 +07:00
co-authored by Claude Sonnet 5.5
parent 047c23beff
commit 55a834a2f8
53 changed files with 1463 additions and 119 deletions
@@ -98,7 +98,6 @@ class ServiceClientDetailResource extends JsonResource
$vm = $this->resource->vmware;
$product = $orderItem?->product ?? $this->resource->product;
$custom = $this->resource->customProduct;
$renewalSource = $custom ?? $product;
$hosting = $this->resource->hostingAccount;
$pleskCredentials = $hosting ? $this->resolvePleskCredentials($hosting) : null;
$dnsZone = $this->resolveDnsZone();
@@ -124,14 +123,6 @@ class ServiceClientDetailResource extends JsonResource
'total' => (float) $order->total,
'months' => (int) $orderItem->months,
'billing_label' => $orderItem->billing_label,
'cycle_price' => $custom
? (float) $custom->unit_price * (int) $orderItem->months
: (float) ($product?->pricings()
->where('billing_cycle', $orderItem->billing_cycle)
->where('is_active', 1)
->value('price') ?? 0),
'addon_total' => \App\Support\RenewalPricing::addonTotal($this->resource, 1),
'renewal_options' => \App\Support\RenewalPricing::options($renewalSource),
] : null,
'vm' => $vm ? [
'ip_address' => $vm->ipAddress?->ip ?? $vm->ip,
@@ -145,6 +136,7 @@ class ServiceClientDetailResource extends JsonResource
] : null,
'hosting' => $hosting ? [
'ip_address' => $hosting->server?->publicHost(),
'server_ip' => $hosting->server?->ip_address,
'plesk_url' => $hosting->server?->panelUrl(),
'username' => ResponseCipher::encrypt($pleskCredentials['login'] ?? null),
'password' => ResponseCipher::encrypt($pleskCredentials['password'] ?? null),