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:
baodev0
2026-10-19 16:37:56 +07:00
co-authored by Claude Sonnet 5.5
parent ea9a53f742
commit 25f23d541e
134 changed files with 4926 additions and 185 deletions
@@ -2,6 +2,7 @@
namespace App\Http\Resources\Api\Admin;
use App\Support\Billing\BillingDuplicateDetector;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -12,6 +13,7 @@ class InvoiceResource extends JsonResource
return [
'id' => $this->id,
'invoice_number' => $this->invoice_number,
'duplicate' => BillingDuplicateDetector::forInvoice($this->id),
'order_num' => $this->whenLoaded('order', fn() => $this->order?->order_num),
'order_items' => $this->when(
$this->relationLoaded('order') && $this->order?->relationLoaded('orderItems'),
@@ -25,6 +27,7 @@ class InvoiceResource extends JsonResource
]),
'sub_total' => $this->sub_total,
'discount_amount' => $this->discount_amount,
'coupon_code' => $this->coupon_code,
'tax' => $this->tax,
'total' => $this->total,
// Công nợ: đã thu bao nhiêu / còn thiếu bao nhiêu. Kế toán cần thấy ngay trên
@@ -2,6 +2,7 @@
namespace App\Http\Resources\Api\Admin;
use App\Support\Billing\BillingDuplicateDetector;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -12,6 +13,7 @@ class OrderResource extends JsonResource
return [
'id' => $this->id,
'order_num' => $this->order_num,
'duplicate' => BillingDuplicateDetector::forOrder($this->id),
'customer' => $this->whenLoaded('customer', fn() => [
'id' => $this->customer->id,
'uuid' => $this->customer->uuid,
@@ -95,6 +97,7 @@ class OrderResource extends JsonResource
])),
'sub_total' => $this->sub_total,
'discount_amount' => $this->discount_amount,
'coupon_code' => $this->coupon_code,
'total' => $this->total,
// Order has no payment_id FK — `payment` relation is always null. Fall back to
// the raw payment_method string instead of letting whenLoaded() mask it with null.
@@ -2,6 +2,7 @@
namespace App\Http\Resources\Api\Admin;
use App\Support\BillingCycleCatalog;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -37,6 +38,8 @@ class ProductResource extends JsonResource
'bandwidth' => $this->bandwidth,
'domain_count' => $this->domain_count,
'features' => $this->features,
'short_description' => $this->short_description,
'description' => $this->description,
'setup_fee' => $this->setup_fee,
'min_price' => $minPrice,
'billing_type' => $this->whenLoaded('pricings', function () {
@@ -52,6 +55,9 @@ class ProductResource extends JsonResource
'price_annually' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'annually'))->price),
'price_biennially' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'biennially'))->price),
'price_triennially' => $this->whenLoaded('pricings', fn () => optional($this->pricings->firstWhere('billing_cycle', 'triennially'))->price),
'custom_prices' => $this->whenLoaded('pricings', fn () => $this->pricings
->filter(fn ($pricing) => str_starts_with((string) $pricing->billing_cycle, BillingCycleCatalog::CODE_PREFIX))
->mapWithKeys(fn ($pricing) => [$pricing->billing_cycle => $pricing->price])),
'price_onetime' => $this->whenLoaded('pricings', fn () => optional($this->pricings->first(fn ($pricing) => in_array($pricing->billing_cycle, ['onetime', 'one_time'], true)))->price),
'is_active' => $this->is_active,
'created_at' => $this->created_at,