368 lines
13 KiB
PHP
368 lines
13 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\Provision\RetryProvisioningOrchestrator;
|
|
use App\Models\Customer;
|
|
use App\Models\Invoice;
|
|
use App\Models\InvoiceItem;
|
|
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\OrderRepositoryInterface;
|
|
use App\Repositories\Api\Service\ServiceRepositoryInterface;
|
|
use App\Services\InvoicePaymentService;
|
|
use App\Support\RenewalPricing;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Support\VatNumbering;
|
|
|
|
class OrderQueryService
|
|
{
|
|
public function __construct(
|
|
private readonly OrderRepositoryInterface $orderRepository,
|
|
private readonly ServiceRepositoryInterface $productRepo,
|
|
private readonly InvoicePaymentService $invoicePaymentService,
|
|
) {
|
|
}
|
|
|
|
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',
|
|
'invoices.attachments',
|
|
'customer',
|
|
'vmWare.ipAddress',
|
|
'vmWare.os',
|
|
])
|
|
->where('customer_id', $customerId)
|
|
->where('order_num', $orderNumber)
|
|
->first();
|
|
}
|
|
|
|
public function pickOrderInvoice(Order $order, ?string $invoiceNumber): ?Invoice
|
|
{
|
|
if (! $invoiceNumber) {
|
|
return $order->invoice;
|
|
}
|
|
|
|
return $order->invoices()
|
|
->where('invoice_number', $invoiceNumber)
|
|
->with(['invoiceItems', 'attachments', 'customer', 'payment'])
|
|
->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 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, Customer $customer, int $months): array
|
|
{
|
|
$issued = $this->issueRenewal($serviceId, $customer, $months);
|
|
if (! $issued['ok']) {
|
|
return $issued;
|
|
}
|
|
|
|
return $this->payRenewalWithBalance($issued['invoice'], $customer, $serviceId);
|
|
}
|
|
|
|
public function renewServiceByAdmin(int $serviceId, Customer $customer, int $months, string $payment, int $adminId, string $note = ''): array
|
|
{
|
|
$issued = $this->issueRenewal($serviceId, $customer, $months);
|
|
if (! $issued['ok']) {
|
|
return $issued;
|
|
}
|
|
|
|
if ($payment === 'balance') {
|
|
return $this->payRenewalWithBalance($issued['invoice'], $customer, $serviceId);
|
|
}
|
|
|
|
try {
|
|
$invoice = $this->invoicePaymentService->markPaidByAdmin($issued['invoice'], $adminId, 'admin', null, $note);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return ['ok' => false, 'message' => $e->getMessage(), 'code' => 422];
|
|
}
|
|
|
|
return $this->renewalResult($invoice, $serviceId);
|
|
}
|
|
|
|
private function issueRenewal(int $serviceId, Customer $customer, int $months): array
|
|
{
|
|
$service = Service::query()
|
|
->with(['orderItem.product'])
|
|
->where('id', $serviceId)
|
|
->where('customer_id', $customer->id)
|
|
->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];
|
|
}
|
|
|
|
$product = $service->product ?? $orderItem->product;
|
|
$option = RenewalPricing::option($product, $months);
|
|
|
|
if ($option === null) {
|
|
$validMonths = array_column(RenewalPricing::options($product), 'months');
|
|
|
|
return [
|
|
'ok' => false,
|
|
'message' => $validMonths === []
|
|
? 'Sản phẩm hiện không còn bán, không thể gia hạn.'
|
|
: 'Thời hạn gia hạn không hợp lệ với sản phẩm này. Vui lòng chọn: ' . implode(', ', $validMonths) . ' tháng.',
|
|
'code' => 422,
|
|
];
|
|
}
|
|
|
|
$cycles = $months / OrderHelper::cycleToMonths($orderItem->billing_cycle);
|
|
$renewalPrice = $option['price'];
|
|
|
|
$invoice = DB::transaction(function () use ($service, $orderItem, $renewalPrice, $cycles, $months) {
|
|
$service = Service::query()->whereKey($service->id)->lockForUpdate()->first();
|
|
$pending = $this->pendingRenewalInvoice($service);
|
|
|
|
if ($pending && ((int) $pending->amount_paid > 0 || $this->renewalMonths($pending) === $months)) {
|
|
return $pending;
|
|
}
|
|
|
|
return $this->issueRenewalInvoice($service, $orderItem, $renewalPrice, $cycles, $months, $pending);
|
|
});
|
|
|
|
return ['ok' => true, 'invoice' => $invoice];
|
|
}
|
|
|
|
private function payRenewalWithBalance(Invoice $invoice, Customer $customer, int $serviceId): array
|
|
{
|
|
try {
|
|
$invoice = $this->invoicePaymentService->payWithBalance($invoice, $customer);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return [
|
|
'ok' => false,
|
|
'message' => $e->getMessage() . ' Hóa đơn gia hạn #' . $invoice->invoice_number . ' đang chờ thanh toán.',
|
|
'code' => 422,
|
|
'data' => $this->renewalPayload($invoice),
|
|
];
|
|
}
|
|
|
|
return $this->renewalResult($invoice, $serviceId);
|
|
}
|
|
|
|
private function renewalResult(Invoice $invoice, int $serviceId): array
|
|
{
|
|
return [
|
|
'ok' => true,
|
|
'message' => 'Gia hạn dịch vụ thành công.',
|
|
'data' => $this->renewalPayload($invoice) + [
|
|
'expires_at' => Service::find($serviceId)?->expires_at?->toISOString(),
|
|
],
|
|
];
|
|
}
|
|
|
|
private function pendingRenewalInvoice(Service $service): ?Invoice
|
|
{
|
|
if (! $service->invoice_id) {
|
|
return null;
|
|
}
|
|
|
|
return Invoice::query()
|
|
->where('id', $service->invoice_id)
|
|
->where('invoice_type', 2)
|
|
->whereIn('status', [InvoiceStatus::UNPAID->value, InvoiceStatus::OVERDUE->value])
|
|
->first();
|
|
}
|
|
|
|
private function renewalMonths(Invoice $invoice): int
|
|
{
|
|
return (int) $invoice->invoiceItems()->where('type', 'renewal')->value('months');
|
|
}
|
|
|
|
private function issueRenewalInvoice(Service $service, OrderItem $orderItem, int $renewalPrice, float $cycles, int $months, ?Invoice $draft): Invoice
|
|
{
|
|
$price = $renewalPrice + RenewalPricing::addonTotal($service, $cycles);
|
|
$periodStart = $service->expires_at && $service->expires_at->isFuture() ? $service->expires_at->copy() : now();
|
|
|
|
$invoice = $draft ?? new Invoice([
|
|
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
|
'status' => InvoiceStatus::UNPAID->value,
|
|
]);
|
|
$invoice->invoiceItems()->delete();
|
|
$invoice->fill([
|
|
'customer_id' => $service->customer_id,
|
|
'order_id' => $orderItem->order_id,
|
|
'invoice_type' => 2,
|
|
'sub_total' => $price,
|
|
'discount_amount' => 0,
|
|
'tax' => 0,
|
|
'total' => $price,
|
|
'due_date' => now()->addDays(7)->toDateString(),
|
|
'billing_period_start' => $periodStart->toDateString(),
|
|
'billing_period_end' => $periodStart->copy()->addMonths($months)->toDateString(),
|
|
])->save();
|
|
|
|
InvoiceItem::create([
|
|
'invoice_id' => $invoice->id,
|
|
'product_id' => $service->product_id ?: $orderItem->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) {
|
|
InvoiceItem::create($addonItem + ['invoice_id' => $invoice->id]);
|
|
}
|
|
|
|
VatNumbering::settle($invoice);
|
|
$service->update(['invoice_id' => $invoice->id]);
|
|
|
|
return $invoice;
|
|
}
|
|
|
|
private function renewalPayload(Invoice $invoice): array
|
|
{
|
|
return [
|
|
'order_id' => $invoice->order_id,
|
|
'invoice_id' => $invoice->id,
|
|
'invoice_number' => $invoice->invoice_number,
|
|
'total' => (float) $invoice->total,
|
|
'status' => $invoice->status,
|
|
'payment_url' => $invoice->clientPath(),
|
|
];
|
|
}
|
|
}
|