orderRepository->update($order->id, ['status' => 1]); if ($provisionModules) { $this->provisionOrderItems($order); } }); } public function setStatus(Order $order, int $status): void { $this->orderRepository->update($order->id, ['status' => $status]); } /** * Xoá đơn hàng cùng hóa đơn và các dòng hóa đơn liên quan. */ public function deleteOrder(Order $order): void { DB::transaction(function () use ($order) { if ($order->invoice) { $order->invoice->invoiceItems()->delete(); $order->invoice->delete(); } $order->delete(); }); } private function provisionOrderItems(Order $order): void { $jobs = []; $addon = []; $product = 0; foreach ($order->orderItem as $item) { switch ((int) $item->type_id) { case 1: $itemData = $this->addonRepository->find($item->relid); if ($itemData) { $price = match ((int) $item->billing) { 2 => $item->amount / 1, 3 => $item->amount / 3, 4 => $item->amount / 6, 5 => $item->amount / 12, default => 0 }; $addon[strtolower($itemData->name)] = (int) $price / $itemData->price; $product = $item->parent_id; } break; case 3: $hostingServer = $this->moduleHostingServerRepository->get_active(); if ($hostingServer) { $this->moduleHostingRepository->update($item->relid, ['module_server_id' => $hostingServer->first()->id]); $jobs[] = new CreateHostingJobs($item->relid); } break; // case 4 (VPS/Cloud): xử lý bởi CreateVirtualMachineJob, đã dispatch // ngay lúc tạo đơn trong createOrder() — không provision lại ở đây. } } if (count($order->orderItem) === count($addon)) { dispatch(new UpgradeVirtualMachine($product, $addon, $order->id))->delay(now()->addSeconds(10)); } foreach ($jobs as $item) { dispatch($item)->delay(now()->addSeconds(10)); } } public function createOrder(array $data): Order { $customer = Customer::find($data['customer_id']); if (!$customer || (int) $customer->status !== 1) { throw new \InvalidArgumentException('Tài khoản khách hàng không hoạt động. Không thể tạo đơn hàng.'); } $paymentRecord = Payment::find((int) $data['payment_method']); $isBalancePayment = $paymentRecord && (int) $paymentRecord->type === 0; // Balance payment → auto-confirm order, otherwise use requested order status. $effectiveOrderStatus = $isBalancePayment ? OrderStatus::ACTIVE->value : (int) $data['status']; $effectiveInvoiceStatus = match ($effectiveOrderStatus) { OrderStatus::ACTIVE->value => InvoiceStatus::PAID->value, OrderStatus::CANCELLED->value => InvoiceStatus::CANCELLED->value, default => InvoiceStatus::UNPAID->value, }; [$productItems, $relIdData, $total] = $this->buildProductItems($data); // Giá niêm yết đã bao gồm thuế (khớp với client-facing OrderService::create()) — // không cộng thêm thuế lên trên ở đây. $orderTotal = (int) $total; $invoiceData = [ 'invoice_number' => Helper::generateUniqueCode(), 'due_date' => Carbon::today()->addDays(7)->toDateString(), 'paid_at' => $effectiveInvoiceStatus === InvoiceStatus::PAID->value ? Carbon::now() : null, 'sub_total' => $total, 'tax' => 0, 'total' => $orderTotal, 'payment_method' => $paymentRecord?->id, 'customer_id' => $data['customer_id'], 'status' => $effectiveInvoiceStatus, ]; $orderData = [ 'order_num' => Helper::generateUniqueCode(), 'date' => Carbon::today()->toDateString(), 'customer_id' => $data['customer_id'], 'discount_id' => $data['discount_id'] ?? null, 'sub_total' => $total, 'total' => $orderTotal, // `payment` has no `code` column. Store its primary key in the // legacy string column so the selected method can be resolved // consistently on both orders and invoices. 'payment_method' => $paymentRecord?->id, 'payment_proof' => $data['payment_proof'] ?? null, 'user_id' => Auth::guard('admin')->id(), 'status' => $effectiveOrderStatus, // Order.paid_at mirrors invoice.paid_at — set together, same rule as // InvoicePaymentService::activateOrder()/activateRenewal(). 'paid_at' => $invoiceData['paid_at'], 'client_ip' => request()->getClientIp(), ]; $order = DB::transaction(function () use ($invoiceData, $orderData, $productItems, $relIdData, $data, $orderTotal, $isBalancePayment, $effectiveInvoiceStatus) { $invoice = $this->invoiceRepository->create($invoiceData); $productItems = array_map(fn ($item) => array_merge($item, ['invoice_id' => $invoice->id]), $productItems); if ($data['create_invoice'] ?? false) { $orderData['invoice_id'] = $invoice->id; } $order = $this->orderRepository->create($orderData); $invoice->update(['order_id' => $order->id]); $relIdData = array_map(function ($item) use ($order) { $item['data']['order_id'] = $order->id; return $item; }, $relIdData); $this->createModules($relIdData); $invoiceItemData = array_map(fn ($item) => [ 'invoice_id' => $item['invoice_id'], 'product_id' => $item['product_id'], 'type' => 'service', 'months' => $item['months'], 'quantity' => $item['quantity'], 'unit_price' => $item['price'], 'total_price' => $item['price'] * $item['quantity'], ], $productItems); $this->invoiceItemRepository->insert($invoiceItemData); VatNumbering::settle($invoice); $now = now(); OrderItem::insert(array_map(fn ($item) => [ 'order_id' => $order->id, 'product_id' => $item['product_id'], 'billing_cycle' => $item['billing_cycle'], 'months' => $item['months'], 'os_id' => $item['os_id'], 'quantity' => $item['quantity'], 'server_name' => $item['server_name'], 'price' => $item['price'], 'created_at' => $now, 'updated_at' => $now, ], $productItems)); $vmTypes = [ProductTypeEnums::VPS, ProductTypeEnums::CLOUD]; foreach (OrderItem::where('order_id', $order->id)->get() as $orderItem) { $type = ProductTypeEnums::tryFrom($orderItem->product?->category?->type ?? ''); if (in_array($type, $vmTypes)) { CreateVirtualMachineJob::dispatch($orderItem)->delay(now()->addSeconds(10)); } } if ($isBalancePayment) { $customer = Customer::lockForUpdate()->find($data['customer_id']); if ($customer->total < $orderTotal) { throw new \Exception('Số dư tài khoản không đủ để thanh toán đơn hàng.'); } $customer->decrement('total', $orderTotal); } if ($effectiveInvoiceStatus === InvoiceStatus::PAID->value) { Transaction::firstOrCreate( [ 'customer_id' => $data['customer_id'], 'trans' => $invoice->invoice_number, 'type' => 'invoice', ], [ 'uuid' => (string) Str::uuid(), 'public_code' => Transaction::newPublicCode('INV-PAY'), 'amount' => $orderTotal, 'user_id' => Auth::guard('admin')->id(), 'status' => TransactionStatus::COMPLETED->value, 'note' => 'Admin tạo đơn hàng và xác nhận thanh toán hóa đơn #' . $invoice->invoice_number, ] ); } return $order; }); if (AutoEmailGate::allows('order_created')) { OrderCreatedJob::dispatch([ 'customer_email' => $customer->email, 'client_name' => $customer->fullname, 'order_num' => $orderData['order_num'], 'invoice_num' => $invoiceData['invoice_number'], 'order_total' => number_format($orderTotal, 0, ',', '.'), 'company_name' => setting('general.company_name') ?: config('app.name'), 'company_logo' => setting('general.company_logo') ?: '', ]); } return $order; } private function buildProductItems(array $data): array { $total = 0; $productItems = []; $relIdData = []; foreach ($data['product'] as $key => $item) { $product = \App\Models\Product::with('category')->find($item['product_id']); if (!$product) { throw new \InvalidArgumentException("Sản phẩm không tồn tại (ID: {$item['product_id']})."); } $categoryType = ProductTypeEnums::tryFrom($product->category?->type ?? ''); $moduleType = match ($categoryType) { ProductTypeEnums::HOSTING => 3, ProductTypeEnums::VPS, ProductTypeEnums::CLOUD => 4, default => 0, }; $billingDetails = $this->calculateBillingDetails($item['billing']); $isDomain = $categoryType === ProductTypeEnums::DOMAIN; $billingMultiplier = $isDomain ? 1 : $billingDetails['billing']; if ($isDomain) { $domainName = strtolower(trim((string) ($item['server_name'] ?? $item['domain'] ?? ''))); if ($domainName === '') { throw new \InvalidArgumentException('Vui lòng nhập đầy đủ tên miền.'); } $domainPrice = DomainPricing::findByDomain($domainName)->orderPrice(1); $priceSelect = $item['price_custom'] ?? $domainPrice; } else { $priceSelect = $item['price_custom'] ?? $product->price_sale ?? $product->price; } $billingCycle = ProductBillingCycle::fromLegacyCode((int) $item['billing']); $generalCode = Helper::generateUniqueCode(); $productItems[$key] = [ 'product_id' => $product->id, 'product_type' => $categoryType?->value, 'months' => $billingCycle->months(), 'billing_cycle' => $billingCycle->value, 'os_id' => $item['os_id'] ?? null, 'quantity' => (int) ($item['quantity'] ?? 1), 'server_name' => $item['server_name'] ?? $item['domain'] ?? null, 'price' => (int) $priceSelect, 'invoice_type' => 1, 'code' => $generalCode, ]; $relIdData[$key] = [ 'type' => $moduleType, 'code' => $generalCode, 'data' => [ 'customer_id' => $data['customer_id'], 'status' => 0, 'name' => $item['server_name'] ?? $item['domain'] ?? $product->name, 'first_payment' => $priceSelect * $billingMultiplier, 'next_payment' => $priceSelect * $billingMultiplier, ...$billingDetails, ], ]; if ($item['quantity'] > 1) { for ($i = 1; $i < $item['quantity']; $i++) { $newCode = Helper::generateUniqueCode(); $productItems[] = array_merge($productItems[$key], ['code' => $newCode]); $relIdData[] = array_merge($relIdData[$key], ['code' => $newCode]); } } $total += $item['quantity'] * $priceSelect * $billingMultiplier; } return [$productItems, $relIdData, $total]; } private function createModules(array $relIdData): void { foreach ($relIdData as $item) { match ($item['type']) { 3 => $this->moduleHostingRepository->create($item['data']), // type=4 (VPS): không pre-create ở đây. // CreateVirtualMachineJob sẽ tạo module_vmware sau khi VM provisioned xong. default => null, }; } } private function calculateBillingDetails(int|string $billing): array { $months = match ((int) $billing) { 2 => 1, 3 => 3, 4 => 6, 5 => 12, default => 0, }; return [ 'next_due_date' => $months ? Carbon::today()->addMonths($months)->timestamp : null, 'next_invoice' => $months ? Carbon::today()->addMonths($months)->timestamp : null, 'billing' => $months ?: 1, ]; } }