diff --git a/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php b/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php index af01622..9d39a98 100644 --- a/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php +++ b/app/Http/Resources/Api/Client/Service/ServiceClientDetailResource.php @@ -20,7 +20,7 @@ class ServiceClientDetailResource extends JsonResource $orderItem = $this->orderItem; $order = $orderItem?->order; - $vm = $order?->vmWare?->first(); + $vm = $this->resource->vmware; $product = $orderItem?->product; return [ @@ -52,9 +52,11 @@ class ServiceClientDetailResource extends JsonResource 'ip_address' => $vm->ipAddress?->ip, 'hostname' => $vm->hostname, 'username' => $vm->user, - 'password' => $vm->getRawOriginal('password'), + 'password' => $vm->plain_password ?? null, 'port' => $vm->port, 'status' => $vm->status, + 'ip' => $vm->ip ?? null, + 'dns' => $vm->dns ?? null, ] : null, ]; } diff --git a/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php b/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php index abe8402..37c2db5 100644 --- a/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php +++ b/app/Jobs/VirtualMachine/Create/ConfigCLICommand.php @@ -43,7 +43,10 @@ class ConfigCLICommand implements ShouldQueue try { $vm = \App\Models\ModuleVmware::where('vmware_id', $this->vmware_id)->first(); if ($vm) { - $vm->update(['password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine)]); + $vm->update([ + 'password' => \App\Http\Facade\SignRSA::EncryptFunc($PasswordVirtualMachine), + 'plain_password' => $PasswordVirtualMachine, + ]); } } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu password: ' . $e->getMessage()); diff --git a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php index 5cddf14..ed9ffc7 100644 --- a/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php +++ b/app/Jobs/VirtualMachine/Create/CreateVirtualMachineJob.php @@ -14,6 +14,7 @@ use App\Models\ModuleVmware; use App\Models\ModuleIpAddress; use App\Models\IpPool; use App\Models\Log as CustomerLog; +use App\Models\Service; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -231,28 +232,33 @@ class CreateVirtualMachineJob implements ShouldQueue $firstPayment = (int) $this->item->price * ($months ?: 1); $nextDueDate = $months > 0 ? now()->addMonths($months)->timestamp : null; + $initialPassword = 'TGS@' . \Illuminate\Support\Str::random(8); + $moduleVmware = ModuleVmware::create([ - 'name' => $serverName, - 'module_ip_address' => $moduleIpAddress->id, - 'port' => 22, - 'order_id' => $this->item->order->id, - 'first_payment' => $firstPayment, - 'next_payment' => $firstPayment, - 'next_due_date' => $nextDueDate, - 'next_invoice' => $nextDueDate, - 'user' => 'administrator', - 'password' => 'TGS@123', - 'hostname' => $serverName, - 'vmware_id' => $vmId, - 'vcenter_id' => $vcenter->id, - 'os_id' => $this->item->os_id ?? 1, - 'customer_id' => $customer->id, - 'status' => 1, + 'name' => $serverName, + 'module_ip_address' => $moduleIpAddress->id, + 'port' => 22, + 'order_id' => $this->item->order->id, + 'first_payment' => $firstPayment, + 'next_payment' => $firstPayment, + 'next_due_date' => $nextDueDate, + 'next_invoice' => $nextDueDate, + 'user' => 'administrator', + 'password' => $initialPassword, + 'plain_password' => $initialPassword, + 'hostname' => $serverName, + 'vmware_id' => $vmId, + 'vcenter_id' => $vcenter->id, + 'os_id' => $this->item->os_id ?? 1, + 'customer_id' => $customer->id, + 'status' => 1, ]); + // Explicit sync: update services table directly (belt + suspenders vs observer) + $this->syncServiceRecord($moduleVmware); + AssignIPJob::dispatch($vmId, $moduleVmware->id, $moduleIpAddress->ip) ->delay(now()->addMinutes(5)); - // Service record sẽ được tạo tự động bởi ModuleVmwareObserver::created() } catch (\Exception $e) { Log::error('❌ Lỗi khi lưu dữ liệu VM và IP address: ' . $e->getMessage(), [ 'vmware_id' => $vmId, @@ -459,4 +465,43 @@ class CreateVirtualMachineJob implements ShouldQueue Log::error('Không thể gửi email thông báo lỗi VM: ' . $e->getMessage()); } } + + /** + * Sync module_vmware info → services table. + * Link: module_vmware.order_id → order_items.order_id → services.order_item_id + */ + private function syncServiceRecord(ModuleVmware $moduleVmware): void + { + try { + $orderItem = OrderItem::where('order_id', $moduleVmware->order_id)->first(); + + if (!$orderItem) { + Log::warning('syncServiceRecord: không tìm thấy OrderItem', [ + 'order_id' => $moduleVmware->order_id, + 'module_vmware_id' => $moduleVmware->id, + ]); + return; + } + + $months = (int) ($orderItem->months ?? 0); + $updated = Service::where('order_item_id', $orderItem->id) + ->update([ + 'vm_id' => $moduleVmware->id, + 'status' => 'active', + 'expires_at' => $months > 0 ? now()->addMonths($months) : null, + ]); + + Log::info('syncServiceRecord: service synced', [ + 'module_vmware_id' => $moduleVmware->id, + 'vmware_id' => $moduleVmware->vmware_id, + 'order_item_id' => $orderItem->id, + 'rows_updated' => $updated, + ]); + } catch (\Throwable $e) { + Log::error('syncServiceRecord: exception — ' . $e->getMessage(), [ + 'module_vmware_id' => $moduleVmware->id, + 'order_id' => $moduleVmware->order_id, + ]); + } + } } diff --git a/app/Models/ModuleVmware.php b/app/Models/ModuleVmware.php index f31fac4..d4e8a12 100644 --- a/app/Models/ModuleVmware.php +++ b/app/Models/ModuleVmware.php @@ -27,7 +27,11 @@ class ModuleVmware extends Model 'vcenter_id', 'os_id', 'customer_id', - 'status' + 'status', + 'service_id', + 'ip', + 'dns', + 'plain_password', ]; // NOTE: password must NOT use 'hashed' cast — it is a VM guest credential @@ -63,6 +67,12 @@ class ModuleVmware extends Model return $this->hasMany(\App\Models\Service::class, 'vm_id', 'vmware_id'); } + /** Direct link via service_id FK (new). */ + public function service() + { + return $this->belongsTo(\App\Models\Service::class, 'service_id'); + } + public function getStatus() { return match ($this->status) { diff --git a/app/Models/Service.php b/app/Models/Service.php index 3bdfec7..2564db4 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -61,10 +61,10 @@ class Service extends Model return $this->hasMany(ServiceAddon::class, 'service_id'); } - /** The VPS/VM associated with this service (if applicable). */ + /** The VPS/VM associated with this service — FK: services.vm_id → module_vmware.id */ public function vmware() { - return $this->belongsTo(ModuleVmware::class, 'vm_id', 'vmware_id'); + return $this->belongsTo(ModuleVmware::class, 'vm_id'); } // ------------------------------------------------------------------------- diff --git a/app/Observers/ModuleVmwareObserver.php b/app/Observers/ModuleVmwareObserver.php index 0ab943f..074f0aa 100644 --- a/app/Observers/ModuleVmwareObserver.php +++ b/app/Observers/ModuleVmwareObserver.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace App\Observers; -use App\Models\Invoice; use App\Models\ModuleVmware; use App\Models\OrderItem; use App\Models\Service; @@ -101,35 +100,33 @@ class ModuleVmwareObserver { try { $orderItem = OrderItem::where('order_id', $moduleVmware->order_id) - ->where('os_id', $moduleVmware->os_id) ->first(); - $invoice = Invoice::where('order_id', $moduleVmware->order_id)->first(); + if (!$orderItem) { + Log::warning('ModuleVmwareObserver: không tìm thấy OrderItem', [ + 'order_id' => $moduleVmware->order_id, + 'module_vmware_id' => $moduleVmware->id, + ]); + return; + } - Service::firstOrCreate( - ['order_item_id' => $orderItem?->id], - [ - 'customer_id' => $moduleVmware->customer_id, - 'product_id' => $orderItem?->product_id, - 'invoice_id' => $invoice?->id, - 'vm_id' => $moduleVmware->vmware_id, - 'server_name' => $moduleVmware->hostname ?? $moduleVmware->name, - 'status' => 'active', - 'auto_renew' => false, - 'started_at' => now(), - 'expires_at' => ($orderItem && (int) $orderItem->months > 0) - ? now()->addMonths((int) $orderItem->months) - : null, - ] - ); + $updated = Service::where('order_item_id', $orderItem->id) + ->update([ + 'vm_id' => $moduleVmware->id, + 'status' => 'active', + 'expires_at' => (int) $orderItem->months > 0 + ? now()->addMonths((int) $orderItem->months) + : null, + ]); Log::info('Service record synced for ModuleVmware', [ 'module_vmware_id' => $moduleVmware->id, 'vmware_id' => $moduleVmware->vmware_id, - 'order_item_id' => $orderItem?->id, + 'order_item_id' => $orderItem->id, + 'rows_updated' => $updated, ]); } catch (\Throwable $e) { - Log::error('Lỗi khi tạo Service từ ModuleVmwareObserver: ' . $e->getMessage(), [ + Log::error('Lỗi khi sync Service từ ModuleVmwareObserver: ' . $e->getMessage(), [ 'module_vmware_id' => $moduleVmware->id, 'order_id' => $moduleVmware->order_id, ]); diff --git a/app/Repositories/Api/ServiceClient/ServiceClientRepository.php b/app/Repositories/Api/ServiceClient/ServiceClientRepository.php index 4107286..73bc0ed 100644 --- a/app/Repositories/Api/ServiceClient/ServiceClientRepository.php +++ b/app/Repositories/Api/ServiceClient/ServiceClientRepository.php @@ -13,14 +13,14 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient } /** - * Lấy danh sách sản phẩm đang hoạt động + * Lấy toàn bộ dịch vụ của customer (mọi trạng thái) */ public function getActiveServices(int $limit = 10) { return $this->model ->with(['orderItem' => fn($q) => $q->with('product')]) - ->where('status', 'active') - ->orderBy('started_at', 'asc') + ->orderByRaw("FIELD(status, 'active', 'pending', 'suspended', 'expired', 'cancelled', 'terminated')") + ->orderBy('started_at', 'desc') ->paginate($limit); } @@ -50,7 +50,8 @@ class ServiceClientRepository extends BaseApiRepository implements ServiceClient ->with([ 'orderItem.os.osGroup', 'orderItem.product', - 'orderItem.order.vmWare.ipAddress', + 'orderItem.order', + 'vmware.ipAddress', ]) ->where('uuid', $uuid) ->first(); diff --git a/app/Services/Api/Order/OrderService.php b/app/Services/Api/Order/OrderService.php index 4211de6..a260af9 100644 --- a/app/Services/Api/Order/OrderService.php +++ b/app/Services/Api/Order/OrderService.php @@ -4,14 +4,11 @@ namespace App\Services\Api\Order; use App\Enums\Order\OrderStatus; use App\Helpers\OrderHelper; -use App\Jobs\VirtualMachine\Create\CreateVMJobs; +use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob; use App\Models\Addon; use App\Models\Log as CustomerActivityLog; -use App\Models\ModuleIpAddress; -use App\Models\ModuleVmware; use App\Models\Order; use App\Models\OrderItem; -use App\Models\Os; use App\Models\Service; use App\Models\ServiceAddon; use App\Repositories\Api\Invoice\InvoiceRepository; @@ -239,59 +236,12 @@ class OrderService private function dispatchVmJob(Order $order, OrderItem $item, Service $service, $customer): void { try { - $os = Os::with('osGroup.host')->find($item->os_id); + dispatch(new CreateVirtualMachineJob($item)); - if (!$os?->osGroup?->module_host_id) { - Log::warning('dispatchVmJob: OS/OsGroup/Host not found', [ - 'order_id' => $order->id, - 'os_id' => $item->os_id, - ]); - return; - } - - $ip = ModuleIpAddress::where('module_host_id', $os->osGroup->module_host_id) - ->where('status', 0) - ->lockForUpdate() - ->first(); - - if (!$ip) { - Log::warning('dispatchVmJob: no available IP', [ - 'order_id' => $order->id, - 'module_host_id' => $os->osGroup->module_host_id, - ]); - return; - } - - $vmName = $item->server_name ?? ('VM-' . $order->id); - $ip->update(['status' => 1, 'vm_name' => $vmName]); - - $moduleVmware = ModuleVmware::create([ - 'name' => $vmName, - 'module_ip_address_id' => $ip->id, - 'port' => 22, - 'order_id' => $order->id, - 'first_payment' => $order->total, - 'next_payment' => $order->total, - 'next_due_date' => $service->expires_at, - 'next_invoice' => 0, - 'user' => 'Administrator', - 'password' => '', - 'hostname' => strtolower($vmName), - 'vmware_id' => null, - 'os_id' => $item->os_id, - 'customer_id' => $customer->id, - 'status' => 0, - ]); - - // Store module_vmware.id as vm_id on Service for quick lookup - $service->update(['vm_id' => (string) $moduleVmware->id]); - - dispatch(new CreateVMJobs($moduleVmware->id))->delay(now()->addSeconds(10)); - - Log::info('dispatchVmJob: CreateVMJobs dispatched', [ - 'order_id' => $order->id, - 'service_id' => $service->id, - 'module_vmware_id' => $moduleVmware->id, + Log::info('dispatchVmJob: CreateVirtualMachineJob dispatched', [ + 'order_id' => $order->id, + 'service_id' => $service->id, + 'order_item_id' => $item->id, ]); } catch (Exception $e) { Log::error('dispatchVmJob: exception', [ diff --git a/database/migrations/2026_04_16_000001_add_service_ip_dns_to_module_vmware.php b/database/migrations/2026_04_16_000001_add_service_ip_dns_to_module_vmware.php new file mode 100644 index 0000000..80b1261 --- /dev/null +++ b/database/migrations/2026_04_16_000001_add_service_ip_dns_to_module_vmware.php @@ -0,0 +1,30 @@ +unsignedBigInteger('service_id')->nullable()->index()->after('id'); + $table->foreign('service_id') + ->references('id') + ->on('services') + ->cascadeOnDelete(); + } + }); + } + + public function down(): void + { + Schema::table('module_vmware', function (Blueprint $table) { + if (Schema::hasColumn('module_vmware', 'service_id')) { + $table->dropForeign(['service_id']); + $table->dropColumn('service_id'); + } + }); + } +}; diff --git a/database/migrations/2026_04_16_000002_add_plain_password_to_module_vmware.php b/database/migrations/2026_04_16_000002_add_plain_password_to_module_vmware.php new file mode 100644 index 0000000..ddedbdd --- /dev/null +++ b/database/migrations/2026_04_16_000002_add_plain_password_to_module_vmware.php @@ -0,0 +1,26 @@ +text('plain_password')->nullable()->after('password') + ->comment('Plaintext VM password — shown to customer on service detail'); + } + }); + } + + public function down(): void + { + Schema::table('module_vmware', function (Blueprint $table) { + if (Schema::hasColumn('module_vmware', 'plain_password')) { + $table->dropColumn('plain_password'); + } + }); + } +}; diff --git a/database/migrations/2026_04_16_000003_alter_services_vm_id_to_fk.php b/database/migrations/2026_04_16_000003_alter_services_vm_id_to_fk.php new file mode 100644 index 0000000..ae78349 --- /dev/null +++ b/database/migrations/2026_04_16_000003_alter_services_vm_id_to_fk.php @@ -0,0 +1,38 @@ +dropColumn('vm_id'); + }); + + // Step 2: add as proper integer FK → module_vmware.id + Schema::table('services', function (Blueprint $table) { + $table->unsignedBigInteger('vm_id')->nullable()->index()->after('invoice_id') + ->comment('FK → module_vmware.id (set after VM provisioning completes)'); + + $table->foreign('vm_id') + ->references('id') + ->on('module_vmware') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('services', function (Blueprint $table) { + $table->dropForeign(['vm_id']); + $table->dropColumn('vm_id'); + }); + + Schema::table('services', function (Blueprint $table) { + $table->string('vm_id')->nullable()->index()->after('invoice_id'); + }); + } +};