feat(billing): renewals, coupons, VAT numbering, billing cycles and invoice payment tracking
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ea9a53f742
commit
25f23d541e
@@ -12,6 +12,7 @@ use App\Repositories\Api\Order\OrderRepositoryInterface;
|
||||
use App\Repositories\Api\Service\ServiceRepository;
|
||||
use App\Services\InvoicePaymentService;
|
||||
use App\Services\AdminPaymentNotifier;
|
||||
use App\Services\Coupon\CouponService;
|
||||
use App\Support\AddonQuantityValidator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -23,6 +24,7 @@ class OrderService
|
||||
protected InvoiceRepositoryInterface $invoiceRepo,
|
||||
protected InvoicePaymentService $invoicePaymentService,
|
||||
protected AdminPaymentNotifier $adminPaymentNotifier,
|
||||
protected CouponService $couponService,
|
||||
) {}
|
||||
|
||||
// =========================================================================
|
||||
@@ -41,40 +43,43 @@ class OrderService
|
||||
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.');
|
||||
}
|
||||
['product' => $product, 'base_price' => $basePrice, 'addons_meta' => $addonsMeta, 'sub_total' => $subTotal] = $this->quote($data);
|
||||
$couponCode = $data['coupon_code'] ?? null;
|
||||
|
||||
$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.');
|
||||
}
|
||||
$expectedTotal = $couponCode
|
||||
? $subTotal - $this->couponService->preview($couponCode, $customer, $product, $data['billing_cycle'], $subTotal)['discount']
|
||||
: $subTotal;
|
||||
|
||||
// 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) {
|
||||
if ($customer->total < $expectedTotal) {
|
||||
throw new \InvalidArgumentException(sprintf(
|
||||
'Số dư không đủ. Hiện có: %s VNĐ, cần: %s VNĐ.',
|
||||
number_format($customer->total),
|
||||
number_format($total)
|
||||
number_format($expectedTotal)
|
||||
));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($data, $customer, $product, $basePrice, $total, $addonsMeta) {
|
||||
return DB::transaction(function () use ($data, $customer, $product, $basePrice, $subTotal, $addonsMeta, $couponCode) {
|
||||
$applied = $couponCode
|
||||
? $this->couponService->lockAndApply($couponCode, $customer, $product, $data['billing_cycle'], $subTotal)
|
||||
: null;
|
||||
$discount = $applied['discount'] ?? 0;
|
||||
|
||||
$order = $this->orderRepo->create([
|
||||
'order_num' => OrderHelper::generateOrderNumber(),
|
||||
'customer_id' => $customer->id,
|
||||
'sub_total' => $basePrice,
|
||||
'discount_amount' => 0,
|
||||
'total' => $total,
|
||||
'sub_total' => $subTotal,
|
||||
'discount_amount' => $discount,
|
||||
'coupon_id' => $applied['coupon']->id ?? null,
|
||||
'coupon_code' => $applied['coupon']->code ?? null,
|
||||
'total' => $subTotal - $discount,
|
||||
'status' => OrderStatus::PENDING->value,
|
||||
'client_ip' => request()?->ip(),
|
||||
]);
|
||||
|
||||
if ($applied) {
|
||||
$this->couponService->record($applied['coupon'], $order, $discount);
|
||||
}
|
||||
|
||||
$this->orderRepo->createItem($order->id, [
|
||||
'product_id' => $product->id,
|
||||
'billing_cycle' => $data['billing_cycle'],
|
||||
@@ -95,6 +100,34 @@ class OrderService
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Giá niêm yết (chu kỳ + addon) tính phía server; nguồn duy nhất cho create() và preview mã giảm giá.
|
||||
*
|
||||
* @return array{product: Product, base_price: int, addons_meta: array, sub_total: int}
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function quote(array $data): array
|
||||
{
|
||||
$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.');
|
||||
}
|
||||
|
||||
$addonsMeta = $this->resolveAddons($data['addons'] ?? [], $product);
|
||||
|
||||
return [
|
||||
'product' => $product,
|
||||
'base_price' => $basePrice,
|
||||
'addons_meta' => $addonsMeta,
|
||||
'sub_total' => $basePrice + array_sum(array_column($addonsMeta, 'subtotal')),
|
||||
];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// pay — Step 2: Customer pays the pending order
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user