Add feature tests for domain order management
- Implement AdminDomainOrderTest to cover admin functionalities for domain orders including listing, confirming registration, KYC updates, activation, rejection, and refunds. - Implement ClientDomainOrderTest to cover client functionalities for placing domain orders, including validation for balance, duplicate orders, and domain normalization. - Ensure proper handling of domain pricing and service statuses throughout the tests.
This commit is contained in:
@@ -5,10 +5,11 @@ namespace App\Http\Controllers\Api\Admin\Customer;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\CreateCustomerRequest;
|
||||
use App\Http\Resources\Api\Admin\CustomerResource;
|
||||
use App\Jobs\Mail\PasswordRecoverJob;
|
||||
use App\Mail\Customer\ManualCustomerEmail;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
use App\Models\Template;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\Log;
|
||||
use App\Repositories\Customer\CustomerRepositoryInterface;
|
||||
@@ -25,6 +26,7 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Enums\Order\OrderStatus;
|
||||
@@ -233,11 +235,31 @@ class CustomerController extends Controller
|
||||
|
||||
public function trash(Request $request): JsonResponse
|
||||
{
|
||||
$query = $this->customerRepository->onlyTrashed([], ['hosting', 'vps', 'domain']);
|
||||
$perPage = min((int) $request->get('per_page', 15), 500);
|
||||
|
||||
$query = Customer::onlyTrashed()
|
||||
->with(['hosting', 'vps', 'domain']);
|
||||
|
||||
if ($filter = $request->get('filter')) {
|
||||
$query->where(function ($q) use ($filter) {
|
||||
$filter = trim($filter);
|
||||
$q->where('fullname', 'LIKE', "%{$filter}%")
|
||||
->orWhere('email', 'LIKE', "%{$filter}%")
|
||||
->orWhere('phone', 'LIKE', "%{$filter}%");
|
||||
});
|
||||
}
|
||||
|
||||
$paginator = $query->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => CustomerResource::collection($query->get())
|
||||
'data' => [
|
||||
'items' => CustomerResource::collection($paginator),
|
||||
'total' => $paginator->total(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -462,6 +484,105 @@ class CustomerController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function sendEmail(Request $request, string $uuid): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'template_id' => 'nullable|integer|exists:template,id',
|
||||
'subject' => 'required_without:template_id|nullable|string|max:255',
|
||||
'message' => 'required_without:template_id|nullable|string',
|
||||
'to' => 'nullable|string|max:1000',
|
||||
'cc' => 'nullable|string|max:1000',
|
||||
'bcc' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
||||
|
||||
if (!$customer) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => "Can't find customer by UUID #" . $uuid,
|
||||
], 404);
|
||||
}
|
||||
|
||||
$template = null;
|
||||
$subject = trim((string) ($data['subject'] ?? ''));
|
||||
$message = trim((string) ($data['message'] ?? ''));
|
||||
|
||||
if (!empty($data['template_id'])) {
|
||||
$template = Template::query()
|
||||
->whereKey($data['template_id'])
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if (!$template) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Template không tồn tại hoặc đang bị tắt.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$subject = trim((string) $template->subject);
|
||||
$message = trim((string) $template->message);
|
||||
}
|
||||
|
||||
if ($subject === '' || $message === '') {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Subject và content không được để trống.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$to = $this->parseEmailRecipients($data['to'] ?? null);
|
||||
if ($to === []) {
|
||||
$to = [$customer->email];
|
||||
}
|
||||
|
||||
$cc = $this->parseEmailRecipients($data['cc'] ?? null);
|
||||
$bcc = $this->parseEmailRecipients($data['bcc'] ?? null);
|
||||
|
||||
if ($to === []) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Không có địa chỉ email nhận hợp lệ.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$mailer = Mail::to($to);
|
||||
if ($cc !== []) {
|
||||
$mailer->cc($cc);
|
||||
}
|
||||
if ($bcc !== []) {
|
||||
$mailer->bcc($bcc);
|
||||
}
|
||||
|
||||
$mailer->send(new ManualCustomerEmail($subject, $message));
|
||||
|
||||
$admin = Auth::guard('admin')->user();
|
||||
Log::create([
|
||||
'agent' => 'admin',
|
||||
'client_ip' => $request->ip(),
|
||||
'description' => sprintf(
|
||||
'%s gửi email cho customer %s%s',
|
||||
$admin->username ?? $admin->email ?? 'Admin',
|
||||
$customer->email,
|
||||
$template ? sprintf(' bằng template #%s', $template->id) : '',
|
||||
),
|
||||
'customer_id' => $customer->id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã gửi email cho khách hàng.',
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function addCredit(Request $request, string $uuid): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
@@ -510,6 +631,20 @@ class CustomerController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
private function parseEmailRecipients(?string $value): array
|
||||
{
|
||||
if (!$value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect(preg_split('/[,;]+/', $value) ?: [])
|
||||
->map(fn($email) => trim((string) $email))
|
||||
->filter(fn($email) => $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function addTotal(Request $request, string $uuid): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
<?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\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 Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
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,
|
||||
) {}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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,
|
||||
'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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Domain;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AdminDomainPricingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Default domain pricing data (used as fallback and for seeding)
|
||||
*/
|
||||
private function getDefaultData(): array
|
||||
{
|
||||
return [
|
||||
'domains' => [
|
||||
['tld' => '.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 400000, 'renewal' => 400000],
|
||||
['tld' => '.com.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 300000, 'renewal' => 300000],
|
||||
['tld' => '.net.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 300000, 'renewal' => 300000],
|
||||
['tld' => '.org.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 300000, 'renewal' => 300000],
|
||||
['tld' => '.com', 'setupFee' => 'Miễn phí', 'firstYear' => 320000, 'renewal' => 340000],
|
||||
['tld' => '.net', 'setupFee' => 'Miễn phí', 'firstYear' => 369000, 'renewal' => 409000],
|
||||
['tld' => '.org', 'setupFee' => 'Miễn phí', 'firstYear' => 339000, 'renewal' => 369000],
|
||||
['tld' => '.biz', 'setupFee' => 'Miễn phí', 'firstYear' => 399000, 'renewal' => 409000],
|
||||
['tld' => '.info', 'setupFee' => 'Miễn phí', 'firstYear' => 490000, 'renewal' => 509000],
|
||||
['tld' => '.pro', 'setupFee' => 'Miễn phí', 'firstYear' => 469000, 'renewal' => 489000],
|
||||
['tld' => '.name', 'setupFee' => 'Miễn phí', 'firstYear' => 259000, 'renewal' => 259000],
|
||||
['tld' => '.ws', 'setupFee' => 'Miễn phí', 'firstYear' => 909000, 'renewal' => 909000],
|
||||
['tld' => '.tv', 'setupFee' => 'Miễn phí', 'firstYear' => 765000, 'renewal' => 819000],
|
||||
['tld' => '.mobi', 'setupFee' => 'Miễn phí', 'firstYear' => 589000, 'renewal' => 589000],
|
||||
['tld' => '.uk', 'setupFee' => 'Miễn phí', 'firstYear' => 537000, 'renewal' => 537000],
|
||||
['tld' => '.asia', 'setupFee' => 'Miễn phí', 'firstYear' => 380000, 'renewal' => 452000],
|
||||
['tld' => '.me', 'setupFee' => 'Miễn phí', 'firstYear' => 1029000, 'renewal' => 1029000],
|
||||
['tld' => '.us', 'setupFee' => 'Miễn phí', 'firstYear' => 323000, 'renewal' => 323000],
|
||||
],
|
||||
'services' => [
|
||||
['name' => 'Chứng thực Rapid SSL', 'setupFee' => 230000, 'maintenance' => 750000, 'note' => '01 Domain'],
|
||||
['name' => 'Chứng thực Quick SSL', 'setupFee' => 230000, 'maintenance' => 2300000, 'note' => '01 Domain'],
|
||||
['name' => 'Chứng thực Wildcard SSL', 'setupFee' => 230000, 'maintenance' => 3300000, 'note' => 'Unlimited Subdomain'],
|
||||
['name' => 'Chứng thực True BusinessID', 'setupFee' => 540000, 'maintenance' => 3900000, 'note' => '01 Domain'],
|
||||
['name' => 'Chứng thực True BusinessID WildCard', 'setupFee' => 900000, 'maintenance' => 15000000, 'note' => 'Unlimited Subdomain'],
|
||||
['name' => 'Đăng ký DNS riêng', 'setupFee' => 200000, 'maintenance' => 'Miễn phí', 'note' => "dns1.tenmiencuaban.com\ndns2.tenmiencuaban.com"],
|
||||
['name' => 'Bảo mật thông tin tên miền', 'setupFee' => 'Miễn phí', 'maintenance' => 90000, 'note' => 'Toàn bộ thông tin Whois tên miền của bạn sẽ được giấu kín hoàn toàn với mọi công cụ Whois'],
|
||||
['name' => 'Khóa tên miền', 'setupFee' => 'Miễn phí', 'maintenance' => 'Miễn phí', 'note' => 'Tăng cường bảo mật cho tên miền'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /administrator/domain-pricing
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$setting = Setting::where('key', 'domain_pricing')->first();
|
||||
|
||||
if ($setting && !empty($setting->value)) {
|
||||
$value = json_decode((string) $setting->value, true);
|
||||
if (is_array($value) && !empty($value)) {
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $value,
|
||||
'message' => 'Lấy bảng giá tên miền thành công.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $this->getDefaultData(),
|
||||
'message' => 'Lấy bảng giá tên miền thành công (mặc định).',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /administrator/domain-pricing
|
||||
*/
|
||||
public function update(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'domains' => 'sometimes|array',
|
||||
'services' => 'sometimes|array',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $validator->errors()->first(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$current = Setting::where('key', 'domain_pricing')->first();
|
||||
$existing = $current ? json_decode((string) $current->value, true) : $this->getDefaultData();
|
||||
|
||||
if ($request->has('domains')) {
|
||||
$existing['domains'] = $request->input('domains');
|
||||
}
|
||||
if ($request->has('services')) {
|
||||
$existing['services'] = $request->input('services');
|
||||
}
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'domain_pricing'],
|
||||
['value' => json_encode($existing, JSON_UNESCAPED_UNICODE)]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Cập nhật bảng giá tên miền thành công.',
|
||||
'data' => $existing,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /administrator/domain-pricing/reset
|
||||
* Reset to default values.
|
||||
*/
|
||||
public function reset(): JsonResponse
|
||||
{
|
||||
$default = $this->getDefaultData();
|
||||
|
||||
Setting::updateOrCreate(
|
||||
['key' => 'domain_pricing'],
|
||||
['value' => json_encode($default, JSON_UNESCAPED_UNICODE)]
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đã reset bảng giá tên miền về mặc định.',
|
||||
'data' => $default,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Domain;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class InetDomainController extends Controller
|
||||
{
|
||||
private const BASE_URL = 'https://developers.inet.vn/api/gateway/v1/domain';
|
||||
|
||||
/**
|
||||
* Lấy API key từ settings
|
||||
*/
|
||||
private function getApiKey(): ?string
|
||||
{
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$value = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
return $value['inet_api_key'] ?? $value['api_inet'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Headers mặc định cho iNET API
|
||||
*/
|
||||
private function defaultHeaders(): array
|
||||
{
|
||||
$apiKey = $this->getApiKey();
|
||||
|
||||
if (! $apiKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'Authorization' => 'Bearer ' . $apiKey,
|
||||
'Accept' => 'application/json',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /administrator/inet-domain/whois/{domain}
|
||||
* Tra cứu thông tin WHOIS của tên miền.
|
||||
*/
|
||||
public function whois(string $domain, Request $request): JsonResponse
|
||||
{
|
||||
$apiKey = $this->getApiKey();
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chưa cấu hình API key iNET (api_inet).',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$url = self::BASE_URL . '/whois/' . urlencode($domain);
|
||||
|
||||
if ($request->boolean('refreshCache')) {
|
||||
$url .= '?refreshCache=true';
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders($this->defaultHeaders())
|
||||
->timeout(30)
|
||||
->get($url);
|
||||
|
||||
return $this->proxyResponse($response);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Lỗi kết nối iNET API: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /administrator/inet-domain/check-available
|
||||
* Kiểm tra tên miền có khả dụng không.
|
||||
*/
|
||||
public function checkAvailable(Request $request): JsonResponse
|
||||
{
|
||||
$apiKey = $this->getApiKey();
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chưa cấu hình API key iNET (api_inet).',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $validator->errors()->first(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$url = self::BASE_URL . '/check-available?name=' . urlencode($request->input('name'));
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders($this->defaultHeaders())
|
||||
->timeout(30)
|
||||
->get($url);
|
||||
|
||||
return $this->proxyResponse($response);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Lỗi kết nối iNET API: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /administrator/inet-domain/bulk-check-available
|
||||
* Kiểm tra nhiều tên miền cùng lúc.
|
||||
*/
|
||||
public function bulkCheckAvailable(Request $request): JsonResponse
|
||||
{
|
||||
$apiKey = $this->getApiKey();
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chưa cấu hình API key iNET (api_inet).',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'domains' => 'required|array|min:1',
|
||||
'domains.*' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $validator->errors()->first(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
...$this->defaultHeaders(),
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->timeout(30)
|
||||
->post(self::BASE_URL . '/bulk-check-available', [
|
||||
'domains' => $request->input('domains'),
|
||||
]);
|
||||
|
||||
return $this->proxyResponse($response);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Lỗi kết nối iNET API: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /administrator/inet-domain/ai-suggest
|
||||
* Gợi ý tên miền bằng AI.
|
||||
*/
|
||||
public function aiSuggest(Request $request): JsonResponse
|
||||
{
|
||||
$apiKey = $this->getApiKey();
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chưa cấu hình API key iNET (api_inet).',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'tlds' => 'required|array|min:1',
|
||||
'tlds.*' => 'required|string|max:50',
|
||||
'message' => 'required|string|max:500',
|
||||
'maxLength' => 'nullable|integer|min:1|max:100',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $validator->errors()->first(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'tlds' => $request->input('tlds'),
|
||||
'message' => $request->input('message'),
|
||||
];
|
||||
|
||||
if ($request->has('maxLength')) {
|
||||
$payload['maxLength'] = $request->input('maxLength');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
...$this->defaultHeaders(),
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->timeout(30)
|
||||
->post(self::BASE_URL . '/ai-suggest', $payload);
|
||||
|
||||
return $this->proxyResponse($response);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Lỗi kết nối iNET API: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /administrator/inet-domain/suffixes
|
||||
* Lấy danh sách đuôi tên miền (TLD) và bảng giá.
|
||||
*/
|
||||
public function suffixes(): JsonResponse
|
||||
{
|
||||
$apiKey = $this->getApiKey();
|
||||
|
||||
if (! $apiKey) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Chưa cấu hình API key iNET (api_inet).',
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::withHeaders($this->defaultHeaders())
|
||||
->timeout(30)
|
||||
->get(self::BASE_URL . '/suffixes');
|
||||
|
||||
return $this->proxyResponse($response);
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => 'Lỗi kết nối iNET API: ' . $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Xử lý response từ iNET API và trả về client.
|
||||
*/
|
||||
private function proxyResponse(\Illuminate\Http\Client\Response $response): JsonResponse
|
||||
{
|
||||
$status = $response->status();
|
||||
|
||||
if ($response->failed()) {
|
||||
$body = $response->json() ?? ['message' => $response->body()];
|
||||
return response()->json([
|
||||
'error' => true,
|
||||
'message' => $body['message'] ?? 'iNET API trả về lỗi HTTP ' . $status,
|
||||
'data' => $body,
|
||||
], $status >= 400 && $status < 600 ? $status : 500);
|
||||
}
|
||||
|
||||
$body = $response->json();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $body,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Admin\Media;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
private const IMAGE_MIME_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/webp',
|
||||
'image/gif',
|
||||
'image/svg+xml',
|
||||
];
|
||||
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'keyword' => ['sometimes', 'nullable', 'string', 'max:255'],
|
||||
'type' => ['sometimes', 'nullable', 'in:all,image,file'],
|
||||
'page' => ['sometimes', 'integer', 'min:1'],
|
||||
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||
]);
|
||||
|
||||
$keyword = Str::lower((string) ($validated['keyword'] ?? ''));
|
||||
$type = $validated['type'] ?? 'all';
|
||||
$page = (int) ($validated['page'] ?? 1);
|
||||
$perPage = (int) ($validated['per_page'] ?? 24);
|
||||
|
||||
$files = collect(Storage::disk('local')->allFiles())
|
||||
->filter(fn (string $path) => ! Str::startsWith(basename($path), '.'))
|
||||
->map(fn (string $path) => $this->formatFile($path))
|
||||
->filter(function (array $file) use ($keyword, $type) {
|
||||
if ($type === 'image' && ! $file['is_image']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === 'file' && $file['is_image']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($keyword === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Str::contains(Str::lower($file['name']), $keyword)
|
||||
|| Str::contains(Str::lower($file['path']), $keyword)
|
||||
|| Str::contains(Str::lower((string) $file['mime_type']), $keyword);
|
||||
})
|
||||
->sortByDesc('last_modified')
|
||||
->values();
|
||||
|
||||
$total = $files->count();
|
||||
$items = $files->slice(($page - 1) * $perPage, $perPage)->values();
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'items' => $items,
|
||||
'current_page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => $total,
|
||||
'last_page' => (int) ceil($total / $perPage),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function formatFile(string $path): array
|
||||
{
|
||||
$disk = Storage::disk('local');
|
||||
$mimeType = $disk->mimeType($path) ?: 'application/octet-stream';
|
||||
$isPublic = Str::startsWith($path, 'public/');
|
||||
$publicPath = $isPublic ? Str::after($path, 'public/') : null;
|
||||
|
||||
return [
|
||||
'id' => sha1($path),
|
||||
'disk' => $isPublic ? 'public' : 'local',
|
||||
'name' => basename($path),
|
||||
'path' => $path,
|
||||
'directory' => trim(dirname($path), '.'),
|
||||
'mime_type' => $mimeType,
|
||||
'is_image' => in_array($mimeType, self::IMAGE_MIME_TYPES, true),
|
||||
'size' => $disk->size($path),
|
||||
'last_modified' => date(DATE_ATOM, $disk->lastModified($path)),
|
||||
'url' => $publicPath ? Storage::disk('public')->url($publicPath) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class SettingController extends Controller
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'setting' => 'sometimes|array',
|
||||
'setting.api_inet' => 'sometimes|string|nullable',
|
||||
'smtp' => 'sometimes|array',
|
||||
'smtp.host' => 'sometimes|string',
|
||||
'smtp.port' => 'sometimes|integer',
|
||||
|
||||
@@ -193,9 +193,30 @@ class ClientController extends Controller
|
||||
$client = $result['client'];
|
||||
$this->activityLogger->logAuth('register', 'customer', $client->id, $client->email);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Đăng ký thành công. Vui lòng kiểm tra email và nhập mã xác thực để kích hoạt tài khoản.',
|
||||
]);
|
||||
}
|
||||
|
||||
public function verifyEmail(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'code' => ['required', 'string', 'size:6'],
|
||||
]);
|
||||
|
||||
$result = $this->authService->verifyEmail(
|
||||
$request->input('email'),
|
||||
$request->input('code'),
|
||||
);
|
||||
|
||||
$client = $result['client'];
|
||||
$this->activityLogger->logAuth('email_verified', 'customer', $client->id, $client->email);
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => [
|
||||
'data' => [
|
||||
'client' => new ClientResource($client->loadCount(['vps', 'hosting', 'domain'])),
|
||||
'token' => $result['token'],
|
||||
'refresh_token' => $result['refresh_token'],
|
||||
@@ -204,6 +225,20 @@ class ClientController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function resendVerifyCode(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
]);
|
||||
|
||||
$this->authService->resendVerifyCode($request->input('email'));
|
||||
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'message' => 'Nếu email tồn tại và chưa xác thực, mã xác thực mới đã được gửi.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/client/logout",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\Client\Domain;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\Client\Domain\StoreDomainOrderRequest;
|
||||
use App\Mail\Admin\DomainOrderNewNotification;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Template;
|
||||
use App\Services\Api\Domain\DomainOrderService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class DomainOrderController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DomainOrderService $domainOrderService,
|
||||
) {}
|
||||
|
||||
public function store(StoreDomainOrderRequest $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->domainOrderService->placeOrder($request->validated(), $request->user());
|
||||
|
||||
$this->notifyAdminNewDomainOrder($request->user(), $result);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Đặt mua tên miền thành công.',
|
||||
'notice' => 'Việc đặt mua không đồng nghĩa tên miền đã được đăng ký thành công. Đơn hàng sẽ được nhân viên kiểm tra và xử lý. Nếu tên miền không còn khả dụng, số dư đã thanh toán sẽ được hoàn lại vào tài khoản.',
|
||||
'data' => [
|
||||
'order_num' => $result['order']->order_num,
|
||||
'domain_name' => $result['order']->orderItems->first()?->domain_name,
|
||||
'status' => 'pending',
|
||||
'total' => $result['invoice']->total,
|
||||
'invoice_number' => $result['invoice']->invoice_number,
|
||||
],
|
||||
], 201);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 422);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('DomainOrderController@store', [
|
||||
'error' => $e->getMessage(),
|
||||
'customer_id' => $request->user()?->id,
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Đặt mua tên miền thất bại. Vui lòng thử lại.',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function notifyAdminNewDomainOrder($customer, array $result): void
|
||||
{
|
||||
$domainName = $result['order']->orderItems->first()?->domain_name ?? '';
|
||||
$orderNum = $result['order']->order_num;
|
||||
$total = number_format((int) $result['invoice']->total) . ' VNĐ';
|
||||
|
||||
Notification::notifyAdmins([
|
||||
'type' => 'domain_order_new',
|
||||
'title' => 'Đơn đăng ký tên miền mới',
|
||||
'body' => "Khách hàng {$customer->fullname} vừa đặt mua tên miền {$domainName} (#{$orderNum})",
|
||||
'link' => '/domain-orders',
|
||||
]);
|
||||
|
||||
try {
|
||||
$setting = Setting::where('key', 'setting')->first();
|
||||
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
|
||||
$adminEmail = $settingValue['admin_email'] ?? null;
|
||||
|
||||
if (empty($adminEmail)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$template = Template::where('name', 'Domain Order New')->first();
|
||||
if (!$template) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adminUrl = rtrim(env('ADMIN_URL', config('app.url')), '/') . '/domain-orders';
|
||||
|
||||
Mail::to($adminEmail)->send(new DomainOrderNewNotification($template, [
|
||||
'company_name' => $settingValue['company'] ?? config('app.name'),
|
||||
'company_logo' => $settingValue['logo'] ?? '',
|
||||
'domain_name' => $domainName,
|
||||
'customer_name' => $customer->fullname,
|
||||
'customer_email' => $customer->email,
|
||||
'order_num' => $orderNum,
|
||||
'order_total' => $total,
|
||||
'created_at' => now()->format('d/m/Y H:i:s'),
|
||||
'admin_url' => $adminUrl,
|
||||
]));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('DomainOrderController: không thể gửi email admin: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Client\Domain;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DomainPricingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Default domain pricing data (fallback when settings not configured)
|
||||
*/
|
||||
private function getDefaultDomainPricing(): array
|
||||
{
|
||||
return [
|
||||
'domains' => [
|
||||
['tld' => '.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 400000, 'renewal' => 400000],
|
||||
['tld' => '.com.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 300000, 'renewal' => 300000],
|
||||
['tld' => '.net.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 300000, 'renewal' => 300000],
|
||||
['tld' => '.org.vn', 'setupFee' => 'Miễn phí', 'firstYear' => 300000, 'renewal' => 300000],
|
||||
['tld' => '.com', 'setupFee' => 'Miễn phí', 'firstYear' => 320000, 'renewal' => 340000],
|
||||
['tld' => '.net', 'setupFee' => 'Miễn phí', 'firstYear' => 369000, 'renewal' => 409000],
|
||||
['tld' => '.org', 'setupFee' => 'Miễn phí', 'firstYear' => 339000, 'renewal' => 369000],
|
||||
['tld' => '.biz', 'setupFee' => 'Miễn phí', 'firstYear' => 399000, 'renewal' => 409000],
|
||||
['tld' => '.info', 'setupFee' => 'Miễn phí', 'firstYear' => 490000, 'renewal' => 509000],
|
||||
['tld' => '.pro', 'setupFee' => 'Miễn phí', 'firstYear' => 469000, 'renewal' => 489000],
|
||||
['tld' => '.name', 'setupFee' => 'Miễn phí', 'firstYear' => 259000, 'renewal' => 259000],
|
||||
['tld' => '.ws', 'setupFee' => 'Miễn phí', 'firstYear' => 909000, 'renewal' => 909000],
|
||||
['tld' => '.tv', 'setupFee' => 'Miễn phí', 'firstYear' => 765000, 'renewal' => 819000],
|
||||
['tld' => '.mobi', 'setupFee' => 'Miễn phí', 'firstYear' => 589000, 'renewal' => 589000],
|
||||
['tld' => '.uk', 'setupFee' => 'Miễn phí', 'firstYear' => 537000, 'renewal' => 537000],
|
||||
['tld' => '.asia', 'setupFee' => 'Miễn phí', 'firstYear' => 380000, 'renewal' => 452000],
|
||||
['tld' => '.me', 'setupFee' => 'Miễn phí', 'firstYear' => 1029000, 'renewal' => 1029000],
|
||||
['tld' => '.us', 'setupFee' => 'Miễn phí', 'firstYear' => 323000, 'renewal' => 323000],
|
||||
],
|
||||
'services' => [
|
||||
['name' => 'Chứng thực Rapid SSL', 'setupFee' => 230000, 'maintenance' => 750000, 'note' => '01 Domain'],
|
||||
['name' => 'Chứng thực Quick SSL', 'setupFee' => 230000, 'maintenance' => 2300000, 'note' => '01 Domain'],
|
||||
['name' => 'Chứng thực Wildcard SSL', 'setupFee' => 230000, 'maintenance' => 3300000, 'note' => 'Unlimited Subdomain'],
|
||||
['name' => 'Chứng thực True BusinessID', 'setupFee' => 540000, 'maintenance' => 3900000, 'note' => '01 Domain'],
|
||||
['name' => 'Chứng thực True BusinessID WildCard', 'setupFee' => 900000, 'maintenance' => 15000000, 'note' => 'Unlimited Subdomain'],
|
||||
['name' => 'Đăng ký DNS riêng', 'setupFee' => 200000, 'maintenance' => 'Miễn phí', 'note' => "dns1.tenmiencuaban.com\ndns2.tenmiencuaban.com"],
|
||||
['name' => 'Bảo mật thông tin tên miền', 'setupFee' => 'Miễn phí', 'maintenance' => 90000, 'note' => 'Toàn bộ thông tin Whois tên miền của bạn sẽ được giấu kín hoàn toàn với mọi công cụ Whois'],
|
||||
['name' => 'Khóa tên miền', 'setupFee' => 'Miễn phí', 'maintenance' => 'Miễn phí', 'note' => 'Tăng cường bảo mật cho tên miền'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /v1/domain-pricing
|
||||
* Returns domain pricing list and addon services.
|
||||
* Public endpoint (no auth required).
|
||||
*/
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$setting = Setting::where('key', 'domain_pricing')->first();
|
||||
|
||||
if ($setting && !empty($setting->value)) {
|
||||
$value = json_decode((string) $setting->value, true);
|
||||
if (is_array($value) && !empty($value)) {
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $value,
|
||||
'message' => 'Lấy bảng giá tên miền thành công.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default data
|
||||
return response()->json([
|
||||
'error' => false,
|
||||
'data' => $this->getDefaultDomainPricing(),
|
||||
'message' => 'Lấy bảng giá tên miền thành công (mặc định).',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user