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
@@ -23,6 +23,7 @@ use App\Support\RenewalPricing;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Support\VatNumbering;
|
||||
|
||||
class OrderQueryService
|
||||
{
|
||||
@@ -113,6 +114,7 @@ class OrderQueryService
|
||||
'invoice.customer',
|
||||
'invoice.payment',
|
||||
'orderItems.product',
|
||||
'invoices.attachments',
|
||||
'customer',
|
||||
'vmWare.ipAddress',
|
||||
'vmWare.os',
|
||||
@@ -122,6 +124,18 @@ class OrderQueryService
|
||||
->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()
|
||||
@@ -171,6 +185,36 @@ class OrderQueryService
|
||||
}
|
||||
|
||||
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'])
|
||||
@@ -188,25 +232,23 @@ class OrderQueryService
|
||||
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);
|
||||
$product = $service->product ?? $orderItem->product;
|
||||
$option = RenewalPricing::option($product, $months);
|
||||
|
||||
if ($option === null) {
|
||||
$validMonths = array_column(RenewalPricing::options($product), 'months');
|
||||
|
||||
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.',
|
||||
'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 = intdiv($months, $cycleMonths);
|
||||
|
||||
$currentPrice = $this->productRepo->getPricing((int) ($service->product_id ?: $orderItem->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;
|
||||
$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();
|
||||
@@ -219,6 +261,11 @@ class OrderQueryService
|
||||
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) {
|
||||
@@ -230,11 +277,16 @@ class OrderQueryService
|
||||
];
|
||||
}
|
||||
|
||||
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->fresh()->expires_at?->toISOString(),
|
||||
'expires_at' => Service::find($serviceId)?->expires_at?->toISOString(),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -257,7 +309,7 @@ class OrderQueryService
|
||||
return (int) $invoice->invoiceItems()->where('type', 'renewal')->value('months');
|
||||
}
|
||||
|
||||
private function issueRenewalInvoice(Service $service, OrderItem $orderItem, int $renewalPrice, int $cycles, int $months, ?Invoice $draft): Invoice
|
||||
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();
|
||||
@@ -295,6 +347,7 @@ class OrderQueryService
|
||||
InvoiceItem::create($addonItem + ['invoice_id' => $invoice->id]);
|
||||
}
|
||||
|
||||
VatNumbering::settle($invoice);
|
||||
$service->update(['invoice_id' => $invoice->id]);
|
||||
|
||||
return $invoice;
|
||||
|
||||
Reference in New Issue
Block a user