serviceRepository->filter( ['customer_uuid' => $uuid], [ 'product', 'orderItem', 'invoice', 'vmware.os.osGroup', ...Service::HOSTING_ACCOUNT_RELATIONS, ], ); ModuleVmware::resolveVirtualMachineIds($services->pluck('vmware')); return $services; } /** * Xoá nhiều dịch vụ của khách hàng. Chỉ xoá bản ghi trong bảng `services`, * không xoá VM (module_vmware / virtual_machines) liên kết. */ public function bulkDeleteServices(string $uuid, array $ids): int { $customer = $this->resolveCustomer($uuid); return Service::where('customer_id', $customer->id)->whereIn('id', $ids)->delete(); } /** * Cập nhật thông tin dịch vụ của khách hàng (server_name/hostname, status, ngày,...). * Đồng bộ hostname sang module_vmware nếu service đã gắn VM. */ public function updateService(string $uuid, int $serviceId, array $data): Service { $customer = $this->resolveCustomer($uuid); $service = Service::where('customer_id', $customer->id)->where('id', $serviceId)->first(); abort_if(!$service, 404, 'Không tìm thấy dịch vụ'); return DB::transaction(function () use ($service, $data) { $vmFields = ['os_id', 'vm_cpu', 'vm_ram_mb', 'vm_disk_gb', 'vm_ip', 'vm_dns', 'vm_user', 'vm_password', 'vm_port']; $service->fill(collect($data)->except(['price', ...$vmFields])->all()); $service->save(); if (array_key_exists('price', $data) && $data['price'] !== null && $service->orderItem) { $service->orderItem->update(['price' => $data['price']]); } // Đồng bộ sang module_vmware nếu có gán VM if ($service->vm_id) { $vmData = array_filter([ 'hostname' => $data['server_name'] ?? null, 'os_id' => $data['os_id'] ?? null, 'cpu' => $data['vm_cpu'] ?? null, 'ram_mb' => $data['vm_ram_mb'] ?? null, 'disk_gb' => $data['vm_disk_gb'] ?? null, 'ip' => $data['vm_ip'] ?? null, 'dns' => $data['vm_dns'] ?? null, 'user' => $data['vm_user'] ?? null, 'password' => $data['vm_password'] ?? null, 'plain_password' => $data['vm_password'] ?? null, 'port' => $data['vm_port'] ?? null, ], fn($v) => $v !== null); if ($vmData) { ModuleVmware::findOrFail($service->vm_id)->update($vmData); } } $service->load(['product', 'orderItem', 'invoice', 'vmware.os.osGroup']); ModuleVmware::resolveVirtualMachineIds([$service->vmware]); return $service; }); } /** * Gửi email nhắc gia hạn dịch vụ cho khách hàng. */ public function notifyRenewal(string $uuid, int $serviceId): void { $customer = $this->resolveCustomer($uuid); $service = Service::where('customer_id', $customer->id)->where('id', $serviceId) ->with(['product', 'orderItem']) ->first(); abort_if(!$service, 404, 'Không tìm thấy dịch vụ'); abort_if(!$customer->email, 422, 'Khách hàng chưa có email'); Mail::to($customer->email)->send(new ServiceRenewalReminderMail($customer, $service)); } /** * Create a service for a customer. For VPS products, accepts a * `virtual_machine_id` from the vCenter inventory and auto-creates the * matching module_vmware row so `services.vm_id` can be linked. */ public function createService(string $uuid, array $data, string $clientIp): Service { $customer = $this->resolveCustomer($uuid); $product = Product::with('pricings')->find($data['product_id']); abort_if( $product->product_type === 'vps' && empty($data['virtual_machine_id']), 422, 'VPS service requires a virtual_machine_id', ); $billingCycle = $data['billing_cycle'] ?? 'monthly'; $months = OrderHelper::cycleToMonths($billingCycle); $pricing = $product->pricings->where('billing_cycle', $billingCycle)->first(); // Số tiền đã thanh toán: admin nhập tay để khớp với thực tế (có thể lệch giá niêm yết do khuyến mãi/thoả thuận) $basePrice = isset($data['amount_paid']) ? (int) $data['amount_paid'] : (int) ($pricing->price ?? 0); $paymentMethod = $data['payment_method']; // Đối với balance: kiểm tra số dư trước khi vào transaction abort_if( $paymentMethod === 'balance' && (int) $customer->total < $basePrice, 422, sprintf( 'Số dư khách không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.', number_format((int) $customer->total), number_format($basePrice), ), ); return DB::transaction(function () use ($customer, $product, $data, $billingCycle, $months, $basePrice, $paymentMethod, $clientIp) { // 1) Order + OrderItem (mô phỏng flow customer đặt hàng) $order = Order::create([ 'order_num' => OrderHelper::generateOrderNumber(), 'customer_id' => $customer->id, 'date' => now()->toDateString(), 'sub_total' => $basePrice, 'discount_amount' => 0, 'total' => $basePrice, 'status' => OrderStatus::ACTIVE->value, 'payment_method' => $paymentMethod, 'client_ip' => $clientIp, ]); $orderItem = OrderItem::create([ 'order_id' => $order->id, 'product_id' => $product->id, 'billing_cycle' => $billingCycle, 'months' => $months, 'server_name' => $data['server_name'] ?? null, 'quantity' => 1, 'price' => $basePrice, ]); // Ngày bắt đầu/hết hạn dùng chung cho cả module_vmware (next_due_date/next_invoice) và services. $startedAt = $data['started_at'] ?? now(); $expiresAt = $data['expires_at'] ?? Carbon::parse($startedAt)->addMonths($months); // 2) ModuleVmware: snapshot từ virtual_machines. `order_id` phải trỏ về // order vừa tạo — client console-ticket/rebuild resolve VM qua // service->orderItem->order->vmWare, không qua services.vm_id. $vmId = $this->createModuleVmwareIfNeeded($data, $customer, $order, $basePrice, $expiresAt); // 3) Invoice — đánh dấu PAID luôn vì admin đứng ra tạo & xác nhận thanh toán $invoice = Invoice::create([ 'invoice_number' => OrderHelper::generateOrderNumber('INV'), 'customer_id' => $customer->id, 'order_id' => $order->id, 'invoice_type' => 1, 'date' => now()->toDateString(), 'sub_total' => $basePrice, 'discount_amount' => 0, 'tax' => 0, 'total' => $basePrice, 'status' => InvoiceStatus::PAID->value, 'payment_method' => $paymentMethod, 'paid_at' => now(), 'due_date' => now()->addMonths($months)->toDateString(), ]); InvoiceItem::create([ 'invoice_id' => $invoice->id, 'product_id' => $product->id, 'type' => 'service', 'description' => $product->name ?? 'Dịch vụ', 'months' => $months, 'quantity' => 1, 'unit_price' => $basePrice, 'total_price' => $basePrice, ]); // 4) Payment processing if ($paymentMethod === 'balance') { $customer->total = (int) $customer->total - $basePrice; $customer->save(); } $proofPaths = $data['_proof_paths'] ?? []; Transaction::create([ 'uuid' => (string) Str::uuid(), 'customer_id' => $customer->id, 'user_id' => Auth::id(), 'trans' => $order->order_num, 'amount' => $basePrice, 'type' => $paymentMethod === 'transfer' ? 'transfer' : 'balance', 'status' => 1, // paid 'note' => $paymentMethod === 'transfer' ? 'Admin tạo dịch vụ - thanh toán chuyển khoản' : 'Admin tạo dịch vụ - trừ số dư khách hàng', 'proof_image' => !empty($proofPaths) ? json_encode($proofPaths) : null, ]); // 5) Service — updateOrCreate để tránh trùng khi cùng VM được gán lại $serviceLookup = $vmId ? ['customer_id' => $customer->id, 'vm_id' => $vmId] : [ 'customer_id' => $customer->id, 'product_id' => $product->id, 'vm_id' => null, 'server_name' => $data['server_name'] ?? null, ]; $service = Service::updateOrCreate($serviceLookup, [ 'customer_id' => $customer->id, 'product_id' => $product->id, 'vm_id' => $vmId, 'order_item_id' => $orderItem->id, 'invoice_id' => $invoice->id, 'server_name' => $data['server_name'] ?? null, 'status' => $data['status'] ?? 'active', 'auto_renew' => (bool) ($data['auto_renew'] ?? false), 'started_at' => $startedAt, 'expires_at' => $expiresAt, 'note' => $data['note'] ?? null, ]); // module_vmware.service_id chỉ biết được sau khi Service đã tồn tại. if ($vmId) { ModuleVmware::where('id', $vmId)->update(['service_id' => $service->id]); } return $service->load(['product', 'vmware.os.osGroup']); }); } private function createModuleVmwareIfNeeded(array $data, Customer $customer, Order $order, int $basePrice, string|Carbon $expiresAt): ?int { if (empty($data['virtual_machine_id'])) { return null; } $vm = VirtualMachines::with('datacenter')->find($data['virtual_machine_id']); $vcenterId = $vm->datacenter->vcenter_id ?? null; $moduleVm = ModuleVmware::updateOrCreate( ['virtual_machine_id' => $vm->id], [ 'customer_id' => $customer->id, 'order_id' => $order->id, 'name' => $vm->name, 'vmware_id' => $vm->vm_id, 'vcenter_id' => $vcenterId, 'hostname' => $data['server_name'] ?? $vm->name, 'cpu' => $data['vm_cpu'] ?? $vm->cpu, 'ram_mb' => $data['vm_ram_mb'] ?? $vm->ram_mb, 'disk_gb' => $data['vm_disk_gb'] ?? null, 'os_id' => $data['os_id'] ?? null, 'ip' => $data['vm_ip'] ?? $vm->ip, 'dns' => $data['vm_dns'] ?? null, 'user' => $data['vm_user'] ?? null, 'password' => $data['vm_password'] ?? null, 'plain_password' => $data['vm_password'] ?? null, 'port' => $data['vm_port'] ?? null, 'status' => 1, 'first_payment' => $basePrice, 'next_payment' => $basePrice, 'next_due_date' => Carbon::parse($expiresAt)->timestamp, 'next_invoice' => Carbon::parse($expiresAt)->timestamp, ], ); return $moduleVm->id; } /** * List virtual machines from vCenter inventory for the VPS picker. * Annotates each VM with `assigned_service_id` if already linked to a service. */ public function listAvailableVms(string $search): Collection { $assignments = DB::table('module_vmware as mv') ->join('services as s', 's.vm_id', '=', 'mv.id') ->whereNotNull('mv.virtual_machine_id') ->select('mv.virtual_machine_id', 's.id as service_id', 's.customer_id as service_customer_id') ->get() ->keyBy('virtual_machine_id'); return DB::table('virtual_machines as vm') ->leftJoin('module_datacenter as dc', 'dc.id', '=', 'vm.datacenter_id') ->leftJoin('module_host as h', 'h.id', '=', 'vm.host_id') ->leftJoin('module_vcenter as vc', 'vc.id', '=', 'dc.vcenter_id') ->select( 'vm.id', 'vm.vm_id', 'vm.name', 'vm.power_state', 'vm.cpu', 'vm.ram_mb', 'vm.ip', 'vc.host as vcenter_name', 'dc.name as datacenter_name', 'h.name as host_name', ) ->where('vm.is_template', 0) ->where('vm.is_system', 0) ->when($search, fn($q) => $q->where(function ($w) use ($search) { $w->where('vm.name', 'like', "%{$search}%") ->orWhere('vm.vm_id', 'like', "%{$search}%") ->orWhere('vm.ip', 'like', "%{$search}%"); })) ->orderBy('vm.name') ->limit(200) ->get() ->map(function ($vm) use ($assignments) { $a = $assignments->get($vm->id); $vm->assigned_service_id = $a->service_id ?? null; $vm->assigned_customer_id = $a->service_customer_id ?? null; return $vm; }); } }