Files
portal-api/app/Http/Controllers/Api/Admin/Domain/AdminDomainOrderController.php
T
baodev0andClaude Opus 5.5 30898ea46f feat: email gửi tay/nhắc gia hạn, gói hosting, sửa dữ liệu sản phẩm
- Gửi email theo template cho 1 khách, tab lịch sử email, nhắc gia hạn dịch vụ hàng loạt
- Mail lưu from/bcc/sent_by; mẫu MARKETING và mẫu cũ trong TemplateSeeder
- Gói hosting: giới hạn, trạng thái, lấy plan Plesk qua CLI gateway
- Backfill payment_method và ngày đơn hàng còn thiếu
- Bổ sung field CPU/RAM/Disk/Băng thông cho loại sản phẩm vps và cloud

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:32:05 +07:00

379 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin\Domain;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Admin\Domain\ConfirmRegistrationRequest;
use App\Http\Requests\Api\Admin\Domain\RejectAndRefundRequest;
use App\Http\Requests\Api\Admin\Domain\UpdateKycUrlRequest;
use App\Http\Requests\Api\Admin\Domain\UpdateRegistrantInfoRequest;
use App\Models\Notification;
use App\Models\Order;
use App\Models\Service;
use App\Repositories\Api\Domain\DomainOrderRepository;
use App\Services\Api\Domain\DomainOrderService;
use App\Services\Api\Domain\DomainRegistrantService;
use App\Services\Email\EmailTemplateService;
use App\Support\EmailTemplateShell;
use App\Support\MailGlobals;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
class AdminDomainOrderController extends Controller
{
public function __construct(
private readonly DomainOrderRepository $repo,
private readonly DomainOrderService $service,
private readonly DomainRegistrantService $registrantService,
) {}
public function pendingCount(): JsonResponse
{
$count = $this->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<string, string>
*/
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 <strong>03 ngày</strong> 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(),
];
}
}