Files
portal-api/app/Services/OrderService.php
T

250 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Enums\Product\ProductBillingCycle;
use App\Enums\Product\ProductTypeEnums;
use App\Http\Helpers\Helper;
use App\Jobs\Hosting\CreateHostingJobs;
use App\Jobs\Mail\Customer\OrderCreatedJob;
use App\Jobs\VirtualMachine\Create\CreateVirtualMachineJob;
use App\Models\Customer;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Payment;
use App\Repositories\Invoice\InvoiceRepositoryInterface;
use App\Repositories\InvoiceItem\InvoiceItemRepositoryInterface;
use App\Repositories\Module\Hosting\HostingRepositoryInterface as ModuleHostingRepositoryInterface;
use App\Repositories\Module\Vmware\VmwareRepositoryInterface;
use App\Repositories\Order\OrderRepositoryInterface;
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class OrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository,
private readonly InvoiceRepositoryInterface $invoiceRepository,
private readonly InvoiceItemRepositoryInterface $invoiceItemRepository,
private readonly ModuleHostingRepositoryInterface $moduleHostingRepository,
private readonly VmwareRepositoryInterface $moduleVmwareRepository,
) {}
public function createOrder(array $data): Order
{
$customer = Customer::find($data['customer_id']);
if (!$customer || (int) $customer->status !== 1) {
throw new \InvalidArgumentException('Tài khoản khách hàng không hoạt động. Không thể tạo đơn hàng.');
}
$paymentRecord = Payment::find((int) $data['payment_method']);
$isBalancePayment = $paymentRecord && (int) $paymentRecord->type === 0;
// Balance payment → auto-confirm order & invoice
$effectiveStatus = $isBalancePayment ? 1 : (int) $data['status'];
[$productItems, $relIdData, $total] = $this->buildProductItems($data);
$orderTotal = (int) ($total + ($total * (10 / 100)));
$invoiceData = [
'invoice_number' => Helper::generateUniqueCode(),
'due_date' => Carbon::today()->toDateString(),
'paid_at' => $effectiveStatus === 1 ? Carbon::now() : null,
'sub_total' => $total,
'tax' => $total * (10 / 100),
'total' => $orderTotal,
'payment_method' => $paymentRecord?->id,
'customer_id' => $data['customer_id'],
'status' => $effectiveStatus,
];
$orderData = [
'order_num' => Helper::generateUniqueCode(),
'customer_id' => $data['customer_id'],
'discount_id' => $data['discount_id'] ?? null,
'sub_total' => $total,
'total' => $orderTotal,
'payment_method' => $paymentRecord?->code ?? null,
'payment_proof' => $data['payment_proof'] ?? null,
'user_id' => Auth::guard('admin')->id(),
'status' => $effectiveStatus,
'client_ip' => request()->getClientIp(),
];
$order = DB::transaction(function () use ($invoiceData, $orderData, $productItems, $relIdData, $data, $orderTotal, $isBalancePayment) {
$invoice = $this->invoiceRepository->create($invoiceData);
$productItems = array_map(fn ($item) => array_merge($item, ['invoice_id' => $invoice->id]), $productItems);
if ($data['create_invoice'] ?? false) {
$orderData['invoice_id'] = $invoice->id;
}
$order = $this->orderRepository->create($orderData);
$invoice->update(['order_id' => $order->id]);
$relIdData = array_map(function ($item) use ($order) {
$item['data']['order_id'] = $order->id;
return $item;
}, $relIdData);
$this->createModules($relIdData);
$invoiceItemData = array_map(fn ($item) => [
'invoice_id' => $item['invoice_id'],
'product_id' => $item['product_id'],
'type' => 'service',
'months' => $item['months'],
'quantity' => $item['quantity'],
'unit_price' => $item['price'],
'total_price' => $item['price'] * $item['quantity'],
], $productItems);
$this->invoiceItemRepository->insert($invoiceItemData);
$now = now();
OrderItem::insert(array_map(fn ($item) => [
'order_id' => $order->id,
'product_id' => $item['product_id'],
'billing_cycle' => $item['billing_cycle'],
'months' => $item['months'],
'os_id' => $item['os_id'],
'quantity' => $item['quantity'],
'server_name' => $item['server_name'],
'price' => $item['price'],
'created_at' => $now,
'updated_at' => $now,
], $productItems));
$vmTypes = [ProductTypeEnums::VPS, ProductTypeEnums::CLOUD];
foreach (OrderItem::where('order_id', $order->id)->get() as $orderItem) {
$type = ProductTypeEnums::tryFrom($orderItem->product?->category?->type ?? '');
if (in_array($type, $vmTypes)) {
CreateVirtualMachineJob::dispatch($orderItem)->delay(now()->addSeconds(10));
}
}
if ($isBalancePayment) {
$customer = Customer::lockForUpdate()->find($data['customer_id']);
if ($customer->total < $orderTotal) {
throw new \Exception('Số dư tài khoản không đủ để thanh toán đơn hàng.');
}
$customer->decrement('total', $orderTotal);
}
return $order;
});
OrderCreatedJob::dispatch([
'customer_email' => $customer->email,
'client_name' => $customer->fullname,
'order_num' => $orderData['order_num'],
'invoice_num' => $invoiceData['invoice_number'],
'order_total' => number_format($orderTotal, 0, ',', '.'),
'company_name' => setting('general.company_name') ?: config('app.name'),
'company_logo' => setting('general.company_logo') ?: '',
]);
return $order;
}
private function buildProductItems(array $data): array
{
$total = 0;
$productItems = [];
$relIdData = [];
foreach ($data['product'] as $key => $item) {
$product = \App\Models\Product::with('category')->find($item['product_id']);
if (!$product) {
throw new \InvalidArgumentException("Sản phẩm không tồn tại (ID: {$item['product_id']}).");
}
$categoryType = ProductTypeEnums::tryFrom($product->category?->type ?? '');
$moduleType = match ($categoryType) {
ProductTypeEnums::HOSTING => 3,
ProductTypeEnums::VPS, ProductTypeEnums::CLOUD => 4,
default => 0,
};
$billingDetails = $this->calculateBillingDetails($item['billing']);
$priceSelect = $item['price_custom'] ?? $product->price_sale ?? $product->price;
$billingCycle = ProductBillingCycle::fromLegacyCode((int) $item['billing']);
$generalCode = Helper::generateUniqueCode();
$productItems[$key] = [
'product_id' => $product->id,
'product_type' => $categoryType?->value,
'months' => $billingCycle->months(),
'billing_cycle' => $billingCycle->value,
'os_id' => $item['os_id'] ?? null,
'quantity' => (int) ($item['quantity'] ?? 1),
'server_name' => $item['server_name'] ?? $item['domain'] ?? null,
'price' => (int) $priceSelect,
'invoice_type' => 1,
'code' => $generalCode,
];
$relIdData[$key] = [
'type' => $moduleType,
'code' => $generalCode,
'data' => [
'customer_id' => $data['customer_id'],
'status' => 0,
'name' => $item['server_name'] ?? $item['domain'] ?? $product->name,
'first_payment' => $priceSelect * $billingDetails['billing'],
'next_payment' => ($item['price_custom'] ?? $product->price) * $billingDetails['billing'],
...$billingDetails,
],
];
if ($item['quantity'] > 1) {
for ($i = 1; $i < $item['quantity']; $i++) {
$newCode = Helper::generateUniqueCode();
$productItems[] = array_merge($productItems[$key], ['code' => $newCode]);
$relIdData[] = array_merge($relIdData[$key], ['code' => $newCode]);
}
}
$total += $item['quantity'] * ($item['price_custom'] ?? ($product->price_sale ?? $product->price)) * $billingDetails['billing'];
}
return [$productItems, $relIdData, $total];
}
private function createModules(array $relIdData): void
{
foreach ($relIdData as $item) {
match ($item['type']) {
3 => $this->moduleHostingRepository->create($item['data']),
// type=4 (VPS): không pre-create ở đây.
// CreateVirtualMachineJob sẽ tạo module_vmware sau khi VM provisioned xong.
default => null,
};
}
}
private function calculateBillingDetails(int|string $billing): array
{
$months = match ((int) $billing) {
2 => 1,
3 => 3,
4 => 6,
5 => 12,
default => 0,
};
return [
'next_due_date' => $months ? Carbon::today()->addMonths($months)->timestamp : null,
'next_invoice' => $months ? Carbon::today()->addMonths($months)->timestamp : null,
'billing' => $months ?: 1,
];
}
}