orderRepo->getOrders( $request->user()->id, $request->only(['total_price', 'pending']) ); return response()->json(['success' => true, 'data' => $orders]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * GET /v1/client/orders — customer's order list */ public function getClientOrders(Request $request): JsonResponse { try { $orders = Order::query() ->with(['invoice']) ->where('customer_id', $request->user()->id) ->orderByDesc('created_at') ->get(); $data = $orders->map(fn(Order $order) => [ 'id' => $order->id, 'order_num' => $order->order_num, 'status' => $order->status, 'status_text' => OrderStatus::from((int) $order->status)->label(), 'total' => (float) $order->total, 'created_at' => $this->isoDate($order->created_at), 'invoice' => $order->invoice ? [ 'id' => $order->invoice->id, 'invoice_number' => $order->invoice->invoice_number, 'status' => $order->invoice->status, 'total' => $order->invoice->total, ] : null, ]); return response()->json(['success' => true, 'data' => $data]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * GET /v1/client/orders/{id} — order detail */ public function getClientOrderDetail(Request $request, int $id): JsonResponse { try { $order = Order::query() ->with(['invoice', 'orderItems.product', 'customer']) ->where('customer_id', $request->user()->id) ->where('id', $id) ->first(); if (!$order) { return $this->notFound('Đơn hàng không tồn tại.'); } return response()->json(['success' => true, 'data' => $this->formatOrderDetail($order)]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * POST /v1/orders — create a new order * * @OA\Post(path="/orders", operationId="storeOrder", tags={"Orders"}, * security={{"bearerAuth":{}}}, * @OA\RequestBody(required=true, @OA\JsonContent( * required={"product_id","billing_cycle"}, * @OA\Property(property="product_id", type="string", example="uuid-here"), * @OA\Property(property="billing_cycle", type="string", example="annually", * enum={"monthly","quarterly","semi_annually","annually","biennially"}), * @OA\Property(property="server_name", type="string", example="my-vps-01"), * @OA\Property(property="os_id", type="integer", example=1), * @OA\Property(property="addons", type="array", @OA\Items( * @OA\Property(property="addon_id", type="integer", example=1), * @OA\Property(property="qty", type="integer", example=2) * )) * )), * @OA\Response(response=201, description="Order created successfully") * ) */ public function storeOrder(StoreOrderRequest $request): JsonResponse { try { $customer = $request->user(); $data = $request->validated(); $product = Product::where('uuid', $data['product_id'])->first(); if (!$product) { throw new \Exception('Sản phẩm không tìm thấy hoặc đã bị xóa.'); } $data['product_id'] = $product->id; $result = $this->orderService->create($data, $customer); $order = $result['order']->load(['orderItems.product', 'invoice', 'customer']); $invoice = $result['invoice']; return response()->json([ 'success' => true, 'message' => 'Tạo đơn hàng thành công.', 'notice' => 'Dịch vụ VPS sẽ được khởi tạo trong 5-10 phút sau khi thanh toán.', 'data' => [ 'order' => $this->formatOrderSummary($order), 'invoice' => $this->formatInvoiceSummary($invoice), 'items' => $this->formatOrderItems($order->orderItems), ], ], 201); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * POST /v1/orders/{id}/pay — pay for a pending order * * @OA\Post(path="/orders/{id}/pay", operationId="payOrder", tags={"Orders"}, * security={{"bearerAuth":{}}}, * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\RequestBody(required=true, @OA\JsonContent( * required={"payment_method"}, * @OA\Property(property="payment_method", type="string", * enum={"credit","bank_transfer","vnpay","momo"}) * )), * @OA\Response(response=200, description="Payment successful") * ) */ public function payOrder(int $id, PayOrderRequest $request): JsonResponse { try { $order = $this->orderRepo->find($id); if (!$order) { return $this->notFound('Đơn hàng không tồn tại.'); } if ($order->customer_id !== $request->user()->id) { return response()->json(['success' => false, 'message' => 'Không có quyền truy cập.'], 403); } $this->orderService->pay($order, $request->payment_method); return response()->json(['success' => true, 'message' => 'Thanh toán thành công.']); } catch (\Exception $e) { return $this->error($e->getMessage()); } } // ========================================================================= // Invoices // ========================================================================= /** * GET /v1/client/invoices — customer's invoice list */ public function getClientInvoices(Request $request): JsonResponse { try { $invoices = Invoice::query() ->with(['order.orderItems.product']) ->where('customer_id', $request->user()->id) ->orderByDesc('created_at') ->get(); $data = $invoices->map(fn(Invoice $invoice) => [ 'id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'service_name' => $invoice->order?->orderItems->first()?->product?->name ?? ('Đơn hàng ' . $invoice->order?->order_num), 'total' => (float) $invoice->total, 'status' => $invoice->status, 'status_text' => InvoiceStatus::tryFrom((int) $invoice->status)?->label() ?? '-', 'created_at' => $this->isoDate($invoice->created_at), 'order_id' => $invoice->order?->id, 'order_num' => $invoice->order?->order_num, ])->values(); return response()->json(['success' => true, 'data' => $data]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * GET /v1/client/invoices/{id} — invoice detail */ public function getClientInvoiceDetail(Request $request, int $id): JsonResponse { try { $invoice = Invoice::query() ->with([ 'order.orderItems.product', 'order.vmWare.ipAddress', 'order.vmWare.os', 'invoiceItems', 'customer', 'attachments', ]) ->where('customer_id', $request->user()->id) ->where('id', $id) ->first(); if (!$invoice) { return $this->notFound('Hóa đơn không tồn tại.'); } return response()->json(['success' => true, 'data' => $this->formatInvoiceDetail($invoice)]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * GET /v1/client/invoices/order/{orderNumber} — invoice detail by order number */ public function getClientInvoiceDetailByOrderNum(Request $request, string $orderNumber): JsonResponse { try { $order = Order::query() ->with([ 'invoice.invoiceItems', 'invoice.attachments', 'invoice.customer', 'orderItems.product', 'customer', 'vmWare.ipAddress', 'vmWare.os', ]) ->where('customer_id', $request->user()->id) ->where('order_num', $orderNumber) ->first(); if (!$order?->invoice) { return $this->notFound('Hóa đơn không tồn tại.'); } // Reuse formatInvoiceDetail with already-loaded relations $invoice = $order->invoice; // Attach order + vmWare so formatter can use them $invoice->setRelation('order', $order); $invoice->setRelation('customer', $order->customer); return response()->json(['success' => true, 'data' => $this->formatInvoiceDetail($invoice)]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } // ========================================================================= // Services (dich-vu) // ========================================================================= /** * GET /v1/client/dich-vu — list of customer's services */ public function getClientServices(Request $request): JsonResponse { try { $services = Service::query() ->with(['product', 'addons', 'invoice', 'orderItem']) ->where('customer_id', $request->user()->id) ->orderByDesc('created_at') ->get(); $data = $services->map(function (Service $service) { $vmware = $service->vm_id ? ModuleVmware::with(['ipAddress', 'os'])->find((int) $service->vm_id) : null; return [ 'id' => $service->id, 'service_name' => $service->product?->name ?? $service->server_name, 'server_name' => $service->server_name, 'product_type' => $service->product?->product_type, 'billing_cycle' => $service->orderItem?->billing_cycle, 'status' => $service->status, 'status_text' => $service->status_label, 'auto_renew' => (bool) $service->auto_renew, 'started_at' => $this->isoDate($service->started_at), 'expires_at' => $this->isoDate($service->expires_at), 'invoice_id' => $service->invoice_id, 'invoice_number' => $service->invoice?->invoice_number, 'addons' => $service->addons->map(fn($a) => [ 'type' => $a->addon_type, 'value' => $a->addon_value, 'price' => $a->price, ])->all(), 'vmware' => $vmware ? [ 'id' => $vmware->id, 'name' => $vmware->name, 'hostname' => $vmware->hostname, 'ip_address' => $vmware->ipAddress?->ip, 'os_name' => $vmware->os?->name, 'status' => $vmware->status, ] : null, ]; })->values(); return response()->json(['success' => true, 'data' => $data]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * GET /v1/client/dich-vu/{uuid} — service detail by UUID */ public function getClientServiceDetail(Request $request, string $uuid): JsonResponse { try { $service = Service::query() ->with(['product', 'addons', 'invoice', 'orderItem']) ->where('uuid', $uuid) ->where('customer_id', $request->user()->id) ->first(); if (!$service) { return $this->notFound('Không tìm thấy dịch vụ.'); } $vmware = $service->vm_id ? ModuleVmware::with(['ipAddress', 'os', 'vCenter'])->find((int) $service->vm_id) : null; $data = [ 'id' => $service->id, 'uuid' => $service->uuid, 'service_name' => $service->product?->name ?? $service->server_name, 'server_name' => $service->server_name, 'product_type' => $service->product?->product_type, 'billing_cycle' => $service->orderItem?->billing_cycle, 'status' => $service->status, 'status_text' => $service->status_label, 'auto_renew' => (bool) $service->auto_renew, 'started_at' => $this->isoDate($service->started_at), 'expires_at' => $this->isoDate($service->expires_at), 'note' => $service->note, 'invoice_id' => $service->invoice_id, 'invoice_number' => $service->invoice?->invoice_number, 'product' => $service->product ? [ 'id' => $service->product->id, 'uuid' => $service->product->uuid, 'name' => $service->product->name, 'product_type' => $service->product->product_type, 'specs' => [ 'cpu' => $service->product->cpu, 'ram' => $service->product->ram, 'disk' => $service->product->disk, 'bandwidth' => $service->product->bandwidth, ], ] : null, 'addons' => $service->addons->map(fn($a) => [ 'type' => $a->addon_type, 'value' => $a->addon_value, 'price' => $a->price, ])->all(), 'vmware' => $vmware ? [ 'id' => $vmware->id, 'name' => $vmware->name, 'hostname' => $vmware->hostname, 'ip_address' => $vmware->ipAddress?->ip, 'os_name' => $vmware->os?->name, 'status' => $vmware->status, ] : null, ]; return response()->json(['success' => true, 'data' => $data]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } /** * PATCH /v1/client/dich-vu/{id}/auto-renew — toggle auto-renewal on a service */ public function updateClientServiceAutoRenew(Request $request, int $id): JsonResponse { try { $validated = $request->validate(['auto_renew' => ['required', 'boolean']]); $service = Service::query() ->where('id', $id) ->where('customer_id', $request->user()->id) ->first(); if (!$service) { return $this->notFound('Không tìm thấy dịch vụ.'); } $service->update(['auto_renew' => (bool) $validated['auto_renew']]); return response()->json([ 'success' => true, 'message' => 'Cập nhật tự động gia hạn thành công.', 'data' => ['id' => $service->id, 'auto_renew' => (bool) $service->auto_renew], ]); } catch (\Exception $e) { return $this->error($e->getMessage()); } } // ========================================================================= // Private — formatters // ========================================================================= private function formatOrderSummary(Order $order): array { return [ 'id' => $order->id, 'order_num' => $order->order_num, 'status' => $order->status, 'status_text' => OrderStatus::from((int) $order->status)->label(), 'sub_total' => $order->sub_total, 'total' => $order->total, 'created_at' => $this->isoDate($order->created_at), ]; } private function formatInvoiceSummary(Invoice $invoice): array { return [ 'id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'sub_total' => $invoice->sub_total, 'tax' => $invoice->tax, 'total' => $invoice->total, 'status' => $invoice->status, 'due_date' => $this->isoDate($invoice->due_date), ]; } private function formatOrderItems($items): array { return $items->map(fn($item) => [ 'id' => $item->id, 'billing_cycle' => $item->billing_cycle, 'months' => $item->months, 'server_name' => $item->server_name, 'os_id' => $item->os_id, 'price' => (float) $item->price, 'product' => $item->product ? [ 'id' => $item->product->id, 'uuid' => $item->product->uuid, 'name' => $item->product->name, 'product_type' => $item->product->product_type, 'specs' => [ 'cpu' => $item->product->cpu, 'ram' => $item->product->ram, 'disk' => $item->product->disk, 'bandwidth' => $item->product->bandwidth, ], ] : null, ])->all(); } private function formatOrderDetail(Order $order): array { $invoice = $order->invoice; return [ 'id' => $order->id, 'order_num' => $order->order_num, 'status' => $order->status, 'status_text' => OrderStatus::from((int) $order->status)->label(), 'total' => (float) $order->total, 'created_at' => $this->isoDate($order->created_at), 'customer' => [ 'id' => $order->customer?->id, 'name' => $order->customer?->fullname, 'email' => $order->customer?->email, ], 'invoice' => $invoice ? [ 'id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'status' => $invoice->status, 'status_text' => InvoiceStatus::tryFrom((int) $invoice->status)?->label() ?? '-', 'sub_total' => $invoice->sub_total, 'tax' => $invoice->tax, 'total' => $invoice->total, 'due_date' => $this->isoDate($invoice->due_date), 'paid_at' => $this->isoDate($invoice->paid_at), ] : null, 'items' => $this->formatOrderItems($order->orderItems), ]; } private function formatInvoiceDetail(Invoice $invoice): array { $order = $invoice->order; $vmware = $order?->vmWare->first(); return [ 'id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'invoice_type' => $invoice->invoice_type, 'status' => $invoice->status, 'status_text' => InvoiceStatus::tryFrom((int) $invoice->status)?->label() ?? '-', 'sub_total' => (float) $invoice->sub_total, 'discount_amount' => (float) ($invoice->discount_amount ?? 0), 'tax' => (float) $invoice->tax, 'total' => (float) $invoice->total, 'payment_method' => $invoice->payment_method, 'auto_renew' => (bool) $invoice->auto_renew, 'created_at' => $this->isoDate($invoice->created_at), 'due_date' => $this->isoDate($invoice->due_date), 'paid_at' => $this->isoDate($invoice->paid_at), 'customer' => $invoice->customer ? [ 'id' => $invoice->customer->id, 'name' => $invoice->customer->fullname, 'email' => $invoice->customer->email, 'address' => $invoice->customer->address, ] : null, 'order' => $order ? [ 'id' => $order->id, 'order_num' => $order->order_num, 'total' => (float) $order->total, ] : null, 'service' => ($firstItem = $order?->orderItems->first()) ? [ 'product_id' => $firstItem->product?->id, 'product_uuid' => $firstItem->product?->uuid, 'name' => $firstItem->product?->name, 'billing_cycle' => $firstItem->billing_cycle, 'server_name' => $firstItem->server_name, 'specs' => [ 'cpu' => $firstItem->product?->cpu, 'ram' => $firstItem->product?->ram, 'disk' => $firstItem->product?->disk, ], ] : null, 'items' => ($invoice->relationLoaded('invoiceItems') ? $invoice->invoiceItems : collect())->map(fn($item) => [ 'id' => $item->id, 'type' => $item->type, 'description' => $item->description, 'months' => $item->months, 'quantity' => $item->quantity, 'unit_price' => (float) $item->unit_price, 'total_price' => (float) $item->total_price, ])->all(), 'attachments' => ($invoice->relationLoaded('attachments') ? $invoice->attachments : collect())->map(fn($a) => [ 'id' => $a->id, 'filename' => $a->filename, 'url' => $a->url, 'mime_type' => $a->mime_type, ])->all(), 'vmware' => $vmware ? [ 'id' => $vmware->id, 'name' => $vmware->name, 'hostname' => $vmware->hostname, 'ip_address' => $vmware->ipAddress?->ip, 'os_name' => $vmware->os?->name, 'status' => $vmware->status, 'next_due_date' => $this->isoDate($vmware->next_due_date), ] : null, ]; } // ========================================================================= // Micro-helpers // ========================================================================= private function isoDate($value): ?string { if (empty($value)) { return null; } try { if ($value instanceof \DateTimeInterface) { return Carbon::instance($value)->toISOString(); } if (is_numeric($value)) { return Carbon::createFromTimestamp((int) $value)->toISOString(); } return Carbon::parse($value)->toISOString(); } catch (\Throwable) { return null; } } private function error(string $message, int $code = 400): JsonResponse { return response()->json(['success' => false, 'message' => $message], $code); } private function notFound(string $message): JsonResponse { return response()->json(['success' => false, 'message' => $message], 404); } }