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:
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user