repo->countPending(); return response()->json(['error' => false, 'data' => ['count' => $count]]); } public function index(Request $request): JsonResponse { $orders = $this->repo->getDomainOrders( $request->only(['status', 'customer', 'domain', 'from', 'to', 'per_page']) ); $items = collect($orders->items())->map(fn ($order) => $this->formatOrder($order)); return response()->json([ 'error' => false, 'data' => [ 'items' => $items, 'total' => $orders->total(), 'per_page' => $orders->perPage(), 'current_page' => $orders->currentPage(), 'last_page' => $orders->lastPage(), ], ]); } public function show(int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); if (!$order) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } return response()->json([ 'error' => false, 'data' => $this->formatOrder($order), ]); } public function confirmRegistration(ConfirmRegistrationRequest $request, int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); if (!$order) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } try { $service = $this->service->confirmRegistration($order, $request->validated()); return response()->json([ 'error' => false, 'message' => 'Xác nhận đăng ký tên miền thành công.', 'data' => $this->formatService($service), ]); } catch (\InvalidArgumentException $e) { return response()->json(['error' => true, 'message' => $e->getMessage()], 422); } catch (\Throwable $e) { Log::error('AdminDomainOrderController@confirmRegistration', ['error' => $e->getMessage(), 'id' => $id]); return response()->json(['error' => true, 'message' => 'Xác nhận thất bại. Vui lòng thử lại.'], 500); } } public function updateKyc(UpdateKycUrlRequest $request, int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); if (!$order) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } try { $service = $this->service->updateKycUrl($order, $request->validated()['kyc_url']); return response()->json([ 'error' => false, 'message' => 'Cập nhật link KYC thành công.', 'data' => $this->formatService($service), ]); } catch (\InvalidArgumentException $e) { return response()->json(['error' => true, 'message' => $e->getMessage()], 422); } catch (\Throwable $e) { Log::error('AdminDomainOrderController@updateKyc', ['error' => $e->getMessage(), 'id' => $id]); return response()->json(['error' => true, 'message' => 'Cập nhật KYC thất bại. Vui lòng thử lại.'], 500); } } public function activate(int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); if (!$order) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } try { $service = $this->service->activate($order); $this->notifyCustomerDomainStatus($order, 'approved'); return response()->json([ 'error' => false, 'message' => 'Kích hoạt dịch vụ tên miền thành công.', 'data' => $this->formatService($service), ]); } catch (\InvalidArgumentException $e) { return response()->json(['error' => true, 'message' => $e->getMessage()], 422); } catch (\Throwable $e) { Log::error('AdminDomainOrderController@activate', ['error' => $e->getMessage(), 'id' => $id]); return response()->json(['error' => true, 'message' => 'Kích hoạt thất bại. Vui lòng thử lại.'], 500); } } public function rejectAndRefund(RejectAndRefundRequest $request, int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); if (!$order) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } try { $service = $this->service->rejectAndRefund($order, $request->validated()['reason']); $this->notifyCustomerDomainStatus($order, 'rejected', $request->validated()['reason']); return response()->json([ 'error' => false, 'message' => 'Từ chối và hoàn tiền thành công.', 'data' => $this->formatService($service), ]); } catch (\InvalidArgumentException $e) { return response()->json(['error' => true, 'message' => $e->getMessage()], 422); } catch (\Throwable $e) { Log::error('AdminDomainOrderController@rejectAndRefund', ['error' => $e->getMessage(), 'id' => $id]); return response()->json(['error' => true, 'message' => 'Hoàn tiền thất bại. Vui lòng thử lại.'], 500); } } public function getRegistrant(int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); $service = $order?->orderItems->first()?->service; if (!$order || !$service) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } return response()->json([ 'error' => false, 'data' => $this->registrantService->get($service, $order->customer), ]); } public function updateRegistrant(UpdateRegistrantInfoRequest $request, int $id): JsonResponse { $order = $this->repo->findDomainOrder($id); $service = $order?->orderItems->first()?->service; if (!$order || !$service) { return response()->json([ 'error' => true, 'message' => 'Không tìm thấy đơn hàng domain.', ], 404); } $data = $request->validated(); $service = $this->registrantService->update($service, $data['registrant_type'], $data['registrant_info']); return response()->json([ 'error' => false, 'message' => 'Cập nhật thông tin đăng ký thành công.', 'data' => $this->formatService($service), ]); } public function downloadRegistrantDocx(int $id): Response { $order = $this->repo->findDomainOrder($id); $item = $order?->orderItems->first(); $service = $item?->service; abort_if(!$order || !$service, 404, 'Không tìm thấy đơn hàng domain.'); $registrant = $this->registrantService->get($service, $order->customer); $docx = $this->registrantService->renderDocx($item->domain_name, $registrant['type'], $registrant['info']); $filename = 'phieu-dang-ky-' . $item->domain_name . '.docx'; return response($docx, 200, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', ]); } // ========================================================================= // Private helpers // ========================================================================= private function notifyCustomerDomainStatus(Order $order, string $status, string $reason = ''): void { $customer = $order->customer; $domainName = $order->orderItems->first()?->domain_name ?? ''; $orderNum = $order->order_num; if (!$customer) { return; } [$title, $body, $scenario] = $status === 'approved' ? ["Tên miền {$domainName} đã được duyệt", "Tên miền {$domainName} (#{$orderNum}) đã được kích hoạt thành công.", 'domain_order_approved'] : ["Tên miền {$domainName} bị từ chối", "Tên miền {$domainName} (#{$orderNum}) đã bị từ chối. Lý do: {$reason}. Số dư đã được hoàn lại.", 'domain_order_rejected']; Notification::notifyCustomer($customer, [ 'type' => 'domain_order_' . $status, 'title' => $title, 'body' => $body, 'link' => '/don-hang', ]); try { if (!\App\Models\Template::where('scenario', $scenario)->where('status', 1)->exists()) { return; } app(EmailTemplateService::class)->send($scenario, $customer->email, [ ...MailGlobals::values(), ...$this->domainHandoverVariables($order), 'domain_name' => $domainName, 'client_name' => $customer->fullname, 'customer_email' => $customer->email, 'order_num' => $orderNum, 'reason' => $reason, ]); } catch (\Throwable $e) { Log::error('AdminDomainOrderController: không thể gửi email khách hàng: ' . $e->getMessage()); } } /** * @return array */ private function domainHandoverVariables(Order $order): array { $service = $order->orderItems->first()?->service?->fresh(); if (!$service) { return []; } return [ 'registered_at' => $service->started_at?->format('d/m/Y') ?? '', 'expires_at' => $service->expires_at?->format('d/m/Y') ?? '', 'kyc_block' => $service->kyc_url ? $this->kycBlock($service->kyc_url) : '', ]; } private function kycBlock(string $kycUrl): string { return EmailTemplateShell::heading('📋 Lưu ý quan trọng') . EmailTemplateShell::paragraph('Căn cứ Khoản 2 Điều 9 và Khoản 6 Điều 21 Thông tư số 48/2025/TT-BKHCN ngày 25/12/2025 của Bộ Khoa học và Công nghệ về Quản lý và sử dụng tài nguyên Internet, chủ thể đăng ký tên miền là cá nhân có trách nhiệm hoàn thiện hồ sơ đăng ký thông qua hình thức xác thực điện tử (eKYC) trong vòng 03 ngày kể từ thời điểm nhận được email này.') . EmailTemplateShell::button(htmlspecialchars($kycUrl, ENT_QUOTES, 'UTF-8'), '✅ Xác thực eKYC ngay') . EmailTemplateShell::note('Việc không hoàn thành xác thực đúng hạn có thể ảnh hưởng đến quyền sử dụng tên miền của Quý khách.', '⏰ Lưu ý:'); } // ========================================================================= // Private formatters // ========================================================================= private function formatOrder(Order $order): array { $item = $order->orderItems->first(); $service = $item?->service; return [ 'id' => $order->id, 'order_num' => $order->order_num, 'status' => $order->status, 'total' => $order->total, 'payment_method' => $order->payment_method, 'created_at' => $order->created_at?->toIso8601String(), 'customer' => $order->customer ? [ 'id' => $order->customer->id, 'uuid' => $order->customer->uuid, 'fullname' => $order->customer->fullname, 'email' => $order->customer->email, ] : null, 'domain_name' => $item?->domain_name, 'months' => $item?->months, 'invoice' => $order->invoice ? [ 'id' => $order->invoice->id, 'invoice_number' => $order->invoice->invoice_number, 'status' => $order->invoice->status, 'total' => $order->invoice->total, 'paid_at' => $order->invoice->paid_at?->toIso8601String(), ] : null, 'service' => $service ? $this->formatService($service) : null, ]; } private function formatService(Service $service): array { return [ 'id' => $service->id, 'uuid' => $service->uuid, 'status' => $service->status, 'server_name' => $service->server_name, 'registrar_reference' => $service->registrar_reference, 'kyc_url' => $service->kyc_url, 'rejection_reason' => $service->rejection_reason, 'registrant_type' => $service->registrant_type, 'registrant_info' => $service->registrant_info, 'auto_renew' => $service->auto_renew, 'started_at' => $service->started_at?->toIso8601String(), 'expires_at' => $service->expires_at?->toIso8601String(), 'refunded_at' => $service->refunded_at?->toIso8601String(), 'cancelled_at' => $service->cancelled_at?->toIso8601String(), 'created_at' => $service->created_at?->toIso8601String(), ]; } }