feat: implement VM snapshot management, enhance client resource loading, and refactor deployment workflow for production
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Invoice;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Jobs\VirtualMachine\Provision\SendVmCredentialsEmailJob;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Setting;
|
||||
use App\Services\InvoicePdfService;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceTestController extends Controller
|
||||
{
|
||||
public function sendInvoiceEmail(Request $request, string $invoiceNumber): JsonResponse
|
||||
{
|
||||
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
|
||||
->where('invoice_number', $invoiceNumber)
|
||||
->first();
|
||||
|
||||
if (!$invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Không tìm thấy hóa đơn.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$customer = $invoice->customer;
|
||||
if (!$customer?->email) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Khách hàng không có email.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdfService = app(InvoicePdfService::class);
|
||||
$invoicePdf = $pdfService->generate($invoice);
|
||||
$invoicePdfFileName = $pdfService->generateFileName($invoice);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Lỗi tạo PDF: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
|
||||
$vm = ModuleVmware::with(['customer', 'os.osGroup', 'order.orderItems'])
|
||||
->where('order_id', $invoice->order_id)
|
||||
->first();
|
||||
|
||||
if ($vm) {
|
||||
SendVmCredentialsEmailJob::dispatch($vm->id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đã dispatch SendVmCredentialsEmailJob (có PDF hóa đơn đính kèm).',
|
||||
'data' => [
|
||||
'invoice_id' => $invoice->id,
|
||||
'invoice_number' => $invoice->invoice_number,
|
||||
'customer_email' => $customer->email,
|
||||
'vm_id' => $vm->id,
|
||||
'pdf_file_name' => $invoicePdfFileName,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value) : null;
|
||||
|
||||
$emailData = [
|
||||
'company_name' => $settingValue->company ?? 'Thế Giới Số',
|
||||
'company_logo' => $settingValue->logo ?? '',
|
||||
'client_name' => trim($customer->firstname . ' ' . $customer->lastname),
|
||||
'product_name' => $invoice->invoiceItems->first()?->product?->name ?? 'Dịch vụ',
|
||||
'product_billing' => 'N/A',
|
||||
'product_price' => number_format((float) $invoice->total, 0, ',', '.'),
|
||||
'service_host' => 'N/A',
|
||||
'service_username' => 'N/A',
|
||||
'service_port' => 'N/A',
|
||||
'service_password' => 'N/A',
|
||||
'os_name' => 'N/A',
|
||||
'service_due_exp' => $invoice->due_date ? $invoice->due_date->format('d/m/Y') : 'N/A',
|
||||
];
|
||||
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer, $invoicePdf, $invoicePdfFileName);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đã gửi email hóa đơn (không có VM liên kết, sử dụng thông tin hóa đơn).',
|
||||
'data' => [
|
||||
'invoice_id' => $invoice->id,
|
||||
'invoice_number' => $invoice->invoice_number,
|
||||
'customer_email' => $customer->email,
|
||||
'pdf_file_name' => $invoicePdfFileName,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function previewInvoicePdf(Request $request, string $invoiceNumber)
|
||||
{
|
||||
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
|
||||
->where('invoice_number', $invoiceNumber)
|
||||
->first();
|
||||
|
||||
if (!$invoice) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Không tìm thấy hóa đơn.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value) : null;
|
||||
|
||||
return Pdf::loadView('pdf.invoice', [
|
||||
'invoice' => $invoice,
|
||||
'invoiceItems' => $invoice->invoiceItems,
|
||||
'customer' => $invoice->customer,
|
||||
'order' => $invoice->order,
|
||||
'companyName' => $settingValue->company ?? 'Thế Giới Số',
|
||||
'companyAddress' => $settingValue->address ?? '',
|
||||
'companyPhone' => $settingValue->phone ?? '',
|
||||
'companyEmail' => $settingValue->email ?? '',
|
||||
])->stream($invoice->invoice_number . '.pdf');
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class ClientController extends Controller
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'client' => new ClientResource(auth('client')->user()),
|
||||
'client' => new ClientResource(auth('client')->user()->loadCount(['vps', 'hosting', 'domain'])),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ class ClientController extends Controller
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'client' => new ClientResource($result['client']),
|
||||
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
|
||||
'token' => $result['token'],
|
||||
'refresh_token' => $result['refresh_token'],
|
||||
'expires_in' => config('jwt.ttl', 60) * 60, // seconds
|
||||
@@ -185,7 +185,7 @@ class ClientController extends Controller
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'client' => new ClientResource($result['client']),
|
||||
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
|
||||
'token' => $result['token'],
|
||||
'refresh_token' => $result['refresh_token'],
|
||||
'expires_in' => config('jwt.ttl', 60) * 60,
|
||||
@@ -282,7 +282,7 @@ class ClientController extends Controller
|
||||
'token' => $result['token'],
|
||||
'refresh_token' => $result['refresh_token'],
|
||||
'expires_in' => config('jwt.ttl', 60) * 60,
|
||||
'client' => new ClientResource($result['client']),
|
||||
'client' => new ClientResource($result['client']->loadCount(['vps', 'hosting', 'domain'])),
|
||||
],
|
||||
]);
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
@@ -304,7 +304,7 @@ class ClientController extends Controller
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật thông tin thành công',
|
||||
'data' => [
|
||||
'client' => new ClientResource($user),
|
||||
'client' => new ClientResource($user->loadCount(['vps', 'hosting', 'domain'])),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\Api\Client\Service\ServiceClientDetailResource;
|
||||
use \App\Http\Resources\Api\Client\Service\ServiceClientResource;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\Order;
|
||||
use App\Models\Service;
|
||||
use App\Repositories\Api\ServiceClient\ServiceClientRepository;
|
||||
use App\Services\Api\ServiceClient\ServiceClientService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ServiceController extends Controller
|
||||
@@ -103,4 +105,61 @@ class ServiceController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function getTicketByOrderNum(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
{
|
||||
$orderNum = (string) $request->query('order_num', '');
|
||||
if ($orderNum === '') {
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu order_num'], 422);
|
||||
}
|
||||
|
||||
$customer = $request->user();
|
||||
$order = Order::with(['vmWare.vCenter'])
|
||||
->where('customer_id', $customer->id)
|
||||
->where('order_num', $orderNum)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy đơn hàng'], 404);
|
||||
}
|
||||
|
||||
$vm = $order->vmWare->first();
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Đơn hàng chưa có VM'], 404);
|
||||
}
|
||||
|
||||
$vcenter = $vm->vCenter;
|
||||
if (!$vcenter) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy thông tin vCenter'], 404);
|
||||
}
|
||||
|
||||
if (!$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vào vCenter']);
|
||||
}
|
||||
|
||||
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
|
||||
|
||||
if ($consoleTicket === 'Login_Again') {
|
||||
if ($serviceVCenter->loginServer($vcenter)) {
|
||||
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
|
||||
} else {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập lại vào vCenter']);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_string($consoleTicket)) {
|
||||
return response()->json(['error' => true, 'message' => "Failed to get console ticket: $consoleTicket"]);
|
||||
}
|
||||
|
||||
if ($consoleTicket['error']) {
|
||||
return response()->json(['error' => true, 'message' => $consoleTicket['data']]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $consoleTicket['data'],
|
||||
'order_num' => $order->order_num,
|
||||
'vm_id' => $vm->id,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Service\Concerns;
|
||||
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait ResolvesVm
|
||||
{
|
||||
protected function resolveVmById(int|string $productId, Request $request): ModuleVmware|JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$vm = null;
|
||||
|
||||
if (is_string($productId) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $productId)) {
|
||||
$service = Service::with('vmware.ipAddress.host.datacenter.vCenter', 'vmware.vCenter')
|
||||
->where('uuid', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
if ($service && $service->vmware) {
|
||||
$vm = $service->vmware;
|
||||
}
|
||||
} elseif (is_numeric($productId)) {
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->find((int) $productId);
|
||||
} else {
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->where('vmware_id', $productId)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
|
||||
if ((int) $vm->customer_id !== (int) $customer->id) {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
return $vm;
|
||||
}
|
||||
|
||||
protected function getConsoleTicket(\App\Http\Services\vCenter $serviceVCenter, ModuleVmware $vm): array|JsonResponse
|
||||
{
|
||||
$vcenter = $vm->vCenter;
|
||||
if (!$vcenter) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy thông tin vCenter'], 404);
|
||||
}
|
||||
|
||||
if (!$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vào vCenter']);
|
||||
}
|
||||
|
||||
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
|
||||
|
||||
if ($consoleTicket === 'Login_Again') {
|
||||
if ($serviceVCenter->loginServer($vcenter)) {
|
||||
$consoleTicket = $serviceVCenter->GetConsoleTickets($vm->vmware_id);
|
||||
} else {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập lại vào vCenter']);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_string($consoleTicket)) {
|
||||
return response()->json(['error' => true, 'message' => "Failed to get console ticket: $consoleTicket"]);
|
||||
}
|
||||
|
||||
if ($consoleTicket['error']) {
|
||||
return response()->json(['error' => true, 'message' => $consoleTicket['data']]);
|
||||
}
|
||||
|
||||
return $consoleTicket;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,14 @@ use App\Enums\Order\OrderStatus;
|
||||
use App\Helpers\OrderHelper;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\VirtualMachine\Action\VMJobs;
|
||||
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
|
||||
use App\Jobs\VirtualMachine\Provision\RetryProvisioningOrchestrator;
|
||||
use App\Models\Log as CustomerActivityLog;
|
||||
use App\Http\Requests\Api\Order\PayOrderRequest;
|
||||
use App\Http\Requests\Api\Order\StoreOrderRequest;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\ModuleVmware;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
@@ -194,6 +196,41 @@ class OrderController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /v1/client/orders/{id}/retry — retry VM provisioning for a paid order
|
||||
*
|
||||
* Dispatches RetryProvisioningOrchestrator which:
|
||||
* 1. Searches vSphere for existing VM by server_name
|
||||
* 2. If found → links VM to module_vmware + services
|
||||
* 3. If not found → dispatches CreateVirtualMachineJob
|
||||
* 4. Each failure step notifies Telegram
|
||||
*/
|
||||
public function retryOrderProvisioning(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$order = Order::where('id', $id)
|
||||
->where('customer_id', $request->user()->id)
|
||||
->first();
|
||||
|
||||
if (!$order) {
|
||||
return $this->notFound('Đơn hàng không tồn tại.');
|
||||
}
|
||||
|
||||
if (!$order->invoice || (int) $order->invoice->status !== InvoiceStatus::PAID->value) {
|
||||
return $this->error('Đơn hàng chưa được thanh toán.');
|
||||
}
|
||||
|
||||
RetryProvisioningOrchestrator::dispatch($order->id, $request->user()->id);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đã yêu cầu khởi tạo lại dịch vụ. Hệ thống sẽ kiểm tra và xử lý trong vài phút.',
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Invoices
|
||||
// =========================================================================
|
||||
@@ -305,15 +342,13 @@ class OrderController extends Controller
|
||||
{
|
||||
try {
|
||||
$services = Service::query()
|
||||
->with(['product', 'addons', 'invoice', 'orderItem'])
|
||||
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os'])
|
||||
->where('customer_id', $request->user()->id)
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
$data = $services->map(function (Service $service) {
|
||||
$vmware = $service->vm_id
|
||||
? ModuleVmware::with(['ipAddress', 'os'])->find((int) $service->vm_id)
|
||||
: null;
|
||||
$vmware = $service->vmware;
|
||||
|
||||
return [
|
||||
'id' => $service->id,
|
||||
@@ -357,7 +392,7 @@ class OrderController extends Controller
|
||||
{
|
||||
try {
|
||||
$service = Service::query()
|
||||
->with(['product', 'addons', 'invoice', 'orderItem'])
|
||||
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os', 'vmware.vCenter'])
|
||||
->where('uuid', $uuid)
|
||||
->where('customer_id', $request->user()->id)
|
||||
->first();
|
||||
@@ -366,9 +401,7 @@ class OrderController extends Controller
|
||||
return $this->notFound('Không tìm thấy dịch vụ.');
|
||||
}
|
||||
|
||||
$vmware = $service->vm_id
|
||||
? ModuleVmware::with(['ipAddress', 'os', 'vCenter'])->find((int) $service->vm_id)
|
||||
: null;
|
||||
$vmware = $service->vmware;
|
||||
|
||||
$data = [
|
||||
'id' => $service->id,
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Service;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Snapshot\CreateSnapshot;
|
||||
use App\Jobs\VirtualMachine\Snapshot\RevertSnapshot;
|
||||
use App\Jobs\VirtualMachine\Snapshot\RemoveSnapshot;
|
||||
use App\Models\ModuleSnapshot;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SnapshotController extends Controller
|
||||
{
|
||||
use ResolvesVm;
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
$vm = $this->resolveVmById($payload['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$today = now()->format('d-m-Y');
|
||||
$existingCount = ModuleSnapshot::where('module_vmware_id', $vm->id)
|
||||
->whereDate('created_at', today())
|
||||
->count();
|
||||
$snapshotName = sprintf('%s-%s-%d', $vm->name, $today, $existingCount + 1);
|
||||
|
||||
$snapshot = ModuleSnapshot::create([
|
||||
'name' => $snapshotName,
|
||||
'description' => $payload['description'] ?? null,
|
||||
'customer_id' => $customer->id,
|
||||
'module_vmware_id' => $vm->id,
|
||||
'status' => 'pending',
|
||||
'snapshot_ref' => null,
|
||||
]);
|
||||
|
||||
CreateSnapshot::dispatch($snapshot->id);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi yêu cầu tạo snapshot vào hàng đợi',
|
||||
'data' => [
|
||||
'id' => $snapshot->id,
|
||||
'name' => $snapshot->name,
|
||||
'description' => $snapshot->description,
|
||||
'status' => 'pending',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function index(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
{
|
||||
$productParam = $request->query('product');
|
||||
if (!$productParam) {
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVmById($productParam, $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$remote = $serviceVCenter->getSnapShot($vm);
|
||||
if (is_string($remote) || ($remote['error'] ?? true)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => is_string($remote) ? $remote : ($remote['message'] ?? 'Không thể lấy danh sách snapshot'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
$remoteItems = $remote['data'] ?? [];
|
||||
foreach ($remoteItems as $item) {
|
||||
$ref = (string) ($item['id'] ?? $item['name'] ?? '');
|
||||
$name = (string) ($item['name'] ?? $ref);
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$snapshot = ModuleSnapshot::query()
|
||||
->where('module_vmware_id', $vm->id)
|
||||
->where(function ($q) use ($ref, $name) {
|
||||
$q->where('snapshot_ref', $ref)->orWhere('name', $name);
|
||||
})
|
||||
->first();
|
||||
|
||||
$payload = [
|
||||
'name' => $name,
|
||||
'description' => $item['description'] ?? null,
|
||||
'customer_id' => $request->user()->id,
|
||||
'module_vmware_id' => $vm->id,
|
||||
'status' => 'success',
|
||||
'snapshot_ref' => $ref ?: $name,
|
||||
'size_gb' => isset($item['size_gb']) ? (float) $item['size_gb'] : null,
|
||||
'size_last_synced_at' => now(),
|
||||
];
|
||||
|
||||
if ($snapshot) {
|
||||
$snapshot->update($payload);
|
||||
} else {
|
||||
ModuleSnapshot::create($payload);
|
||||
}
|
||||
}
|
||||
|
||||
$dbSnapshots = ModuleSnapshot::query()
|
||||
->where('module_vmware_id', $vm->id)
|
||||
->where('customer_id', $request->user()->id)
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->map(function ($snap) {
|
||||
return [
|
||||
'id' => $snap->id,
|
||||
'name' => $snap->name,
|
||||
'description' => $snap->description,
|
||||
'snapshot_ref' => $snap->snapshot_ref,
|
||||
'status' => $snap->status,
|
||||
'size_gb' => $snap->size_gb,
|
||||
'created_at' => $snap->created_at?->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $dbSnapshots,
|
||||
]);
|
||||
}
|
||||
|
||||
public function revert(Request $request): JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required'],
|
||||
'snapshot_id' => ['required', 'numeric'],
|
||||
]);
|
||||
|
||||
$vm = $this->resolveVmById($payload['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$snapshot = ModuleSnapshot::where('id', $payload['snapshot_id'])
|
||||
->where('module_vmware_id', $vm->id)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$snapshot) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404);
|
||||
}
|
||||
|
||||
RevertSnapshot::dispatch($vm->id, $snapshot->id);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi yêu cầu revert snapshot vào hàng đợi',
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required'],
|
||||
'snapshot_id' => ['required', 'numeric'],
|
||||
]);
|
||||
|
||||
$vm = $this->resolveVmById($payload['product'], $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$snapshot = ModuleSnapshot::where('id', $payload['snapshot_id'])
|
||||
->where('module_vmware_id', $vm->id)
|
||||
->where('customer_id', $customer->id)
|
||||
->first();
|
||||
|
||||
if (!$snapshot) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy snapshot'], 404);
|
||||
}
|
||||
|
||||
RemoveSnapshot::dispatch($vm->id, $snapshot->id);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi yêu cầu xóa snapshot vào hàng đợi',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,10 @@ use App\Actions\VM\ChangeIpVMAction;
|
||||
use App\Actions\VM\ChangePortVMAction;
|
||||
use App\Actions\VM\PowerOffVMAction;
|
||||
use App\Actions\VM\PowerOnVMAction;
|
||||
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -16,6 +18,9 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class VmController extends Controller
|
||||
{
|
||||
use ResolvesVm {
|
||||
ResolvesVm::resolveVmById as resolveVmForClient;
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Power ON
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -240,6 +245,65 @@ class VmController extends Controller
|
||||
], 400);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Console Ticket
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function consoleTicket(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
{
|
||||
$productId = $request->get('product');
|
||||
if (!$productId) {
|
||||
return response()->json(['error' => true, 'message' => 'Please choose product']);
|
||||
}
|
||||
|
||||
$vm = ModuleVmware::with('vCenter')->find((int) $productId);
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => "Can't find product"]);
|
||||
}
|
||||
|
||||
$result = $this->getConsoleTicket($serviceVCenter, $vm);
|
||||
if ($result instanceof JsonResponse) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $result['data'],
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Restart VM
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function restart(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
{
|
||||
$productId = $request->input('product');
|
||||
if (!$productId) {
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$vm = $this->resolveVmForClient($productId, $request);
|
||||
if ($vm instanceof JsonResponse) {
|
||||
return $vm;
|
||||
}
|
||||
|
||||
$result = $serviceVCenter->restartVM($vm, $vm->vmware_id);
|
||||
|
||||
if (is_string($result) || ($result['error'] ?? true)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => is_string($result) ? $result : ($result['data'] ?? 'Không thể restart VM'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi lệnh restart VM',
|
||||
'data' => $result['data'] ?? 'success',
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Service;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Models\ModuleVmware;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class VmRebuildController extends Controller
|
||||
{
|
||||
use ResolvesVm;
|
||||
|
||||
public function options(Request $request): JsonResponse
|
||||
{
|
||||
$productId = (int) $request->query('product');
|
||||
if (!$productId) {
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$vm = ModuleVmware::with(['order.orderItems.product.osVersions.osGroup'])
|
||||
->find($productId);
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
if ((int) $vm->customer_id !== (int) $request->user()->id) {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$product = optional($vm->order?->orderItems?->first())->product;
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
|
||||
}
|
||||
|
||||
$osOptions = $product->osVersions
|
||||
->where('status', 1)
|
||||
->map(fn($os) => [
|
||||
'id' => $os->id,
|
||||
'name' => $os->name,
|
||||
'version' => $os->version,
|
||||
'group' => $os->osGroup?->name,
|
||||
])
|
||||
->values();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'product_id' => $product->id,
|
||||
'product_uuid' => $product->uuid,
|
||||
'product_name' => $product->name,
|
||||
'os_options' => $osOptions,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rebuild(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'os_id' => ['required', 'integer', 'exists:os,id'],
|
||||
]);
|
||||
|
||||
$vm = ModuleVmware::with(['order.orderItems.product', 'vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->find($payload['product']);
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
if ((int) $vm->customer_id !== (int) $customer->id) {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$product = optional($vm->order?->orderItems?->first())->product;
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
|
||||
}
|
||||
|
||||
$allowedOs = $product->osVersions()->where('status', 1)->pluck('id')->toArray();
|
||||
if (!in_array((int) $payload['os_id'], array_map('intval', $allowedOs), true)) {
|
||||
return response()->json(['error' => true, 'message' => 'OS không hợp lệ cho product hiện tại'], 422);
|
||||
}
|
||||
|
||||
DB::table('reinstall_logs')->insert([
|
||||
'product_id' => $vm->id,
|
||||
'product_type' => 4,
|
||||
'custome_id' => $customer->id,
|
||||
'os_id' => $payload['os_id'],
|
||||
'status' => 'pending',
|
||||
'initiator' => 'client',
|
||||
'note' => 'Client request rebuild from portal-client',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$result = $serviceVCenter->reBuildVPS((string) $vm->name);
|
||||
if (is_string($result) || ($result['error'] ?? false)) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => is_string($result) ? $result : ($result['message'] ?? 'Không thể rebuild VM'),
|
||||
], 400);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi yêu cầu rebuild VM',
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function addonOptions(Request $request): JsonResponse
|
||||
{
|
||||
$productId = (int) $request->query('product');
|
||||
if (!$productId) {
|
||||
return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422);
|
||||
}
|
||||
|
||||
$vm = ModuleVmware::with(['order.orderItems.product.addons'])
|
||||
->find($productId);
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
if ((int) $vm->customer_id !== (int) $request->user()->id) {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$product = optional($vm->order?->orderItems?->first())->product;
|
||||
if (!$product) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404);
|
||||
}
|
||||
|
||||
$addons = $product->addons()->where('status', true)->get()->map(function ($addon) {
|
||||
return [
|
||||
'id' => $addon->id,
|
||||
'code' => strtolower((string) $addon->code),
|
||||
'name' => $addon->name,
|
||||
'price' => (float) $addon->price,
|
||||
'min' => (int) ($addon->min ?? 0),
|
||||
'max' => (int) ($addon->max ?? 0),
|
||||
'step' => (int) ($addon->step ?? 1),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'product_id' => $product->id,
|
||||
'product_name' => $product->name,
|
||||
'base_specs' => [
|
||||
'cpu' => (int) ($product->cpu ?? 0),
|
||||
'ram' => (int) ($product->ram ?? 0),
|
||||
'disk' => (int) ($product->disk ?? 0),
|
||||
'bandwidth' => (int) ($product->bandwidth ?? 0),
|
||||
'gpu' => $product->gpu,
|
||||
],
|
||||
'addons' => $addons->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function upgradeAddons(Request $request, vCenter $serviceVCenter): JsonResponse
|
||||
{
|
||||
$customer = $request->user();
|
||||
$payload = $request->validate([
|
||||
'product' => ['required', 'integer'],
|
||||
'cpu' => ['nullable', 'integer', 'min:0'],
|
||||
'ram' => ['nullable', 'integer', 'min:0'],
|
||||
'disk' => ['nullable', 'integer', 'min:0'],
|
||||
'bandwidth' => ['nullable', 'integer', 'min:0'],
|
||||
'gpu' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
|
||||
->find($payload['product']);
|
||||
if (!$vm) {
|
||||
return response()->json(['error' => true, 'message' => 'Không tìm thấy VM'], 404);
|
||||
}
|
||||
if ((int) $vm->customer_id !== (int) $customer->id) {
|
||||
return response()->json(['error' => true, 'message' => 'Bạn không có quyền thao tác VM này'], 403);
|
||||
}
|
||||
|
||||
$vcenter = $vm->vCenter ?? $vm->ipAddress?->host?->datacenter?->vCenter;
|
||||
if (!$vcenter || !$serviceVCenter->loginServer($vcenter)) {
|
||||
return response()->json(['error' => true, 'message' => 'Không thể đăng nhập vCenter'], 400);
|
||||
}
|
||||
|
||||
$applied = [];
|
||||
|
||||
if (isset($payload['cpu']) && $payload['cpu'] > 0) {
|
||||
$res = $serviceVCenter->UpgradeCPUVirtualMachine($vm->vmware_id, (int) $payload['cpu']);
|
||||
if (is_array($res) && !($res['error'] ?? true)) {
|
||||
$applied[] = 'cpu';
|
||||
}
|
||||
}
|
||||
if (isset($payload['ram']) && $payload['ram'] > 0) {
|
||||
$res = $serviceVCenter->UpgradeMemoryVirtualMachine($vm->vmware_id, (int) $payload['ram']);
|
||||
if (is_array($res) && !($res['error'] ?? true)) {
|
||||
$applied[] = 'ram';
|
||||
}
|
||||
}
|
||||
if (isset($payload['disk']) && $payload['disk'] > 0) {
|
||||
$res = $serviceVCenter->UpgradeDiskVirtualMachine($vm, (int) $payload['disk']);
|
||||
if (is_array($res) && !($res['error'] ?? true)) {
|
||||
$applied[] = 'disk';
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi yêu cầu nâng cấp addons',
|
||||
'data' => [
|
||||
'applied' => $applied,
|
||||
'note' => 'bandwidth/gpu hiện được ghi nhận yêu cầu, chưa có luồng tự động áp vào VM',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Repositories\Setting\SettingCachedRepository;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
if (!function_exists('setting')) {
|
||||
function setting(string $key = '')
|
||||
{
|
||||
// Cách gọi setting('setting') để lấy toàn bộ group json
|
||||
$repo = app()->make(\App\Repositories\Setting\SettingCachedRepository::class);
|
||||
|
||||
// Nếu có dấu chấm => lấy theo group
|
||||
if (Str::contains($key, '.')) {
|
||||
[$group, $subKey] = explode('.', $key, 2);
|
||||
$groupData = $repo->getByKey($group);
|
||||
@@ -30,26 +27,28 @@ if (!function_exists('setting')) {
|
||||
if (!function_exists('format_price')) {
|
||||
function format_price($price, $suffix = '₫')
|
||||
{
|
||||
if ($price === null)
|
||||
if ($price === null) {
|
||||
return '';
|
||||
}
|
||||
return number_format($price, 0, ',', '.') . ' ' . $suffix;
|
||||
}
|
||||
}
|
||||
if (!function_exists('format_point')) {
|
||||
function format_point($point, $suffix = '𝑝')
|
||||
{
|
||||
if ($point === null)
|
||||
if ($point === null) {
|
||||
return '';
|
||||
}
|
||||
return number_format($point, 0, ',', '.') . ' ' . $suffix;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!function_exists('format_day')) {
|
||||
function format_day($date, $format = 'd/m/Y')
|
||||
{
|
||||
if (empty($date))
|
||||
if (empty($date)) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return \Carbon\Carbon::parse($date)->format($format);
|
||||
} catch (\Exception $e) {
|
||||
@@ -83,7 +82,7 @@ if (!function_exists('generate_order_num')) {
|
||||
function generate_order_num(string $prefix = 'TGS', int $length = 6): string
|
||||
{
|
||||
do {
|
||||
$randomNumber = random_int(10 ** ($length - 1), (10 ** $length) - 1);
|
||||
$randomNumber = random_int(10 ** ($length - 1), 10 ** $length - 1);
|
||||
$orderNum = $prefix . $randomNumber;
|
||||
} while (\App\Models\Order::where('order_num', $orderNum)->exists());
|
||||
|
||||
@@ -104,4 +103,4 @@ function is_active_route($path)
|
||||
function show_class($path)
|
||||
{
|
||||
return request()->is(...(array) $path) ? 'show' : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ class ClientResource extends JsonResource
|
||||
'point_total' => $this->point,
|
||||
],
|
||||
'service' => [
|
||||
'vps' => $this->vps()->count(),
|
||||
'vps' => $this->vps_count ?? $this->vps()->count(),
|
||||
'proxy' => 0,
|
||||
'server' => 0,
|
||||
'hosting' => $this->hosting()->count(),
|
||||
'domain' => $this->domain()->count(),
|
||||
'hosting' => $this->hosting_count ?? $this->hosting()->count(),
|
||||
'domain' => $this->domain_count ?? $this->domain()->count(),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -103,6 +103,39 @@ class vCenter
|
||||
return "Login_Again";
|
||||
}
|
||||
|
||||
/**
|
||||
* Search VM by exact name in vCenter.
|
||||
* Returns the first matching VM array or null if not found.
|
||||
*/
|
||||
public function findVirtualMachineByName(string $vmName): ?array
|
||||
{
|
||||
$check = $this->checkApiExits();
|
||||
if (!$check) {
|
||||
Log::warning('findVirtualMachineByName: API check failed');
|
||||
return null;
|
||||
}
|
||||
|
||||
$response = Http::withoutVerifying()
|
||||
->withHeaders([
|
||||
'vmware-api-session-id' => $this->token
|
||||
])
|
||||
->get("https://$this->apiUri/rest/vcenter/vm", [
|
||||
'filter.names.1' => $vmName,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::warning('findVirtualMachineByName: HTTP failed', ['status' => $response->status()]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$json = $response->json();
|
||||
if (isset($json['type']) || empty($json['value'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $json['value'][0] ?? null;
|
||||
}
|
||||
|
||||
public function checkApiExits(): bool
|
||||
{
|
||||
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -19,27 +19,23 @@ class SendInformationVirtualMachine implements ShouldQueue
|
||||
|
||||
public $data = [];
|
||||
public $customer;
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($data = [],$customer = [])
|
||||
|
||||
protected ?string $invoicePdf;
|
||||
protected ?string $invoicePdfFileName;
|
||||
|
||||
public function __construct($data = [], $customer = [], ?string $invoicePdf = null, ?string $invoicePdfFileName = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->customer = $customer;
|
||||
$this->invoicePdf = $invoicePdf;
|
||||
$this->invoicePdfFileName = $invoicePdfFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
|
||||
$email = new CreateVirtualMachine(2,$this->data);
|
||||
$email = new CreateVirtualMachine(2, $this->data, $this->invoicePdf, $this->invoicePdfFileName);
|
||||
|
||||
Mail::to($this->customer->email)->send($email);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class AssignIpToGuestJob implements ShouldQueue
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
public function handle(VsphereHostSshService $ssh): void
|
||||
public function handle(VsphereHostSshService $ssh, vCenter $vCenterService): void
|
||||
{
|
||||
Log::info('AssignIpToGuestJob: start', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
@@ -198,6 +198,38 @@ class AssignIpToGuestJob implements ShouldQueue
|
||||
]);
|
||||
|
||||
ModuleVmware::where('id', $this->moduleVmwareId)->update(['status' => 0]);
|
||||
|
||||
// Still send credential email — try to get IP from vSphere guest identity
|
||||
// so the customer at least gets server info even if IP assignment failed
|
||||
try {
|
||||
$moduleVmware = ModuleVmware::find($this->moduleVmwareId);
|
||||
if ($moduleVmware && !$moduleVmware->ip) {
|
||||
$vcenter = $moduleVmware->vCenter ?? vCenter::first();
|
||||
if ($vcenter && $vCenterService->loginServer($vcenter)) {
|
||||
$guestInfo = $vCenterService->GetIPAddressVirtualMachine($this->vmId);
|
||||
if (isset($guestInfo['error']) && $guestInfo['error'] === false) {
|
||||
$guestIp = $guestInfo['data']['ip_address'] ?? null;
|
||||
if ($guestIp) {
|
||||
$moduleVmware->update(['ip' => $guestIp, 'status' => 1]);
|
||||
Log::info('AssignIpToGuestJob: recovered IP from guest identity', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'ip' => $guestIp,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SendVmCredentialsEmailJob::dispatch($this->moduleVmwareId);
|
||||
Log::info('AssignIpToGuestJob: dispatched SendVmCredentialsEmailJob after failed attempts', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('AssignIpToGuestJob: failed to send email after exhausted attempts', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function failed(\Throwable $e): void
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Http\Services\Telegram;
|
||||
use App\Http\Services\vCenter;
|
||||
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Os;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Service;
|
||||
use App\Models\vCenter as VcenterModel;
|
||||
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\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* RetryProvisioningOrchestrator
|
||||
*
|
||||
* Orchestrator Job — retries VM provisioning for a paid order.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Load order_items by order_id → get server_name
|
||||
* 2. Resolve vcenter via DB chain (os → osGroup → host → datacenter → vcenter)
|
||||
* 3. Login vCenter, search VM by server_name
|
||||
* 4. If VM EXISTS in vSphere → link vm-id to module_vmware + services
|
||||
* → dispatch AssignIpToGuestJob (if no IP) + SendVmCredentialsEmailJob
|
||||
* 5. If VM NOT in vSphere → dispatch CreateVirtualMachineJob
|
||||
* 6. Each failure step notifies Telegram with specific error info
|
||||
*/
|
||||
class RetryProvisioningOrchestrator implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 1;
|
||||
public int $timeout = 300;
|
||||
|
||||
public function __construct(
|
||||
public readonly int $orderId,
|
||||
public readonly int $customerId,
|
||||
) {}
|
||||
|
||||
public function handle(vCenter $vCenterService, Telegram $telegram): void
|
||||
{
|
||||
Log::info('RetryProvisioningOrchestrator: start', [
|
||||
'order_id' => $this->orderId,
|
||||
'customer_id' => $this->customerId,
|
||||
]);
|
||||
|
||||
$orderItems = OrderItem::where('order_id', $this->orderId)->get();
|
||||
|
||||
if ($orderItems->isEmpty()) {
|
||||
$this->failStep('order_items', 'Không tìm thấy order_items cho order #' . $this->orderId, $telegram);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($orderItems as $item) {
|
||||
$this->processItem($item, $vCenterService, $telegram);
|
||||
}
|
||||
}
|
||||
|
||||
private function processItem(OrderItem $item, vCenter $vCenterService, Telegram $telegram): void
|
||||
{
|
||||
$serverName = $item->server_name;
|
||||
|
||||
if (empty($serverName)) {
|
||||
$this->failStep('order_item', "OrderItem #{$item->id} không có server_name", $telegram);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Already linked and has IP? Skip ─────────────────────────────
|
||||
$existingService = Service::where('order_item_id', $item->id)->whereNull('deleted_at')->first();
|
||||
if ($existingService && $existingService->vm_id) {
|
||||
$vmware = ModuleVmware::find($existingService->vm_id);
|
||||
if ($vmware && $vmware->ip) {
|
||||
Log::info('RetryProvisioningOrchestrator: already linked with IP, skipping', [
|
||||
'order_item_id' => $item->id,
|
||||
'service_id' => $existingService->id,
|
||||
'vm_id' => $existingService->vm_id,
|
||||
'ip' => $vmware->ip,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 1: Resolve vcenter from DB chain ────────────────────────
|
||||
$vcenterModel = $this->resolveVcenter($item, $telegram);
|
||||
|
||||
if (!$vcenterModel) {
|
||||
$vcenterModel = VcenterModel::first();
|
||||
if (!$vcenterModel) {
|
||||
$this->failStep('vcenter', 'Không tìm thấy vCenter nào trong hệ thống cho order_item #' . $item->id, $telegram);
|
||||
return;
|
||||
}
|
||||
Log::warning('RetryProvisioningOrchestrator: fallback to first vcenter', ['vcenter_id' => $vcenterModel->id]);
|
||||
}
|
||||
|
||||
// ── Step 2: Login vCenter and search VM ─────────────────────────
|
||||
$loginResult = $vCenterService->loginServer($vcenterModel);
|
||||
if (!$loginResult) {
|
||||
$this->failStep('vcenter_login', 'Không thể đăng nhập vào vCenter ' . $vcenterModel->host . ' cho VM ' . $serverName, $telegram);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search by REST API first
|
||||
$vm = $vCenterService->findVirtualMachineByName($serverName);
|
||||
|
||||
// If REST search fails, try listing all VMs and filter manually
|
||||
if (!$vm) {
|
||||
Log::info('RetryProvisioningOrchestrator: REST search returned null, trying list-all fallback', [
|
||||
'server_name' => $serverName,
|
||||
]);
|
||||
$allVms = $vCenterService->getAllVirtualMachine();
|
||||
if (is_array($allVms)) {
|
||||
foreach ($allVms as $candidate) {
|
||||
if (isset($candidate['name']) && $candidate['name'] === $serverName) {
|
||||
$vm = $candidate;
|
||||
Log::info('RetryProvisioningOrchestrator: found VM via list-all fallback', [
|
||||
'server_name' => $serverName,
|
||||
'vm_id' => $candidate['vm'] ?? null,
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($vm) {
|
||||
// ── VM EXISTS → link to DB + assign IP + send email ────────────
|
||||
$vmId = $vm['vm'] ?? null;
|
||||
if (!$vmId) {
|
||||
$this->failStep('vm_search', 'Tìm thấy VM nhưng không lấy được vm_id cho ' . $serverName, $telegram);
|
||||
return;
|
||||
}
|
||||
|
||||
Log::info('RetryProvisioningOrchestrator: VM found in vSphere', [
|
||||
'server_name' => $serverName,
|
||||
'vm_id' => $vmId,
|
||||
'vcenter' => $vcenterModel->host,
|
||||
]);
|
||||
|
||||
try {
|
||||
$moduleVmware = $this->linkVmToDb($item, $vmId, $vcenterModel, $serverName);
|
||||
|
||||
// Determine OS family for AssignIpToGuestJob
|
||||
$osModel = $item->os_id ? Os::find($item->os_id) : null;
|
||||
$osFamily = $this->resolveOsFamily($osModel);
|
||||
|
||||
// If VM has no IP yet → dispatch AssignIpToGuestJob
|
||||
if (!$moduleVmware->ip) {
|
||||
AssignIpToGuestJob::dispatch(
|
||||
moduleVmwareId: $moduleVmware->id,
|
||||
vmId: $vmId,
|
||||
osFamily: $osFamily,
|
||||
guestPassword: $moduleVmware->plain_password ?? \App\Services\VmProvisionService::TEMPLATE_PASSWORD,
|
||||
);
|
||||
Log::info('RetryProvisioningOrchestrator: dispatched AssignIpToGuestJob', [
|
||||
'module_vmware_id' => $moduleVmware->id,
|
||||
'vm_id' => $vmId,
|
||||
]);
|
||||
} else {
|
||||
// Already has IP → just send credential email
|
||||
SendVmCredentialsEmailJob::dispatch($moduleVmware->id);
|
||||
Log::info('RetryProvisioningOrchestrator: dispatched SendVmCredentialsEmailJob', [
|
||||
'module_vmware_id' => $moduleVmware->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$telegram->SendMessage(
|
||||
"✅ Phục hồi VM thành công\n"
|
||||
. "━━━━━━━━━━━━━━━━━━━━\n"
|
||||
. "🖥️ Tên VM: {$serverName}\n"
|
||||
. "🔑 VM ID: {$vmId}\n"
|
||||
. "📦 Đơn hàng: #{$this->orderId}\n"
|
||||
. "🌐 Có IP: " . ($moduleVmware->ip ? 'Có (' . $moduleVmware->ip . ')' : 'Chưa có — đang gán IP') . "\n"
|
||||
. "⏱️ Thời gian: " . now()->format('d/m/Y H:i:s')
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->failStep('link_vm', 'Lỗi liên kết VM ' . $serverName . ': ' . $e->getMessage(), $telegram);
|
||||
}
|
||||
} else {
|
||||
// ── VM NOT in vSphere → dispatch CreateVirtualMachineJob ──────
|
||||
Log::info('RetryProvisioningOrchestrator: VM not found in vSphere, dispatching CreateVirtualMachineJob', [
|
||||
'server_name' => $serverName,
|
||||
'order_item_id' => $item->id,
|
||||
]);
|
||||
|
||||
CreateVirtualMachineJob::dispatch($item, true);
|
||||
|
||||
$telegram->SendMessage(
|
||||
"🆕 Tạo VM mới\n"
|
||||
. "━━━━━━━━━━━━━━━━━━━━\n"
|
||||
. "🖥️ Tên VM: {$serverName}\n"
|
||||
. "📦 Đơn hàng: #{$this->orderId}\n"
|
||||
. "📝 VM không có trong vSphere, đã gửi job tạo mới\n"
|
||||
. "⏱️ Thời gian: " . now()->format('d/m/Y H:i:s')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve vcenter from DB chain:
|
||||
* Path 1: order_items.os_id → os → osGroup → host → datacenter → vcenter
|
||||
* Path 2: invoices → invoice_items → product_id → os_product → os → osGroup → host → datacenter → vcenter
|
||||
* Path 3: module_vmware with same order_id (partial provisioning)
|
||||
*/
|
||||
private function resolveVcenter(OrderItem $item, Telegram $telegram): ?VcenterModel
|
||||
{
|
||||
if ($item->os_id) {
|
||||
$os = Os::with('osGroup.host.datacenter.vCenter')->find($item->os_id);
|
||||
$vcenter = $os?->osGroup?->host?->datacenter?->vCenter;
|
||||
if ($vcenter) {
|
||||
Log::info('resolveVcenter: found via os → osGroup chain', ['vcenter_id' => $vcenter->id]);
|
||||
return $vcenter;
|
||||
}
|
||||
}
|
||||
|
||||
$invoice = Invoice::where('order_id', $item->order_id)->first();
|
||||
if ($invoice) {
|
||||
$invoiceItem = $invoice->invoiceItems()->whereNotNull('product_id')->first();
|
||||
if ($invoiceItem) {
|
||||
$osRecord = DB::table('os_product')
|
||||
->where('product_id', $invoiceItem->product_id)
|
||||
->first();
|
||||
|
||||
if ($osRecord) {
|
||||
$os = Os::with('osGroup.host.datacenter.vCenter')->find($osRecord->os_id);
|
||||
$vcenter = $os?->osGroup?->host?->datacenter?->vCenter;
|
||||
if ($vcenter) {
|
||||
Log::info('resolveVcenter: found via invoice_items → os_product chain', ['vcenter_id' => $vcenter->id]);
|
||||
return $vcenter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$existingVmware = ModuleVmware::where('order_id', $item->order_id)->first();
|
||||
if ($existingVmware?->vcenter_id) {
|
||||
$vcenter = VcenterModel::find($existingVmware->vcenter_id);
|
||||
if ($vcenter) {
|
||||
Log::info('resolveVcenter: found via existing module_vmware', ['vcenter_id' => $vcenter->id]);
|
||||
return $vcenter;
|
||||
}
|
||||
}
|
||||
|
||||
$this->failStep('resolve_vcenter', 'Không thể tìm vcenter cho order_item #' . $item->id, $telegram);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link an existing VM from vSphere to module_vmware + services tables.
|
||||
* Returns the ModuleVmware record.
|
||||
*/
|
||||
private function linkVmToDb(OrderItem $item, string $vmId, VcenterModel $vcenterModel, string $serverName): ModuleVmware
|
||||
{
|
||||
$order = $item->order;
|
||||
$customer = $order->customer;
|
||||
|
||||
$months = (int) ($item->months ?? 0);
|
||||
$firstPayment = (int) $item->price * ($months ?: 1);
|
||||
$nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null;
|
||||
|
||||
$moduleVmware = ModuleVmware::where('vmware_id', $vmId)
|
||||
->where('vcenter_id', $vcenterModel->id)
|
||||
->first();
|
||||
|
||||
if (!$moduleVmware) {
|
||||
$moduleVmware = ModuleVmware::create([
|
||||
'name' => $serverName,
|
||||
'module_ip_address' => null,
|
||||
'ip' => null,
|
||||
'port' => 3389,
|
||||
'order_id' => $order->id,
|
||||
'first_payment' => $firstPayment,
|
||||
'next_payment' => $firstPayment,
|
||||
'next_due_date' => $nextDueDate,
|
||||
'next_invoice' => $nextDueDate,
|
||||
'user' => 'Administrator',
|
||||
'password' => \App\Services\VmProvisionService::TEMPLATE_PASSWORD,
|
||||
'plain_password' => \App\Services\VmProvisionService::TEMPLATE_PASSWORD,
|
||||
'hostname' => $serverName,
|
||||
'vmware_id' => $vmId,
|
||||
'vcenter_id' => $vcenterModel->id,
|
||||
'os_id' => $item->os_id ?? 1,
|
||||
'customer_id' => $customer->id,
|
||||
'status' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
$service = Service::withTrashed()->firstOrNew([
|
||||
'order_item_id' => $item->id,
|
||||
]);
|
||||
|
||||
if ($service->trashed()) {
|
||||
$service->restore();
|
||||
}
|
||||
|
||||
$service->fill([
|
||||
'customer_id' => $customer->id,
|
||||
'product_id' => $item->product_id,
|
||||
'invoice_id' => $order->invoice?->id,
|
||||
'vm_id' => $moduleVmware->id,
|
||||
'server_name' => $serverName,
|
||||
'status' => 'active',
|
||||
'started_at' => now(),
|
||||
'expires_at' => $months > 0 ? now()->addMonths($months) : null,
|
||||
]);
|
||||
|
||||
if (!$service->exists && !$service->uuid) {
|
||||
$service->uuid = (string) \Illuminate\Support\Str::uuid();
|
||||
}
|
||||
|
||||
$service->save();
|
||||
|
||||
if (!$moduleVmware->service_id) {
|
||||
$moduleVmware->update(['service_id' => $service->id]);
|
||||
}
|
||||
|
||||
Log::info('RetryProvisioningOrchestrator: VM linked to DB', [
|
||||
'module_vmware_id' => $moduleVmware->id,
|
||||
'service_id' => $service->id,
|
||||
'vmware_id' => $vmId,
|
||||
]);
|
||||
|
||||
return $moduleVmware;
|
||||
}
|
||||
|
||||
private function resolveOsFamily(?Os $os): string
|
||||
{
|
||||
if (!$os) {
|
||||
return 'LINUX';
|
||||
}
|
||||
|
||||
$groupName = strtolower($os->osGroup?->name ?? '');
|
||||
if (str_contains($groupName, 'windows')) {
|
||||
return 'WINDOWS';
|
||||
}
|
||||
|
||||
return 'LINUX';
|
||||
}
|
||||
|
||||
private function failStep(string $step, string $message, Telegram $telegram): void
|
||||
{
|
||||
Log::error("RetryProvisioningOrchestrator [{$step}]: {$message}", [
|
||||
'order_id' => $this->orderId,
|
||||
]);
|
||||
|
||||
$telegram->SendErrorMessage(
|
||||
"Bước: {$step}\n"
|
||||
. "Đơn hàng: #{$this->orderId}\n"
|
||||
. "Lỗi: {$message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Jobs\VirtualMachine\Provision;
|
||||
|
||||
use App\Jobs\Mail\SendInformationVirtualMachine;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\ModuleVmware;
|
||||
use App\Models\Setting;
|
||||
use App\Services\InvoicePdfService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -17,7 +19,7 @@ use Illuminate\Support\Facades\Log;
|
||||
/**
|
||||
* SendVmCredentialsEmailJob
|
||||
*
|
||||
* Sends the final VM credential email to the customer.
|
||||
* Sends the final VM credential email to the customer with an attached PDF invoice.
|
||||
* Always loads fresh data from DB so it gets the real IP and
|
||||
* the final password saved by ChangePasswordJob.
|
||||
*
|
||||
@@ -80,12 +82,35 @@ class SendVmCredentialsEmailJob implements ShouldQueue
|
||||
: 'N/A',
|
||||
];
|
||||
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer);
|
||||
// Generate invoice PDF attachment (synchronous, no queue)
|
||||
$invoicePdf = null;
|
||||
$invoicePdfFileName = null;
|
||||
|
||||
$invoice = Invoice::where('order_id', $vm->order_id)
|
||||
->with(['invoiceItems.product', 'customer', 'order.orderItems'])
|
||||
->first();
|
||||
|
||||
if ($invoice) {
|
||||
try {
|
||||
$pdfService = app(InvoicePdfService::class);
|
||||
$invoicePdf = $pdfService->generate($invoice);
|
||||
$invoicePdfFileName = $pdfService->generateFileName($invoice);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('SendVmCredentialsEmailJob: failed to generate invoice PDF', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'invoice_id' => $invoice->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
SendInformationVirtualMachine::dispatch($emailData, $customer, $invoicePdf, $invoicePdfFileName);
|
||||
|
||||
Log::info('SendVmCredentialsEmailJob: email dispatched', [
|
||||
'module_vmware_id' => $this->moduleVmwareId,
|
||||
'customer_email' => $customer->email,
|
||||
'ip' => $vm->ip,
|
||||
'invoice_attached' => $invoicePdf !== null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Mail;
|
||||
use App\Models\Template;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Attachment;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
@@ -16,22 +17,17 @@ class CreateVirtualMachine extends Mailable
|
||||
protected $template = null;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($template = 0, $data = [])
|
||||
protected ?string $invoicePdf = null;
|
||||
protected ?string $invoicePdfFileName = null;
|
||||
|
||||
public function __construct($template = 0, $data = [], ?string $invoicePdf = null, ?string $invoicePdfFileName = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->template = Template::find($template);
|
||||
$this->invoicePdf = $invoicePdf;
|
||||
$this->invoicePdfFileName = $invoicePdfFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*
|
||||
* @return Envelope
|
||||
*/
|
||||
public function envelope()
|
||||
{
|
||||
return new Envelope(
|
||||
@@ -39,11 +35,6 @@ class CreateVirtualMachine extends Mailable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*
|
||||
* @return Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
return new Content(
|
||||
@@ -83,13 +74,15 @@ class CreateVirtualMachine extends Mailable
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function attachments()
|
||||
public function attachments(): array
|
||||
{
|
||||
if ($this->invoicePdf !== null && $this->invoicePdfFileName !== null) {
|
||||
return [
|
||||
Attachment::fromData(fn () => $this->invoicePdf, $this->invoicePdfFileName)
|
||||
->mime('application/pdf'),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+10
-5
@@ -33,6 +33,7 @@ class Product extends Model
|
||||
'is_active',
|
||||
'sort_order',
|
||||
'product_categories_id',
|
||||
'module_ip_address',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
@@ -130,6 +131,11 @@ class Product extends Model
|
||||
* Relationship: Product has many addons (through product type)
|
||||
* Addons are linked via type in product_addon table
|
||||
*/
|
||||
public function ipAddress()
|
||||
{
|
||||
return $this->belongsTo(ModuleIpAddress::class, 'module_ip_address');
|
||||
}
|
||||
|
||||
public function addons()
|
||||
{
|
||||
return $this->hasMany(Addon::class, 'type', 'product_type')
|
||||
@@ -141,11 +147,10 @@ class Product extends Model
|
||||
*/
|
||||
public function getMonthlyPricingAttribute()
|
||||
{
|
||||
return $this->pricings()
|
||||
->where('is_active', 1)
|
||||
->where('billing_cycle', 'monthly')
|
||||
->first()
|
||||
?? $this->pricings()->where('is_active', 1)->orderBy('price')->first();
|
||||
$pricings = $this->pricings;
|
||||
|
||||
return $pricings->firstWhere('billing_cycle', 'monthly')
|
||||
?? $pricings->where('is_active', 1)->sortBy('price')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Observers\ModuleIpAddressObserver;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -57,10 +56,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
\App\Repositories\Employee\EmployeeRepositoryInterface::class,
|
||||
\App\Repositories\Employee\EmployeeRepository::class
|
||||
);
|
||||
|
||||
if (File::exists(base_path('app/Http/Helpers/function_helper.php'))) {
|
||||
File::requireOnce(base_path('app/Http/Helpers/function_helper.php'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -119,282 +119,132 @@ class RepositoryProvider extends ServiceProvider
|
||||
public function register()
|
||||
{
|
||||
//Addon
|
||||
$this->app->bind(
|
||||
AddonRepositoryInterface::class,
|
||||
AddonCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(AddonRepositoryInterface::class, AddonCachedRepository::class);
|
||||
//Banned
|
||||
$this->app->bind(
|
||||
BannedRepositoryInterface::class,
|
||||
BannedCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(BannedRepositoryInterface::class, BannedCachedRepository::class);
|
||||
//Customer
|
||||
$this->app->bind(
|
||||
CustomerRepositoryInterface::class,
|
||||
CustomerCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(CustomerRepositoryInterface::class, CustomerCachedRepository::class);
|
||||
//Customer
|
||||
$this->app->bind(
|
||||
CustomerRepositoryInterface::class,
|
||||
CustomerCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(CustomerCachedRepository::class);
|
||||
//Customer Log
|
||||
$this->app->bind(
|
||||
CustomerLogRepositoryInterface::class,
|
||||
CustomerLogCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(CustomerLogRepositoryInterface::class, CustomerLogCachedRepository::class);
|
||||
//Discount
|
||||
$this->app->bind(
|
||||
DiscountRepositoryInterface::class,
|
||||
DiscountCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(DiscountRepositoryInterface::class, DiscountCachedRepository::class);
|
||||
//Domain
|
||||
$this->app->bind(
|
||||
DomainRepositoryInterface::class,
|
||||
DomainCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(DomainRepositoryInterface::class, DomainCachedRepository::class);
|
||||
//Domain Type
|
||||
$this->app->bind(
|
||||
DomainTypeRepositoryInterface::class,
|
||||
DomainTypeCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(DomainTypeRepositoryInterface::class, DomainTypeCachedRepository::class);
|
||||
//File
|
||||
$this->app->bind(
|
||||
FileRepositoryInterface::class,
|
||||
FileCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(FileRepositoryInterface::class, FileCachedRepository::class);
|
||||
//Group
|
||||
$this->app->bind(
|
||||
GroupRepositoryInterface::class,
|
||||
GroupCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(GroupRepositoryInterface::class, GroupCachedRepository::class);
|
||||
//Hosting
|
||||
$this->app->bind(
|
||||
HostingRepositoryInterface::class,
|
||||
HostingCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(HostingRepositoryInterface::class, HostingCachedRepository::class);
|
||||
//Invoice
|
||||
$this->app->bind(
|
||||
InvoiceRepositoryInterface::class,
|
||||
InvoiceCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(InvoiceRepositoryInterface::class, InvoiceCachedRepository::class);
|
||||
//Invoice Attachment
|
||||
$this->app->bind(
|
||||
InvoiceAttachmentRepositoryInterface::class,
|
||||
InvoiceAttachmentRepository::class
|
||||
);
|
||||
$this->app->singleton(InvoiceAttachmentRepositoryInterface::class, InvoiceAttachmentRepository::class);
|
||||
//Invoice Item
|
||||
$this->app->bind(
|
||||
InvoiceItemRepositoryInterface::class,
|
||||
InvoiceItemCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(InvoiceItemRepositoryInterface::class, InvoiceItemCachedRepository::class);
|
||||
//Log
|
||||
$this->app->bind(
|
||||
LogRepositoryInterface::class,
|
||||
LogCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(LogRepositoryInterface::class, LogCachedRepository::class);
|
||||
//Mail
|
||||
$this->app->bind(
|
||||
MailRepositoryInterface::class,
|
||||
MailCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(MailRepositoryInterface::class, MailCachedRepository::class);
|
||||
//Note
|
||||
$this->app->bind(
|
||||
NoteRepositoryInterface::class,
|
||||
NoteCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(NoteRepositoryInterface::class, NoteCachedRepository::class);
|
||||
//Order
|
||||
$this->app->bind(
|
||||
OrderRepositoryInterface::class,
|
||||
OrderCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(OrderRepositoryInterface::class, OrderCachedRepository::class);
|
||||
//Os
|
||||
$this->app->bind(
|
||||
OsRepositoryInterface::class,
|
||||
OsCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(OsRepositoryInterface::class, OsCachedRepository::class);
|
||||
// Os Group
|
||||
$this->app->bind(
|
||||
OsGroupRepositoryInterface::class,
|
||||
OsGroupCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(OsGroupRepositoryInterface::class, OsGroupCachedRepository::class);
|
||||
// Payment
|
||||
$this->app->bind(
|
||||
PaymentReppositoryInterface::class,
|
||||
PaymentCachedReppository::class
|
||||
);
|
||||
$this->app->singleton(PaymentReppositoryInterface::class, PaymentCachedReppository::class);
|
||||
// Menu
|
||||
$this->app->bind(
|
||||
MenuRepositoryInterface::class,
|
||||
MenuCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(MenuRepositoryInterface::class, MenuCachedRepository::class);
|
||||
//Post
|
||||
$this->app->bind(
|
||||
PostRepositoryInterface::class,
|
||||
PostCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(PostRepositoryInterface::class, PostCachedRepository::class);
|
||||
//Setting
|
||||
$this->app->bind(
|
||||
SettingRepositoryInterface::class,
|
||||
SettingCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(SettingRepositoryInterface::class, SettingCachedRepository::class);
|
||||
//Staff
|
||||
$this->app->bind(
|
||||
StaffRepositoryInterface::class,
|
||||
StaffRepository::class
|
||||
);
|
||||
$this->app->singleton(StaffRepositoryInterface::class, StaffRepository::class);
|
||||
//Template
|
||||
$this->app->bind(
|
||||
TemplateRepositoryInterface::class,
|
||||
TemplateCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(TemplateRepositoryInterface::class, TemplateCachedRepository::class);
|
||||
//Ticket
|
||||
$this->app->bind(
|
||||
TicketRepositoryInterface::class,
|
||||
TicketCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(TicketRepositoryInterface::class, TicketCachedRepository::class);
|
||||
//Ticket Reply
|
||||
$this->app->bind(
|
||||
TicketReplyRepositoryInterface::class,
|
||||
TicketReplyCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(TicketReplyRepositoryInterface::class, TicketReplyCachedRepository::class);
|
||||
//Transaction
|
||||
$this->app->bind(
|
||||
TransactionRepositoryInterface::class,
|
||||
TransactionCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(TransactionRepositoryInterface::class, TransactionCachedRepository::class);
|
||||
//Service
|
||||
$this->app->bind(
|
||||
ServiceRepositoryInterface::class,
|
||||
ServiceRepository::class
|
||||
);
|
||||
$this->app->singleton(ServiceRepositoryInterface::class, ServiceRepository::class);
|
||||
//Module Datacenter
|
||||
$this->app->bind(
|
||||
DatacenterRepositoryInterface::class,
|
||||
DatacenterCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(DatacenterRepositoryInterface::class, DatacenterCachedRepository::class);
|
||||
//Module Datastore
|
||||
$this->app->bind(
|
||||
DatastoreRepositoryInterface::class,
|
||||
DatastoreCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(DatastoreRepositoryInterface::class, DatastoreCachedRepository::class);
|
||||
// Module DnsServer
|
||||
$this->app->bind(
|
||||
DnsServerRepositoryInterface::class,
|
||||
DnsServerCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(DnsServerRepositoryInterface::class, DnsServerCachedRepository::class);
|
||||
//Module Domain
|
||||
$this->app->bind(
|
||||
ModuleDomainRepositoryInterface::class,
|
||||
ModuleDomainCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(ModuleDomainRepositoryInterface::class, ModuleDomainCachedRepository::class);
|
||||
//Module Folder
|
||||
$this->app->bind(
|
||||
FolderRepositoryInterface::class,
|
||||
FolderCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(FolderRepositoryInterface::class, FolderCachedRepository::class);
|
||||
//Host
|
||||
$this->app->bind(
|
||||
HostRepositoryInterface::class,
|
||||
HostCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(HostRepositoryInterface::class, HostCachedRepository::class);
|
||||
//Hosting
|
||||
$this->app->bind(
|
||||
ModuleHostingRepositoryInterface::class,
|
||||
ModuleHostingCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(ModuleHostingRepositoryInterface::class, ModuleHostingCachedRepository::class);
|
||||
//Hosting Server
|
||||
$this->app->bind(
|
||||
HostingServerRepositoryInterface::class,
|
||||
HostingServerCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(HostingServerRepositoryInterface::class, HostingServerCachedRepository::class);
|
||||
//IpAddress
|
||||
$this->app->bind(
|
||||
IpAddressRepositoryInterface::class,
|
||||
IpAddressCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(IpAddressRepositoryInterface::class, IpAddressCachedRepository::class);
|
||||
//Network
|
||||
$this->app->bind(
|
||||
NetworkRepositoryInterface::class,
|
||||
NetworkCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(NetworkRepositoryInterface::class, NetworkCachedRepository::class);
|
||||
//Snapshot
|
||||
$this->app->bind(
|
||||
SnapshotRepositoryInterface::class,
|
||||
SnapshotCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(SnapshotRepositoryInterface::class, SnapshotCachedRepository::class);
|
||||
//Snapshot Limit
|
||||
$this->app->bind(
|
||||
SnapshotLimitRepositoryInterface::class,
|
||||
SnapshotLimitCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(SnapshotLimitRepositoryInterface::class, SnapshotLimitCachedRepository::class);
|
||||
//vmware
|
||||
$this->app->bind(
|
||||
VmwareRepositoryInterface::class,
|
||||
VmwareCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(VmwareRepositoryInterface::class, VmwareCachedRepository::class);
|
||||
//User
|
||||
$this->app->bind(
|
||||
UserRepositoryInterface::class,
|
||||
UserCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(UserRepositoryInterface::class, UserCachedRepository::class);
|
||||
//vCenter
|
||||
$this->app->bind(
|
||||
vCenterRepositoryInterface::class,
|
||||
vCenterCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(vCenterRepositoryInterface::class, vCenterCachedRepository::class);
|
||||
//vps
|
||||
$this->app->bind(
|
||||
VPSRepositoryInterface::class,
|
||||
VPSCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(VPSRepositoryInterface::class, VPSCachedRepository::class);
|
||||
|
||||
//Module IP Pool
|
||||
$this->app->bind(
|
||||
IPPoolRepositoryInterface::class,
|
||||
IPPoolCachedRepository::class
|
||||
);
|
||||
$this->app->singleton(IPPoolRepositoryInterface::class, IPPoolCachedRepository::class);
|
||||
|
||||
//Module VirtualMachine
|
||||
$this->app->bind(
|
||||
VirtualMachineRepositoryInterface::class,
|
||||
VirtualMachineRepository::class
|
||||
);
|
||||
$this->app->singleton(VirtualMachineRepositoryInterface::class, VirtualMachineRepository::class);
|
||||
|
||||
// Product
|
||||
$this->app->bind(
|
||||
ProductRepositoryInterface::class,
|
||||
ProductRepository::class
|
||||
);
|
||||
$this->app->singleton(ProductRepositoryInterface::class, ProductRepository::class);
|
||||
|
||||
// Product Category
|
||||
$this->app->bind(
|
||||
ProductCategoryRepositoryInterface::class,
|
||||
ProductCategoryRepository::class
|
||||
);
|
||||
$this->app->singleton(ProductCategoryRepositoryInterface::class, ProductCategoryRepository::class);
|
||||
|
||||
// Client API
|
||||
$this->app->bind(
|
||||
$this->app->singleton(
|
||||
\App\Repositories\Api\Client\ClientRepositoryInterface::class,
|
||||
\App\Repositories\Api\Client\ClientRepository::class
|
||||
\App\Repositories\Api\Client\ClientRepository::class,
|
||||
);
|
||||
|
||||
// Service API
|
||||
$this->app->bind(
|
||||
$this->app->singleton(
|
||||
\App\Repositories\Api\Service\ServiceRepositoryInterface::class,
|
||||
\App\Repositories\Api\Service\ServiceRepository::class
|
||||
\App\Repositories\Api\Service\ServiceRepository::class,
|
||||
);
|
||||
|
||||
// Service Client API
|
||||
$this->app->bind(
|
||||
$this->app->singleton(
|
||||
\App\Repositories\Api\ServiceClient\ServiceClientRepositoryInterface::class,
|
||||
\App\Repositories\Api\ServiceClient\ServiceClientRepository::class
|
||||
\App\Repositories\Api\ServiceClient\ServiceClientRepository::class,
|
||||
);
|
||||
|
||||
// DNS Manager
|
||||
$this->app->bind(DnsManagerServerRepositoryInterface::class, DnsManagerServerRepository::class);
|
||||
$this->app->bind(DnsDomainRepositoryInterface::class, DnsDomainRepository::class);
|
||||
$this->app->bind(DnsRecordRepositoryInterface::class, DnsRecordRepository::class);
|
||||
$this->app->singleton(DnsManagerServerRepositoryInterface::class, DnsManagerServerRepository::class);
|
||||
$this->app->singleton(DnsDomainRepositoryInterface::class, DnsDomainRepository::class);
|
||||
$this->app->singleton(DnsRecordRepositoryInterface::class, DnsRecordRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,28 +38,20 @@ class RouteServiceProvider extends ServiceProvider
|
||||
$this->configureRateLimiting();
|
||||
|
||||
$this->routes(function () {
|
||||
// 1. Agency API — domain riêng → chỉ client.php
|
||||
Route::domain(config('app.api_agency_url'))
|
||||
->middleware(['api', 'json.response'])
|
||||
Route::middleware(['api', 'json.response'])
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/client.php'));
|
||||
|
||||
// 2. System API — bind về APP_URL
|
||||
Route::domain(config('app.url'))
|
||||
->prefix('api')
|
||||
Route::prefix('api')
|
||||
->middleware(['api', 'json.response'])
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/api.php'));
|
||||
|
||||
// 3. Admin Backend routes
|
||||
Route::domain(config('app.url'))
|
||||
->middleware('web')
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/admin.php'));
|
||||
|
||||
// 4. Web routes
|
||||
Route::domain(config('app.url'))
|
||||
->middleware('web')
|
||||
Route::middleware('web')
|
||||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web.php'));
|
||||
});
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Setting;
|
||||
use Barryvdh\DomPDF\Facade\Pdf;
|
||||
|
||||
class InvoicePdfService
|
||||
{
|
||||
public function generate(Invoice $invoice): string
|
||||
{
|
||||
$invoice->load(['invoiceItems.product', 'customer', 'order.orderItems']);
|
||||
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value) : null;
|
||||
|
||||
$customer = $invoice->customer;
|
||||
$order = $invoice->order;
|
||||
|
||||
$data = [
|
||||
'invoice' => $invoice,
|
||||
'invoiceItems' => $invoice->invoiceItems,
|
||||
'customer' => $customer,
|
||||
'order' => $order,
|
||||
'companyName' => $settingValue->company ?? 'Thế Giới Số',
|
||||
'companyAddress' => $settingValue->address ?? '',
|
||||
'companyPhone' => $settingValue->phone ?? '',
|
||||
'companyEmail' => $settingValue->email ?? '',
|
||||
];
|
||||
|
||||
return Pdf::loadView('pdf.invoice', $data)->output();
|
||||
}
|
||||
|
||||
public function generateFileName(Invoice $invoice): string
|
||||
{
|
||||
return 'Hoa-don-' . $invoice->invoice_number . '.pdf';
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Reference in New Issue
Block a user