- Nạp tiền: mã thanh toán sống 30 phút (ManualDepositDraft), khôi phục/huỷ mã, lệnh dọn mã hết hạn; total_pending chỉ tính giao dịch đã nộp chứng từ, admin duyệt/từ chối/xoá giao dịch - Gia hạn dịch vụ: phát hành invoice gia hạn trên order gốc và trừ số dư ngay - Đồng bộ domain từ Plesk theo server, ServiceOrderRecorder tạo order/invoice cho dịch vụ admin dựng - Danh sách dịch vụ trả field theo loại (service_plan, dns_records_count), đếm dịch vụ theo loại Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
380 lines
15 KiB
PHP
380 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(),
|
|
'updated_at' => $service->updated_at?->toIso8601String(),
|
|
];
|
|
}
|
|
}
|