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); $orderTotal = (int) ($total + ($total * (10 / 100))); $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' => $total * (10 / 100), 'total' => $orderTotal, 'payment_method' => $paymentRecord?->id, 'customer_id' => $data['customer_id'], 'status' => $effectiveInvoiceStatus, ]; $orderData = [ 'order_num' => Helper::generateUniqueCode(), 'customer_id' => $data['customer_id'], 'discount_id' => $data['discount_id'] ?? null, 'sub_total' => $total, 'total' => $orderTotal, 'payment_method' => $paymentRecord?->code ?? null, 'payment_proof' => $data['payment_proof'] ?? null, 'user_id' => Auth::guard('admin')->id(), 'status' => $effectiveOrderStatus, '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); $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' => 'INV-PAY-' . now()->format('Ymd') . '-' . strtoupper(Str::random(6)), '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; }); 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']); $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 * $billingDetails['billing'], 'next_payment' => ($item['price_custom'] ?? $product->price) * $billingDetails['billing'], ...$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'] * ($item['price_custom'] ?? ($product->price_sale ?? $product->price)) * $billingDetails['billing']; } 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, ]; } }