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

1742 lines
64 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\Mail\Customer\ManualCustomerEmail;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Note;
use App\Models\Order;
use App\Models\Template;
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\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
{
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
{
$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->validated();
$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);
}
try {
DB::beginTransaction();
$data = $request->validated();
unset($data['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
{
$perPage = min((int) $request->get('per_page', 15), 500);
$query = Customer::onlyTrashed()
->with(['hosting', 'vps', 'domain']);
if ($filter = $request->get('filter')) {
$query->where(function ($q) use ($filter) {
$filter = trim($filter);
$q->where('fullname', 'LIKE', "%{$filter}%")
->orWhere('email', 'LIKE', "%{$filter}%")
->orWhere('phone', 'LIKE', "%{$filter}%");
});
}
$paginator = $query->paginate($perPage);
return response()->json([
'error' => false,
'data' => [
'items' => CustomerResource::collection($paginator),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
],
]);
}
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();
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, new password has been sent to customer email',
]);
} 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),
]);
$passwordMail = new \App\Mail\Customer\PasswordResetByAdmin([
'fullname' => $customer->fullname,
'email' => $customer->email,
'password' => $password,
]);
\Illuminate\Support\Facades\Mail::to($customer->email)->send($passwordMail);
\App\Models\Mail::create([
'subject' => $passwordMail->envelope()->subject,
'to' => $customer->email,
'customer_id' => $customer->id,
'message' => $passwordMail->render(),
]);
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 sendEmail(Request $request, string $uuid): JsonResponse
{
$data = $request->validate([
'template_id' => 'nullable|integer|exists:template,id',
'subject' => 'required_without:template_id|nullable|string|max:255',
'message' => 'required_without:template_id|nullable|string',
'to' => 'nullable|string|max:1000',
'cc' => 'nullable|string|max:1000',
'bcc' => 'nullable|string|max:1000',
]);
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json([
'error' => true,
'message' => "Can't find customer by UUID #" . $uuid,
], 404);
}
$template = null;
$subject = trim((string) ($data['subject'] ?? ''));
$message = trim((string) ($data['message'] ?? ''));
if (!empty($data['template_id'])) {
$template = Template::query()
->whereKey($data['template_id'])
->where('status', 1)
->first();
if (!$template) {
return response()->json([
'error' => true,
'message' => 'Template không tồn tại hoặc đang bị tắt.',
], 422);
}
$subject = trim((string) $template->subject);
$message = trim((string) $template->message);
}
if ($subject === '' || $message === '') {
return response()->json([
'error' => true,
'message' => 'Subject và content không được để trống.',
], 422);
}
$to = $this->parseEmailRecipients($data['to'] ?? null);
if ($to === []) {
$to = [$customer->email];
}
$cc = $this->parseEmailRecipients($data['cc'] ?? null);
$bcc = $this->parseEmailRecipients($data['bcc'] ?? null);
if ($to === []) {
return response()->json([
'error' => true,
'message' => 'Không có địa chỉ email nhận hợp lệ.',
], 422);
}
try {
$mailer = Mail::to($to);
if ($cc !== []) {
$mailer->cc($cc);
}
if ($bcc !== []) {
$mailer->bcc($bcc);
}
$mailer->send(new ManualCustomerEmail($subject, $message));
\App\Models\Mail::create([
'template_id' => $template->id ?? 0,
'subject' => $subject,
'to' => implode(', ', $to),
'cc' => implode(', ', $cc),
'customer_id' => $customer->id,
'message' => $message,
]);
$admin = Auth::guard('admin')->user();
Log::create([
'agent' => 'admin',
'client_ip' => $request->ip(),
'description' => sprintf(
'%s gửi email cho customer %s%s',
$admin->username ?? $admin->email ?? 'Admin',
$customer->email,
$template ? sprintf(' bằng template #%s', $template->id) : '',
),
'customer_id' => $customer->id,
]);
return response()->json([
'error' => false,
'message' => 'Đã gửi email cho khách hàng.',
]);
} catch (\Throwable $e) {
return response()->json([
'error' => true,
'message' => $e->getMessage(),
], 500);
}
}
public function addCredit(Request $request, string $uuid): JsonResponse
{
$request->validate([
'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);
}
$admin = Auth::guard('admin')->user();
try {
DB::beginTransaction();
$amount = (int) $request->amount;
$isAdd = $request->action === "+";
if ($isAdd) {
$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,
]);
}
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([
'error' => false,
'message' => 'Credit updated successfully'
]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json([
'error' => true,
'message' => $e->getMessage()
], 500);
}
}
private function parseEmailRecipients(?string $value): array
{
if (!$value) {
return [];
}
return collect(preg_split('/[,;]+/', $value) ?: [])
->map(fn($email) => trim((string) $email))
->filter(fn($email) => $email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL))
->unique()
->values()
->all();
}
public function addTotal(Request $request, string $uuid): JsonResponse
{
$request->validate([
'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']
);
// Frontend đọc ngày thanh toán qua `date_paid` — alias từ cột paid_at.
$invoices->each(fn($invoice) => $invoice->date_paid = $invoice->paid_at);
return response()->json([
'error' => false,
'data' => $invoices
]);
}
/**
* Sửa hóa đơn của khách hàng, kể cả số tiền (sub_total/tax/total). Không
* tự đồng bộ lại invoice_items hay customer.total.
*/
public function updateInvoice(string $uuid, int $invoiceId, Request $request): JsonResponse
{
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
}
$invoice = Invoice::where('customer_id', $customer->id)
->where('id', $invoiceId)
->first();
if (!$invoice) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy hóa đơn'], 404);
}
$data = $request->validate([
'invoice_number' => 'nullable|string|max:50',
'status' => 'nullable|integer|in:1,2,3,4,5',
'payment_method' => 'nullable|in:transfer,balance',
'date' => 'nullable|date',
'due_date' => 'nullable|date',
'date_paid' => 'nullable|date',
'note' => 'nullable|string',
'sub_total' => 'nullable|integer|min:0',
'tax' => 'nullable|integer|min:0',
'total' => 'nullable|integer|min:0',
]);
// `date_paid` là tên field phía frontend — cột thật trong DB là paid_at.
if (array_key_exists('date_paid', $data)) {
$data['paid_at'] = $data['date_paid'];
unset($data['date_paid']);
}
// Đi qua repository (không gọi $invoice->update() trực tiếp) để cache
// của invoiceRepository->filter() (dùng ở getInvoices) được xoá theo,
// nếu không danh sách hóa đơn sẽ hiển thị dữ liệu cũ cho tới khi cache hết hạn.
$this->invoiceRepository->update($invoiceId, $data);
$invoice = $invoice->refresh();
$invoice->date_paid = $invoice->paid_at;
return response()->json([
'error' => false,
'message' => 'Đã cập nhật hóa đơn',
'data' => $invoice->load('payment'),
]);
}
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
]);
}
/**
* Sửa thông tin giao dịch, kể cả amount (để sửa số tiền nhập/hiển thị sai).
* Không tự động cộng/trừ lại customer.total khi sửa amount — chiều +/- của
* amount phụ thuộc vào type (balance trừ tiền, transfer/admin_add_total
* cộng tiền,...) và không có field nào lưu lại chiều đó, nên tự suy luận
* dễ làm sai số dư hơn là sửa đúng. Nếu cần đồng bộ lại số dư, dùng
* "Cộng tiền" ở tab Tổng quan sau khi sửa xong.
*/
public function updateTransaction(string $uuid, int $transactionId, Request $request): JsonResponse
{
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
}
$transaction = Transaction::where('customer_id', $customer->id)
->where('id', $transactionId)
->first();
if (!$transaction) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy giao dịch'], 404);
}
$data = $request->validate([
'trans' => 'nullable|string|max:255',
'note' => 'nullable|string',
'type' => 'nullable|string|max:50',
'amount' => 'nullable|integer',
]);
// Đi qua repository để cache của transactionRepository->filter()
// (dùng ở getTransactions) được xoá theo — xem ghi chú ở updateInvoice.
$this->transactionRepository->update($transactionId, $data);
return response()->json([
'error' => false,
'message' => 'Đã cập nhật giao dịch',
'data' => $transaction->refresh()->load('user'),
]);
}
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', 'vmware.os.osGroup']
);
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',
'price' => 'nullable|numeric|min:0',
// module_vmware fields
'os_id' => 'nullable|integer|exists:os,id',
'vm_cpu' => 'nullable|integer|min:1',
'vm_ram_mb' => 'nullable|integer|min:1',
'vm_disk_gb' => 'nullable|integer|min:1',
'vm_ip' => 'nullable|string|max:45',
'vm_dns' => 'nullable|string|max:100',
'vm_user' => 'nullable|string|max:255',
'vm_password' => 'nullable|string|max:255',
'vm_port' => 'nullable|integer|min:1|max:65535',
]);
try {
DB::beginTransaction();
$service->fill(collect($data)->except([
'price', 'os_id', 'vm_cpu', 'vm_ram_mb', 'vm_disk_gb', 'vm_ip', 'vm_dns', 'vm_user', 'vm_password', 'vm_port',
])->all());
$service->save();
if (array_key_exists('price', $data) && $data['price'] !== null && $service->orderItem) {
$service->orderItem->update(['price' => $data['price']]);
}
// Đồng bộ sang module_vmware nếu có gán VM
if ($service->vm_id) {
$vmData = array_filter([
'hostname' => $data['server_name'] ?? null,
'os_id' => $data['os_id'] ?? null,
'cpu' => $data['vm_cpu'] ?? null,
'ram_mb' => $data['vm_ram_mb'] ?? null,
'disk_gb' => $data['vm_disk_gb'] ?? null,
'ip' => $data['vm_ip'] ?? null,
'dns' => $data['vm_dns'] ?? null,
'user' => $data['vm_user'] ?? null,
'password' => $data['vm_password'] ?? null,
'plain_password' => $data['vm_password'] ?? null,
'port' => $data['vm_port'] ?? null,
], fn ($v) => $v !== null);
if ($vmData) {
\App\Models\ModuleVmware::where('id', $service->vm_id)->update($vmData);
}
}
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', 'vmware.os.osGroup']),
]);
} 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',
'amount_paid' => 'nullable|numeric|min:0',
'proof_images' => 'nullable|array',
'proof_images.*' => 'file|image|max:5120',
// module_vmware fields — admin can override the vCenter snapshot / fill in what's not tracked there (disk, OS, credentials, port)
'os_id' => 'nullable|integer|exists:os,id',
'vm_cpu' => 'nullable|integer|min:1',
'vm_ram_mb' => 'nullable|integer|min:1',
'vm_disk_gb' => 'nullable|integer|min:1',
'vm_ip' => 'nullable|string|max:45',
'vm_dns' => 'nullable|string|max:100',
'vm_user' => 'nullable|string|max:255',
'vm_password' => 'nullable|string|max:255',
'vm_port' => 'nullable|integer|min:1|max:65535',
]);
$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();
// Số tiền đã thanh toán: admin nhập tay để khớp với thực tế (có thể lệch giá niêm yết do khuyến mãi/thoả thuận)
$basePrice = isset($data['amount_paid'])
? (int) $data['amount_paid']
: (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) Order + OrderItem (mô phỏng flow customer đặt hàng)
$order = Order::create([
'order_num' => OrderHelper::generateOrderNumber(),
'customer_id' => $customer->id,
'date' => now()->toDateString(),
'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,
]);
// Ngày bắt đầu/hết hạn dùng chung cho cả module_vmware (next_due_date/next_invoice) và services.
$startedAt = $data['started_at'] ?? now();
$expiresAt = $data['expires_at']
?? \Carbon\Carbon::parse($startedAt)->addMonths($months);
// 2) ModuleVmware: snapshot từ virtual_machines. `order_id` phải trỏ về
// order vừa tạo — client console-ticket/rebuild resolve VM qua
// service->orderItem->order->vmWare, không qua services.vm_id.
$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,
'order_id' => $order->id,
'name' => $vm->name,
'vmware_id' => $vm->vm_id,
'vcenter_id' => $vcenterId,
'hostname' => $data['server_name'] ?? $vm->name,
'cpu' => $data['vm_cpu'] ?? $vm->cpu,
'ram_mb' => $data['vm_ram_mb'] ?? $vm->ram_mb,
'disk_gb' => $data['vm_disk_gb'] ?? null,
'os_id' => $data['os_id'] ?? null,
'ip' => $data['vm_ip'] ?? $vm->ip,
'dns' => $data['vm_dns'] ?? null,
'user' => $data['vm_user'] ?? null,
'password' => $data['vm_password'] ?? null,
'plain_password' => $data['vm_password'] ?? null,
'port' => $data['vm_port'] ?? null,
'status' => 1,
'first_payment' => $basePrice,
'next_payment' => $basePrice,
'next_due_date' => \Carbon\Carbon::parse($expiresAt)->timestamp,
'next_invoice' => \Carbon\Carbon::parse($expiresAt)->timestamp,
]
);
$vmId = $moduleVm->id;
}
// 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,
'date' => now()->toDateString(),
'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
$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,
]
);
// module_vmware.service_id chỉ biết được sau khi Service đã tồn tại.
if ($vmId) {
\App\Models\ModuleVmware::where('id', $vmId)->update(['service_id' => $service->id]);
}
DB::commit();
return response()->json([
'error' => false,
'message' => 'Service created',
'data' => $service->load(['product', 'vmware.os.osGroup']),
]);
} 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 as vm')
->leftJoin('module_datacenter as dc', 'dc.id', '=', 'vm.datacenter_id')
->leftJoin('module_host as h', 'h.id', '=', 'vm.host_id')
->leftJoin('module_vcenter as vc', 'vc.id', '=', 'dc.vcenter_id')
->select(
'vm.id', 'vm.vm_id', 'vm.name', 'vm.power_state', 'vm.cpu', 'vm.ram_mb', 'vm.ip',
'vc.host as vcenter_name',
'dc.name as datacenter_name',
'h.name as host_name',
)
->where('vm.is_template', 0)
->where('vm.is_system', 0)
->when($search, fn ($q) => $q->where(function ($w) use ($search) {
$w->where('vm.name', 'like', "%{$search}%")
->orWhere('vm.vm_id', 'like', "%{$search}%")
->orWhere('vm.ip', 'like', "%{$search}%");
}))
->orderBy('vm.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
]);
}
/**
* Sửa đơn hàng của khách hàng, kể cả số tiền (sub_total/discount_amount/
* total). Không tự đồng bộ lại invoice/invoice_item hay customer.total —
* nếu số tiền hóa đơn liên kết cũng cần sửa, sửa riêng ở updateInvoice.
*/
public function updateOrder(string $uuid, int $orderId, Request $request): JsonResponse
{
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
}
$order = Order::where('customer_id', $customer->id)
->where('id', $orderId)
->first();
if (!$order) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy đơn hàng'], 404);
}
$data = $request->validate([
'order_num' => 'nullable|string|max:255',
'status' => 'nullable|integer|in:0,1,2,3',
'payment_method' => 'nullable|in:transfer,balance',
'date' => 'nullable|date',
'sub_total' => 'nullable|integer|min:0',
'discount_amount' => 'nullable|integer|min:0',
'total' => 'nullable|integer|min:0',
]);
// Đi qua repository để cache của orderRepository->filter()
// (dùng ở getOrders) được xoá theo — xem ghi chú ở updateInvoice.
$this->orderRepository->update($orderId, $data);
return response()->json([
'error' => false,
'message' => 'Đã cập nhật đơn hàng',
'data' => $order->refresh()->load(['payment', 'invoice']),
]);
}
public function updateStatus(string $uuid, Request $request): JsonResponse
{
$request->validate(['status' => ['required', 'integer', 'in:0,1,2,3']]);
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer by UUID #" . $uuid], 404);
}
$customer->status = $request->input('status');
$customer->save();
return response()->json(['error' => false, 'message' => 'Cập nhật trạng thái thành công']);
}
public function bulkDelete(Request $request): JsonResponse
{
$request->validate(['uuids' => ['required', 'array', 'min:1'], 'uuids.*' => ['string']]);
$uuids = $request->input('uuids');
$customers = Customer::whereIn('uuid', $uuids)->get();
try {
DB::beginTransaction();
foreach ($customers as $c) {
Invoice::where('customer_id', $c->id)->delete();
Order::where('customer_id', $c->id)->delete();
Transaction::where('customer_id', $c->id)->delete();
$c->delete();
}
DB::commit();
return response()->json(['error' => false, 'message' => "Đã xoá {$customers->count()} khách hàng"]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
public function bulkRestore(Request $request): JsonResponse
{
$request->validate(['uuids' => ['required', 'array', 'min:1'], 'uuids.*' => ['string']]);
$customers = Customer::onlyTrashed()->whereIn('uuid', $request->input('uuids'))->get();
try {
DB::beginTransaction();
foreach ($customers as $c) {
$c->restore();
Invoice::withTrashed()->where('customer_id', $c->id)->restore();
Order::withTrashed()->where('customer_id', $c->id)->restore();
Transaction::withTrashed()->where('customer_id', $c->id)->restore();
}
DB::commit();
return response()->json(['error' => false, 'message' => "Đã khôi phục {$customers->count()} khách hàng"]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
public function bulkForceDelete(Request $request): JsonResponse
{
$request->validate(['uuids' => ['required', 'array', 'min:1'], 'uuids.*' => ['string']]);
$customers = Customer::onlyTrashed()->whereIn('uuid', $request->input('uuids'))->get();
try {
DB::beginTransaction();
foreach ($customers as $c) {
Invoice::withTrashed()->where('customer_id', $c->id)->forceDelete();
Order::withTrashed()->where('customer_id', $c->id)->forceDelete();
Transaction::withTrashed()->where('customer_id', $c->id)->forceDelete();
$c->forceDelete();
}
DB::commit();
return response()->json(['error' => false, 'message' => "Đã xoá vĩnh viễn {$customers->count()} khách hàng"]);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
}
public function bulkStatus(Request $request): JsonResponse
{
$request->validate([
'uuids' => ['required', 'array', 'min:1'],
'uuids.*' => ['string'],
'status' => ['required', 'integer', 'in:0,1,2,3'],
]);
$count = Customer::whereIn('uuid', $request->input('uuids'))
->update(['status' => $request->input('status')]);
return response()->json(['error' => false, 'message' => "Đã cập nhật trạng thái {$count} khách hàng"]);
}
/**
* Ghi chú của khách hàng (không gắn với dịch vụ cụ thể).
*/
public function getCustomerNotes(string $uuid): JsonResponse
{
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
}
$notes = Note::with('user:id,username')
->where('customer_id', $customer->id)
->whereNull('service_id')
->latest()
->get();
return response()->json(['error' => false, 'data' => $notes]);
}
public function storeCustomerNote(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(['note' => 'required|string']);
$note = Note::create([
'note' => $data['note'],
'customer_id' => $customer->id,
'user_id' => Auth::id(),
]);
return response()->json([
'error' => false,
'message' => 'Đã thêm ghi chú',
'data' => $note->load('user:id,username'),
]);
}
public function destroyCustomerNote(string $uuid, int $noteId): JsonResponse
{
return $this->destroyNote($uuid, $noteId);
}
/**
* Ghi chú gắn với 1 dịch vụ cụ thể của khách hàng.
*/
public function getServiceNotes(string $uuid, int $serviceId): JsonResponse
{
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
}
$notes = Note::with('user:id,username')
->where('customer_id', $customer->id)
->where('service_id', $serviceId)
->latest()
->get();
return response()->json(['error' => false, 'data' => $notes]);
}
public function storeServiceNote(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);
}
$data = $request->validate(['note' => 'required|string']);
$note = Note::create([
'note' => $data['note'],
'customer_id' => $customer->id,
'service_id' => $serviceId,
'user_id' => Auth::id(),
]);
return response()->json([
'error' => false,
'message' => 'Đã thêm ghi chú',
'data' => $note->load('user:id,username'),
]);
}
public function destroyServiceNote(string $uuid, int $serviceId, int $noteId): JsonResponse
{
return $this->destroyNote($uuid, $noteId);
}
private function destroyNote(string $uuid, int $noteId): JsonResponse
{
$customer = $this->customerRepository->findBy('uuid', $uuid);
if (!$customer) {
return response()->json(['error' => true, 'message' => "Can't find customer"], 404);
}
$deleted = Note::where('id', $noteId)->where('customer_id', $customer->id)->delete();
if (!$deleted) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy ghi chú'], 404);
}
return response()->json(['error' => false, 'message' => 'Đã xoá ghi chú']);
}
private function cycleToMonths(?string $cycle): int
{
return match ($cycle) {
'monthly' => 1,
'quarterly' => 3,
'semi_annually' => 6,
'annually' => 12,
'biennially' => 24,
'triennially' => 36,
default => 1,
};
}
}