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

169 lines
6.6 KiB
PHP

<?php
namespace App\Services\Api\Order;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Models\Order;
use App\Models\Product;
use App\Repositories\Api\Invoice\InvoiceRepositoryInterface;
use App\Repositories\Api\Order\OrderRepositoryInterface;
use App\Repositories\Api\Service\ServiceRepository;
use App\Services\InvoicePaymentService;
use App\Services\AdminPaymentNotifier;
use App\Support\AddonQuantityValidator;
use Illuminate\Support\Facades\DB;
class OrderService
{
public function __construct(
protected OrderRepositoryInterface $orderRepo,
protected ServiceRepository $productRepo,
protected InvoiceRepositoryInterface $invoiceRepo,
protected InvoicePaymentService $invoicePaymentService,
protected AdminPaymentNotifier $adminPaymentNotifier,
) {}
// =========================================================================
// create — Step 1: Customer places an order
// =========================================================================
/**
* Validate → calculate price → persist Order + OrderItem + Invoice.
*
* @return array{order: Order, invoice: \App\Models\Invoice}
* @throws \InvalidArgumentException
*/
public function create(array $data, $customer): array
{
if ($this->orderRepo->hasPendingOrder($customer->id)) {
throw new \InvalidArgumentException('Bạn có đơn hàng chưa thanh toán. Vui lòng thanh toán trước khi tạo đơn mới.');
}
$product = $this->productRepo->find($data['product_id']);
if (!$product) {
throw new \InvalidArgumentException('Sản phẩm không tồn tại.');
}
$basePrice = $this->productRepo->getPricing($product->id, $data['billing_cycle']);
if (is_null($basePrice)) {
throw new \InvalidArgumentException('Không tìm thấy giá cho chu kỳ thanh toán này.');
}
// Resolve addons → enriched meta with price & subtotal
$addonsMeta = $this->resolveAddons($data['addons'] ?? [], $product);
$addonsPrice = array_sum(array_column($addonsMeta, 'subtotal'));
$total = $basePrice + $addonsPrice;
if ($customer->total < $total) {
throw new \InvalidArgumentException(sprintf(
'Số dư không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
number_format($customer->total),
number_format($total)
));
}
return DB::transaction(function () use ($data, $customer, $product, $basePrice, $total, $addonsMeta) {
$order = $this->orderRepo->create([
'order_num' => OrderHelper::generateOrderNumber(),
'customer_id' => $customer->id,
'sub_total' => $basePrice,
'discount_amount' => 0,
'total' => $total,
'status' => OrderStatus::PENDING->value,
'client_ip' => request()?->ip(),
]);
$this->orderRepo->createItem($order->id, [
'product_id' => $product->id,
'billing_cycle' => $data['billing_cycle'],
'months' => OrderHelper::cycleToMonths($data['billing_cycle']),
'server_name' => $data['server_name'] ?? null,
'os_id' => $data['os_id'] ?? null,
'quantity' => 1,
'price' => $basePrice,
'addons' => $addonsMeta, // serialised to addons_meta JSON
]);
// Reload items so invoice repo can iterate them
$order->load('orderItems.product');
$invoice = $this->invoiceRepo->createFromOrder($order);
return ['order' => $order, 'invoice' => $invoice];
});
}
// =========================================================================
// pay — Step 2: Customer pays the pending order
// =========================================================================
/**
* Debit customer balance → mark Order+Invoice paid → provision Service records.
*
* @throws \InvalidArgumentException
*/
public function pay(Order $order, string $method): Order
{
if (in_array($order->status, [OrderStatus::ACTIVE->value, OrderStatus::PROCESSING->value], true)) {
throw new \InvalidArgumentException('Đơn hàng đã được thanh toán hoặc đang xử lý.');
}
$processedOrder = DB::transaction(function () use ($order, $method) {
$customer = $order->customer;
if ($customer->total < $order->total) {
throw new \InvalidArgumentException(sprintf(
'Số dư không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
number_format($customer->total),
number_format($order->total)
));
}
$invoice = $order->invoice;
if (! $invoice) {
throw new \InvalidArgumentException('Không tìm thấy hóa đơn của đơn hàng.');
}
if ((int) $invoice->status === InvoiceStatus::PAID->value) {
throw new \InvalidArgumentException('Đơn hàng đã được thanh toán hoặc đang xử lý.');
}
if ($order->status === OrderStatus::CANCELLED->value) {
$order->update(['status' => OrderStatus::PENDING->value]);
}
$paidInvoice = $this->invoicePaymentService->payWithBalance($invoice, $customer, $method);
$processedOrder = $order->fresh(['orderItems', 'invoice', 'customer']);
return $processedOrder;
});
$this->adminPaymentNotifier->notify($processedOrder->invoice, (int) $processedOrder->total, 'order');
return $processedOrder;
}
// =========================================================================
// Helpers
// =========================================================================
/**
* Enrich raw addon entries with resolved price & subtotal.
*
* Input: [['addon_id' => 1, 'qty' => 2], ...]
* Output: [['addon_id' => 1, 'qty' => 2, 'price' => 50000, 'subtotal' => 100000], ...]
*
* @throws \Illuminate\Validation\ValidationException
*/
private function resolveAddons(array $raw, Product $product): array
{
$activeAddons = $product->addons()->where('status', true)->get()->keyBy('id');
return AddonQuantityValidator::resolve($raw, $activeAddons);
}
}