feat: create PostCategoryFactory for generating post category data feat: create PostFactory for generating post data feat: add retry_requested_at column to services table feat: add vcenter provisioning fields to products table feat: add module_host_id to os table feat: create email_notification_templates table for email templates feat: add scenario and format fields to template table feat: add admin fields to transactions table fix: update ticket table columns and types feat: add uuid to ticket table and backfill existing records fix: make ticket_reply user_id nullable feat: create notifications table for user notifications fix: make ticket uuid nullable feat: create post_categories table for blog categories feat: create posts table for blog posts feat: add invoice email template for PDF generation
571 lines
24 KiB
PHP
571 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs\VirtualMachine\Create;
|
|
|
|
use App\Http\Services\Telegram;
|
|
use App\Http\Services\vCenter;
|
|
use App\Jobs\Mail\Customer\VMCreationFailedJob;
|
|
use App\Jobs\VirtualMachine\Provision\ChangePasswordViaVsphereJob;
|
|
use App\Services\VmProvisionService;
|
|
use App\Models\ModuleHost;
|
|
use App\Models\OrderItem;
|
|
use App\Models\Os;
|
|
use App\Models\ModuleVmware;
|
|
use App\Models\Log as CustomerLog;
|
|
use App\Models\Service;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class CreateVirtualMachineJob implements ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
/** Cho phép job chạy tối đa 10 phút (VM clone có thể mất 5-8 phút) */
|
|
public int $timeout = 600;
|
|
|
|
/** Không retry tự động — lỗi VM cần admin kiểm tra thủ công */
|
|
public int $tries = 1;
|
|
|
|
public OrderItem $item;
|
|
public bool $isRetry = false;
|
|
|
|
/**
|
|
* Create a new job instance.
|
|
*/
|
|
public function __construct(OrderItem $item, bool $isRetry = false)
|
|
{
|
|
$this->item = $item;
|
|
$this->isRetry = $isRetry;
|
|
}
|
|
|
|
/**
|
|
* Execute the job.
|
|
*/
|
|
public function handle(vCenter $vCenter, Telegram $telegram)
|
|
{
|
|
try {
|
|
$product = $this->item->getProductAttribute();
|
|
|
|
if (!$product) {
|
|
$errorMessage = "Không tìm thấy sản phẩm từ OrderItem #{$this->item->id}";
|
|
Log::warning($errorMessage);
|
|
$telegram->SendErrorMessage($errorMessage, 'CreateVirtualMachineJob - Missing Product');
|
|
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
$osModel = Os::with(['host.datacenter.vCenter', 'osGroup.host.datacenter.vCenter'])->find($this->item->os_id);
|
|
|
|
if (!$osModel) {
|
|
$errorMessage = "Không tìm thấy OS id={$this->item->os_id} trong OrderItem #{$this->item->id}. os_id không hợp lệ.";
|
|
Log::error($errorMessage);
|
|
$telegram->SendErrorMessage([
|
|
'error' => $errorMessage,
|
|
'os_id' => $this->item->os_id,
|
|
'order_item_id' => $this->item->id,
|
|
'product_id' => $product->id,
|
|
], 'CreateVirtualMachineJob - Invalid OS ID');
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$template = $osModel->template_id;
|
|
|
|
// Ưu tiên 1: host gán trực tiếp trên OS
|
|
$host = $osModel->host;
|
|
$datacenter = $host?->datacenter;
|
|
$vcenter = $datacenter?->vCenter;
|
|
|
|
// Ưu tiên 2: host từ osGroup (cũ)
|
|
if (!$host || !$datacenter || !$vcenter) {
|
|
$groupHost = $osModel->osGroup?->host;
|
|
$host = $host ?? $groupHost;
|
|
$datacenter = $datacenter ?? $groupHost?->datacenter;
|
|
$vcenter = $vcenter ?? $groupHost?->datacenter?->vCenter;
|
|
|
|
if ($groupHost) {
|
|
Log::info('CreateVirtualMachineJob: dùng host từ osGroup làm fallback', [
|
|
'os_id' => $osModel->id,
|
|
'os_group_id' => $osModel->os_group_id,
|
|
'host_id' => $groupHost->id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// Ưu tiên 3: host từ sản phẩm (cấu hình tab Provisioning)
|
|
if ((!$host || !$datacenter || !$vcenter) && $product->module_host_id) {
|
|
$productHost = ModuleHost::with('datacenter.vCenter')->find($product->module_host_id);
|
|
if ($productHost) {
|
|
$host = $host ?? $productHost;
|
|
$datacenter = $datacenter ?? $productHost->datacenter;
|
|
$vcenter = $vcenter ?? $productHost->datacenter?->vCenter;
|
|
|
|
Log::info('CreateVirtualMachineJob: dùng host từ sản phẩm làm fallback', [
|
|
'product_id' => $product->id,
|
|
'host_id' => $productHost->id,
|
|
'datacenter_id' => $productHost->datacenter?->id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
if (!$vcenter || !$host || !$datacenter) {
|
|
$errorMessage = "Thiếu thông tin host/datacenter/vcenter từ sản phẩm ID #{$product->id}. "
|
|
. "Vui lòng cấu hình vCenter trong tab Provisioning của sản phẩm, "
|
|
. "hoặc gán host cho OS group của OS được chọn.";
|
|
Log::error($errorMessage);
|
|
$telegram->SendErrorMessage([
|
|
'error' => $errorMessage,
|
|
'product_id' => $product->id,
|
|
'order_item_id' => $this->item->id,
|
|
'os_id' => $this->item->os_id,
|
|
'os_group_name' => $osModel->osGroup?->name,
|
|
'product_module_host_id' => $product->module_host_id,
|
|
'host_id' => $host?->id,
|
|
'datacenter_id' => $datacenter?->id,
|
|
'vcenter_id' => $vcenter?->id,
|
|
], 'CreateVirtualMachineJob - Missing Infrastructure');
|
|
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Login to vCenter
|
|
if (!$vCenter->loginServer($vcenter)) {
|
|
$errorMessage = "Login vào vCenter thất bại với host: {$vcenter->host}";
|
|
Log::error($errorMessage);
|
|
$telegram->SendErrorMessage([
|
|
'error' => $errorMessage,
|
|
'vcenter_host' => $vcenter->host,
|
|
'vcenter_user' => $vcenter->user,
|
|
'product_id' => $product->id,
|
|
'order_item_id' => $this->item->id
|
|
], 'CreateVirtualMachineJob - vCenter Login Failed');
|
|
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Lấy thông tin addon (CPU, RAM)
|
|
$addonData = [
|
|
'cpu' => 0,
|
|
'ram' => 0,
|
|
'disk' => 0
|
|
];
|
|
|
|
// Lấy addon từ order item
|
|
if ($this->item->addons) {
|
|
foreach ($this->item->addons as $orderAddon) {
|
|
$addon = $orderAddon->addonItems;
|
|
if (!$addon)
|
|
continue;
|
|
|
|
$addonName = strtolower(trim($addon->name));
|
|
$quantity = (int) $orderAddon->qty;
|
|
|
|
switch ($addonName) {
|
|
case 'cpu':
|
|
$addonData['cpu'] += $quantity;
|
|
break;
|
|
case 'ram':
|
|
$addonData['ram'] += $quantity;
|
|
break;
|
|
case 'disk':
|
|
case 'ssd':
|
|
$addonData['disk'] += $quantity;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Xác định OS family để chọn username và luồng cấu hình phù hợp
|
|
$osGroupName = $osModel->osGroup?->name ?? '';
|
|
$isWindows = str_contains(strtolower($osGroupName), 'windows');
|
|
$vmUser = $isWindows ? 'administrator' : 'root';
|
|
$vmPort = $isWindows ? 3389 : 22;
|
|
|
|
$result = $vCenter->CreateVirtualMachine([
|
|
'item' => $this->item,
|
|
'host' => $host,
|
|
'datacenter' => $datacenter,
|
|
'vcenter' => $vcenter,
|
|
'template' => $template,
|
|
'addon' => $addonData,
|
|
]);
|
|
Log::info('CreateVirtualMachineJob - Result: ' . json_encode($result));
|
|
if (is_array($result) && ($result['error'] ?? false)) {
|
|
$errorMessage = $this->translateVcenterError($result);
|
|
Log::warning($errorMessage);
|
|
$telegram->SendErrorMessage([
|
|
'error' => $result['error'] ?? 'Unknown error',
|
|
'result' => $result,
|
|
'product_id' => $product->id,
|
|
'order_item_id' => $this->item->id,
|
|
'host_name' => $host?->name,
|
|
'datacenter_name' => $datacenter?->name,
|
|
'vcenter_host' => $vcenter?->host
|
|
], 'CreateVirtualMachineJob - VM Creation Failed');
|
|
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return false;
|
|
} elseif ($result === 'Login_Again') {
|
|
$errorMessage = 'Tạo máy ảo thất bại: cần đăng nhập lại.';
|
|
Log::error($errorMessage);
|
|
$telegram->SendErrorMessage([
|
|
'error' => $errorMessage,
|
|
'product_id' => $product->id,
|
|
'order_item_id' => $this->item->id,
|
|
'vcenter_host' => $vcenter?->host
|
|
], 'CreateVirtualMachineJob - vCenter Re-login Required');
|
|
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return false;
|
|
} else {
|
|
Log::info('Tạo máy ảo thành công: ' . json_encode($result));
|
|
|
|
// Lấy vm_id từ kết quả
|
|
$vmId = is_array($result) ? ($result['data'] ?? 'unknown') : $result;
|
|
|
|
if ($vmId !== 'unknown' && $vmId !== 'UNKNOWN') {
|
|
try {
|
|
// vm_id được lưu vào tbl.services qua ModuleVmwareObserver::created()
|
|
$startResult = $this->startVirtualMachineDirectly($vCenter, $vmId);
|
|
|
|
// VM started — SSH vào vSphere host để đổi mật khẩu qua PS1 script
|
|
|
|
try {
|
|
$customer = $this->item->order->customer;
|
|
$serverName = $this->item->server_name ?? 'VM-' . $vmId;
|
|
|
|
$months = (int) ($this->item->months ?? 0);
|
|
$firstPayment = (int) $this->item->price * ($months ?: 1);
|
|
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
|
|
|
|
// Create ModuleVmware without IP — AssignIpToGuestJob will
|
|
// pick, assign, and verify the real IP later, then write it to DB.
|
|
$moduleVmware = ModuleVmware::create([
|
|
'name' => $serverName,
|
|
'module_ip_address' => null, // set by AssignIpToGuestJob
|
|
'ip' => null, // set by AssignIpToGuestJob
|
|
'port' => $vmPort,
|
|
'order_id' => $this->item->order->id,
|
|
'first_payment' => $firstPayment,
|
|
'next_payment' => $firstPayment,
|
|
'next_due_date' => $nextDueDate,
|
|
'next_invoice' => $nextDueDate,
|
|
'user' => $vmUser,
|
|
'password' => VmProvisionService::TEMPLATE_PASSWORD,
|
|
'plain_password' => VmProvisionService::TEMPLATE_PASSWORD,
|
|
'hostname' => $serverName,
|
|
'vmware_id' => $vmId,
|
|
'vcenter_id' => $vcenter->id,
|
|
'os_id' => $this->item->os_id ?? 1,
|
|
'customer_id' => $customer->id,
|
|
'status' => 1,
|
|
]);
|
|
|
|
// Sync services table
|
|
$this->syncServiceRecord($moduleVmware);
|
|
|
|
// Step 1 of guest provisioning: change template password.
|
|
// On success ChangePasswordViaVsphereJob dispatches AssignIpToGuestJob,
|
|
// which handles IP selection, assignment, verification, and email.
|
|
ChangePasswordViaVsphereJob::dispatch(
|
|
$moduleVmware->id,
|
|
$vmId,
|
|
$isWindows ? VmProvisionService::OS_WINDOWS : VmProvisionService::OS_LINUX,
|
|
);
|
|
|
|
Log::info('ChangePasswordViaVsphereJob dispatched', [
|
|
'module_vmware_id' => $moduleVmware->id,
|
|
'vm_id' => $vmId,
|
|
'os_family' => $isWindows ? 'WINDOWS' : 'LINUX',
|
|
]);
|
|
} catch (\Exception $e) {
|
|
Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [
|
|
'vmware_id' => $vmId,
|
|
'order_item_id' => $this->item->id,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('❌ Lỗi khi lưu dữ liệu VM hoặc start VM: ' . $e->getMessage(), [
|
|
'vm_id' => $vmId,
|
|
'order_id' => $this->item->order->id,
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
}
|
|
}
|
|
|
|
$telegram->SendJobRunInfo(
|
|
'CreateVirtualMachineJob',
|
|
'success',
|
|
[
|
|
'product_id' => $product->id,
|
|
'order_item_id' => $this->item->id,
|
|
'result' => $result,
|
|
'addon_applied' => !empty($addonData) && array_sum($addonData) > 0,
|
|
'vm_id_saved' => $vmId !== 'unknown' && $vmId !== 'UNKNOWN',
|
|
'vm_started' => isset($startResult) && is_array($startResult) && $startResult['error'] === false
|
|
]
|
|
);
|
|
$this->writeCustomerSuccessLog();
|
|
|
|
try {
|
|
$order = $this->item->order;
|
|
$customer = $order->customer;
|
|
broadcast(new \App\Events\OrderProvisioned(
|
|
orderId: $order->id,
|
|
orderNum: $order->order_num,
|
|
customerName: trim($customer->firstname . ' ' . $customer->lastname),
|
|
vmId: (string) $vmId,
|
|
success: true,
|
|
message: 'Máy ảo đã được khởi tạo thành công cho khách hàng ' . trim($customer->firstname . ' ' . $customer->lastname) . ' (Đơn #' . $order->order_num . ')',
|
|
));
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Không thể broadcast OrderProvisioned: ' . $e->getMessage());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} catch (\Throwable $e) {
|
|
$errorMessage = 'Lỗi khi tạo máy ảo (Job): ' . $e->getMessage();
|
|
Log::error($errorMessage, [
|
|
'exception' => $e,
|
|
'order_item_id' => $this->item->id,
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
|
|
$telegram->SendErrorMessage([
|
|
'error' => $e->getMessage(),
|
|
'file' => $e->getFile(),
|
|
'line' => $e->getLine(),
|
|
'order_item_id' => $this->item->id,
|
|
'trace' => substr($e->getTraceAsString(), 0, 500) // Giới hạn độ dài trace
|
|
], 'CreateVirtualMachineJob - Exception');
|
|
|
|
if (!$this->isRetry) {
|
|
$this->sendFailureNotification($errorMessage);
|
|
$this->writeCustomerFailureLog($errorMessage);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function writeCustomerSuccessLog(): void
|
|
{
|
|
try {
|
|
$order = $this->item->order;
|
|
$customer = $order?->customer;
|
|
|
|
if (!$order || !$customer) {
|
|
return;
|
|
}
|
|
|
|
CustomerLog::create([
|
|
'agent' => 'system',
|
|
'client_ip' => null,
|
|
'customer_id' => $customer->id,
|
|
'description' => 'Khởi tạo dịch vụ thành công cho đơn hàng #' . $order->order_num
|
|
. ' (product_id: ' . $this->item->product_id . ', os_id: ' . $this->item->os_id . ').',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Không thể ghi log thành công khởi tạo VM: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
private function writeCustomerFailureLog(string $errorMessage): void
|
|
{
|
|
try {
|
|
$order = $this->item->order;
|
|
$customer = $order?->customer;
|
|
|
|
if (!$order || !$customer) {
|
|
return;
|
|
}
|
|
|
|
CustomerLog::create([
|
|
'agent' => 'system',
|
|
'client_ip' => null,
|
|
'customer_id' => $customer->id,
|
|
'description' => 'Khởi tạo dịch vụ thất bại cho đơn hàng #' . $order->order_num
|
|
. ' (product_id: ' . $this->item->product_id . ', os_id: ' . $this->item->os_id . ').'
|
|
. ' Lỗi: ' . $errorMessage . '. '
|
|
. 'Nếu có lỗi xảy ra khi khởi tạo dịch vụ quý khách nhấn khởi tạo tại đây, hoặc quý khách có thể liên hệ với kỹ thuật support@tgs.com.vn',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('Không thể ghi log thất bại khởi tạo VM: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start VM directly using vCenter API
|
|
*/
|
|
private function startVirtualMachineDirectly(vCenter $vCenter, string $vmId): array|string
|
|
{
|
|
try {
|
|
// Kiểm tra xem đã có session chưa
|
|
$check = $vCenter->checkApiExits();
|
|
if (!$check) {
|
|
return 'Login_Again';
|
|
}
|
|
|
|
// Gọi API start VM trực tiếp
|
|
$response = Http::withoutVerifying()
|
|
->withHeaders([
|
|
'vmware-api-session-id' => $vCenter->token
|
|
])
|
|
->post("https://{$vCenter->apiUri}/rest/vcenter/vm/{$vmId}/power/start");
|
|
|
|
if ($response->status() === 200) {
|
|
return [
|
|
'error' => false,
|
|
'data' => 'success'
|
|
];
|
|
}
|
|
|
|
return [
|
|
'error' => true,
|
|
'data' => $response->body()
|
|
];
|
|
} catch (\Exception $e) {
|
|
Log::error('Lỗi khi start VM: ' . $e->getMessage());
|
|
return [
|
|
'error' => true,
|
|
'data' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Send failure notification to customer
|
|
*/
|
|
private function sendFailureNotification(string $errorMessage): void
|
|
{
|
|
try {
|
|
$customer = $this->item->order->customer;
|
|
$product = $this->item->getProductAttribute();
|
|
|
|
VMCreationFailedJob::dispatch([
|
|
'customer_email' => $customer->email,
|
|
'customer_name' => trim($customer->firstname . ' ' . $customer->lastname),
|
|
'product_name' => $product?->name ?? 'Unknown Product',
|
|
'order_id' => $this->item->order->id,
|
|
'order_num' => $this->item->order->order_num,
|
|
'error_message' => $errorMessage,
|
|
'order_item_id' => $this->item->id
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('Không thể gửi email thông báo lỗi VM: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync module_vmware info → services table.
|
|
* Link: module_vmware.order_id → order_items.order_id → services.order_item_id
|
|
*/
|
|
private function syncServiceRecord(ModuleVmware $moduleVmware): void
|
|
{
|
|
try {
|
|
$orderItem = OrderItem::where('order_id', $moduleVmware->order_id)->first();
|
|
|
|
if (!$orderItem) {
|
|
Log::warning('syncServiceRecord: không tìm thấy OrderItem', [
|
|
'order_id' => $moduleVmware->order_id,
|
|
'module_vmware_id' => $moduleVmware->id,
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$months = (int) ($orderItem->months ?? 0);
|
|
$updated = Service::where('order_item_id', $orderItem->id)
|
|
->update([
|
|
'vm_id' => $moduleVmware->id,
|
|
'status' => 'active',
|
|
'expires_at' => $months > 0 ? now()->addMonths($months) : null,
|
|
]);
|
|
|
|
Log::info('syncServiceRecord: service synced', [
|
|
'module_vmware_id' => $moduleVmware->id,
|
|
'vmware_id' => $moduleVmware->vmware_id,
|
|
'order_item_id' => $orderItem->id,
|
|
'rows_updated' => $updated,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('syncServiceRecord: exception — ' . $e->getMessage(), [
|
|
'module_vmware_id' => $moduleVmware->id,
|
|
'order_id' => $moduleVmware->order_id,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gửi email thông báo VM đã khởi tạo thành công cho khách hàng.
|
|
* Gọi ngay sau khi tạo module_vmware — cho cả Linux lẫn Windows.
|
|
*/
|
|
/**
|
|
* Dịch error từ vCenter API sang tiếng Việt dễ hiểu.
|
|
*/
|
|
private function translateVcenterError(array $result): string
|
|
{
|
|
$data = $result['data'] ?? [];
|
|
$errorType = is_array($data) ? ($data['error_type'] ?? '') : '';
|
|
$serverName = $this->item->server_name ?? '';
|
|
|
|
$map = [
|
|
'ALREADY_EXISTS' => "Tên máy chủ \"{$serverName}\" đã tồn tại trong hệ thống. Vui lòng liên hệ hỗ trợ để được xử lý hoặc chọn tên máy chủ khác.",
|
|
'NOT_FOUND' => 'Không tìm thấy template hoặc tài nguyên trên vCenter. Vui lòng liên hệ hỗ trợ.',
|
|
'RESOURCE_INACCESSIBLE' => 'Tài nguyên vCenter không khả dụng. Vui lòng thử lại sau hoặc liên hệ hỗ trợ.',
|
|
'INVALID_ARGUMENT' => 'Thông tin cấu hình máy chủ không hợp lệ. Vui lòng liên hệ hỗ trợ.',
|
|
'UNABLE_TO_ALLOCATE_RESOURCE' => 'Không đủ tài nguyên để khởi tạo máy chủ. Vui lòng liên hệ hỗ trợ.',
|
|
'TIMED_OUT' => 'Yêu cầu khởi tạo máy chủ hết thời gian chờ. Vui lòng thử lại sau.',
|
|
];
|
|
|
|
if ($errorType && isset($map[$errorType])) {
|
|
return $map[$errorType];
|
|
}
|
|
|
|
// Fallback: không để lộ JSON thô ra email khách hàng
|
|
return "Không thể khởi tạo máy chủ ảo lúc này. Chúng tôi đã ghi nhận và sẽ xử lý trong thời gian sớm nhất. Vui lòng liên hệ hỗ trợ nếu cần.";
|
|
}
|
|
|
|
private function getBillingCycleLabel(?int $months): string
|
|
{
|
|
return match ($months) {
|
|
1 => 'Hàng tháng',
|
|
3 => '3 Tháng',
|
|
6 => '6 Tháng',
|
|
12 => 'Hàng năm',
|
|
24 => '2 Năm',
|
|
default => $months . ' Tháng',
|
|
};
|
|
}
|
|
}
|