365 lines
14 KiB
PHP
365 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Service;
|
|
|
|
use App\Enums\Invoice\InvoiceStatus;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Controllers\Api\Service\Concerns\ResolvesVm;
|
|
use App\Helpers\OrderHelper;
|
|
use App\Models\Invoice;
|
|
use App\Models\InvoiceItem;
|
|
use App\Models\Service;
|
|
use App\Services\ActivityLogger;
|
|
use App\Services\InvoicePaymentService;
|
|
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
|
|
use App\Support\AddonQuantityValidator;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Throwable;
|
|
|
|
class VmRebuildController extends Controller
|
|
{
|
|
use ResolvesVm;
|
|
|
|
public function __construct(protected ActivityLogger $activityLogger) {}
|
|
|
|
public function options(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.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,
|
|
];
|
|
}
|
|
}
|