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.
This commit is contained in:
baodev0
2026-08-22 17:30:55 +07:00
parent eec2c5906e
commit c377c7e195
58 changed files with 3063 additions and 418 deletions
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Api\Admin\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\UpdateProfileRequest;
use App\Http\Resources\Api\Admin\AdminUserResource;
use App\Services\ActivityLogger;
use Illuminate\Http\JsonResponse;
@@ -61,6 +62,19 @@ class AuthController extends Controller
]);
}
public function updateProfile(UpdateProfileRequest $request): JsonResponse
{
$user = Auth::guard('admin')->user();
$user->update($request->validated());
return response()->json([
'error' => false,
'message' => 'Cập nhật hồ sơ thành công.',
'data' => ['user' => new AdminUserResource($user->fresh())],
]);
}
public function refresh(): JsonResponse
{
try {
@@ -31,6 +31,7 @@ use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Enums\TransactionStatus;
use App\Helpers\OrderHelper;
class CustomerController extends Controller
@@ -73,8 +74,6 @@ class CustomerController extends Controller
public function store(CreateCustomerRequest $request): JsonResponse
{
$request->validated();
$existingCustomer = $this->customerRepository->findBy('email', $request->get('email'));
if ($existingCustomer) {
return response()->json([
@@ -85,7 +84,7 @@ class CustomerController extends Controller
try {
DB::beginTransaction();
$data = $request->all();
$data = $request->validated();
$data['password'] = Hash::make($request->password);
$data['email_marketing'] = $request->email_marketing === "on" ? 1 : 0;
@@ -170,11 +169,10 @@ class CustomerController extends Controller
], 404);
}
$request->validated();
try {
DB::beginTransaction();
$data = $request->except(['_token', 'password']);
$data = $request->validated();
unset($data['password']);
if (!empty($request->password)) {
$data['password'] = Hash::make($request->password);
@@ -413,10 +411,15 @@ class CustomerController extends Controller
$this->customerRepository->update($customer->id, ['password' => Hash::make($password)]);
DB::commit();
Mail::to($customer->email)->send(new \App\Mail\Customer\PasswordResetByAdmin([
'fullname' => $customer->fullname,
'email' => $customer->email,
'password' => $password,
]));
return response()->json([
'error' => false,
'message' => 'Password reset successfully',
'data' => ['new_password' => $password]
'message' => 'Password reset successfully, new password has been sent to customer email',
]);
} catch (\Exception $e) {
DB::rollBack();
@@ -616,12 +619,15 @@ class CustomerController extends Controller
], 404);
}
$admin = Auth::guard('admin')->user();
try {
DB::beginTransaction();
$amount = (int) $request->amount;
if ($request->action === "+") {
$isAdd = $request->action === "+";
if ($isAdd) {
$this->customerRepository->update($customer->id, [
'credit' => (int) $customer->credit + $amount,
'total' => (int) $customer->total + $amount,
@@ -633,6 +639,22 @@ class CustomerController extends Controller
]);
}
Transaction::create([
'uuid' => (string) Str::uuid(),
'trans' => 'ADMIN' . now()->format('YmdHis'),
'amount' => $amount,
'customer_id' => $customer->id,
'user_id' => $admin?->id,
'status' => TransactionStatus::COMPLETED->value,
'type' => $isAdd ? 'admin_add_credit' : 'admin_deduct_credit',
'note' => sprintf(
'%s %s tiền trực tiếp cho khách hàng: %s VNĐ',
$admin->username ?? $admin->email ?? 'Admin',
$isAdd ? 'cộng' : 'trừ',
number_format($amount)
),
]);
DB::commit();
return response()->json([
@@ -19,7 +19,7 @@ use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
// Các type được tính là "nạp tiền vào ví"
private const DEPOSIT_TYPES = ['manual', 'vnpay', 'deposit'];
private const DEPOSIT_TYPES = ['manual', 'vnpay', 'deposit', 'admin_add_total', 'admin_add_credit'];
/** Map Order status int → string FE */
private function orderStatusLabel(int $status): string
@@ -8,6 +8,7 @@ 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;
@@ -16,16 +17,19 @@ 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 DomainOrderRepository $repo,
private readonly DomainOrderService $service,
private readonly DomainRegistrantService $registrantService,
) {}
public function pendingCount(): JsonResponse
@@ -188,6 +192,65 @@ class AdminDomainOrderController extends Controller
}
}
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
// =========================================================================
@@ -280,6 +343,8 @@ class AdminDomainOrderController extends Controller
'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(),
@@ -5,85 +5,67 @@ declare(strict_types=1);
namespace App\Http\Controllers\Api\Admin\Domain;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Models\DomainPricing;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
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'],
],
];
}
private const DEFAULT_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],
];
private const DEFAULT_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).',
'data' => DomainPricing::toResponseArray(),
'message' => 'Lấy bảng giá tên miền thành công.',
]);
}
/**
* PUT /administrator/domain-pricing
* Full replace of domains/services, mirroring the admin table's "save whole blob" UX.
*/
public function update(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'domains' => 'sometimes|array',
'services' => 'sometimes|array',
'domains' => 'sometimes|array',
'services' => 'sometimes|array',
]);
if ($validator->fails()) {
@@ -93,25 +75,22 @@ class AdminDomainPricingController extends Controller
], 422);
}
$current = Setting::where('key', 'domain_pricing')->first();
$existing = $current ? json_decode((string) $current->value, true) : $this->getDefaultData();
DB::transaction(function () use ($request) {
if ($request->has('domains')) {
DomainPricing::domains()->delete();
$this->insertDomains($request->input('domains'));
}
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)]
);
if ($request->has('services')) {
DomainPricing::services()->delete();
$this->insertServices($request->input('services'));
}
});
return response()->json([
'error' => false,
'message' => 'Cập nhật bảng giá tên miền thành công.',
'data' => $existing,
'data' => DomainPricing::toResponseArray(),
]);
}
@@ -121,17 +100,44 @@ class AdminDomainPricingController extends Controller
*/
public function reset(): JsonResponse
{
$default = $this->getDefaultData();
Setting::updateOrCreate(
['key' => 'domain_pricing'],
['value' => json_encode($default, JSON_UNESCAPED_UNICODE)]
);
DB::transaction(function () {
DomainPricing::query()->delete();
$this->insertDomains(self::DEFAULT_DOMAINS);
$this->insertServices(self::DEFAULT_SERVICES);
});
return response()->json([
'error' => false,
'message' => 'Đã reset bảng giá tên miền về mặc định.',
'data' => $default,
'data' => DomainPricing::toResponseArray(),
]);
}
private function insertDomains(array $domains): void
{
foreach (array_values($domains) as $i => $d) {
DomainPricing::create([
'type' => 'domain',
'tld' => $d['tld'] ?? null,
'setup_fee' => (string) ($d['setupFee'] ?? 'Miễn phí'),
'first_year' => (int) ($d['firstYear'] ?? 0),
'recurring_fee' => (string) ($d['renewal'] ?? 0),
'sort_order' => $i,
]);
}
}
private function insertServices(array $services): void
{
foreach (array_values($services) as $i => $s) {
DomainPricing::create([
'type' => 'service',
'name' => $s['name'] ?? null,
'setup_fee' => (string) ($s['setupFee'] ?? 'Miễn phí'),
'recurring_fee' => (string) ($s['maintenance'] ?? 'Miễn phí'),
'note' => $s['note'] ?? null,
'sort_order' => $i,
]);
}
}
}
@@ -1,131 +0,0 @@
<?php
namespace App\Http\Controllers\Api\Admin\Invoice;
use App\Http\Controllers\Controller;
use App\Jobs\Mail\SendInformationVirtualMachine;
use App\Jobs\VirtualMachine\Provision\SendVmCredentialsEmailJob;
use App\Models\Invoice;
use App\Models\ModuleVmware;
use App\Models\Setting;
use App\Services\InvoicePdfService;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class InvoiceTestController extends Controller
{
public function sendInvoiceEmail(Request $request, string $invoiceNumber): JsonResponse
{
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
->where('invoice_number', $invoiceNumber)
->first();
if (!$invoice) {
return response()->json([
'success' => false,
'message' => 'Không tìm thấy hóa đơn.',
], 404);
}
$customer = $invoice->customer;
if (!$customer?->email) {
return response()->json([
'success' => false,
'message' => 'Khách hàng không có email.',
], 400);
}
try {
$pdfService = app(InvoicePdfService::class);
$invoicePdf = $pdfService->generate($invoice);
$invoicePdfFileName = $pdfService->generateFileName($invoice);
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Lỗi tạo PDF: ' . $e->getMessage(),
], 500);
}
$vm = ModuleVmware::with(['customer', 'os.osGroup', 'order.orderItems'])
->where('order_id', $invoice->order_id)
->first();
if ($vm) {
SendVmCredentialsEmailJob::dispatch($vm->id);
return response()->json([
'success' => true,
'message' => 'Đã dispatch SendVmCredentialsEmailJob (có PDF hóa đơn đính kèm).',
'data' => [
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'customer_email' => $customer->email,
'vm_id' => $vm->id,
'pdf_file_name' => $invoicePdfFileName,
],
]);
}
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value) : null;
$emailData = [
'company_name' => $settingValue->company ?? 'Thế Giới Số',
'company_logo' => $settingValue->logo ?? '',
'client_name' => trim($customer->firstname . ' ' . $customer->lastname),
'product_name' => $invoice->invoiceItems->first()?->product?->name ?? 'Dịch vụ',
'product_billing' => 'N/A',
'product_price' => number_format((float) $invoice->total, 0, ',', '.'),
'service_host' => 'N/A',
'service_username' => 'N/A',
'service_port' => 'N/A',
'service_password' => 'N/A',
'os_name' => 'N/A',
'service_due_exp' => $invoice->due_date ? $invoice->due_date->format('d/m/Y') : 'N/A',
];
SendInformationVirtualMachine::dispatch($emailData, $customer, $invoicePdf, $invoicePdfFileName);
return response()->json([
'success' => true,
'message' => 'Đã gửi email hóa đơn (không có VM liên kết, sử dụng thông tin hóa đơn).',
'data' => [
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'customer_email' => $customer->email,
'pdf_file_name' => $invoicePdfFileName,
],
]);
}
public function previewInvoicePdf(Request $request, string $invoiceNumber)
{
$invoice = Invoice::with(['invoiceItems.product', 'customer', 'order.orderItems'])
->where('invoice_number', $invoiceNumber)
->first();
if (!$invoice) {
return response()->json([
'success' => false,
'message' => 'Không tìm thấy hóa đơn.',
], 404);
}
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value) : null;
return Pdf::loadView('pdf.invoice', [
'invoice' => $invoice,
'invoiceItems' => $invoice->invoiceItems,
'customer' => $invoice->customer,
'order' => $invoice->order,
'companyName' => $settingValue->company ?? 'Thế Giới Số',
'companyAddress' => $settingValue->address ?? '',
'companyPhone' => $settingValue->phone ?? '',
'companyEmail' => $settingValue->email ?? '',
])
->setPaper('a4', 'portrait')
->stream($invoice->invoice_number . '.pdf');
}
}
@@ -43,11 +43,9 @@ class PaymentController extends Controller
public function store(PaymentRequest $request): JsonResponse
{
$request->validated();
try {
DB::beginTransaction();
$payment = $this->paymentRepository->create($request->all());
$payment = $this->paymentRepository->create($request->validated());
DB::commit();
return response()->json([
@@ -0,0 +1,313 @@
<?php
namespace App\Http\Controllers\Api\Admin\Report;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\TransactionStatus;
use App\Exports\Reports\ReportRowsExport;
use App\Http\Controllers\Controller;
use App\Models\Customer;
use App\Models\DnsDomain;
use App\Models\ExportJob;
use App\Models\Invoice;
use App\Models\Service;
use App\Models\Transaction;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Excel as ExcelWriter;
use Maatwebsite\Excel\Facades\Excel;
class ExportController extends Controller
{
private const PREVIEW_LIMIT = 20;
public function preview(Request $request): JsonResponse
{
$data = $request->validate([
'type' => 'required|string',
'filters' => 'array',
'fields' => 'array',
]);
$query = $this->baseQuery($data['type'], $data['filters'] ?? []);
$total = (clone $query)->count();
$items = $query->limit(self::PREVIEW_LIMIT)->get()
->map(fn ($model) => $this->rowValues($model, $data['type']));
return response()->json([
'error' => false,
'data' => ['items' => $items, 'total' => $total],
]);
}
public function store(Request $request): JsonResponse
{
$data = $request->validate([
'type' => 'required|string',
'format' => 'required|in:csv,xlsx',
'filters' => 'array',
'fields' => 'array',
]);
$labels = $this->fieldLabels($data['type']);
$fields = !empty($data['fields']) ? $data['fields'] : array_keys($labels);
$rows = $this->baseQuery($data['type'], $data['filters'] ?? [])->get()
->map(fn ($model) => $this->rowValues($model, $data['type']));
$uuid = (string) Str::uuid();
$filename = "exports/{$data['type']}_{$uuid}.{$data['format']}";
$writerType = $data['format'] === 'xlsx' ? ExcelWriter::XLSX : ExcelWriter::CSV;
Excel::store(new ReportRowsExport($rows, $fields, $labels), $filename, 'local', $writerType);
$job = ExportJob::create([
'uuid' => $uuid,
'type' => $data['type'],
'format' => $data['format'],
'filters' => $data['filters'] ?? [],
'fields' => $fields,
'status' => 'completed',
'file_path' => $filename,
'total_rows' => $rows->count(),
'created_by' => auth('admin')->id(),
'completed_at' => now(),
]);
return response()->json(['error' => false, 'data' => ['id' => $job->uuid]]);
}
public function show(string $uuid): JsonResponse
{
$job = ExportJob::where('uuid', $uuid)->firstOrFail();
return response()->json([
'error' => false,
'data' => [
'id' => $job->uuid,
'type' => $job->type,
'format' => $job->format,
'status' => $job->status,
'progress' => $job->status === 'completed' ? 100 : 0,
'processed' => $job->total_rows ?? 0,
'total' => $job->total_rows ?? 0,
'error' => $job->error,
],
]);
}
public function history(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 100);
$query = ExportJob::query()->with('creator')->latest();
if ($request->filled('filter')) {
$query->where('type', 'like', '%' . $request->get('filter') . '%');
}
if ($request->filled('status')) {
$query->where('status', $request->get('status'));
}
$paginator = $query->paginate($perPage);
return response()->json([
'error' => false,
'data' => [
'items' => $paginator->getCollection()->map(fn (ExportJob $job) => [
'id' => $job->uuid,
'type' => $job->type,
'format' => $job->format,
'records' => $job->total_rows ?? 0,
'created_by' => $job->creator?->username ?? '—',
'created_at' => $job->created_at?->toIso8601String(),
'status' => $job->status,
]),
'total' => $paginator->total(),
],
]);
}
public function download(string $uuid)
{
$job = ExportJob::where('uuid', $uuid)->firstOrFail();
abort_unless($job->status === 'completed' && $job->file_path, 404);
return Storage::disk('local')->download($job->file_path, basename($job->file_path));
}
private function baseQuery(string $type, array $filters): Builder
{
return match ($type) {
'client', 'clients' => Customer::query()
->when($type === 'client', fn (Builder $q) => $q->where('id', $filters['client_id'] ?? 0))
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
'domains' => DnsDomain::query()
->with(['customer', 'server', 'ns'])
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
'invoices' => Invoice::query()
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to']))
->when(!empty($filters['invoice_status']), fn (Builder $q) => $q->where(
'status',
$filters['invoice_status'] === 'paid' ? InvoiceStatus::PAID->value : InvoiceStatus::UNPAID->value
)),
'services' => Service::query()
->with(['product', 'orderItem'])
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
'transactions' => Transaction::query()
->when(!empty($filters['date_from']), fn (Builder $q) => $q->whereDate('created_at', '>=', $filters['date_from']))
->when(!empty($filters['date_to']), fn (Builder $q) => $q->whereDate('created_at', '<=', $filters['date_to'])),
default => throw new \InvalidArgumentException("Loại xuất dữ liệu không được hỗ trợ: {$type}"),
};
}
private function rowValues($model, string $type): array
{
return match ($type) {
'client', 'clients' => [
'id' => $model->id,
'name' => $model->fullname,
'company' => $model->company,
'email' => $model->email,
'phone' => $model->phone,
'address' => $model->address,
'city' => $model->city,
'state' => $model->state,
'postcode' => $model->postcode,
'country' => $model->country,
'status' => match ((int) $model->status) {
1 => 'Actived',
2 => 'De-actived',
default => 'Banned',
},
'created_at' => optional($model->created_at)->format('Y-m-d H:i'),
],
'domains' => [
'domain' => $model->domain_name,
'customer' => $model->customer
->map(fn ($c) => $c->email ? "{$c->fullname} ({$c->email})" : $c->fullname)
->filter()
->implode(', '),
'server' => $model->server?->name,
'nameservers' => $model->ns->pluck('value')->filter()->implode(', '),
'status' => match ((int) $model->is_created) {
1 => 'Đã khởi tạo',
2 => 'Không hoạt động',
default => 'Chưa khởi tạo',
},
'created_at' => optional($model->created_at)->format('Y-m-d H:i'),
],
'invoices' => [
'id' => $model->id,
'client_id' => $model->customer_id,
'invoice_number' => $model->invoice_number,
'date' => optional($model->created_at)->format('Y-m-d'),
'due_date' => optional($model->due_date)->format('Y-m-d'),
'subtotal' => $model->sub_total,
'tax' => $model->tax,
'total' => $model->total,
'status' => InvoiceStatus::tryFrom((int) $model->status)?->label() ?? $model->status,
'payment_method' => $model->payment_method,
],
'services' => [
'id' => $model->id,
'client_id' => $model->customer_id,
'product_id' => $model->product_id,
'product_name' => $model->product?->name,
'domain' => $model->server_name,
'billing_cycle' => $model->orderItem?->billing_cycle,
'amount' => $model->orderItem?->price,
'next_due_date' => optional($model->expires_at)->format('Y-m-d'),
'status' => $model->status,
],
'transactions' => [
'id' => $model->id,
'client_id' => $model->customer_id,
'date' => optional($model->created_at)->format('Y-m-d H:i'),
'description' => $model->trans,
'amount' => $model->amount,
'gateway' => $model->type,
'status' => TransactionStatus::tryFrom((int) $model->status)?->label() ?? $model->status,
],
default => [],
};
}
private function fieldLabels(string $type): array
{
return match ($type) {
'client', 'clients' => [
'id' => 'Client ID',
'name' => 'Name',
'company' => 'Company',
'email' => 'Email',
'phone' => 'Phone',
'address' => 'Address',
'city' => 'City',
'state' => 'State',
'postcode' => 'Postcode',
'country' => 'Country',
'status' => 'Status',
'created_at' => 'Created At',
],
'domains' => [
'domain' => 'Domain',
'customer' => 'Khách hàng',
'server' => 'DNS Server',
'nameservers' => 'NS',
'status' => 'Trạng thái',
'created_at' => 'Ngày tạo',
],
'invoices' => [
'id' => 'Invoice ID',
'client_id' => 'Client ID',
'invoice_number' => 'Invoice Number',
'date' => 'Date',
'due_date' => 'Due Date',
'subtotal' => 'Subtotal',
'tax' => 'Tax',
'total' => 'Total',
'status' => 'Status',
'payment_method' => 'Payment Method',
],
'services' => [
'id' => 'Service ID',
'client_id' => 'Client ID',
'product_id' => 'Product ID',
'product_name' => 'Product Name',
'domain' => 'Domain',
'billing_cycle' => 'Billing Cycle',
'amount' => 'Amount',
'next_due_date' => 'Next Due Date',
'status' => 'Status',
],
'transactions' => [
'id' => 'Transaction ID',
'client_id' => 'Client ID',
'date' => 'Date',
'description' => 'Description',
'amount' => 'Amount',
'gateway' => 'Gateway',
'status' => 'Status',
],
default => [],
};
}
}
@@ -103,6 +103,41 @@ class RolePermissionController extends Controller
]);
}
/**
* POST /v1/administrator/role-permissions
* Tạo một vai trò mới (chỉ Full Administrator)
*/
public function store(Request $request): JsonResponse
{
if (!$this->ensureFullAdmin()) {
return response()->json(['error' => true, 'message' => 'Bạn không có quyền truy cập'], 403);
}
$name = trim($request->input('name', ''));
if (!$name) {
return response()->json(['error' => true, 'message' => 'Tên vai trò không được trống'], 422);
}
if ($name === 'Full Administrator') {
return response()->json(['error' => true, 'message' => 'Tên vai trò không hợp lệ'], 422);
}
if (Role::where('guard_name', 'admin')->where('name', $name)->exists()) {
return response()->json(['error' => true, 'message' => "Vai trò \"{$name}\" đã tồn tại"], 422);
}
$role = Role::create(['name' => $name, 'guard_name' => 'admin']);
return response()->json([
'error' => false,
'data' => [
'id' => $role->id,
'name' => $role->name,
'permissions' => [],
],
], 201);
}
/**
* POST /v1/administrator/role-permissions/permissions
* Tạo một nhóm permissions mới theo module (chỉ Full Administrator)
@@ -7,9 +7,10 @@ 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\Mail\Customer\DomainOrderReceivedNotification;
use App\Models\Customer;
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;
@@ -26,7 +27,7 @@ class DomainOrderController extends Controller
try {
$result = $this->domainOrderService->placeOrder($request->validated(), $request->user());
$this->notifyAdminNewDomainOrder($request->user(), $result);
$this->notifyDomainOrderPlaced($request->user(), $result);
return response()->json([
'success' => true,
@@ -59,11 +60,12 @@ class DomainOrderController extends Controller
}
}
private function notifyAdminNewDomainOrder($customer, array $result): void
private function notifyDomainOrderPlaced(Customer $customer, array $result): void
{
$domainName = $result['order']->orderItems->first()?->domain_name ?? '';
$orderNum = $result['order']->order_num;
$total = number_format((int) $result['invoice']->total) . ' VNĐ';
$createdAt = now()->format('d/m/Y H:i:s');
Notification::notifyAdmins([
'type' => 'domain_order_new',
@@ -72,35 +74,41 @@ class DomainOrderController extends Controller
'link' => '/domain-orders',
]);
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
$companyName = $settingValue['company'] ?? config('app.name');
$adminEmail = $settingValue['admin_email'] ?? null;
$mailData = [
'company_name' => $companyName,
'domain_name' => $domainName,
'customer_name' => $customer->fullname,
'customer_email' => $customer->email,
'order_num' => $orderNum,
'order_total' => $total,
'created_at' => $createdAt,
];
if (!empty($adminEmail)) {
try {
$adminUrl = rtrim(env('ADMIN_URL', config('app.url')), '/') . '/domain-orders';
Mail::to($adminEmail)->send(
new DomainOrderNewNotification([...$mailData, 'admin_url' => $adminUrl])
);
} catch (\Throwable $e) {
Log::error('DomainOrderController: không thể gửi email admin: ' . $e->getMessage());
}
}
try {
$setting = Setting::where('key', 'setting')->first();
$settingValue = $setting ? json_decode((string) $setting->value, true) : [];
$adminEmail = $settingValue['admin_email'] ?? null;
$orderUrl = rtrim(config('app.frontend_url', ''), '/') . '/don-hang';
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,
]));
Mail::to($customer->email)->send(
new DomainOrderReceivedNotification([...$mailData, 'order_url' => $orderUrl])
);
} catch (\Throwable $e) {
Log::error('DomainOrderController: không thể gửi email admin: ' . $e->getMessage());
Log::error('DomainOrderController: không thể gửi email khách hàng: ' . $e->getMessage());
}
}
}
@@ -3,50 +3,11 @@
namespace App\Http\Controllers\Api\Client\Domain;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Models\DomainPricing;
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.
@@ -54,24 +15,10 @@ class DomainPricingController extends Controller
*/
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).',
'data' => DomainPricing::toResponseArray(),
'message' => 'Lấy bảng giá tên miền thành công.',
]);
}
}
@@ -4,12 +4,14 @@ namespace App\Http\Controllers\Api\Client\Service;
use App\Http\Controllers\Controller;
use App\Http\Resources\Api\Service\VpsServiceCollection;
use App\Http\Resources\Api\Service\Order\InvoiceSummaryResource;
use App\Http\Resources\Api\Client\Service\ServiceClientDetailResource;
use App\Http\Services\vCenter;
use App\Models\Order;
use App\Models\Service;
use App\Repositories\Api\ServiceClient\ServiceClientRepository;
use App\Services\ActivityLogger;
use App\Services\Api\Order\OrderQueryService;
use App\Services\Api\ServiceClient\ServiceClientService;
use App\Services\VirtualMachine\Rebuild\VmRebuildService;
use Illuminate\Http\JsonResponse;
@@ -66,6 +68,23 @@ class ServiceController extends Controller
->response();
}
public function overview(Request $request, OrderQueryService $orderQueryService): JsonResponse
{
$customerId = (int) $request->user()->id;
$services = $this->serviceClientService->getActiveServices($customerId, null, 5);
$invoices = $orderQueryService->getCustomerInvoices($customerId)->take(5);
return response()->json([
'error' => false,
'message' => 'Lấy dữ liệu thành công',
'data' => [
'services' => (new VpsServiceCollection($services))->collection,
'invoices' => InvoiceSummaryResource::collection($invoices),
],
]);
}
public function indexWebDesign(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 15), 100);
@@ -24,7 +24,9 @@ trait ResolvesVm
}
} elseif (is_numeric($productId)) {
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->find((int) $productId);
->where('id', (int) $productId)
->where('customer_id', $customer->id)
->first();
} else {
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->where('vmware_id', $productId)
@@ -33,14 +33,13 @@ class OrderControllerRefactor extends Controller
private readonly OrderQueryService $orderQueryService,
private readonly ActivityLogger $activityLogger,
private readonly InvoicePaymentService $invoicePaymentService,
) {
}
) {}
public function getList(Request $request): JsonResponse
{
$orders = $this->orderQueryService->getOrdersForCustomer(
(int) $request->user()->id,
ClientOrderFilters::fromArray($request->only(['total_price', 'pending']))
ClientOrderFilters::fromArray($request->only(['total_price', 'pending'])),
);
return response()->json(['success' => true, 'data' => $orders]);
@@ -67,7 +66,7 @@ class OrderControllerRefactor extends Controller
{
$order = $this->orderQueryService->findCustomerOrder((int) $request->user()->id, $id);
if (! $order) {
if (!$order) {
return response()->json(['success' => false, 'message' => 'Đơn hàng không tồn tại.'], 404);
}
@@ -81,8 +80,11 @@ class OrderControllerRefactor extends Controller
$data = $request->validated();
$product = \App\Models\Product::where('uuid', $data['product_id'])->first();
if (! $product) {
return response()->json(['success' => false, 'message' => 'Sản phẩm không tìm thấy hoặc đã bị xóa.'], 404);
if (!$product) {
return response()->json(
['success' => false, 'message' => 'Sản phẩm không tìm thấy hoặc đã bị xóa.'],
404,
);
}
$data['product_id'] = $product->id;
@@ -90,16 +92,19 @@ class OrderControllerRefactor extends Controller
$order = $result['order']->load(['orderItems.product', 'invoice', 'customer']);
return response()->json([
'success' => true,
'message' => 'Tạo đơn hàng thành công.',
'notice' => 'Dịch vụ VPS sẽ được khởi tạo trong 5-10 phút sau khi thanh toán.',
'data' => [
'order' => new OrderDetailResource($order),
'invoice' => new InvoiceDetailResource($result['invoice']->load(['customer', 'order'])),
'items' => OrderItemResource::collection($order->orderItems),
return response()->json(
[
'success' => true,
'message' => 'Tạo đơn hàng thành công.',
'notice' => 'Dịch vụ VPS sẽ được khởi tạo trong 5-10 phút sau khi thanh toán.',
'data' => [
'order' => new OrderDetailResource($order),
'invoice' => new InvoiceDetailResource($result['invoice']->load(['customer', 'order'])),
'items' => OrderItemResource::collection($order->orderItems),
],
],
], 201);
201,
);
} catch (\Throwable $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 400);
}
@@ -109,7 +114,7 @@ class OrderControllerRefactor extends Controller
{
$order = $this->orderRepository->findByOrderNum($orderNum, (int) $request->user()->id);
if (! $order) {
if (!$order) {
return response()->json(['success' => false, 'message' => 'Đơn hàng không tồn tại.'], 404);
}
@@ -131,13 +136,13 @@ class OrderControllerRefactor extends Controller
$customerId = (int) $request->user()->id;
$order = $this->orderRepository->findForCustomer($id, $customerId);
if (! $order) {
if (!$order) {
return response()->json(['success' => false, 'message' => 'Đơn hàng không tồn tại.'], 404);
}
$result = $this->orderQueryService->retryProvisioning($order, $customerId);
if (! $result['ok']) {
if (!$result['ok']) {
return response()->json(['success' => false, 'message' => $result['message']], $result['code']);
}
@@ -162,7 +167,7 @@ class OrderControllerRefactor extends Controller
{
$invoice = $this->orderQueryService->findCustomerInvoice((int) $request->user()->id, $id);
if (! $invoice) {
if (!$invoice) {
return response()->json(['success' => false, 'message' => 'Hóa đơn không tồn tại.'], 404);
}
@@ -173,20 +178,23 @@ class OrderControllerRefactor extends Controller
{
$order = $this->orderQueryService->findCustomerInvoiceByOrderNum((int) $request->user()->id, $orderNumber);
if (! $order?->invoice) {
if (!$order?->invoice) {
return response()->json(['success' => false, 'message' => 'Hóa đơn không tồn tại.'], 404);
}
$order->load('vmWare.os.osGroup', 'vmWare.virtualMachine', 'vmWare.service');
return response()->json(['success' => true, 'data' => new InvoiceDetailResource($order->invoice->setRelation('order', $order))]);
return response()->json([
'success' => true,
'data' => new InvoiceDetailResource($order->invoice->setRelation('order', $order)),
]);
}
public function getClientInvoiceDetailByNumber(Request $request, string $invoiceNumber): JsonResponse
{
$invoice = $this->orderQueryService->findCustomerInvoiceByNumber((int) $request->user()->id, $invoiceNumber);
if (! $invoice) {
if (!$invoice) {
return response()->json(['success' => false, 'message' => 'Hóa đơn không tồn tại.'], 404);
}
@@ -201,7 +209,7 @@ class OrderControllerRefactor extends Controller
{
$invoice = $this->orderQueryService->findCustomerInvoiceByNumber((int) $request->user()->id, $invoiceNumber);
if (! $invoice) {
if (!$invoice) {
return response()->json(['success' => false, 'message' => 'Hóa đơn không tồn tại.'], 404);
}
@@ -230,26 +238,29 @@ class OrderControllerRefactor extends Controller
{
$order = $this->orderQueryService->findCustomerInvoiceByOrderNum((int) $request->user()->id, $orderNum);
if (! $order) {
if (!$order) {
return response()->json(['success' => false, 'message' => 'Đơn hàng không tồn tại.'], 404);
}
return response()->json(['success' => true, 'data' => new OrderDetailResource($order)]);
}
public function downloadInvoicePdfByOrderNum(Request $request, string $orderNum, InvoicePdfService $pdfService): Response
{
public function downloadInvoicePdfByOrderNum(
Request $request,
string $orderNum,
InvoicePdfService $pdfService,
): Response {
$order = $this->orderQueryService->findCustomerInvoiceByOrderNum((int) $request->user()->id, $orderNum);
if (! $order?->invoice) {
if (!$order?->invoice) {
abort(404, 'Hóa đơn không tồn tại.');
}
$pdf = $pdfService->generate($order->invoice);
$pdf = $pdfService->generate($order->invoice);
$filename = $pdfService->generateFileName($order->invoice);
return response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
]);
}
@@ -265,7 +276,7 @@ class OrderControllerRefactor extends Controller
{
$service = $this->orderQueryService->findCustomerService((int) $request->user()->id, $uuid);
if (! $service) {
if (!$service) {
return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404);
}
@@ -276,12 +287,9 @@ class OrderControllerRefactor extends Controller
{
$validated = $request->validate(['auto_renew' => ['required', 'boolean']]);
$service = \App\Models\Service::query()
->where('id', $id)
->where('customer_id', $request->user()->id)
->first();
$service = \App\Models\Service::query()->where('id', $id)->where('customer_id', $request->user()->id)->first();
if (! $service) {
if (!$service) {
return response()->json(['success' => false, 'message' => 'Không tìm thấy dịch vụ.'], 404);
}
@@ -290,19 +298,29 @@ class OrderControllerRefactor extends Controller
$this->activityLogger->log(
event: 'updated',
logName: 'Service',
description: "Customer {$request->user()->email} " . ($validated['auto_renew'] ? 'bật' : 'tắt') . " tự động gia hạn dịch vụ #{$service->id}",
description: "Customer {$request->user()->email} " .
($validated['auto_renew'] ? 'bật' : 'tắt') .
" tự động gia hạn dịch vụ #{$service->id}",
subject: $service,
newValues: ['auto_renew' => (bool) $validated['auto_renew']],
);
return response()->json(['success' => true, 'message' => 'Cập nhật tự động gia hạn thành công.', 'data' => ['id' => $service->id, 'auto_renew' => (bool) $service->auto_renew]]);
return response()->json([
'success' => true,
'message' => 'Cập nhật tự động gia hạn thành công.',
'data' => ['id' => $service->id, 'auto_renew' => (bool) $service->auto_renew],
]);
}
public function renewService(RenewServiceRequest $request, int $id): JsonResponse
{
$result = $this->orderQueryService->renewService($id, (int) $request->user()->id, (int) $request->validated()['months']);
$result = $this->orderQueryService->renewService(
$id,
(int) $request->user()->id,
(int) $request->validated()['months'],
);
if (! $result['ok']) {
if (!$result['ok']) {
return response()->json(['success' => false, 'message' => $result['message']], $result['code']);
}
@@ -484,7 +484,9 @@ class VmController extends Controller
} elseif (is_numeric($productId)) {
// Numeric ID
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])
->find((int) $productId);
->where('id', (int) $productId)
->where('customer_id', $customer->id)
->first();
} else {
// vmware_id string (e.g., "vm-3238")
$vm = ModuleVmware::with(['vCenter', 'ipAddress.host.datacenter.vCenter'])