1153 lines
40 KiB
PHP
1153 lines
40 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin\Customer;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Requests\CreateCustomerRequest;
|
|
use App\Http\Resources\Api\Admin\CustomerResource;
|
|
use App\Jobs\Mail\PasswordRecoverJob;
|
|
use App\Models\Customer;
|
|
use App\Models\Invoice;
|
|
use App\Models\Order;
|
|
use App\Models\Transaction;
|
|
use App\Models\Log;
|
|
use App\Repositories\Customer\CustomerRepositoryInterface;
|
|
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
|
use App\Repositories\InvoiceItem\InvoiceItemRepositoryInterface;
|
|
use App\Repositories\Module\Domain\DomainRepositoryInterface;
|
|
use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingRepositoryInterface;
|
|
use App\Repositories\Module\Vmware\VmwareRepositoryInterface;
|
|
use App\Repositories\Order\OrderRepositoryInterface;
|
|
use App\Repositories\Service\ServiceRepositoryInterface;
|
|
use App\Repositories\Transaction\TransactionRepositoryInterface;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Str;
|
|
use App\Enums\Invoice\InvoiceStatus;
|
|
use App\Enums\Order\OrderStatus;
|
|
use App\Helpers\OrderHelper;
|
|
|
|
class CustomerController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly CustomerRepositoryInterface $customerRepository,
|
|
private readonly InvoiceRepositoryInterface $invoiceRepository,
|
|
private readonly InvoiceItemRepositoryInterface $invoiceItemRepository,
|
|
private readonly ModuleHostingRepositoryInterface $moduleHostingRepository,
|
|
private readonly VmwareRepositoryInterface $vmwareRepository,
|
|
private readonly OrderRepositoryInterface $orderRepository,
|
|
private readonly TransactionRepositoryInterface $transactionRepository,
|
|
private readonly DomainRepositoryInterface $moduleDomainRepository,
|
|
private readonly ServiceRepositoryInterface $serviceRepository,
|
|
) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$perPage = min((int) $request->get('per_page', 15), 500);
|
|
$query = $this->customerRepository->filter(
|
|
$request->all(),
|
|
[],
|
|
['hosting', 'vps', 'domain'],
|
|
['*'],
|
|
true
|
|
);
|
|
$paginator = $query->paginate($perPage);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => [
|
|
'items' => CustomerResource::collection($paginator),
|
|
'total' => $paginator->total(),
|
|
'per_page' => $paginator->perPage(),
|
|
'current_page' => $paginator->currentPage(),
|
|
'last_page' => $paginator->lastPage(),
|
|
],
|
|
]);
|
|
}
|
|
|
|
public function store(CreateCustomerRequest $request): JsonResponse
|
|
{
|
|
$request->validated();
|
|
|
|
$existingCustomer = $this->customerRepository->findBy('email', $request->get('email'));
|
|
if ($existingCustomer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Email already exists on the system!'
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
$data = $request->all();
|
|
$data['password'] = Hash::make($request->password);
|
|
$data['email_marketing'] = $request->email_marketing === "on" ? 1 : 0;
|
|
|
|
$customer = $this->customerRepository->create($data);
|
|
if ($request->exists('note')) {
|
|
Customer::query()
|
|
->whereKey($customer->id)
|
|
->update(['note' => $request->input('note')]);
|
|
$customer = Customer::query()->find($customer->id);
|
|
}
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Customer created successfully',
|
|
'data' => new CustomerResource($customer)
|
|
], 201);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function show(string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$customer = $this->customerRepository->findWithRelationships(
|
|
$customer->id,
|
|
['mail', 'file'],
|
|
[
|
|
'invoice' => [
|
|
'invoices_status_count_1' => ['key' => 'status', 'condition' => '=', 'value' => 1],
|
|
'invoices_status_count_0' => ['key' => 'status', 'condition' => '=', 'value' => 0]
|
|
],
|
|
'hosting' => 'hosting_count',
|
|
'vps' => 'vps_count',
|
|
'domain' => 'domain_count',
|
|
'ticket' => 'ticket_count'
|
|
],
|
|
[
|
|
'invoice' => [
|
|
'invoices_status_1' => ['key' => 'status', 'condition' => '=', 'value' => 1, 'sum' => 'total'],
|
|
'invoices_status_0' => ['key' => 'status', 'condition' => '=', 'value' => 0, 'sum' => 'total']
|
|
]
|
|
]
|
|
);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$customer->refresh();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => new CustomerResource($customer)
|
|
]);
|
|
}
|
|
|
|
public function update(string $uuid, CreateCustomerRequest $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$request->validated();
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
$data = $request->except(['_token', 'password']);
|
|
|
|
if (!empty($request->password)) {
|
|
$data['password'] = Hash::make($request->password);
|
|
}
|
|
|
|
$this->customerRepository->update($customer->id, $data);
|
|
if ($request->exists('note')) {
|
|
Customer::query()
|
|
->whereKey($customer->id)
|
|
->update(['note' => $request->input('note')]);
|
|
}
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Customer updated successfully',
|
|
'data' => new CustomerResource(Customer::query()->find($customer->id))
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function destroy(string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
Invoice::where('customer_id', $customer->id)->delete();
|
|
Order::where('customer_id', $customer->id)->delete();
|
|
Transaction::where('customer_id', $customer->id)->delete();
|
|
$customer->delete();
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Customer deleted successfully'
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function trash(Request $request): JsonResponse
|
|
{
|
|
$query = $this->customerRepository->onlyTrashed([], ['hosting', 'vps', 'domain']);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => CustomerResource::collection($query->get())
|
|
]);
|
|
}
|
|
|
|
public function restore(string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
if (!$customer) {
|
|
$customer = $this->customerRepository->onlyTrashed()->where('uuid', $uuid)->first();
|
|
}
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
$customer->restore();
|
|
Invoice::withTrashed()->where('customer_id', $customer->id)->restore();
|
|
Order::withTrashed()->where('customer_id', $customer->id)->restore();
|
|
Transaction::withTrashed()->where('customer_id', $customer->id)->restore();
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Customer restored successfully'
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function forceDelete(string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->onlyTrashed()->where('uuid', $uuid)->first();
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
Invoice::withTrashed()->where('customer_id', $customer->id)->forceDelete();
|
|
Order::withTrashed()->where('customer_id', $customer->id)->forceDelete();
|
|
Transaction::withTrashed()->where('customer_id', $customer->id)->forceDelete();
|
|
$customer->forceDelete();
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Customer permanently deleted'
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tạo mã impersonate một lần (one-time code) để admin đăng nhập hộ khách hàng
|
|
* vào giao diện portal-client. Trả về `code` (hiệu lực 120s) — portal-client
|
|
* sẽ đổi code lấy token qua endpoint public `v1/client/impersonate/exchange`.
|
|
*/
|
|
public function loginAs(Request $request, string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid,
|
|
], 404);
|
|
}
|
|
|
|
if ((int) $customer->status !== 1) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Khách hàng không hoạt động, không thể đăng nhập hộ.',
|
|
], 422);
|
|
}
|
|
|
|
try {
|
|
$token = \Tymon\JWTAuth\Facades\JWTAuth::fromUser($customer);
|
|
$refreshToken = app(\App\Services\Api\Client\ClientAuthService::class)
|
|
->createRefreshToken($customer->id);
|
|
|
|
$clientResource = (new \App\Http\Resources\Api\Client\ClientResource(
|
|
$customer->loadCount(['vps', 'hosting', 'domain'])
|
|
))->resolve($request);
|
|
|
|
$code = Str::random(48);
|
|
\Illuminate\Support\Facades\Cache::put("impersonate:{$code}", [
|
|
'token' => $token,
|
|
'refresh_token' => $refreshToken,
|
|
'client' => $clientResource,
|
|
], 120);
|
|
|
|
$admin = Auth::guard('admin')->user();
|
|
|
|
Log::create([
|
|
'agent' => 'admin',
|
|
'client_ip' => $request->ip(),
|
|
'description' => sprintf(
|
|
'%s đăng nhập hộ khách hàng %s',
|
|
$admin->username ?? $admin->email ?? 'Admin',
|
|
$customer->email,
|
|
),
|
|
'customer_id' => $customer->id,
|
|
]);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => ['code' => $code],
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function resetPassword(Request $request, string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$password = Str::random(10);
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
$this->customerRepository->update($customer->id, ['password' => Hash::make($password)]);
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Password reset successfully',
|
|
'data' => ['new_password' => $password]
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function sendPasswordEmail(Request $request, string $uuid): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'password' => 'required|string|min:8|max:64',
|
|
]);
|
|
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid,
|
|
], 404);
|
|
}
|
|
|
|
$password = (string) $request->input('password');
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$this->customerRepository->update($customer->id, [
|
|
'password' => Hash::make($password),
|
|
]);
|
|
|
|
\Illuminate\Support\Facades\Mail::to($customer->email)->send(
|
|
new \App\Mail\Customer\PasswordResetByAdmin([
|
|
'fullname' => $customer->fullname,
|
|
'email' => $customer->email,
|
|
'password' => $password,
|
|
])
|
|
);
|
|
|
|
Log::create([
|
|
'agent' => 'admin',
|
|
'client_ip' => $request->ip(),
|
|
'description' => sprintf(
|
|
'Admin gửi lại mật khẩu mới cho customer %s',
|
|
$customer->email,
|
|
),
|
|
'customer_id' => $customer->id,
|
|
]);
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Đã gửi mật khẩu mới cho khách hàng.',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function addCredit(Request $request, string $uuid): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'amount' => 'required|numeric|min:1',
|
|
'action' => 'required|in:+,-'
|
|
]);
|
|
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$amount = (int) $request->amount;
|
|
|
|
if ($request->action === "+") {
|
|
$this->customerRepository->update($customer->id, [
|
|
'credit' => (int) $customer->credit + $amount,
|
|
'total' => (int) $customer->total + $amount,
|
|
]);
|
|
} else {
|
|
$this->customerRepository->update($customer->id, [
|
|
'credit' => (int) $customer->credit - $amount,
|
|
'total' => (int) $customer->total - $amount,
|
|
]);
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Credit updated successfully'
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage()
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function addTotal(Request $request, string $uuid): JsonResponse
|
|
{
|
|
$request->validate([
|
|
'amount' => 'required|numeric|min:1',
|
|
'note' => 'required|string|max:1000',
|
|
]);
|
|
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid,
|
|
], 404);
|
|
}
|
|
|
|
$admin = Auth::guard('admin')->user();
|
|
if (!$admin) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Unauthorized',
|
|
], 401);
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$amount = (int) $request->amount;
|
|
|
|
$transaction = Transaction::create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'trans' => 'ADMIN' . now()->format('YmdHis'),
|
|
'amount' => $amount,
|
|
'customer_id' => $customer->id,
|
|
'user_id' => $admin->id,
|
|
'status' => 1,
|
|
'type' => 'admin_add_total',
|
|
'note' => $request->string('note')->trim()->toString(),
|
|
]);
|
|
|
|
$customer->update([
|
|
'total' => (int) $customer->total + $amount,
|
|
]);
|
|
|
|
Log::create([
|
|
'agent' => 'admin',
|
|
'client_ip' => $request->ip(),
|
|
'description' => sprintf(
|
|
'%s cộng tiền trực tiếp cho customer %s: %s VNĐ - %s',
|
|
$admin->username ?? $admin->email,
|
|
$customer->email,
|
|
number_format($amount),
|
|
$transaction->note,
|
|
),
|
|
'customer_id' => $customer->id,
|
|
]);
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Total updated successfully',
|
|
'data' => [
|
|
'transaction' => [
|
|
'uuid' => $transaction->uuid,
|
|
'amount' => $transaction->amount,
|
|
'note' => $transaction->note,
|
|
'admin' => $admin->username ?? $admin->email,
|
|
],
|
|
],
|
|
]);
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => $e->getMessage(),
|
|
], 500);
|
|
}
|
|
}
|
|
|
|
public function getProducts(string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$hostingServices = $this->moduleHostingRepository->filter(['customer_id' => $customer->id], ['server']);
|
|
$vpsServices = $this->vmwareRepository->filter(
|
|
['customer_id' => $customer->id],
|
|
['ipaddress.host.datacenter.vCenter', 'os.osGroup']
|
|
);
|
|
|
|
$allService = collect();
|
|
|
|
foreach ($hostingServices as $hosting) {
|
|
$allService->push([
|
|
'product_id' => $hosting->id,
|
|
'product_type' => 3,
|
|
'name' => $hosting->name,
|
|
'description' => $hosting->name . ' (Hosting)'
|
|
]);
|
|
}
|
|
|
|
foreach ($vpsServices as $vps) {
|
|
$allService->push([
|
|
'product_id' => $vps->id,
|
|
'product_type' => 4,
|
|
'name' => $vps->name,
|
|
'description' => $vps->name . ' (VPS)'
|
|
]);
|
|
}
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $allService->values()
|
|
]);
|
|
}
|
|
|
|
public function getInvoices(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$invoices = $this->invoiceRepository->filter(
|
|
['customer_id' => $customer->id],
|
|
['payment']
|
|
);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $invoices
|
|
]);
|
|
}
|
|
|
|
public function getTransactions(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$transactions = $this->transactionRepository->filter(
|
|
['customer_id' => $customer->id],
|
|
['user']
|
|
);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $transactions
|
|
]);
|
|
}
|
|
|
|
public function getDomains(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$domains = $this->moduleDomainRepository->filter(
|
|
['customer_id' => $customer->id],
|
|
['customer']
|
|
);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $domains
|
|
]);
|
|
}
|
|
|
|
public function getDnsDomains(string $uuid): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid,
|
|
], 404);
|
|
}
|
|
|
|
$domains = \App\Models\DnsDomain::query()
|
|
->with('server:id,name,ip_address')
|
|
->withCount(['ns', 'record'])
|
|
->whereHas('customer', fn ($q) => $q->where('customers.id', $customer->id))
|
|
->orderByDesc('id')
|
|
->get()
|
|
->map(function ($d) {
|
|
return [
|
|
'id' => $d->id,
|
|
'domain_name' => $d->domain_name,
|
|
'is_created' => (int) $d->is_created,
|
|
'plesk_id' => $d->plesk_id,
|
|
'ns_count' => (int) ($d->ns_count ?? 0),
|
|
'record_count' => (int) ($d->record_count ?? 0),
|
|
'server' => $d->server ? [
|
|
'id' => $d->server->id,
|
|
'name' => $d->server->name,
|
|
'ip_address' => $d->server->ip_address,
|
|
] : null,
|
|
'created_at' => $d->created_at,
|
|
'updated_at' => $d->updated_at,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $domains,
|
|
]);
|
|
}
|
|
|
|
public function getDnsDomainRecords(string $uuid, int $domainId): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid,
|
|
], 404);
|
|
}
|
|
|
|
$owns = \Illuminate\Support\Facades\DB::table('dns_domain_user')
|
|
->where('domain_id', $domainId)
|
|
->where('user_id', $customer->id)
|
|
->exists();
|
|
|
|
if (!$owns) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'Domain không thuộc về khách hàng này',
|
|
], 403);
|
|
}
|
|
|
|
$records = \App\Models\DnsRecord::query()
|
|
->where('dns_manager_domain_id', $domainId)
|
|
->orderBy('type')
|
|
->orderBy('name')
|
|
->get();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $records,
|
|
]);
|
|
}
|
|
|
|
public function getServices(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$services = $this->serviceRepository->filter(
|
|
['customer_uuid' => $uuid],
|
|
['product', 'orderItem', 'invoice']
|
|
);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $services
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Xoá nhiều dịch vụ của khách hàng. Chỉ xoá bản ghi trong bảng `services`,
|
|
* không xoá VM (module_vmware / virtual_machines) liên kết.
|
|
*/
|
|
public function bulkDeleteServices(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
if (!$customer) {
|
|
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
|
|
}
|
|
|
|
$data = $request->validate([
|
|
'ids' => 'required|array|min:1',
|
|
'ids.*' => 'integer',
|
|
]);
|
|
|
|
$deleted = \App\Models\Service::where('customer_id', $customer->id)
|
|
->whereIn('id', $data['ids'])
|
|
->delete();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Đã xoá ' . $deleted . ' dịch vụ',
|
|
'data' => ['deleted' => $deleted],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Cập nhật thông tin dịch vụ của khách hàng (server_name/hostname, status, ngày,...).
|
|
* Đồng bộ hostname sang module_vmware nếu service đã gắn VM.
|
|
*/
|
|
public function updateService(string $uuid, int $serviceId, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
if (!$customer) {
|
|
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
|
|
}
|
|
|
|
$service = \App\Models\Service::where('customer_id', $customer->id)
|
|
->where('id', $serviceId)
|
|
->first();
|
|
if (!$service) {
|
|
return response()->json(['error' => true, 'message' => 'Không tìm thấy dịch vụ'], 404);
|
|
}
|
|
|
|
$data = $request->validate([
|
|
'server_name' => 'nullable|string|max:255',
|
|
'status' => 'nullable|in:pending,active,suspended,expired,cancelled,terminated',
|
|
'auto_renew' => 'nullable|boolean',
|
|
'started_at' => 'nullable|date',
|
|
'expires_at' => 'nullable|date',
|
|
'note' => 'nullable|string',
|
|
]);
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
$service->fill($data);
|
|
$service->save();
|
|
|
|
// Đồng bộ hostname sang module_vmware nếu có gán VM
|
|
if ($service->vm_id && array_key_exists('server_name', $data)) {
|
|
\App\Models\ModuleVmware::where('id', $service->vm_id)
|
|
->update(['hostname' => $data['server_name']]);
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Cập nhật dịch vụ thành công',
|
|
'data' => $service->load(['product', 'orderItem', 'invoice']),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a service for a customer. For VPS products, accepts a
|
|
* `virtual_machine_id` from the vCenter inventory and auto-creates the
|
|
* matching module_vmware row so `services.vm_id` can be linked.
|
|
*/
|
|
public function createService(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
if (!$customer) {
|
|
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
|
|
}
|
|
|
|
$data = $request->validate([
|
|
'product_id' => 'required|integer|exists:products,id',
|
|
'virtual_machine_id' => 'nullable|integer|exists:virtual_machines,id',
|
|
'server_name' => 'nullable|string|max:255',
|
|
'status' => 'nullable|in:pending,active,suspended,expired,cancelled,terminated',
|
|
'auto_renew' => 'nullable|boolean',
|
|
'started_at' => 'nullable|date',
|
|
'expires_at' => 'nullable|date',
|
|
'note' => 'nullable|string',
|
|
// New: billing & payment
|
|
'billing_cycle' => 'nullable|in:monthly,quarterly,semi_annually,annually,biennially,triennially',
|
|
'payment_method' => 'required|in:transfer,balance',
|
|
'proof_images' => 'nullable|array',
|
|
'proof_images.*' => 'file|image|max:5120',
|
|
]);
|
|
|
|
$product = \App\Models\Product::with('pricings')->find($data['product_id']);
|
|
if ($product->product_type === 'vps' && empty($data['virtual_machine_id'])) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => 'VPS service requires a virtual_machine_id',
|
|
], 422);
|
|
}
|
|
|
|
$billingCycle = $data['billing_cycle'] ?? 'monthly';
|
|
$months = $this->cycleToMonths($billingCycle);
|
|
|
|
$pricing = $product->pricings
|
|
->where('billing_cycle', $billingCycle)
|
|
->first();
|
|
$basePrice = (int) ($pricing->price ?? 0);
|
|
|
|
$paymentMethod = $data['payment_method'];
|
|
|
|
// Đối với balance: kiểm tra số dư trước khi vào transaction
|
|
if ($paymentMethod === 'balance' && (int) $customer->total < $basePrice) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => sprintf(
|
|
'Số dư khách không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
|
|
number_format((int) $customer->total),
|
|
number_format($basePrice)
|
|
),
|
|
], 422);
|
|
}
|
|
|
|
// Upload proof images (chuyển khoản)
|
|
$proofPaths = [];
|
|
if ($paymentMethod === 'transfer' && $request->hasFile('proof_images')) {
|
|
foreach ($request->file('proof_images') as $file) {
|
|
$proofPaths[] = $file->store('payment-proofs', 'public');
|
|
}
|
|
}
|
|
|
|
try {
|
|
DB::beginTransaction();
|
|
|
|
// 1) ModuleVmware: snapshot từ virtual_machines
|
|
$vmId = null;
|
|
if (!empty($data['virtual_machine_id'])) {
|
|
$vm = \App\Models\VirtualMachines::with('datacenter')
|
|
->find($data['virtual_machine_id']);
|
|
|
|
$vcenterId = $vm->datacenter->vcenter_id ?? null;
|
|
|
|
$moduleVm = \App\Models\ModuleVmware::updateOrCreate(
|
|
['virtual_machine_id' => $vm->id],
|
|
[
|
|
'customer_id' => $customer->id,
|
|
'name' => $vm->name,
|
|
'vmware_id' => $vm->vm_id,
|
|
'vcenter_id' => $vcenterId,
|
|
'hostname' => $data['server_name'] ?? $vm->name,
|
|
'cpu' => $vm->cpu,
|
|
'ram_mb' => $vm->ram_mb,
|
|
'disk_gb' => $vm->disk_gb ?? null,
|
|
'status' => 1,
|
|
]
|
|
);
|
|
$vmId = $moduleVm->id;
|
|
}
|
|
|
|
// 2) Order + OrderItem (mô phỏng flow customer đặt hàng)
|
|
$order = Order::create([
|
|
'order_num' => OrderHelper::generateOrderNumber(),
|
|
'customer_id' => $customer->id,
|
|
'sub_total' => $basePrice,
|
|
'discount_amount' => 0,
|
|
'total' => $basePrice,
|
|
'status' => OrderStatus::ACTIVE->value,
|
|
'payment_method' => $paymentMethod,
|
|
'client_ip' => $request->ip(),
|
|
]);
|
|
|
|
$orderItem = \App\Models\OrderItem::create([
|
|
'order_id' => $order->id,
|
|
'product_id' => $product->id,
|
|
'billing_cycle' => $billingCycle,
|
|
'months' => $months,
|
|
'server_name' => $data['server_name'] ?? null,
|
|
'quantity' => 1,
|
|
'price' => $basePrice,
|
|
]);
|
|
|
|
// 3) Invoice — đánh dấu PAID luôn vì admin đứng ra tạo & xác nhận thanh toán
|
|
$invoice = Invoice::create([
|
|
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
|
'customer_id' => $customer->id,
|
|
'order_id' => $order->id,
|
|
'invoice_type' => 1,
|
|
'sub_total' => $basePrice,
|
|
'discount_amount' => 0,
|
|
'tax' => 0,
|
|
'total' => $basePrice,
|
|
'status' => InvoiceStatus::PAID->value,
|
|
'payment_method' => $paymentMethod,
|
|
'paid_at' => now(),
|
|
'due_date' => now()->addMonths($months)->toDateString(),
|
|
]);
|
|
|
|
\App\Models\InvoiceItem::create([
|
|
'invoice_id' => $invoice->id,
|
|
'product_id' => $product->id,
|
|
'type' => 'service',
|
|
'description' => $product->name ?? 'Dịch vụ',
|
|
'months' => $months,
|
|
'quantity' => 1,
|
|
'unit_price' => $basePrice,
|
|
'total_price' => $basePrice,
|
|
]);
|
|
|
|
// 4) Payment processing
|
|
if ($paymentMethod === 'balance') {
|
|
$customer->total = (int) $customer->total - $basePrice;
|
|
$customer->save();
|
|
}
|
|
|
|
Transaction::create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'customer_id' => $customer->id,
|
|
'user_id' => Auth::id(),
|
|
'trans' => $order->order_num,
|
|
'amount' => $basePrice,
|
|
'type' => $paymentMethod === 'transfer' ? 'transfer' : 'balance',
|
|
'status' => 1, // paid
|
|
'note' => $paymentMethod === 'transfer'
|
|
? 'Admin tạo dịch vụ - thanh toán chuyển khoản'
|
|
: 'Admin tạo dịch vụ - trừ số dư khách hàng',
|
|
'proof_image' => !empty($proofPaths) ? json_encode($proofPaths) : null,
|
|
]);
|
|
|
|
// 5) Service — updateOrCreate để tránh trùng khi cùng VM được gán lại
|
|
$startedAt = $data['started_at'] ?? now();
|
|
$expiresAt = $data['expires_at']
|
|
?? \Carbon\Carbon::parse($startedAt)->addMonths($months);
|
|
|
|
$serviceLookup = $vmId
|
|
? ['customer_id' => $customer->id, 'vm_id' => $vmId]
|
|
: [
|
|
'customer_id' => $customer->id,
|
|
'product_id' => $product->id,
|
|
'vm_id' => null,
|
|
'server_name' => $data['server_name'] ?? null,
|
|
];
|
|
|
|
$service = \App\Models\Service::updateOrCreate(
|
|
$serviceLookup,
|
|
[
|
|
'customer_id' => $customer->id,
|
|
'product_id' => $product->id,
|
|
'vm_id' => $vmId,
|
|
'order_item_id' => $orderItem->id,
|
|
'invoice_id' => $invoice->id,
|
|
'server_name' => $data['server_name'] ?? null,
|
|
'status' => $data['status'] ?? 'active',
|
|
'auto_renew' => (bool) ($data['auto_renew'] ?? false),
|
|
'started_at' => $startedAt,
|
|
'expires_at' => $expiresAt,
|
|
'note' => $data['note'] ?? null,
|
|
]
|
|
);
|
|
|
|
DB::commit();
|
|
return response()->json([
|
|
'error' => false,
|
|
'message' => 'Service created',
|
|
'data' => $service->load(['product']),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List virtual machines from vCenter inventory for the VPS picker.
|
|
* Annotates each VM with `assigned_service_id` if already linked to a service.
|
|
*/
|
|
public function listAvailableVms(Request $request): JsonResponse
|
|
{
|
|
$search = trim((string) $request->get('q', ''));
|
|
|
|
$assignments = DB::table('module_vmware as mv')
|
|
->join('services as s', 's.vm_id', '=', 'mv.id')
|
|
->whereNotNull('mv.virtual_machine_id')
|
|
->select(
|
|
'mv.virtual_machine_id',
|
|
's.id as service_id',
|
|
's.customer_id as service_customer_id',
|
|
)
|
|
->get()
|
|
->keyBy('virtual_machine_id');
|
|
|
|
$vms = DB::table('virtual_machines')
|
|
->select('id', 'vm_id', 'name', 'power_state', 'cpu', 'ram_mb', 'ip')
|
|
->where('is_template', 0)
|
|
->where('is_system', 0)
|
|
->when($search, fn ($q) => $q->where(function ($w) use ($search) {
|
|
$w->where('name', 'like', "%{$search}%")
|
|
->orWhere('vm_id', 'like', "%{$search}%")
|
|
->orWhere('ip', 'like', "%{$search}%");
|
|
}))
|
|
->orderBy('name')
|
|
->limit(200)
|
|
->get()
|
|
->map(function ($vm) use ($assignments) {
|
|
$a = $assignments->get($vm->id);
|
|
$vm->assigned_service_id = $a->service_id ?? null;
|
|
$vm->assigned_customer_id = $a->service_customer_id ?? null;
|
|
return $vm;
|
|
});
|
|
|
|
return response()->json(['error' => false, 'data' => $vms]);
|
|
}
|
|
|
|
public function getOrders(string $uuid, Request $request): JsonResponse
|
|
{
|
|
$customer = $this->customerRepository->findBy('uuid', $uuid);
|
|
|
|
if (!$customer) {
|
|
return response()->json([
|
|
'error' => true,
|
|
'message' => "Can't find customer by UUID #" . $uuid
|
|
], 404);
|
|
}
|
|
|
|
$orders = $this->orderRepository->filter(
|
|
['client' => $customer->id],
|
|
['customer', 'payment', 'invoice']
|
|
);
|
|
|
|
return response()->json([
|
|
'error' => false,
|
|
'data' => $orders
|
|
]);
|
|
}
|
|
|
|
private function cycleToMonths(?string $cycle): int
|
|
{
|
|
return match ($cycle) {
|
|
'monthly' => 1,
|
|
'quarterly' => 3,
|
|
'semi_annually' => 6,
|
|
'annually' => 12,
|
|
'biennially' => 24,
|
|
'triennially' => 36,
|
|
default => 1,
|
|
};
|
|
}
|
|
}
|