Files
portal-api/app/Services/Api/Order/OrderQueryService.php
T
2026-09-27 11:42:40 +07:00

307 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Api\Order;
use App\DTOs\Order\ClientOrderFilters;
use App\Enums\Invoice\InvoiceStatus;
use App\Helpers\OrderHelper;
use App\Jobs\VirtualMachine\Action\VMJobs;
use App\Jobs\VirtualMachine\Provision\RetryProvisioningOrchestrator;
use App\Models\Invoice;
use App\Models\ModuleVmware;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Service;
use App\Models\VirtualMachines;
use App\Repositories\Api\Order\OrderRepository;
use App\Repositories\Api\Service\ServiceRepositoryInterface;
use App\Support\RenewalPricing;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class OrderQueryService
{
public function __construct(
private readonly OrderRepository $orderRepository,
private readonly ServiceRepositoryInterface $productRepo,
) {
}
public function getOrdersForCustomer(int $customerId, ClientOrderFilters $filters): LengthAwarePaginator
{
return $this->orderRepository->getOrders($customerId, $filters->toArray());
}
public function checkServerName(string $name): array
{
$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();
return [
'available' => ! $exists,
'message' => $exists
? 'Tên server này đã được sử dụng, vui lòng chọn tên khác.'
: null,
];
}
public function findCustomerOrder(int $customerId, int $id): ?Order
{
return Order::query()
->with(['invoice', 'orderItems.product', 'orderItems.service', 'customer'])
->where('customer_id', $customerId)
->where('id', $id)
->first();
}
public function getCustomerInvoices(int $customerId)
{
return Invoice::query()
->with(['order.orderItems.product'])
->where('customer_id', $customerId)
->orderByDesc('created_at')
->get();
}
public function findCustomerInvoice(int $customerId, int $id): ?Invoice
{
return Invoice::query()
->with([
'order.orderItems.product',
'order.vmWare.ipAddress',
'order.vmWare.os',
'customer',
'attachments',
])
->where('customer_id', $customerId)
->where('id', $id)
->first();
}
public function findCustomerInvoiceByNumber(int $customerId, string $invoiceNumber): ?Invoice
{
return Invoice::query()
->with([
'order.orderItems.product',
'order.vmWare.ipAddress',
'order.vmWare.os',
'customer',
'attachments',
'invoiceItems.product',
'services.product',
])
->where('customer_id', $customerId)
->where('invoice_number', $invoiceNumber)
->first();
}
public function findCustomerInvoiceByOrderNum(int $customerId, string $orderNumber): ?Order
{
return Order::query()
->with([
'invoice.invoiceItems',
'invoice.attachments',
'invoice.customer',
'invoice.payment',
'orderItems.product',
'customer',
'vmWare.ipAddress',
'vmWare.os',
])
->where('customer_id', $customerId)
->where('order_num', $orderNumber)
->first();
}
public function getCustomerServices(int $customerId)
{
return Service::query()
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os'])
->where('customer_id', $customerId)
->orderByDesc('created_at')
->get();
}
public function findCustomerService(int $customerId, string $uuid): ?Service
{
return Service::query()
->with(['product', 'addons', 'invoice', 'orderItem', 'vmware.ipAddress', 'vmware.os', 'vmware.vCenter'])
->where('uuid', $uuid)
->where('customer_id', $customerId)
->first();
}
public function canRetryProvisioning(Service $service): bool
{
if ($service->status !== 'pending' || $service->retry_requested_at) {
return false;
}
$paidAt = $service->invoice?->paid_at;
return $paidAt ? Carbon::parse($paidAt)->diffInMinutes(now()) >= 180 : false;
}
public function retryProvisioning(Order $order, int $customerId): array
{
$order = Order::where('id', $order->id)
->with(['invoice', 'orderItems.service'])
->where('customer_id', $customerId)
->first();
if (! $order) {
return ['ok' => false, 'message' => 'Đơn hàng không tồn tại.', 'code' => 404];
}
if (! $order->invoice || (int) $order->invoice->status !== InvoiceStatus::PAID->value) {
return ['ok' => false, 'message' => 'Đơn hàng chưa được thanh toán.', 'code' => 400];
}
$paidAt = $order->invoice->paid_at ? Carbon::parse($order->invoice->paid_at) : null;
if (! $paidAt || $paidAt->diffInMinutes(now()) < 180) {
return ['ok' => false, 'message' => '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ụ.', 'code' => 400];
}
$service = $order->orderItems->first()?->service;
if (! $service || (int) $service->customer_id !== $customerId) {
return ['ok' => false, 'message' => 'Không tìm thấy dịch vụ hợp lệ để khởi tạo lại.', 'code' => 400];
}
if ($service->status !== 'pending') {
return ['ok' => false, 'message' => 'Dịch vụ đã được khởi tạo hoặc không ở trạng thái chờ.', 'code' => 400];
}
if ($service->retry_requested_at) {
return ['ok' => false, 'message' => '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', 'code' => 400];
}
$service->update(['retry_requested_at' => now()]);
RetryProvisioningOrchestrator::dispatch($order->id, $customerId);
return ['ok' => 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.'];
}
public function renewService(int $serviceId, int $customerId, int $months): array
{
$service = Service::query()
->with(['orderItem.product', 'invoice'])
->where('id', $serviceId)
->where('customer_id', $customerId)
->whereNotIn('status', ['terminated', 'cancelled'])
->first();
if (! $service) {
return ['ok' => false, 'message' => 'Không tìm thấy dịch vụ hoặc dịch vụ đã bị hủy.', 'code' => 404];
}
$orderItem = $service->orderItem;
if (! $orderItem) {
return ['ok' => false, 'message' => 'Không thể xác định giá gia hạn dịch vụ.', 'code' => 400];
}
$cycleMonths = OrderHelper::cycleToMonths($orderItem->billing_cycle);
$validMonths = RenewalPricing::validMonths($orderItem->billing_cycle);
if (! in_array($months, $validMonths, true)) {
return [
'ok' => false,
'message' => 'Số tháng gia hạn không hợp lệ với chu kỳ của dịch vụ này. Vui lòng chọn: ' . implode(', ', $validMonths) . ' tháng.',
'code' => 422,
];
}
$cycles = intdiv($months, $cycleMonths);
// Gia hạn luôn dùng giá sản phẩm hiện tại trong bảng giá, không dùng giá đã
// chốt lúc đặt mua ban đầu — giá cũ chỉ để hiển thị trên đơn hàng/hoá đơn gốc.
$currentPrice = $this->productRepo->getPricing((int) $service->product_id, $orderItem->billing_cycle);
if ($currentPrice === null) {
return ['ok' => false, 'message' => 'Sản phẩm hiện không còn bán, không thể gia hạn.', 'code' => 400];
}
$renewalPrice = (int) $currentPrice * $cycles;
$addonTotal = RenewalPricing::addonTotal($service, $cycles);
$price = $renewalPrice + $addonTotal;
if ($service->invoice_id && Invoice::where('id', $service->invoice_id)
->where('invoice_type', 2)
->whereIn('status', [InvoiceStatus::UNPAID->value, InvoiceStatus::OVERDUE->value])
->exists()) {
$invoice = Invoice::find($service->invoice_id);
return [
'ok' => true,
'message' => 'Dịch vụ đã có hóa đơn gia hạn đang chờ thanh toán.',
'data' => [
'order_id' => $invoice?->order_id,
'invoice_id' => $invoice?->id,
'invoice_number' => $invoice?->invoice_number,
'total' => (float) ($invoice?->total ?? 0),
'status' => $invoice?->status,
'payment_url' => url('/client/invoices/' . $invoice?->invoice_number . '/payment'),
],
];
}
// Gia hạn là một billing cycle mới của CÙNG một Order/Service — không tạo Order mới.
// Order gốc chính là order đã tạo ra service này (qua order_item -> order_id).
$periodStart = $service->expires_at && $service->expires_at->isFuture() ? $service->expires_at->copy() : now();
$periodEnd = $periodStart->copy()->addMonths($months);
$invoice = DB::transaction(function () use ($service, $orderItem, $price, $renewalPrice, $cycles, $months, $periodStart, $periodEnd) {
$invoice = Invoice::create([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'customer_id' => $service->customer_id,
'order_id' => $orderItem->order_id,
'invoice_type' => 2,
'sub_total' => $price,
'discount_amount' => 0,
'tax' => 0,
'total' => $price,
'status' => InvoiceStatus::UNPAID->value,
'due_date' => now()->addDays(7)->toDateString(),
'billing_period_start' => $periodStart->toDateString(),
'billing_period_end' => $periodEnd->toDateString(),
]);
\App\Models\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' => $renewalPrice,
'total_price' => $renewalPrice,
]);
foreach (RenewalPricing::addonInvoiceItems($service, $cycles, $months) as $addonItem) {
\App\Models\InvoiceItem::create($addonItem + ['invoice_id' => $invoice->id]);
}
$service->update(['invoice_id' => $invoice->id]);
return $invoice;
});
return [
'ok' => true,
'message' => 'Đã tạo hóa đơn gia hạn. Vui lòng thanh toán để gia hạn dịch vụ.',
'data' => [
'order_id' => $invoice->order_id,
'invoice_id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'total' => (float) $invoice->total,
'status' => $invoice->status,
'payment_url' => url('/client/invoices/' . $invoice->invoice_number . '/payment'),
],
];
}
}