Files
portal-api/app/Http/Controllers/Api/Admin/Domain/AdminDomainOrderController.php
T
baodev0 c377c7e195 feat: add domain registration form and email notifications
- Create a new Blade view for domain registration (`registrant-tochuc.blade.php`) to capture organization and manager details.
- Implement email templates for new domain orders for both admin (`domain-order-new.blade.php`) and customers (`domain-order-received.blade.php`).
- Add CSV exports for client and domain data.
- Introduce tests for customer mass assignment, password reset, login throttling, and permission gating.
- Ensure service renewals use current pricing and validate ownership for virtual machines.
- Validate mass assignment behavior in service model.
2026-08-22 17:30:55 +07:00

357 lines
14 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\Mail\Customer\DomainOrderStatusMail;
use App\Models\Notification;
use App\Models\Order;
use App\Models\Service;
use App\Models\Setting;
use App\Models\Template;
use App\Repositories\Api\Domain\DomainOrderRepository;
use App\Services\Api\Domain\DomainOrderService;
use App\Services\Api\Domain\DomainRegistrantService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
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, $templateName] = $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 {
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
$template = Template::where('name', $templateName)->first();
if (!$template) {
return;
}
Mail::to($customer->email)->send(new DomainOrderStatusMail($template, [
'company_name' => $settingValue['company'] ?? config('app.name'),
'company_logo' => $settingValue['logo'] ?? '',
'domain_name' => $domainName,
'client_name' => $customer->fullname,
'order_num' => $orderNum,
'reason' => $reason,
]));
} catch (\Throwable $e) {
Log::error('AdminDomainOrderController: không thể gửi email khách hàng: ' . $e->getMessage());
}
}
// =========================================================================
// 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(),
];
}
}