update
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\Hosting;
|
||||
|
||||
use App\Enums\Server\ServerType;
|
||||
use App\Models\DnsServer;
|
||||
use App\Models\OrderItem;
|
||||
use App\Services\Plesk\PleskHostingService;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Auto-provisions a Plesk hosting account when a hosting order is paid,
|
||||
* mirroring CreateVirtualMachineJob's role for VPS orders. Reuses the same
|
||||
* PleskHostingService::provision() the admin "create hosting account" form
|
||||
* calls, so behavior (customer creation, subscription, notification emails)
|
||||
* stays identical between manual and automatic provisioning.
|
||||
*/
|
||||
class ProvisionHostingAccountJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/** Plesk subscription creation can take a couple of minutes */
|
||||
public int $timeout = 300;
|
||||
|
||||
/** No auto-retry — hosting provisioning errors need manual review */
|
||||
public int $tries = 1;
|
||||
|
||||
public function __construct(public OrderItem $item)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(PleskHostingService $plesk): void
|
||||
{
|
||||
$service = $this->item->service()->first();
|
||||
$domainName = $this->item->server_name;
|
||||
|
||||
if (! $service || ! $domainName) {
|
||||
Log::channel('Custom')->warning('ProvisionHostingAccountJob skipped: missing service or domain', [
|
||||
'order_item_id' => $this->item->id,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$server = DnsServer::query()
|
||||
->where('status', 'activated')
|
||||
->whereIn('type', [ServerType::HOSTING->value, ServerType::BOTH->value])
|
||||
->first();
|
||||
|
||||
if (! $server) {
|
||||
Log::channel('Custom')->error('ProvisionHostingAccountJob failed: no active hosting server available', [
|
||||
'order_item_id' => $this->item->id,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $plesk->provision([
|
||||
'customer_id' => $this->item->order->customer_id,
|
||||
'service_id' => $service->id,
|
||||
'dns_manager_server_id' => $server->id,
|
||||
'domain_name' => $domainName,
|
||||
'hosting_plan' => $this->item->product?->name ?? 'Default',
|
||||
]);
|
||||
|
||||
if ($result['error']) {
|
||||
Log::channel('Custom')->error('ProvisionHostingAccountJob failed', [
|
||||
'order_item_id' => $this->item->id,
|
||||
'message' => $result['message'] ?? 'Unknown error',
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$service->update(['status' => 'active']);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,14 @@ namespace App\Services;
|
||||
|
||||
use App\Models\EmailNotificationTemplate;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class EmailNotificationTemplateRenderer
|
||||
{
|
||||
private const BANNERS = [
|
||||
'vm_credentials' => 'https://res.cloudinary.com/izpast9i/image/upload/v1786503590/email_banner_cloud_vps.png',
|
||||
];
|
||||
|
||||
public function render(EmailNotificationTemplate $tpl, Invoice $invoice, string $companyName = ''): string
|
||||
{
|
||||
$blocks = collect($tpl->blocks)
|
||||
@@ -19,10 +24,9 @@ class EmailNotificationTemplateRenderer
|
||||
$bodyHtml .= $this->renderBlock($block, $invoice, $companyName);
|
||||
}
|
||||
|
||||
$color = htmlspecialchars($tpl->header_color ?? '#1a56db');
|
||||
$title = htmlspecialchars($tpl->header_title ?? 'Thông báo hóa đơn');
|
||||
$title = $tpl->header_title ?? 'Thông báo hóa đơn';
|
||||
|
||||
return $this->wrapLayout($title, $color, $bodyHtml, $companyName);
|
||||
return $this->wrapLayout($title, $bodyHtml, $companyName, self::BANNERS[$tpl->type] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,22 +46,21 @@ class EmailNotificationTemplateRenderer
|
||||
|
||||
foreach ($blocks as $block) {
|
||||
$bodyHtml .= match ($block['type'] ?? '') {
|
||||
'greeting' => $this->renderGreetingFromData($emailData),
|
||||
'vm_credentials' => $this->renderVmBlock($emailData, $block['config'] ?? []),
|
||||
'product_info' => $this->renderProductInfoBlock($emailData, $block['config'] ?? []),
|
||||
'invoice_info' => $invoice ? $this->renderInvoiceInfo($invoice, $block['config'] ?? []) : '',
|
||||
'greeting' => $this->renderView('emails.partials.greeting', ['name' => $emailData['client_name'] ?? 'Quý khách']),
|
||||
'vm_credentials' => $this->renderView('emails.partials.vm-credentials', ['fields' => $block['config']['fields'] ?? null, 'data' => $emailData]),
|
||||
'product_info' => $this->renderView('emails.partials.product-info', ['fields' => $block['config']['fields'] ?? null, 'data' => $emailData]),
|
||||
'invoice_info' => $invoice ? $this->renderView('emails.partials.invoice-info', ['fields' => $block['config']['fields'] ?? null, 'invoice' => $invoice]) : '',
|
||||
'order_items' => $invoice ? $this->renderOrderItems($invoice) : '',
|
||||
'attachments' => $invoice ? $this->renderAttachments($invoice) : '',
|
||||
'custom_text' => $this->renderCustomText($block['config'] ?? []),
|
||||
'footer' => $this->renderFooterBlock($companyName),
|
||||
'custom_text' => $this->renderView('emails.partials.custom-text', ['content' => $block['config']['content'] ?? '']),
|
||||
'footer' => $this->renderView('emails.partials.signature', ['companyName' => $companyName]),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
$color = htmlspecialchars($tpl->header_color ?? '#1a56db');
|
||||
$title = htmlspecialchars($tpl->header_title ?? 'Thông tin truy cập VPS');
|
||||
$title = $tpl->header_title ?? 'Thông tin truy cập VPS';
|
||||
|
||||
return $this->wrapLayout($title, $color, $bodyHtml, $companyName);
|
||||
return $this->wrapLayout($title, $bodyHtml, $companyName, self::BANNERS[$tpl->type] ?? null);
|
||||
}
|
||||
|
||||
public function renderPreview(EmailNotificationTemplate $tpl): string
|
||||
@@ -75,277 +78,49 @@ class EmailNotificationTemplateRenderer
|
||||
private function renderBlock(array $block, Invoice $invoice, string $companyName): string
|
||||
{
|
||||
return match ($block['type'] ?? '') {
|
||||
'greeting' => $this->renderGreeting($invoice),
|
||||
'invoice_info' => $this->renderInvoiceInfo($invoice, $block['config'] ?? []),
|
||||
'customer_info'=> $this->renderCustomerInfo($invoice, $block['config'] ?? []),
|
||||
'order_items' => $this->renderOrderItems($invoice),
|
||||
'attachments' => $this->renderAttachments($invoice),
|
||||
'custom_text' => $this->renderCustomText($block['config'] ?? []),
|
||||
'footer' => $this->renderFooterBlock($companyName),
|
||||
default => '',
|
||||
'greeting' => $this->renderView('emails.partials.greeting', ['name' => $invoice->customer?->fullname ?? 'Quý khách']),
|
||||
'invoice_info' => $this->renderView('emails.partials.invoice-info', ['fields' => $block['config']['fields'] ?? null, 'invoice' => $invoice]),
|
||||
'customer_info' => $this->renderView('emails.partials.customer-info', ['fields' => $block['config']['fields'] ?? null, 'customer' => $invoice->customer]),
|
||||
'order_items' => $this->renderOrderItems($invoice),
|
||||
'attachments' => $this->renderAttachments($invoice),
|
||||
'custom_text' => $this->renderView('emails.partials.custom-text', ['content' => $block['config']['content'] ?? '']),
|
||||
'footer' => $this->renderView('emails.partials.signature', ['companyName' => $companyName]),
|
||||
default => '',
|
||||
};
|
||||
}
|
||||
|
||||
private function renderGreeting(Invoice $invoice): string
|
||||
{
|
||||
$name = htmlspecialchars($invoice->customer?->fullname ?? 'Quý khách');
|
||||
return "<p>Xin chào <strong>{$name}</strong>,</p>\n";
|
||||
}
|
||||
|
||||
private function renderGreetingFromData(array $data): string
|
||||
{
|
||||
$name = htmlspecialchars($data['client_name'] ?? 'Quý khách');
|
||||
return "<p>Xin chào <strong>{$name}</strong>,</p>\n";
|
||||
}
|
||||
|
||||
private function renderVmBlock(array $data, array $config): string
|
||||
{
|
||||
$fields = $config['fields'] ?? ['service_host', 'service_username', 'service_port', 'service_password', 'os_name'];
|
||||
|
||||
$fieldMap = [
|
||||
'service_host' => ['Địa chỉ IP', $data['service_host'] ?? '—'],
|
||||
'service_username' => ['Username', $data['service_username'] ?? '—'],
|
||||
'service_port' => ['Port', (string)($data['service_port'] ?? '—')],
|
||||
'service_password' => ['Mật khẩu', $data['service_password'] ?? '—'],
|
||||
'os_name' => ['Hệ điều hành', $data['os_name'] ?? '—'],
|
||||
];
|
||||
|
||||
$rows = '';
|
||||
foreach ($fields as $key) {
|
||||
if (!isset($fieldMap[$key])) continue;
|
||||
[$label, $raw] = $fieldMap[$key];
|
||||
$value = htmlspecialchars($raw);
|
||||
// Highlight password with monospace
|
||||
$display = $key === 'service_password'
|
||||
? "<code style=\"background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:13px\">{$value}</code>"
|
||||
: $value;
|
||||
$rows .= "<tr><td style=\"padding:7px 0;font-size:14px;color:#6b7280;width:160px\">{$label}</td>"
|
||||
. "<td style=\"padding:7px 0;font-size:14px;font-weight:600;color:#111827\">{$display}</td></tr>\n";
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div style="background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:20px 24px;margin:20px 0">
|
||||
<p style="margin:0 0 12px;font-size:13px;font-weight:700;color:#0369a1;text-transform:uppercase;letter-spacing:.05em">🖥️ Thông tin truy cập VPS</p>
|
||||
<table style="width:100%;border-collapse:collapse">{$rows}</table>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function renderProductInfoBlock(array $data, array $config): string
|
||||
{
|
||||
$fields = $config['fields'] ?? ['product_name', 'product_billing', 'product_price', 'service_due_exp'];
|
||||
|
||||
$fieldMap = [
|
||||
'product_name' => ['Tên dịch vụ', $data['product_name'] ?? '—'],
|
||||
'product_billing' => ['Chu kỳ thanh toán', $data['product_billing'] ?? '—'],
|
||||
'product_price' => ['Giá dịch vụ', ($data['product_price'] ?? '0') . ' VNĐ'],
|
||||
'service_due_exp' => ['Ngày hết hạn', $data['service_due_exp'] ?? '—'],
|
||||
];
|
||||
|
||||
$rows = '';
|
||||
foreach ($fields as $key) {
|
||||
if (!isset($fieldMap[$key])) continue;
|
||||
[$label, $value] = $fieldMap[$key];
|
||||
$value = htmlspecialchars($value);
|
||||
$rows .= "<tr><td style=\"padding:6px 0;font-size:14px;color:#6b7280;width:160px\">{$label}</td>"
|
||||
. "<td style=\"padding:6px 0;font-size:14px;font-weight:600;color:#111827\">{$value}</td></tr>\n";
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:20px 24px;margin:20px 0">
|
||||
<p style="margin:0 0 12px;font-size:13px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.05em">Thông tin dịch vụ</p>
|
||||
<table style="width:100%;border-collapse:collapse">{$rows}</table>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function renderInvoiceInfo(Invoice $invoice, array $config): string
|
||||
{
|
||||
$fields = $config['fields'] ?? ['invoice_number', 'created_at', 'due_date', 'total', 'status'];
|
||||
|
||||
$rows = '';
|
||||
foreach ($fields as $field) {
|
||||
$rows .= $this->invoiceRow($field, $invoice);
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:20px 24px;margin:20px 0">
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
{$rows}
|
||||
</table>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function invoiceRow(string $field, Invoice $invoice): string
|
||||
{
|
||||
[$label, $value] = match ($field) {
|
||||
'invoice_number' => ['Số hóa đơn', '#' . ($invoice->invoice_number ?? '—')],
|
||||
'created_at' => ['Ngày tạo', $invoice->created_at ? $invoice->created_at->format('d/m/Y') : '—'],
|
||||
'due_date' => ['Hạn thanh toán', $invoice->due_date ? date('d/m/Y', strtotime($invoice->due_date)) : '—'],
|
||||
'total' => ['Tổng tiền', number_format((float)$invoice->total) . ' VNĐ'],
|
||||
'sub_total' => ['Tiền dịch vụ', number_format((float)$invoice->sub_total) . ' VNĐ'],
|
||||
'tax' => ['Thuế', number_format((float)$invoice->tax) . ' VNĐ'],
|
||||
'status' => ['Trạng thái', $this->statusBadge((int)$invoice->status)],
|
||||
'payment_method' => ['Phương thức TT', htmlspecialchars($invoice->payment_method ?? '—')],
|
||||
default => [$field, '—'],
|
||||
};
|
||||
|
||||
return "<tr><td style=\"padding:6px 0;font-size:14px;color:#6b7280;width:160px\">{$label}</td>"
|
||||
. "<td style=\"padding:6px 0;font-size:14px;font-weight:600;color:#111827\">{$value}</td></tr>\n";
|
||||
}
|
||||
|
||||
private function statusBadge(int $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
2 => '<span style="color:#059669">Đã thanh toán</span>',
|
||||
1 => '<span style="color:#dc2626">Chưa thanh toán</span>',
|
||||
default => '<span style="color:#9ca3af">Đã hủy</span>',
|
||||
};
|
||||
}
|
||||
|
||||
private function renderCustomerInfo(Invoice $invoice, array $config): string
|
||||
{
|
||||
$fields = $config['fields'] ?? ['name', 'email'];
|
||||
$customer = $invoice->customer;
|
||||
if (!$customer) return '';
|
||||
|
||||
$rows = '';
|
||||
foreach ($fields as $field) {
|
||||
[$label, $value] = match ($field) {
|
||||
'name' => ['Tên khách hàng', htmlspecialchars($customer->fullname ?? '—')],
|
||||
'email' => ['Email', htmlspecialchars($customer->email ?? '—')],
|
||||
'address' => ['Địa chỉ', htmlspecialchars($customer->address ?? '—')],
|
||||
default => [$field, '—'],
|
||||
};
|
||||
$rows .= "<tr><td style=\"padding:4px 0;font-size:14px;color:#6b7280;width:140px\">{$label}</td>"
|
||||
. "<td style=\"padding:4px 0;font-size:14px;color:#111827\">{$value}</td></tr>\n";
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div style="background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:16px 24px;margin:16px 0">
|
||||
<p style="margin:0 0 10px;font-size:13px;font-weight:600;color:#6b7280;text-transform:uppercase;letter-spacing:.05em">Thông tin khách hàng</p>
|
||||
<table style="width:100%;border-collapse:collapse">{$rows}</table>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function renderOrderItems(Invoice $invoice): string
|
||||
{
|
||||
$order = $invoice->order;
|
||||
if (!$order) return '';
|
||||
|
||||
$order->loadMissing('orderItems.product');
|
||||
$items = $order->orderItems;
|
||||
if ($items->isEmpty()) return '';
|
||||
|
||||
$cycleMap = [
|
||||
'monthly' => 'Hàng tháng', 'quarterly' => 'Hàng quý',
|
||||
'semi_annually' => '6 tháng', 'annually' => 'Hàng năm', 'biennially' => '2 năm',
|
||||
];
|
||||
|
||||
$rows = '';
|
||||
foreach ($items as $item) {
|
||||
$name = htmlspecialchars($item->product?->name ?? '—');
|
||||
$server = htmlspecialchars($item->server_name ?? '—');
|
||||
$cycle = htmlspecialchars($cycleMap[$item->billing_cycle] ?? ($item->billing_cycle ?? '—'));
|
||||
$price = number_format((float) $item->price) . ' VNĐ';
|
||||
|
||||
$rows .= "<tr style=\"border-top:1px solid #e5e7eb\">"
|
||||
. "<td style=\"padding:10px 12px;font-size:13px;font-weight:600;color:#111827\">{$name}</td>"
|
||||
. "<td style=\"padding:10px 12px;font-size:13px;color:#6b7280\">{$server}</td>"
|
||||
. "<td style=\"padding:10px 12px;font-size:13px;color:#6b7280\">{$cycle}</td>"
|
||||
. "<td style=\"padding:10px 12px;font-size:13px;font-weight:600;text-align:right\">{$price}</td>"
|
||||
. "</tr>\n";
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div style="margin:20px 0;border:1px solid #e5e7eb;border-radius:6px;overflow:hidden">
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<thead>
|
||||
<tr style="background:#f3f4f6">
|
||||
<th style="padding:10px 12px;text-align:left;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase">Sản phẩm</th>
|
||||
<th style="padding:10px 12px;text-align:left;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase">Server</th>
|
||||
<th style="padding:10px 12px;text-align:left;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase">Chu kỳ</th>
|
||||
<th style="padding:10px 12px;text-align:right;font-size:12px;color:#6b7280;font-weight:600;text-transform:uppercase">Giá</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{$rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
HTML;
|
||||
return $this->renderView('emails.partials.order-items', ['items' => $order->orderItems]);
|
||||
}
|
||||
|
||||
private function renderAttachments(Invoice $invoice): string
|
||||
{
|
||||
$invoice->loadMissing('attachments');
|
||||
if ($invoice->attachments->isEmpty()) return '';
|
||||
|
||||
$items = '';
|
||||
foreach ($invoice->attachments as $file) {
|
||||
$name = htmlspecialchars($file->filename);
|
||||
$url = htmlspecialchars($file->url ?? '#');
|
||||
$items .= "<li style=\"margin-bottom:6px;font-size:14px\"><a href=\"{$url}\" style=\"color:#1a56db;text-decoration:none\">{$name}</a></li>\n";
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<div style="margin:20px 0">
|
||||
<p style="font-size:14px;font-weight:600;margin:0 0 8px;color:#374151">File đính kèm</p>
|
||||
<ul style="padding-left:16px;margin:0">{$items}</ul>
|
||||
</div>
|
||||
HTML;
|
||||
return $this->renderView('emails.partials.attachments', ['attachments' => $invoice->attachments]);
|
||||
}
|
||||
|
||||
private function renderCustomText(array $config): string
|
||||
private function renderView(string $view, array $data): string
|
||||
{
|
||||
$content = $config['content'] ?? '';
|
||||
if (!$content) return '';
|
||||
|
||||
// If plain text (no HTML tags), wrap in <p>
|
||||
$html = strip_tags($content) === $content
|
||||
? '<p>' . htmlspecialchars($content) . '</p>'
|
||||
: $content;
|
||||
|
||||
return "<div style=\"margin:16px 0;font-size:14px;line-height:1.7;color:#374151\">{$html}</div>\n";
|
||||
}
|
||||
|
||||
private function renderFooterBlock(string $companyName): string
|
||||
{
|
||||
$company = htmlspecialchars($companyName ?: config('app.name', 'Công ty'));
|
||||
$year = date('Y');
|
||||
return "<p style=\"margin:20px 0 0\">Trân trọng,<br><strong>{$company}</strong></p>\n";
|
||||
return View::make($view, $data)->render();
|
||||
}
|
||||
|
||||
// ─── Layout wrapper ───────────────────────────────────────────────────────
|
||||
|
||||
private function wrapLayout(string $title, string $color, string $body, string $companyName): string
|
||||
private function wrapLayout(string $title, string $body, string $companyName, ?string $bannerUrl = null): string
|
||||
{
|
||||
$company = htmlspecialchars($companyName ?: config('app.name', 'Công ty'));
|
||||
$year = date('Y');
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>{$title}</title>
|
||||
</head>
|
||||
<body style="font-family:Arial,sans-serif;background:#f4f4f4;margin:0;padding:0">
|
||||
<div style="max-width:640px;margin:30px auto;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.08)">
|
||||
<div style="background:{$color};padding:24px 32px">
|
||||
<h1 style="color:#fff;margin:0;font-size:22px">{$title}</h1>
|
||||
</div>
|
||||
<div style="padding:32px;color:#374151">
|
||||
{$body}
|
||||
</div>
|
||||
<div style="background:#f9fafb;border-top:1px solid #e5e7eb;padding:20px 32px;text-align:center;font-size:13px;color:#9ca3af">
|
||||
© {$year} {$company}. Đây là email tự động, vui lòng không trả lời.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
return View::make('emails.layouts.notification', [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'companyName' => $companyName ?: config('app.name', 'Thế Giới Số'),
|
||||
'bannerUrl' => $bannerUrl,
|
||||
])->render();
|
||||
}
|
||||
|
||||
// ─── Sample data for preview ──────────────────────────────────────────────
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Enums\Order\OrderStatus;
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Jobs\Hosting\ProvisionHostingAccountJob;
|
||||
use App\Jobs\VirtualMachine\Action\VMJobs;
|
||||
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
|
||||
use App\Models\Addon;
|
||||
@@ -135,6 +136,10 @@ class InvoicePaymentService
|
||||
}
|
||||
|
||||
foreach ($order->orderItems as $item) {
|
||||
$isHostingProvision = ! $item->os_id
|
||||
&& $item->server_name
|
||||
&& $item->product?->product_type === 'hosting';
|
||||
|
||||
$service = Service::firstOrCreate(
|
||||
['order_item_id' => $item->id],
|
||||
[
|
||||
@@ -142,7 +147,7 @@ class InvoicePaymentService
|
||||
'product_id' => $item->product_id,
|
||||
'invoice_id' => $invoice->id,
|
||||
'server_name' => $item->server_name,
|
||||
'status' => $item->os_id ? 'pending' : 'active',
|
||||
'status' => ($item->os_id || $isHostingProvision) ? 'pending' : 'active',
|
||||
'auto_renew' => false,
|
||||
'started_at' => now(),
|
||||
'expires_at' => now()->addMonths($item->months ?: $this->cycleToMonths($item->billing_cycle)),
|
||||
@@ -151,8 +156,12 @@ class InvoicePaymentService
|
||||
|
||||
$this->attachServiceAddons($service, $item);
|
||||
|
||||
if ($service->wasRecentlyCreated && $item->product_id && $item->os_id) {
|
||||
dispatch(new CreateVirtualMachineJob($item));
|
||||
if ($service->wasRecentlyCreated && $item->product_id) {
|
||||
if ($item->os_id) {
|
||||
dispatch(new CreateVirtualMachineJob($item));
|
||||
} elseif ($isHostingProvision) {
|
||||
dispatch(new ProvisionHostingAccountJob($item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,27 @@ class PleskClientService
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return [
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'id' => $existing->plesk_client_id,
|
||||
'login' => $existing->login,
|
||||
'password' => Crypt::decryptString($existing->password),
|
||||
],
|
||||
];
|
||||
if ($this->clientExists($server, (int) $existing->plesk_client_id)) {
|
||||
return [
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'id' => $existing->plesk_client_id,
|
||||
'login' => $existing->login,
|
||||
'password' => Crypt::decryptString($existing->password),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Cached client was deleted directly on Plesk (out-of-band) — the id is
|
||||
// dead, so any call reusing it (e.g. owner_client on domain creation)
|
||||
// fails with "Owner does not exist". Drop the stale row and re-provision.
|
||||
Log::warning('Cached Plesk client no longer exists on server — re-creating', [
|
||||
'plesk_client_id' => $existing->plesk_client_id,
|
||||
'customer_id' => $customer->id,
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
|
||||
$existing->delete();
|
||||
}
|
||||
|
||||
$login = $this->generateUsername($customer);
|
||||
@@ -130,6 +143,20 @@ class PleskClientService
|
||||
}
|
||||
}
|
||||
|
||||
private function clientExists(DnsServer $server, int $pleskClientId): bool
|
||||
{
|
||||
try {
|
||||
$response = $this->plesk->client($server, 15)
|
||||
->get($this->plesk->url($server, 'clients', "/{$pleskClientId}"));
|
||||
|
||||
return $response->successful();
|
||||
} catch (RequestException $e) {
|
||||
Log::error('Plesk client existence check failed', ['error' => $e->getMessage(), 'plesk_client_id' => $pleskClientId]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function findByLogin(DnsServer $server, string $login): ?array
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user