query('product'); if (!$productId) { return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); } $vm = $this->resolveVmById($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $vm->loadMissing(['order.orderItems.product.osVersions.osGroup']); $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, VmRebuildService $rebuildService): JsonResponse { $customer = $request->user(); $payload = $request->validate([ 'product' => ['required'], 'os_id' => ['required', 'integer', 'exists:os,id'], 'password' => ['nullable', 'string', 'min:8', 'max:255'], ]); $vm = $this->resolveVmById($payload['product'], $request); if ($vm instanceof JsonResponse) { return $vm; } $vm->loadMissing(['order.orderItems.product', 'vCenter', 'ipAddress.host.datacenter.vCenter']); $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); } try { $log = $rebuildService->dispatchRebuild( moduleVmwareId: (int) $vm->id, newOsId: (int) $payload['os_id'], initiatorCustomerId: (int) $customer->id, customPassword: !empty($payload['password']) ? (string) $payload['password'] : null, initiator: 'client', note: 'Client request rebuild from portal-client', ); } catch (Throwable $e) { Log::error('VmRebuildController: dispatchRebuild failed', [ 'module_vmware_id' => $vm->id, 'new_os_id' => $payload['os_id'], 'error' => $e->getMessage(), ]); return response()->json([ 'error' => true, 'message' => 'Không thể gửi yêu cầu rebuild, vui lòng thử lại sau.', ], 400); } $this->activityLogger->log( event: 'rebuild', logName: 'VM', description: "Customer {$customer->email} rebuild VM: {$vm->name} với OS ID: {$payload['os_id']}", subject: $vm, newValues: ['os_id' => $payload['os_id']], ); return response()->json([ 'error' => false, 'message' => 'Đã gửi yêu cầu rebuild VM', 'data' => [ 'module_vmware_id' => $vm->id, 'new_os_id' => (int) $payload['os_id'], 'reinstall_log_id' => $log->id, 'status' => $log->status, ], ]); } public function addonOptions(Request $request): JsonResponse { $productId = $request->query('product'); if (!$productId) { return response()->json(['error' => true, 'message' => 'Thiếu product id'], 422); } $vm = $this->resolveVmById($productId, $request); if ($vm instanceof JsonResponse) { return $vm; } $vm->loadMissing(['order.orderItems.product.addons']); $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, 'type' => $addon->type instanceof \BackedEnum ? $addon->type->value : (string) $addon->type, '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), 'prefix' => $addon->prefix, 'input_type' => $addon->input_type ?? 'number', 'sort_order' => (int) ($addon->sort_order ?? 0), ]; }); return response()->json([ 'error' => false, 'data' => $addons->values(), 'meta' => [ '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, ], ], ]); } private const INVOICE_TYPE_UPGRADE = 3; public function upgradeAddons(Request $request): JsonResponse { $customer = $request->user(); $payload = $request->validate([ 'product' => ['required'], 'cpu' => ['nullable', 'integer', 'min:0'], 'ram' => ['nullable', 'integer', 'min:0'], 'disk' => ['nullable', 'integer', 'min:0'], 'bandwidth' => ['nullable', 'integer', 'min:0'], 'gpu' => ['nullable', 'integer', 'min:0'], ]); $vm = $this->resolveVmById($payload['product'], $request); if ($vm instanceof JsonResponse) { return $vm; } $vm->loadMissing(['order.orderItems.product']); $orderItem = $vm->order?->orderItems?->first(); $product = $orderItem?->product; if (!$orderItem || !$product) { return response()->json(['error' => true, 'message' => 'Không tìm thấy product của dịch vụ hiện tại'], 404); } $service = Service::where('order_item_id', $orderItem->id)->first(); if (!$service) { return response()->json(['error' => true, 'message' => 'Không tìm thấy dịch vụ hiện tại'], 404); } $requestedQtyByCode = array_filter([ 'cpu' => (int) ($payload['cpu'] ?? 0), 'ram' => (int) ($payload['ram'] ?? 0), 'disk' => (int) ($payload['disk'] ?? 0), 'bandwidth' => (int) ($payload['bandwidth'] ?? 0), 'gpu' => (int) ($payload['gpu'] ?? 0), ], fn ($qty) => $qty > 0); if (empty($requestedQtyByCode)) { return response()->json(['error' => true, 'message' => 'Vui lòng chọn ít nhất một addon để nâng cấp'], 422); } $activeAddons = $product->addons()->where('status', true)->get()->keyBy('id'); $addonsByCode = $activeAddons->keyBy(fn ($addon) => strtolower((string) $addon->code)); $raw = []; foreach ($requestedQtyByCode as $code => $qty) { $addon = $addonsByCode->get($code); if (!$addon) { return response()->json(['error' => true, 'message' => "Addon \"{$code}\" không áp dụng cho sản phẩm này"], 422); } $raw[] = ['addon_id' => $addon->id, 'qty' => $qty]; } $existingQtyByAddonId = collect(is_array($orderItem->addons_meta) ? $orderItem->addons_meta : []) ->mapWithKeys(fn ($entry) => [(int) ($entry['addon_id'] ?? 0) => (int) ($entry['qty'] ?? 0)]) ->all(); try { $resolved = AddonQuantityValidator::resolve($raw, $activeAddons, $existingQtyByAddonId); } catch (ValidationException $e) { return response()->json([ 'error' => true, 'message' => collect($e->errors())->flatten()->first() ?? 'Yêu cầu nâng cấp không hợp lệ', ], 422); } $existingInvoice = Invoice::where('service_id', $service->id) ->where('invoice_type', self::INVOICE_TYPE_UPGRADE) ->whereIn('status', InvoicePaymentService::PAYABLE_STATUSES) ->first(); if ($existingInvoice) { return response()->json([ 'error' => false, 'message' => 'Dịch vụ đã có hóa đơn nâng cấp đang chờ thanh toán.', 'data' => $this->invoiceResponse($existingInvoice, $vm->order?->order_num), ]); } $cycleMonths = OrderHelper::cycleToMonths($orderItem->billing_cycle); $cycleDays = max(1, $cycleMonths * 30); $remainingDays = $service->expires_at && $service->expires_at->isFuture() ? now()->diffInDays($service->expires_at) : 0; $remainingDays = (int) max(1, min($cycleDays, $remainingDays)); try { $invoice = DB::transaction(function () use ($service, $orderItem, $resolved, $remainingDays, $cycleDays) { $total = 0; $lines = []; foreach ($resolved as $entry) { $unitPrice = $this->prorateUnitPrice((float) $entry['price'], $remainingDays, $cycleDays); $totalPrice = $unitPrice * $entry['qty']; $total += $totalPrice; $lines[] = [ 'product_id' => $orderItem->product_id, 'addon_id' => $entry['addon_id'], 'type' => 'upgrade', 'description' => 'Nâng cấp addon: ' . $entry['name'] . ' x' . $entry['qty'], 'months' => 0, 'quantity' => $entry['qty'], 'unit_price' => $unitPrice, 'total_price' => $totalPrice, ]; } $invoice = Invoice::create([ 'invoice_number' => OrderHelper::generateOrderNumber('INV'), 'customer_id' => $service->customer_id, 'order_id' => $orderItem->order_id, 'service_id' => $service->id, 'invoice_type' => self::INVOICE_TYPE_UPGRADE, 'sub_total' => $total, 'discount_amount' => 0, 'tax' => 0, 'total' => $total, 'status' => InvoiceStatus::UNPAID->value, 'due_date' => now()->addDays(3)->toDateString(), ]); foreach ($lines as $line) { InvoiceItem::create($line + ['invoice_id' => $invoice->id]); } return $invoice; }); } catch (Throwable $e) { Log::error('VmRebuildController: upgradeAddons invoice creation failed', [ 'module_vmware_id' => $vm->id, 'service_id' => $service->id, 'error' => $e->getMessage(), ]); return response()->json(['error' => true, 'message' => 'Không thể tạo yêu cầu nâng cấp, vui lòng thử lại sau.'], 500); } $this->activityLogger->log( event: 'upgrade_addons_requested', logName: 'VM', description: "Customer {$customer->email} yêu cầu nâng cấp addons VM: {$vm->name}, hóa đơn #{$invoice->invoice_number}", subject: $vm, newValues: ['invoice_id' => $invoice->id, 'addons' => $resolved], ); return response()->json([ 'error' => false, 'message' => 'Đã tạo hóa đơn nâng cấp. Vui lòng thanh toán để áp dụng vào VM.', 'data' => $this->invoiceResponse($invoice, $vm->order?->order_num), ]); } private function prorateUnitPrice(float $price, int $remainingDays, int $cycleDays): int { $raw = $price * $remainingDays / $cycleDays; return (int) (ceil($raw / 1000) * 1000); } private function invoiceResponse(Invoice $invoice, ?string $orderNum): array { return [ 'order_id' => $invoice->order_id, 'order_num' => $orderNum, 'invoice_id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'total' => (float) $invoice->total, 'status' => $invoice->status, 'payment_url' => $orderNum ? '/hoa-don/' . $orderNum : null, ]; } }