Files
portal-api/storage/backups/OrderController.original.php
T
2026-06-15 09:11:57 +07:00

918 lines
37 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Service;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Http\Controllers\Controller;
use App\Jobs\VirtualMachine\Action\VMJobs;
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
use App\Jobs\VirtualMachine\Provision\RetryProvisioningOrchestrator;
use App\Models\Log as CustomerActivityLog;
use App\Http\Requests\Api\Order\PayOrderRequest;
use App\Http\Requests\Api\Order\StoreOrderRequest;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\Service;
use App\Models\VirtualMachines;
use App\Repositories\Api\Order\OrderRepository;
use App\Services\ActivityLogger;
use App\Services\Api\Order\OrderService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/**
* @OA\Tag(name="Orders", description="Order & Invoice management")
*/
class OrderController extends Controller
{
public function __construct(
protected OrderService $orderService,
protected OrderRepository $orderRepo,
protected ActivityLogger $activityLogger,
) {}
// =========================================================================
// Orders
// =========================================================================
/**
* GET /v1/orders — paginated order list (admin / internal)
*/
public function getList(Request $request): JsonResponse
{
try {
$orders = $this->orderRepo->getOrders(
$request->user()->id,
$request->only(['total_price', 'pending'])
);
return response()->json(['success' => true, 'data' => $orders]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* GET /v1/orders/check-server-name?name=xxx
* Kiểm tra tên server đã tồn tại chưa (order_items, services, module_vmware, virtual_machines)
*/
public function checkServerName(Request $request): JsonResponse
{
$name = trim($request->query('name', ''));
if ($name === '') {
return response()->json(['available' => false, 'message' => 'Tên server không được để trống.']);
}
$exists =
OrderItem::where('server_name', $name)->exists() ||
Service::where('server_name', $name)->exists() ||
ModuleVmware::where('name', $name)->exists() ||
ModuleVmware::where('hostname', $name)->exists() ||
VirtualMachines::where('name', $name)->exists();
if ($exists) {
return response()->json([
'available' => false,
'message' => 'Tên server này đã được sử dụng, vui lòng chọn tên khác.',
]);
}
return response()->json(['available' => true]);
}
/**
* GET /v1/client/orders — customer's order list
*/
public function getClientOrders(Request $request): JsonResponse
{
try {
$orders = Order::query()
->with(['invoice'])
->where('customer_id', $request->user()->id)
->orderByDesc('created_at')
->get();
$data = $orders->map(fn(Order $order) => [
'id' => $order->id,
'order_num' => $order->order_num,
'status' => $order->status,
'status_text' => OrderStatus::from((int) $order->status)->label(),
'total' => (float) $order->total,
'created_at' => $this->isoDate($order->created_at),
'invoice' => $order->invoice ? [
'id' => $order->invoice->id,
'invoice_number' => $order->invoice->invoice_number,
'status' => $order->invoice->status,
'total' => $order->invoice->total,
] : null,
]);
return response()->json(['success' => true, 'data' => $data]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* GET /v1/client/orders/{id} — order detail
*/
public function getClientOrderDetail(Request $request, int $id): JsonResponse
{
try {
$order = Order::query()
->with(['invoice', 'orderItems.product', 'orderItems.service', 'customer'])
->where('customer_id', $request->user()->id)
->where('id', $id)
->first();
if (!$order) {
return $this->notFound('Đơn hàng không tồn tại.');
}
return response()->json(['success' => true, 'data' => $this->formatOrderDetail($order)]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* POST /v1/orders — create a new order
*
* @OA\Post(path="/orders", operationId="storeOrder", tags={"Orders"},
* security={{"bearerAuth":{}}},
* @OA\RequestBody(required=true, @OA\JsonContent(
* required={"product_id","billing_cycle"},
* @OA\Property(property="product_id", type="string", example="uuid-here"),
* @OA\Property(property="billing_cycle", type="string", example="annually",
* enum={"monthly","quarterly","semi_annually","annually","biennially"}),
* @OA\Property(property="server_name", type="string", example="my-vps-01"),
* @OA\Property(property="os_id", type="integer", example=1),
* @OA\Property(property="addons", type="array", @OA\Items(
* @OA\Property(property="addon_id", type="integer", example=1),
* @OA\Property(property="qty", type="integer", example=2)
* ))
* )),
* @OA\Response(response=201, description="Order created successfully")
* )
*/
public function storeOrder(StoreOrderRequest $request): JsonResponse
{
try {
$customer = $request->user();
$data = $request->validated();
$product = Product::where('uuid', $data['product_id'])->first();
if (!$product) {
throw new \Exception('Sản phẩm không tìm thấy hoặc đã bị xóa.');
}
$data['product_id'] = $product->id;
$result = $this->orderService->create($data, $customer);
$order = $result['order']->load(['orderItems.product', 'invoice', 'customer']);
$invoice = $result['invoice'];
$this->activityLogger->log(
event: 'created',
logName: 'Order',
description: "Customer {$customer->email} tạo đơn hàng #{$order->order_num} - Tổng: " . number_format($order->total) . ' VNĐ',
subject: $order,
newValues: ['order_num' => $order->order_num, 'total' => $order->total, 'product_id' => $data['product_id']],
);
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' => $this->formatOrderSummary($order),
'invoice' => $this->formatInvoiceSummary($invoice),
'items' => $this->formatOrderItems($order->orderItems),
],
], 201);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* POST /v1/orders/{id}/pay — pay for a pending order
*
* @OA\Post(path="/orders/{id}/pay", operationId="payOrder", tags={"Orders"},
* security={{"bearerAuth":{}}},
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
* @OA\RequestBody(required=true, @OA\JsonContent(
* required={"payment_method"},
* @OA\Property(property="payment_method", type="string",
* enum={"credit","bank_transfer","vnpay","momo"})
* )),
* @OA\Response(response=200, description="Payment successful")
* )
*/
public function payOrder(int $id, PayOrderRequest $request): JsonResponse
{
try {
$order = $this->orderRepo->find($id);
if (!$order) {
return $this->notFound('Đơn hàng không tồn tại.');
}
if ($order->customer_id !== $request->user()->id) {
return response()->json(['success' => false, 'message' => 'Không có quyền truy cập.'], 403);
}
$this->orderService->pay($order, $request->payment_method);
$this->activityLogger->log(
event: 'payment',
logName: 'Order',
description: "Customer {$request->user()->email} thanh toán đơn hàng #{$order->order_num} qua {$request->payment_method}",
subject: $order,
newValues: ['payment_method' => $request->payment_method, 'total' => $order->total],
);
return response()->json(['success' => true, 'message' => 'Thanh toán thành công.']);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* POST /v1/client/orders/{id}/retry — retry VM provisioning for a paid order
*
* Dispatches RetryProvisioningOrchestrator which:
* 1. Searches vSphere for existing VM by server_name
* 2. If found → links VM to module_vmware + services
* 3. If not found → dispatches CreateVirtualMachineJob
* 4. Each failure step notifies Telegram
*/
public function retryOrderProvisioning(Request $request, int $id): JsonResponse
{
try {
$order = Order::where('id', $id)
->with(['invoice', 'orderItems.service'])
->where('customer_id', $request->user()->id)
->first();
if (!$order) {
return $this->notFound('Đơn hàng không tồn tại.');
}
if (!$order->invoice || (int) $order->invoice->status !== InvoiceStatus::PAID->value) {
return $this->error('Đơn hàng chưa được thanh toán.');
}
$paidAt = $order->invoice->paid_at ? Carbon::parse($order->invoice->paid_at) : null;
if (!$paidAt || $paidAt->diffInMinutes(now()) < 180) {
return $this->error('Vui lòng chờ khoảng 3 tiếng sau khi thanh toán mới có thể khởi tạo lại dịch vụ.');
}
$service = $order->orderItems->first()?->service;
if (!$service || (int) $service->customer_id !== (int) $request->user()->id) {
return $this->error('Không tìm thấy dịch vụ hợp lệ để khởi tạo lại.');
}
if ($service->status !== 'pending') {
return $this->error('Dịch vụ đã được khởi tạo hoặc không ở trạng thái chờ.');
}
if ($service->retry_requested_at) {
return $this->error('Dịch vụ đã được yêu cầu khởi tạo lại một lần. Vui lòng liên hệ kỹ thuật qua mailto:kythuat@tgs.com.vn');
}
$service->update(['retry_requested_at' => now()]);
RetryProvisioningOrchestrator::dispatch($order->id, $request->user()->id);
$this->activityLogger->log(
event: 'retry_provisioning',
logName: 'Order',
description: "Customer {$request->user()->email} yêu cầu khởi tạo lại dịch vụ cho đơn hàng #{$order->order_num}",
subject: $order,
);
return response()->json([
'success' => true,
'message' => 'Đã yêu cầu khởi tạo lại dịch vụ. Hệ thống sẽ kiểm tra và xử lý trong vài phút.',
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
// =========================================================================
// Invoices
// =========================================================================
/**
* GET /v1/client/invoices — customer's invoice list
*/
public function getClientInvoices(Request $request): JsonResponse
{
try {
$invoices = Invoice::query()
->with(['order.orderItems.product'])
->where('customer_id', $request->user()->id)
->orderByDesc('created_at')
->get();
$data = $invoices->map(fn(Invoice $invoice) => [
'id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'service_name' => $invoice->order?->orderItems->first()?->product?->name
?? ('Đơn hàng ' . $invoice->order?->order_num),
'total' => (float) $invoice->total,
'status' => $invoice->status,
'status_text' => InvoiceStatus::tryFrom((int) $invoice->status)?->label() ?? '-',
'created_at' => $this->isoDate($invoice->created_at),
'order_id' => $invoice->order?->id,
'order_num' => $invoice->order?->order_num,
])->values();
return response()->json(['success' => true, 'data' => $data]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* GET /v1/client/invoices/{id} — invoice detail
*/
public function getClientInvoiceDetail(Request $request, int $id): JsonResponse
{
try {
$invoice = Invoice::query()
->with([
'order.orderItems.product',
'order.vmWare.ipAddress',
'order.vmWare.os',
'customer',
'attachments',
])
->where('customer_id', $request->user()->id)
->where('id', $id)
->first();
if (!$invoice) {
return $this->notFound('Hóa đơn không tồn tại.');
}
return response()->json(['success' => true, 'data' => $this->formatInvoiceDetail($invoice)]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* GET /v1/client/invoices/order/{orderNumber} — invoice detail by order number
*/
public function getClientInvoiceDetailByOrderNum(Request $request, string $orderNumber): JsonResponse
{
try {
$order = Order::query()
->with([
'invoice.invoiceItems',
'invoice.attachments',
'invoice.customer',
'orderItems.product',
'customer',
'vmWare.ipAddress',
'vmWare.os',
])
->where('customer_id', $request->user()->id)
->where('order_num', $orderNumber)
->first();
if (!$order?->invoice) {
return $this->notFound('Hóa đơn không tồn tại.');
}
// Reuse formatInvoiceDetail with already-loaded relations
$invoice = $order->invoice;
// Attach order + vmWare so formatter can use them
$invoice->setRelation('order', $order);
$invoice->setRelation('customer', $order->customer);
return response()->json(['success' => true, 'data' => $this->formatInvoiceDetail($invoice)]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
// =========================================================================
// Services (dich-vu)
// =========================================================================
/**
* GET /v1/client/dich-vu — list of customer's services
*/
public function getClientServices(Request $request): JsonResponse
{
try {
$services = Service::query()
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os'])
->where('customer_id', $request->user()->id)
->orderByDesc('created_at')
->get();
$data = $services->map(function (Service $service) {
$vmware = $service->vmware;
return [
'id' => $service->id,
'service_name' => $service->product?->name ?? $service->server_name,
'server_name' => $service->server_name,
'product_type' => $service->product?->product_type,
'billing_cycle' => $service->orderItem?->billing_cycle,
'status' => $service->status,
'status_text' => $service->status_label,
'auto_renew' => (bool) $service->auto_renew,
'started_at' => $this->isoDate($service->started_at),
'expires_at' => $this->isoDate($service->expires_at),
'invoice_id' => $service->invoice_id,
'invoice_number' => $service->invoice?->invoice_number,
'addons' => $service->addons->map(fn($a) => [
'type' => $a->addon_type,
'value' => $a->addon_value,
'price' => $a->price,
])->all(),
'vmware' => $vmware ? [
'id' => $vmware->id,
'name' => $vmware->name,
'hostname' => $vmware->hostname,
'ip_address' => $vmware->ipAddress?->ip,
'os_name' => $vmware->os?->name,
'status' => $vmware->status,
] : null,
];
})->values();
return response()->json(['success' => true, 'data' => $data]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* GET /v1/client/dich-vu/{uuid} — service detail by UUID
*/
public function getClientServiceDetail(Request $request, string $uuid): JsonResponse
{
try {
$service = Service::query()
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os', 'vmware.vCenter'])
->where('uuid', $uuid)
->where('customer_id', $request->user()->id)
->first();
if (!$service) {
return $this->notFound('Không tìm thấy dịch vụ.');
}
$vmware = $service->vmware;
$data = [
'id' => $service->id,
'uuid' => $service->uuid,
'service_name' => $service->product?->name ?? $service->server_name,
'server_name' => $service->server_name,
'product_type' => $service->product?->product_type,
'billing_cycle' => $service->orderItem?->billing_cycle,
'status' => $service->status,
'status_text' => $service->status_label,
'auto_renew' => (bool) $service->auto_renew,
'started_at' => $this->isoDate($service->started_at),
'expires_at' => $this->isoDate($service->expires_at),
'note' => $service->note,
'invoice_id' => $service->invoice_id,
'invoice_number' => $service->invoice?->invoice_number,
'product' => $service->product ? [
'id' => $service->product->id,
'uuid' => $service->product->uuid,
'name' => $service->product->name,
'product_type' => $service->product->product_type,
'specs' => [
'cpu' => $service->product->cpu,
'ram' => $service->product->ram,
'disk' => $service->product->disk,
'bandwidth' => $service->product->bandwidth,
],
] : null,
'addons' => $service->addons->map(fn($a) => [
'type' => $a->addon_type,
'value' => $a->addon_value,
'price' => $a->price,
])->all(),
'vmware' => $vmware ? [
'id' => $vmware->id,
'name' => $vmware->name,
'hostname' => $vmware->hostname,
'ip_address' => $vmware->ipAddress?->ip,
'os_name' => $vmware->os?->name,
'status' => $vmware->status,
] : null,
];
return response()->json(['success' => true, 'data' => $data]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
/**
* PATCH /v1/client/dich-vu/{id}/auto-renew — toggle auto-renewal on a service
*/
public function updateClientServiceAutoRenew(Request $request, int $id): JsonResponse
{
try {
$validated = $request->validate(['auto_renew' => ['required', 'boolean']]);
$service = Service::query()
->where('id', $id)
->where('customer_id', $request->user()->id)
->first();
if (!$service) {
return $this->notFound('Không tìm thấy dịch vụ.');
}
$service->update(['auto_renew' => (bool) $validated['auto_renew']]);
$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}",
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],
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
// =========================================================================
// Services — renewal
// =========================================================================
/**
* POST /v1/client/dich-vu/{id}/renew
*
* Pay-and-activate renewal in one step:
* 1. Validate balance
* 2. Debit customer balance (inside transaction)
* 3. Create PAID renewal invoice (inside transaction)
* 4. Extend service expires_at + set status = active (inside transaction)
* 5. Dispatch VMJobs 'Start' OUTSIDE transaction to power on suspended VM
*/
public function renewService(Request $request, int $id): JsonResponse
{
try {
$customer = $request->user();
$service = Service::query()
->with(['orderItem.product'])
->where('id', $id)
->where('customer_id', $customer->id)
->whereNotIn('status', ['terminated', 'cancelled'])
->first();
if (!$service) {
return $this->notFound('Không tìm thấy dịch vụ hoặc dịch vụ đã bị hủy.');
}
$orderItem = $service->orderItem;
if (!$orderItem || (int) $orderItem->price <= 0) {
return $this->error('Không thể xác định giá gia hạn dịch vụ.');
}
$validated = $request->validate(['months' => ['required', 'integer', 'min:1', 'max:24']]);
$price = (int) $orderItem->price * (int) $validated['months'];
$months = (int) $validated['months'];
// Validate balance before opening a transaction
if ($customer->total < $price) {
return $this->error(sprintf(
'Số dư không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
number_format($customer->total),
number_format($price)
));
}
$needsVmStart = $service->vm_id !== null;
$invoice = DB::transaction(function () use ($customer, $service, $orderItem, $price, $months) {
// 1. Debit balance
$customer->total -= $price;
$customer->save();
// 2. Activity log
CustomerActivityLog::create([
'agent' => 'api',
'client_ip' => request()?->ip(),
'customer_id' => $customer->id,
'description' => sprintf(
'Gia hạn dịch vụ #%s. Số tiền: %s VNĐ. Số dư còn: %s VNĐ.',
$service->server_name ?? $service->id,
number_format($price),
number_format($customer->total)
),
]);
// 3. Create PAID renewal invoice
$invoice = Invoice::create([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'customer_id' => $customer->id,
'order_id' => null,
'invoice_type' => 2, // renewal
'sub_total' => $price,
'discount_amount' => 0,
'tax' => 0,
'total' => $price,
'status' => InvoiceStatus::PAID->value,
'paid_at' => now(),
'payment_method' => 'credit',
]);
InvoiceItem::create([
'invoice_id' => $invoice->id,
'product_id' => $service->product_id,
'type' => 'renewal',
'description' => 'Gia hạn dịch vụ: ' . ($orderItem->product?->name ?? $service->server_name ?? ''),
'months' => $months,
'quantity' => 1,
'unit_price' => $price,
'total_price' => $price,
]);
// 4. Extend expires_at from today (or current expires_at if still in future)
$baseDate = $service->expires_at && $service->expires_at->isFuture()
? $service->expires_at
: now();
$expiresAt = Carbon::parse($baseDate)->addMonths($months);
$service->update([
'status' => 'active',
'expires_at' => $expiresAt,
'suspended_at' => null,
'invoice_id' => $invoice->id,
]);
return $invoice;
});
// 5. Start VM outside transaction (DB already committed)
if ($needsVmStart) {
dispatch(new VMJobs((int) $service->vm_id, 'Start', false))
->delay(now()->addSeconds(5));
}
$this->activityLogger->log(
event: 'renew',
logName: 'Service',
description: "Customer {$customer->email} gia hạn dịch vụ #{$service->id} ({$service->server_name}) - {$months} tháng - " . number_format($price) . ' VNĐ',
subject: $service,
newValues: ['months' => $months, 'price' => $price, 'invoice_number' => $invoice->invoice_number],
);
return response()->json([
'success' => true,
'message' => 'Gia hạn thành công. Dịch vụ đã được kích hoạt lại.',
'data' => [
'invoice_number' => $invoice->invoice_number,
'total' => (float) $invoice->total,
'expires_at' => $service->fresh()?->expires_at?->toISOString(),
],
]);
} catch (\Exception $e) {
return $this->error($e->getMessage());
}
}
// =========================================================================
// Private — formatters
// =========================================================================
private function formatOrderSummary(Order $order): array
{
return [
'id' => $order->id,
'order_num' => $order->order_num,
'status' => $order->status,
'status_text' => OrderStatus::from((int) $order->status)->label(),
'sub_total' => $order->sub_total,
'total' => $order->total,
'created_at' => $this->isoDate($order->created_at),
];
}
private function formatInvoiceSummary(Invoice $invoice): array
{
return [
'id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'sub_total' => $invoice->sub_total,
'tax' => $invoice->tax,
'total' => $invoice->total,
'status' => $invoice->status,
'due_date' => $this->isoDate($invoice->due_date),
];
}
private function formatOrderItems($items): array
{
return $items->map(fn($item) => [
'id' => $item->id,
'billing_cycle' => $item->billing_cycle,
'months' => $item->months,
'server_name' => $item->server_name,
'os_id' => $item->os_id,
'price' => (float) $item->price,
'service' => $item->service ? [
'id' => $item->service->id,
'status' => $item->service->status,
'status_text' => $item->service->status_label,
'retry_requested_at' => $this->isoDate($item->service->retry_requested_at),
'can_retry_provisioning' => $this->canRetryProvisioning($item->service),
] : null,
'product' => $item->product ? [
'id' => $item->product->id,
'uuid' => $item->product->uuid,
'name' => $item->product->name,
'product_type' => $item->product->product_type,
'specs' => [
'cpu' => $item->product->cpu,
'ram' => $item->product->ram,
'disk' => $item->product->disk,
'bandwidth' => $item->product->bandwidth,
],
] : null,
])->all();
}
private function formatOrderDetail(Order $order): array
{
$invoice = $order->invoice;
return [
'id' => $order->id,
'order_num' => $order->order_num,
'status' => $order->status,
'status_text' => OrderStatus::from((int) $order->status)->label(),
'total' => (float) $order->total,
'created_at' => $this->isoDate($order->created_at),
'customer' => [
'id' => $order->customer?->id,
'name' => $order->customer?->fullname,
'email' => $order->customer?->email,
],
'invoice' => $invoice ? [
'id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'status' => $invoice->status,
'status_text' => InvoiceStatus::tryFrom((int) $invoice->status)?->label() ?? '-',
'sub_total' => $invoice->sub_total,
'tax' => $invoice->tax,
'total' => $invoice->total,
'due_date' => $this->isoDate($invoice->due_date),
'paid_at' => $this->isoDate($invoice->paid_at),
] : null,
'items' => $this->formatOrderItems($order->orderItems),
];
}
private function canRetryProvisioning(Service $service): bool
{
if ($service->status !== 'pending') {
return false;
}
if ($service->retry_requested_at) {
return false;
}
$paidAt = $service->invoice?->paid_at;
if (!$paidAt) {
return false;
}
return Carbon::parse($paidAt)->diffInMinutes(now()) >= 180;
}
private function formatInvoiceDetail(Invoice $invoice): array
{
$order = $invoice->order;
$vmware = $order?->vmWare->first();
return [
'id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'invoice_type' => $invoice->invoice_type,
'status' => $invoice->status,
'status_text' => InvoiceStatus::tryFrom((int) $invoice->status)?->label() ?? '-',
'sub_total' => (float) $invoice->sub_total,
'discount_amount' => (float) ($invoice->discount_amount ?? 0),
'tax' => (float) $invoice->tax,
'total' => (float) $invoice->total,
'payment_method' => $invoice->payment_method,
'auto_renew' => (bool) $invoice->auto_renew,
'created_at' => $this->isoDate($invoice->created_at),
'due_date' => $this->isoDate($invoice->due_date),
'paid_at' => $this->isoDate($invoice->paid_at),
'customer' => $invoice->customer ? [
'id' => $invoice->customer->id,
'name' => $invoice->customer->fullname,
'email' => $invoice->customer->email,
'address' => $invoice->customer->address,
] : null,
'order' => $order ? [
'id' => $order->id,
'order_num' => $order->order_num,
'total' => (float) $order->total,
] : null,
'service' => ($firstItem = $order?->orderItems->first()) ? [
'product_id' => $firstItem->product?->id,
'product_uuid' => $firstItem->product?->uuid,
'name' => $firstItem->product?->name,
'billing_cycle' => $firstItem->billing_cycle,
'server_name' => $firstItem->server_name,
'specs' => [
'cpu' => $firstItem->product?->cpu,
'ram' => $firstItem->product?->ram,
'disk' => $firstItem->product?->disk,
],
] : null,
'items' => $order ? $order->orderItems->map(fn($item) => [
'id' => $item->id,
'product_name' => $item->product?->name,
'server_name' => $item->server_name,
'billing_cycle' => $item->billing_cycle,
'price' => (float) $item->price,
'months' => $item->months,
'os_id' => $item->os_id,
])->values()->all() : [],
'attachments' => ($invoice->relationLoaded('attachments') ? $invoice->attachments : collect())->map(fn($a) => [
'id' => $a->id,
'filename' => $a->filename,
'url' => $a->url,
'mime_type' => $a->mime_type,
])->all(),
'vmware' => $vmware ? [
'id' => $vmware->id,
'name' => $vmware->name,
'hostname' => $vmware->hostname,
'ip_address' => $vmware->ipAddress?->ip,
'os_name' => $vmware->os?->name,
'status' => $vmware->status,
'next_due_date' => $this->isoDate($vmware->next_due_date),
] : null,
];
}
// =========================================================================
// Micro-helpers
// =========================================================================
private function isoDate($value): ?string
{
if (empty($value)) {
return null;
}
try {
if ($value instanceof \DateTimeInterface) {
return Carbon::instance($value)->toISOString();
}
if (is_numeric($value)) {
return Carbon::createFromTimestamp((int) $value)->toISOString();
}
return Carbon::parse($value)->toISOString();
} catch (\Throwable) {
return null;
}
}
private function error(string $message, int $code = 400): JsonResponse
{
return response()->json(['success' => false, 'message' => $message], $code);
}
private function notFound(string $message): JsonResponse
{
return response()->json(['success' => false, 'message' => $message], 404);
}
}