@dainamite/billing 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -19
- package/dist/modules/billing/acl.js +9 -1
- package/dist/modules/billing/acl.js.map +2 -2
- package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
- package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
- package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
- package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
- package/dist/modules/billing/api/accounts/route.js +7 -0
- package/dist/modules/billing/api/accounts/route.js.map +2 -2
- package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
- package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
- package/dist/modules/billing/api/statements/generate/route.js +68 -0
- package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
- package/dist/modules/billing/api/statements/route.js +110 -0
- package/dist/modules/billing/api/statements/route.js.map +7 -0
- package/dist/modules/billing/api/topups/route.js +139 -0
- package/dist/modules/billing/api/topups/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
- package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
- package/dist/modules/billing/api/transactions/route.js +109 -0
- package/dist/modules/billing/api/transactions/route.js.map +7 -0
- package/dist/modules/billing/api/usage/route.js +30 -6
- package/dist/modules/billing/api/usage/route.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
- package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
- package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
- package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
- package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.js +152 -0
- package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.js +147 -0
- package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
- package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
- package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
- package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
- package/dist/modules/billing/commands/accounts.js +26 -1
- package/dist/modules/billing/commands/accounts.js.map +2 -2
- package/dist/modules/billing/commands/index.js +3 -0
- package/dist/modules/billing/commands/index.js.map +2 -2
- package/dist/modules/billing/commands/invoices.js +21 -1
- package/dist/modules/billing/commands/invoices.js.map +2 -2
- package/dist/modules/billing/commands/statements.js +74 -0
- package/dist/modules/billing/commands/statements.js.map +7 -0
- package/dist/modules/billing/commands/topups.js +139 -0
- package/dist/modules/billing/commands/topups.js.map +7 -0
- package/dist/modules/billing/commands/transactions.js +119 -0
- package/dist/modules/billing/commands/transactions.js.map +7 -0
- package/dist/modules/billing/commands/usage.js +72 -0
- package/dist/modules/billing/commands/usage.js.map +2 -2
- package/dist/modules/billing/components/AccountForm.js +53 -1
- package/dist/modules/billing/components/AccountForm.js.map +2 -2
- package/dist/modules/billing/components/PrepaidPanel.js +314 -0
- package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
- package/dist/modules/billing/components/accountNames.js +20 -0
- package/dist/modules/billing/components/accountNames.js.map +7 -0
- package/dist/modules/billing/data/entities.js +290 -0
- package/dist/modules/billing/data/entities.js.map +2 -2
- package/dist/modules/billing/data/entityIds.js +6 -1
- package/dist/modules/billing/data/entityIds.js.map +2 -2
- package/dist/modules/billing/data/validators.js +107 -2
- package/dist/modules/billing/data/validators.js.map +2 -2
- package/dist/modules/billing/events.js +52 -0
- package/dist/modules/billing/events.js.map +2 -2
- package/dist/modules/billing/i18n/en.json +112 -4
- package/dist/modules/billing/i18n/pl.json +112 -4
- package/dist/modules/billing/index.js +1 -1
- package/dist/modules/billing/index.js.map +2 -2
- package/dist/modules/billing/lib/accountStatus.js +106 -0
- package/dist/modules/billing/lib/accountStatus.js.map +7 -0
- package/dist/modules/billing/lib/balanceEvents.js +48 -0
- package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
- package/dist/modules/billing/lib/balanceLedger.js +136 -0
- package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
- package/dist/modules/billing/lib/balanceStatus.js +29 -0
- package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
- package/dist/modules/billing/lib/billRunEngine.js +40 -2
- package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
- package/dist/modules/billing/lib/creditStatus.js +65 -0
- package/dist/modules/billing/lib/creditStatus.js.map +7 -0
- package/dist/modules/billing/lib/money.js +47 -0
- package/dist/modules/billing/lib/money.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConfig.js +45 -0
- package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
- package/dist/modules/billing/lib/prepaidConsume.js +214 -0
- package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
- package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
- package/dist/modules/billing/lib/topupCapture.js +62 -0
- package/dist/modules/billing/lib/topupCapture.js.map +7 -0
- package/dist/modules/billing/lib/topupReceipt.js +97 -0
- package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
- package/dist/modules/billing/lib/topupTransition.js +19 -0
- package/dist/modules/billing/lib/topupTransition.js.map +7 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
- package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
- package/dist/modules/billing/setup.js +32 -1
- package/dist/modules/billing/setup.js.map +2 -2
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
- package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
- package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
- package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
- package/package.json +9 -9
- package/src/modules/billing/__tests__/commands/invoices.test.ts +3 -3
- package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
- package/src/modules/billing/__tests__/commands/transactions.test.ts +107 -0
- package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
- package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
- package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
- package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
- package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
- package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
- package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
- package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
- package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
- package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
- package/src/modules/billing/__tests__/setup.test.ts +28 -2
- package/src/modules/billing/acl.ts +8 -0
- package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
- package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
- package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
- package/src/modules/billing/api/accounts/route.ts +7 -0
- package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
- package/src/modules/billing/api/statements/generate/route.ts +78 -0
- package/src/modules/billing/api/statements/route.ts +117 -0
- package/src/modules/billing/api/topups/route.ts +154 -0
- package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
- package/src/modules/billing/api/transactions/route.ts +116 -0
- package/src/modules/billing/api/usage/route.ts +36 -5
- package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
- package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
- package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
- package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
- package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
- package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
- package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
- package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
- package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
- package/src/modules/billing/commands/accounts.ts +42 -1
- package/src/modules/billing/commands/index.ts +3 -0
- package/src/modules/billing/commands/invoices.ts +26 -2
- package/src/modules/billing/commands/statements.ts +101 -0
- package/src/modules/billing/commands/topups.ts +181 -0
- package/src/modules/billing/commands/transactions.ts +152 -0
- package/src/modules/billing/commands/usage.ts +114 -4
- package/src/modules/billing/components/AccountForm.tsx +66 -0
- package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
- package/src/modules/billing/components/accountNames.ts +23 -0
- package/src/modules/billing/data/entities.ts +383 -0
- package/src/modules/billing/data/entityIds.ts +5 -0
- package/src/modules/billing/data/validators.ts +144 -0
- package/src/modules/billing/events.ts +52 -0
- package/src/modules/billing/i18n/en.json +112 -4
- package/src/modules/billing/i18n/pl.json +112 -4
- package/src/modules/billing/index.ts +1 -1
- package/src/modules/billing/lib/accountStatus.ts +155 -0
- package/src/modules/billing/lib/balanceEvents.ts +74 -0
- package/src/modules/billing/lib/balanceLedger.ts +258 -0
- package/src/modules/billing/lib/balanceStatus.ts +78 -0
- package/src/modules/billing/lib/billRunEngine.ts +66 -5
- package/src/modules/billing/lib/creditStatus.ts +153 -0
- package/src/modules/billing/lib/money.ts +73 -0
- package/src/modules/billing/lib/prepaidConfig.ts +58 -0
- package/src/modules/billing/lib/prepaidConsume.ts +299 -0
- package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
- package/src/modules/billing/lib/topupCapture.ts +117 -0
- package/src/modules/billing/lib/topupReceipt.ts +153 -0
- package/src/modules/billing/lib/topupTransition.ts +31 -0
- package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
- package/src/modules/billing/setup.ts +45 -0
- package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
- package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
- package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
- package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
|
@@ -32,6 +32,8 @@ export class BillingAccount {
|
|
|
32
32
|
[OptionalProps]?:
|
|
33
33
|
| 'taxId'
|
|
34
34
|
| 'lastBillDate'
|
|
35
|
+
| 'billingMode'
|
|
36
|
+
| 'creditLimit'
|
|
35
37
|
| 'isActive'
|
|
36
38
|
| 'createdAt'
|
|
37
39
|
| 'updatedAt'
|
|
@@ -94,6 +96,19 @@ export class BillingAccount {
|
|
|
94
96
|
@Property({ name: 'last_bill_date', type: 'date', columnType: 'date', nullable: true })
|
|
95
97
|
lastBillDate?: Date | null
|
|
96
98
|
|
|
99
|
+
// Billing mode — 'postpaid' (default; the shipped Bill Run → draft →
|
|
100
|
+
// post flow) or 'prepaid' (top up a balance, draw it down in real time;
|
|
101
|
+
// SPEC-002). Immutable after create (like currency_code); validated at
|
|
102
|
+
// API ingress. Existing rows default to postpaid — zero behavior change.
|
|
103
|
+
@Property({ name: 'billing_mode', type: 'text', default: 'postpaid' })
|
|
104
|
+
billingMode: string = 'postpaid'
|
|
105
|
+
|
|
106
|
+
// Credit line extended to this customer (both modes). Prepaid: permitted
|
|
107
|
+
// overdraft below zero. Postpaid: max outstanding AR exposure. `0` = no
|
|
108
|
+
// credit. Reported, never enforced by billing (SPEC-002 assumption #11).
|
|
109
|
+
@Property({ name: 'credit_limit', type: 'numeric', columnType: 'numeric(18, 4)', default: '0' })
|
|
110
|
+
creditLimit: string = '0'
|
|
111
|
+
|
|
97
112
|
@Property({ name: 'is_active', type: 'boolean', default: true })
|
|
98
113
|
isActive: boolean = true
|
|
99
114
|
|
|
@@ -422,6 +437,7 @@ export class BillRunOutcome {
|
|
|
422
437
|
| 'errorMessage'
|
|
423
438
|
| 'warnings'
|
|
424
439
|
| 'draftInvoiceId'
|
|
440
|
+
| 'statementId'
|
|
425
441
|
| 'isActive'
|
|
426
442
|
| 'createdAt'
|
|
427
443
|
| 'updatedAt'
|
|
@@ -458,6 +474,12 @@ export class BillRunOutcome {
|
|
|
458
474
|
@Property({ name: 'draft_invoice_id', type: 'uuid', nullable: true })
|
|
459
475
|
draftInvoiceId?: string | null
|
|
460
476
|
|
|
477
|
+
// FK (string) to the BillingStatement produced for a PREPAID account
|
|
478
|
+
// (SPEC-002). Prepaid accounts get a statement instead of a draft invoice
|
|
479
|
+
// at period close, so this is the prepaid analogue of `draft_invoice_id`.
|
|
480
|
+
@Property({ name: 'statement_id', type: 'uuid', nullable: true })
|
|
481
|
+
statementId?: string | null
|
|
482
|
+
|
|
461
483
|
@Property({ name: 'is_active', type: 'boolean', default: true })
|
|
462
484
|
isActive: boolean = true
|
|
463
485
|
|
|
@@ -531,3 +553,364 @@ export class DraftInvoiceEdit {
|
|
|
531
553
|
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
532
554
|
deletedAt?: Date | null
|
|
533
555
|
}
|
|
556
|
+
|
|
557
|
+
// ─── BillingAccountBalance (SPEC-002) ────────────────────────────
|
|
558
|
+
//
|
|
559
|
+
// The denormalized running balance for a prepaid account — exactly one
|
|
560
|
+
// row per prepaid account. It is the O(1) read AND the row the atomic
|
|
561
|
+
// `UPDATE … RETURNING` debit/credit locks, which is why the balance is
|
|
562
|
+
// NOT a column on `billing_accounts`: the high-write balance update never
|
|
563
|
+
// touches the account row that the Bill Run hot-path index sits on.
|
|
564
|
+
//
|
|
565
|
+
// `balance` is the cache; the source of truth is
|
|
566
|
+
// `SUM(billing_account_transactions.amount)`. The reconciliation invariant
|
|
567
|
+
// `balance == SUM(amount) == latest.balance_after` is unit-tested.
|
|
568
|
+
|
|
569
|
+
@Entity({ tableName: 'billing_account_balances' })
|
|
570
|
+
@Unique({
|
|
571
|
+
name: 'billing_account_balances_account_unique',
|
|
572
|
+
properties: ['tenantId', 'billAccountId'],
|
|
573
|
+
})
|
|
574
|
+
@Index({
|
|
575
|
+
name: 'billing_account_balances_balance_idx',
|
|
576
|
+
properties: ['organizationId', 'tenantId', 'balance'],
|
|
577
|
+
})
|
|
578
|
+
export class BillingAccountBalance {
|
|
579
|
+
[OptionalProps]?:
|
|
580
|
+
| 'lowBalanceThreshold'
|
|
581
|
+
| 'lastMovementAt'
|
|
582
|
+
| 'createdAt'
|
|
583
|
+
| 'updatedAt'
|
|
584
|
+
|
|
585
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
586
|
+
id!: string
|
|
587
|
+
|
|
588
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
589
|
+
organizationId!: string
|
|
590
|
+
|
|
591
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
592
|
+
tenantId!: string
|
|
593
|
+
|
|
594
|
+
// Unique FK (string) to BillingAccount.id — one balance per account.
|
|
595
|
+
@Property({ name: 'bill_account_id', type: 'uuid' })
|
|
596
|
+
billAccountId!: string
|
|
597
|
+
|
|
598
|
+
// Denormalized from the account so the balance row is self-contained.
|
|
599
|
+
@Property({ name: 'currency_code', type: 'text' })
|
|
600
|
+
currencyCode!: string
|
|
601
|
+
|
|
602
|
+
// Running balance. May be NEGATIVE (usage is never rejected). Maintained
|
|
603
|
+
// exclusively by the atomic `UPDATE … RETURNING` in lib/balanceLedger.ts.
|
|
604
|
+
@Property({ name: 'balance', type: 'numeric', columnType: 'numeric(18, 4)', default: '0' })
|
|
605
|
+
balance: string = '0'
|
|
606
|
+
|
|
607
|
+
// When `balance <= threshold` → status `low` + `billing.balance.low`.
|
|
608
|
+
// Null = use the tenant config default
|
|
609
|
+
// (`billing.prepaid.low_balance_threshold_default`).
|
|
610
|
+
@Property({
|
|
611
|
+
name: 'low_balance_threshold',
|
|
612
|
+
type: 'numeric',
|
|
613
|
+
columnType: 'numeric(18, 4)',
|
|
614
|
+
nullable: true,
|
|
615
|
+
})
|
|
616
|
+
lowBalanceThreshold?: string | null
|
|
617
|
+
|
|
618
|
+
// Timestamp of the most recent transaction — a cheap "is this account
|
|
619
|
+
// active?" signal.
|
|
620
|
+
@Property({ name: 'last_movement_at', type: Date, nullable: true })
|
|
621
|
+
lastMovementAt?: Date | null
|
|
622
|
+
|
|
623
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
624
|
+
createdAt: Date = new Date()
|
|
625
|
+
|
|
626
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
627
|
+
updatedAt: Date = new Date()
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ─── BillingAccountTransaction (SPEC-002, append-only) ───────────
|
|
631
|
+
//
|
|
632
|
+
// The source of truth for every balance movement. Append-only — pattern
|
|
633
|
+
// mirrors DraftInvoiceEdit (no `updated_at` / `is_active`; `deleted_at`
|
|
634
|
+
// reserved for compliance erasure only). `balance = SUM(amount)`.
|
|
635
|
+
//
|
|
636
|
+
// The unique `(tenant_id, bill_account_id, source_ref)` is the idempotency
|
|
637
|
+
// gate: a deduplicated usage upload or a retried recurring charge can never
|
|
638
|
+
// double-debit (Postgres treats NULL source_ref as distinct — manual
|
|
639
|
+
// adjustments without a key always insert).
|
|
640
|
+
|
|
641
|
+
@Entity({ tableName: 'billing_account_transactions' })
|
|
642
|
+
@Unique({
|
|
643
|
+
name: 'billing_account_transactions_source_ref_unique',
|
|
644
|
+
properties: ['tenantId', 'billAccountId', 'sourceRef'],
|
|
645
|
+
})
|
|
646
|
+
@Index({
|
|
647
|
+
name: 'billing_account_transactions_history_idx',
|
|
648
|
+
properties: ['organizationId', 'tenantId', 'billAccountId', 'createdAt'],
|
|
649
|
+
})
|
|
650
|
+
@Index({
|
|
651
|
+
name: 'billing_account_transactions_account_type_idx',
|
|
652
|
+
properties: ['billAccountId', 'type', 'createdAt'],
|
|
653
|
+
})
|
|
654
|
+
export class BillingAccountTransaction {
|
|
655
|
+
[OptionalProps]?:
|
|
656
|
+
| 'usageId'
|
|
657
|
+
| 'billingItemId'
|
|
658
|
+
| 'topupId'
|
|
659
|
+
| 'statementId'
|
|
660
|
+
| 'sourceRef'
|
|
661
|
+
| 'metadata'
|
|
662
|
+
| 'userId'
|
|
663
|
+
| 'createdAt'
|
|
664
|
+
| 'deletedAt'
|
|
665
|
+
|
|
666
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
667
|
+
id!: string
|
|
668
|
+
|
|
669
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
670
|
+
organizationId!: string
|
|
671
|
+
|
|
672
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
673
|
+
tenantId!: string
|
|
674
|
+
|
|
675
|
+
@Property({ name: 'bill_account_id', type: 'uuid' })
|
|
676
|
+
billAccountId!: string
|
|
677
|
+
|
|
678
|
+
// 'topup' | 'usage' | 'recurring' | 'one_time' | 'adjustment' | 'reversal'.
|
|
679
|
+
@Property({ type: 'text' })
|
|
680
|
+
type!: string
|
|
681
|
+
|
|
682
|
+
// SIGNED: `> 0` credit, `< 0` debit. Balance = SUM(amount).
|
|
683
|
+
@Property({ type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
684
|
+
amount!: string
|
|
685
|
+
|
|
686
|
+
@Property({ name: 'currency_code', type: 'text' })
|
|
687
|
+
currencyCode!: string
|
|
688
|
+
|
|
689
|
+
// Running balance AFTER this transaction — snapshotted from the atomic
|
|
690
|
+
// `UPDATE … RETURNING`. Enables audit + fast statement math without
|
|
691
|
+
// re-summing the whole ledger.
|
|
692
|
+
@Property({ name: 'balance_after', type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
693
|
+
balanceAfter!: string
|
|
694
|
+
|
|
695
|
+
// FK (string) to BillingAccountUsage for type='usage'.
|
|
696
|
+
@Property({ name: 'usage_id', type: 'uuid', nullable: true })
|
|
697
|
+
usageId?: string | null
|
|
698
|
+
|
|
699
|
+
// FK (string) to the Billing Item that priced this debit.
|
|
700
|
+
@Property({ name: 'billing_item_id', type: 'uuid', nullable: true })
|
|
701
|
+
billingItemId?: string | null
|
|
702
|
+
|
|
703
|
+
// FK (string) to BillingTopup for type='topup'.
|
|
704
|
+
@Property({ name: 'topup_id', type: 'uuid', nullable: true })
|
|
705
|
+
topupId?: string | null
|
|
706
|
+
|
|
707
|
+
// FK (string) to the BillingStatement a period-close charge was rolled into.
|
|
708
|
+
@Property({ name: 'statement_id', type: 'uuid', nullable: true })
|
|
709
|
+
statementId?: string | null
|
|
710
|
+
|
|
711
|
+
@Property({ type: 'text' })
|
|
712
|
+
description!: string
|
|
713
|
+
|
|
714
|
+
// Idempotency key, unique per (tenant_id, bill_account_id, source_ref).
|
|
715
|
+
// usage debits = the usage record's source_ref; recurring =
|
|
716
|
+
// `recurring-{itemId}-{periodEndISO}`; topup = `topup-{paymentId}`.
|
|
717
|
+
@Property({ name: 'source_ref', type: 'text', nullable: true })
|
|
718
|
+
sourceRef?: string | null
|
|
719
|
+
|
|
720
|
+
// e.g. usage_tier_breakdown, uom_code, bill_period_start/end, gateway ids.
|
|
721
|
+
@Property({ type: 'jsonb', nullable: true })
|
|
722
|
+
metadata?: Record<string, unknown> | null
|
|
723
|
+
|
|
724
|
+
// Operator for manual `adjustment` / `reversal`.
|
|
725
|
+
@Property({ name: 'user_id', type: 'uuid', nullable: true })
|
|
726
|
+
userId?: string | null
|
|
727
|
+
|
|
728
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
729
|
+
createdAt: Date = new Date()
|
|
730
|
+
|
|
731
|
+
// Compliance erasure only.
|
|
732
|
+
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
733
|
+
deletedAt?: Date | null
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ─── BillingTopup (SPEC-002 — the "registered top-up") ───────────
|
|
737
|
+
//
|
|
738
|
+
// One row per top-up attempt with a lifecycle (pending → captured | failed
|
|
739
|
+
// | expired | cancelled), linked to its GatewayTransaction, its balance
|
|
740
|
+
// credit transaction, and its VAT receipt invoice. "The balance went up"
|
|
741
|
+
// is always explainable.
|
|
742
|
+
|
|
743
|
+
@Entity({ tableName: 'billing_topups' })
|
|
744
|
+
@Unique({
|
|
745
|
+
name: 'billing_topups_payment_id_unique',
|
|
746
|
+
properties: ['tenantId', 'paymentId'],
|
|
747
|
+
})
|
|
748
|
+
@Index({
|
|
749
|
+
name: 'billing_topups_account_history_idx',
|
|
750
|
+
properties: ['organizationId', 'tenantId', 'billAccountId', 'createdAt'],
|
|
751
|
+
})
|
|
752
|
+
@Index({
|
|
753
|
+
name: 'billing_topups_status_idx',
|
|
754
|
+
properties: ['tenantId', 'status'],
|
|
755
|
+
})
|
|
756
|
+
export class BillingTopup {
|
|
757
|
+
[OptionalProps]?:
|
|
758
|
+
| 'gatewayTransactionId'
|
|
759
|
+
| 'transactionId'
|
|
760
|
+
| 'receiptInvoiceId'
|
|
761
|
+
| 'sourceRef'
|
|
762
|
+
| 'metadata'
|
|
763
|
+
| 'createdAt'
|
|
764
|
+
| 'updatedAt'
|
|
765
|
+
| 'deletedAt'
|
|
766
|
+
|
|
767
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
768
|
+
id!: string
|
|
769
|
+
|
|
770
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
771
|
+
organizationId!: string
|
|
772
|
+
|
|
773
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
774
|
+
tenantId!: string
|
|
775
|
+
|
|
776
|
+
@Property({ name: 'bill_account_id', type: 'uuid' })
|
|
777
|
+
billAccountId!: string
|
|
778
|
+
|
|
779
|
+
// 'pending' | 'captured' | 'failed' | 'expired' | 'cancelled'.
|
|
780
|
+
@Property({ type: 'text' })
|
|
781
|
+
status!: string
|
|
782
|
+
|
|
783
|
+
@Property({ type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
784
|
+
amount!: string
|
|
785
|
+
|
|
786
|
+
@Property({ name: 'currency_code', type: 'text' })
|
|
787
|
+
currencyCode!: string
|
|
788
|
+
|
|
789
|
+
@Property({ name: 'provider_key', type: 'text' })
|
|
790
|
+
providerKey!: string
|
|
791
|
+
|
|
792
|
+
// The UUID we pass to createPaymentSession and match the captured event
|
|
793
|
+
// on. Unique per tenant.
|
|
794
|
+
@Property({ name: 'payment_id', type: 'uuid' })
|
|
795
|
+
paymentId!: string
|
|
796
|
+
|
|
797
|
+
// FK (string) to payment_gateways.GatewayTransaction once the session is
|
|
798
|
+
// created.
|
|
799
|
+
@Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })
|
|
800
|
+
gatewayTransactionId?: string | null
|
|
801
|
+
|
|
802
|
+
// FK (string) to the credit BillingAccountTransaction written on capture.
|
|
803
|
+
@Property({ name: 'transaction_id', type: 'uuid', nullable: true })
|
|
804
|
+
transactionId?: string | null
|
|
805
|
+
|
|
806
|
+
// FK (string) to the core/sales VAT receipt invoice.
|
|
807
|
+
@Property({ name: 'receipt_invoice_id', type: 'uuid', nullable: true })
|
|
808
|
+
receiptInvoiceId?: string | null
|
|
809
|
+
|
|
810
|
+
// Optional idempotency for the initiating request.
|
|
811
|
+
@Property({ name: 'source_ref', type: 'text', nullable: true })
|
|
812
|
+
sourceRef?: string | null
|
|
813
|
+
|
|
814
|
+
// success_url / cancel_url, provider payload echoes.
|
|
815
|
+
@Property({ type: 'jsonb', nullable: true })
|
|
816
|
+
metadata?: Record<string, unknown> | null
|
|
817
|
+
|
|
818
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
819
|
+
createdAt: Date = new Date()
|
|
820
|
+
|
|
821
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
822
|
+
updatedAt: Date = new Date()
|
|
823
|
+
|
|
824
|
+
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
825
|
+
deletedAt?: Date | null
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// ─── BillingStatement (SPEC-002 — period close) ──────────────────
|
|
829
|
+
//
|
|
830
|
+
// A non-fiscal consumption summary for a prepaid account's bill period.
|
|
831
|
+
// Chains cleanly: each statement's `opening_balance` equals the prior
|
|
832
|
+
// statement's `closing_balance`. Anti-duplicate via the unique
|
|
833
|
+
// (tenant_id, bill_account_id, period_start, period_end).
|
|
834
|
+
|
|
835
|
+
@Entity({ tableName: 'billing_statements' })
|
|
836
|
+
@Unique({
|
|
837
|
+
name: 'billing_statements_period_unique',
|
|
838
|
+
properties: ['tenantId', 'billAccountId', 'periodStart', 'periodEnd'],
|
|
839
|
+
})
|
|
840
|
+
@Index({
|
|
841
|
+
name: 'billing_statements_account_period_idx',
|
|
842
|
+
properties: ['organizationId', 'tenantId', 'billAccountId', 'periodEnd'],
|
|
843
|
+
})
|
|
844
|
+
export class BillingStatement {
|
|
845
|
+
[OptionalProps]?:
|
|
846
|
+
| 'billRunId'
|
|
847
|
+
| 'totalAdjustments'
|
|
848
|
+
| 'createdAt'
|
|
849
|
+
| 'updatedAt'
|
|
850
|
+
|
|
851
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
852
|
+
id!: string
|
|
853
|
+
|
|
854
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
855
|
+
organizationId!: string
|
|
856
|
+
|
|
857
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
858
|
+
tenantId!: string
|
|
859
|
+
|
|
860
|
+
@Property({ name: 'bill_account_id', type: 'uuid' })
|
|
861
|
+
billAccountId!: string
|
|
862
|
+
|
|
863
|
+
// FK (string) to the BillRun that generated it (period close reuses the
|
|
864
|
+
// run engine). Null for a manually-generated statement.
|
|
865
|
+
@Property({ name: 'bill_run_id', type: 'uuid', nullable: true })
|
|
866
|
+
billRunId?: string | null
|
|
867
|
+
|
|
868
|
+
@Property({ name: 'period_start', type: 'date', columnType: 'date' })
|
|
869
|
+
periodStart!: Date
|
|
870
|
+
|
|
871
|
+
@Property({ name: 'period_end', type: 'date', columnType: 'date' })
|
|
872
|
+
periodEnd!: Date
|
|
873
|
+
|
|
874
|
+
@Property({ name: 'currency_code', type: 'text' })
|
|
875
|
+
currencyCode!: string
|
|
876
|
+
|
|
877
|
+
// Balance at period_start (= the prior statement's closing_balance).
|
|
878
|
+
@Property({ name: 'opening_balance', type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
879
|
+
openingBalance!: string
|
|
880
|
+
|
|
881
|
+
// Sum of credits in window.
|
|
882
|
+
@Property({ name: 'total_topups', type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
883
|
+
totalTopups!: string
|
|
884
|
+
|
|
885
|
+
// Sum of metered (real-time) usage debits in window (absolute).
|
|
886
|
+
@Property({ name: 'total_usage', type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
887
|
+
totalUsage!: string
|
|
888
|
+
|
|
889
|
+
// Sum of recurring + one_time charges debited at this close (absolute).
|
|
890
|
+
@Property({ name: 'total_recurring', type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
891
|
+
totalRecurring!: string
|
|
892
|
+
|
|
893
|
+
// Signed sum of manual adjustment + reversal movements in the window. Kept as
|
|
894
|
+
// its own term so the statement stays self-consistent (closing reproduces
|
|
895
|
+
// from the shown components) and the opening→closing chain never drifts.
|
|
896
|
+
@Property({ name: 'total_adjustments', type: 'numeric', columnType: 'numeric(18, 4)', default: '0' })
|
|
897
|
+
totalAdjustments: string = '0'
|
|
898
|
+
|
|
899
|
+
// opening + topups + adjustments − usage − recurring (= the period's net).
|
|
900
|
+
@Property({ name: 'closing_balance', type: 'numeric', columnType: 'numeric(18, 4)' })
|
|
901
|
+
closingBalance!: string
|
|
902
|
+
|
|
903
|
+
// Per billing_item_id: { billing_type, uom_code?, quantity, amount }.
|
|
904
|
+
@Property({ name: 'debit_breakdown', type: 'jsonb' })
|
|
905
|
+
debitBreakdown!: Record<string, unknown>
|
|
906
|
+
|
|
907
|
+
// 'generated' | 'sent'.
|
|
908
|
+
@Property({ type: 'text' })
|
|
909
|
+
status!: string
|
|
910
|
+
|
|
911
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
912
|
+
createdAt: Date = new Date()
|
|
913
|
+
|
|
914
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
915
|
+
updatedAt: Date = new Date()
|
|
916
|
+
}
|
|
@@ -20,6 +20,11 @@ export const billingEntityIds = {
|
|
|
20
20
|
run: 'billing:billing_run',
|
|
21
21
|
runOutcome: 'billing:billing_run_outcome',
|
|
22
22
|
draftInvoiceEdit: 'billing:billing_draft_invoice_edit',
|
|
23
|
+
// SPEC-002 (prepaid balance billing)
|
|
24
|
+
balance: 'billing:billing_account_balance',
|
|
25
|
+
transaction: 'billing:billing_account_transaction',
|
|
26
|
+
topup: 'billing:billing_topup',
|
|
27
|
+
statement: 'billing:billing_statement',
|
|
23
28
|
} as const
|
|
24
29
|
|
|
25
30
|
export type BillingEntityId = (typeof billingEntityIds)[keyof typeof billingEntityIds]
|
|
@@ -18,6 +18,9 @@ export const BILL_RUN_OUTCOME_STATUSES = [
|
|
|
18
18
|
'success',
|
|
19
19
|
'success_with_warnings',
|
|
20
20
|
'skipped_existing_draft',
|
|
21
|
+
// SPEC-002: prepaid period close was skipped because a statement already
|
|
22
|
+
// exists for the window.
|
|
23
|
+
'skipped_existing_statement',
|
|
21
24
|
'failed',
|
|
22
25
|
] as const
|
|
23
26
|
export type BillRunOutcomeStatus = (typeof BILL_RUN_OUTCOME_STATUSES)[number]
|
|
@@ -28,6 +31,41 @@ export type DraftInvoiceEditAction = (typeof DRAFT_INVOICE_EDIT_ACTIONS)[number]
|
|
|
28
31
|
export const INVOICE_STATUSES = ['draft', 'posted', 'paid', 'void'] as const
|
|
29
32
|
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number]
|
|
30
33
|
|
|
34
|
+
// ─── Prepaid (SPEC-002) enums ────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export const BILLING_MODES = ['postpaid', 'prepaid'] as const
|
|
37
|
+
export type BillingMode = (typeof BILLING_MODES)[number]
|
|
38
|
+
|
|
39
|
+
export const TRANSACTION_TYPES = [
|
|
40
|
+
'topup',
|
|
41
|
+
'usage',
|
|
42
|
+
'recurring',
|
|
43
|
+
'one_time',
|
|
44
|
+
'adjustment',
|
|
45
|
+
'reversal',
|
|
46
|
+
] as const
|
|
47
|
+
export type TransactionType = (typeof TRANSACTION_TYPES)[number]
|
|
48
|
+
|
|
49
|
+
export const TOPUP_STATUSES = [
|
|
50
|
+
'pending',
|
|
51
|
+
'captured',
|
|
52
|
+
'failed',
|
|
53
|
+
'expired',
|
|
54
|
+
'cancelled',
|
|
55
|
+
] as const
|
|
56
|
+
export type TopupStatus = (typeof TOPUP_STATUSES)[number]
|
|
57
|
+
|
|
58
|
+
export const STATEMENT_STATUSES = ['generated', 'sent'] as const
|
|
59
|
+
export type StatementStatus = (typeof STATEMENT_STATUSES)[number]
|
|
60
|
+
|
|
61
|
+
// Prepaid funds status (the account's own money).
|
|
62
|
+
export const BALANCE_STATUSES = ['ok', 'low', 'exhausted'] as const
|
|
63
|
+
export type BalanceStatus = (typeof BALANCE_STATUSES)[number]
|
|
64
|
+
|
|
65
|
+
// Credit-line status (extended overdraft / AR exposure — both modes).
|
|
66
|
+
export const CREDIT_STATUSES = ['within_limit', 'near_limit', 'over_limit'] as const
|
|
67
|
+
export type CreditStatus = (typeof CREDIT_STATUSES)[number]
|
|
68
|
+
|
|
31
69
|
// `mon`–`sun` for weekly cycles; 1–28 (as text) for the others.
|
|
32
70
|
// Why text for numeric anchors: the column type stays uniform across cycles
|
|
33
71
|
// and we never do arithmetic on the raw value (the engine parses it on use).
|
|
@@ -213,6 +251,10 @@ const listPaginationBase = {
|
|
|
213
251
|
|
|
214
252
|
// ─── BillingAccount payloads ─────────────────────────────────────
|
|
215
253
|
|
|
254
|
+
// numeric(18,4) money inputs arrive as numbers; the command coerces to a
|
|
255
|
+
// 4dp string at persist time.
|
|
256
|
+
const moneyNonNegative = z.number().finite().nonnegative()
|
|
257
|
+
|
|
216
258
|
export const billingAccountCreateSchema = scopedSchema.extend({
|
|
217
259
|
customerId: z.string().min(1).max(255),
|
|
218
260
|
name: z.string().trim().min(1).max(255),
|
|
@@ -229,6 +271,14 @@ export const billingAccountCreateSchema = scopedSchema.extend({
|
|
|
229
271
|
nextBillDate: z.coerce.date(),
|
|
230
272
|
lastBillDate: z.coerce.date().optional(),
|
|
231
273
|
isActive: z.boolean().optional(),
|
|
274
|
+
// SPEC-002: mode is immutable after create (like currencyCode). Default
|
|
275
|
+
// 'postpaid' so existing callers are unaffected.
|
|
276
|
+
billingMode: z.enum(BILLING_MODES).optional(),
|
|
277
|
+
// Credit line (both modes). 0 = no credit.
|
|
278
|
+
creditLimit: moneyNonNegative.optional(),
|
|
279
|
+
// Initial low-balance threshold for a prepaid account's balance row.
|
|
280
|
+
// Null = resolve the tenant config default at read time.
|
|
281
|
+
lowBalanceThreshold: moneyNonNegative.nullable().optional(),
|
|
232
282
|
})
|
|
233
283
|
|
|
234
284
|
export type BillingAccountCreateInput = z.infer<typeof billingAccountCreateSchema>
|
|
@@ -245,6 +295,13 @@ export const billingAccountUpdateSchema = scopedSchema.extend({
|
|
|
245
295
|
nextBillDate: z.coerce.date().optional(),
|
|
246
296
|
lastBillDate: z.coerce.date().nullable().optional(),
|
|
247
297
|
isActive: z.boolean().optional(),
|
|
298
|
+
// SPEC-002: credit_limit is editable (it's an AR/overdraft policy knob);
|
|
299
|
+
// `billingMode` is intentionally NOT updatable — mode is frozen at create
|
|
300
|
+
// (like currencyCode), no live postpaid↔prepaid switch in v1.
|
|
301
|
+
creditLimit: moneyNonNegative.optional(),
|
|
302
|
+
// Editing the prepaid balance row's low-balance threshold. Null clears it
|
|
303
|
+
// back to the tenant config default.
|
|
304
|
+
lowBalanceThreshold: moneyNonNegative.nullable().optional(),
|
|
248
305
|
// `currencyCode` is intentionally NOT updatable — currency is frozen at
|
|
249
306
|
// account create time so historical invoices keep their currency context.
|
|
250
307
|
// `customerId` is also immutable; create a new account if the customer
|
|
@@ -266,6 +323,7 @@ export const billingAccountListQuerySchema = z
|
|
|
266
323
|
customerId: z.string().optional(),
|
|
267
324
|
currencyCode: z.string().optional(),
|
|
268
325
|
billCycle: z.enum(BILL_CYCLES).optional(),
|
|
326
|
+
billingMode: z.enum(BILLING_MODES).optional(),
|
|
269
327
|
isActive: z.coerce.boolean().optional(),
|
|
270
328
|
search: z.string().optional(),
|
|
271
329
|
})
|
|
@@ -559,4 +617,90 @@ export const billingWipeTestInvoicesSchema = scopedSchema.extend({
|
|
|
559
617
|
|
|
560
618
|
export type BillingWipeTestInvoicesInput = z.infer<typeof billingWipeTestInvoicesSchema>
|
|
561
619
|
|
|
620
|
+
// ─── Prepaid: top-ups (SPEC-002 P2) ──────────────────────────────
|
|
621
|
+
|
|
622
|
+
export const billingTopupCreateSchema = scopedSchema.extend({
|
|
623
|
+
// The route maps the `/accounts/{id}/topups` path param into billAccountId.
|
|
624
|
+
billAccountId: z.string().uuid(),
|
|
625
|
+
amount: z.number().finite().positive(),
|
|
626
|
+
// Optional — defaults to the tenant config `prepaid.topup_provider`.
|
|
627
|
+
providerKey: z.string().trim().min(1).max(64).optional(),
|
|
628
|
+
successUrl: z.string().trim().url().max(2048).optional(),
|
|
629
|
+
cancelUrl: z.string().trim().url().max(2048).optional(),
|
|
630
|
+
// Optional idempotency for the initiating request.
|
|
631
|
+
sourceRef: z.string().trim().min(1).max(255).optional(),
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
export type BillingTopupCreateInput = z.infer<typeof billingTopupCreateSchema>
|
|
635
|
+
|
|
636
|
+
export const billingTopupListQuerySchema = z
|
|
637
|
+
.object({
|
|
638
|
+
...listPaginationBase,
|
|
639
|
+
id: z.string().uuid().optional(),
|
|
640
|
+
billAccountId: z.string().uuid().optional(),
|
|
641
|
+
status: z.enum(TOPUP_STATUSES).optional(),
|
|
642
|
+
search: z.string().optional(),
|
|
643
|
+
})
|
|
644
|
+
.passthrough()
|
|
645
|
+
|
|
646
|
+
export type BillingTopupListQuery = z.infer<typeof billingTopupListQuerySchema>
|
|
647
|
+
|
|
648
|
+
// ─── Prepaid: transactions / manual adjustments (SPEC-002 P1/P5) ──
|
|
649
|
+
|
|
650
|
+
export const billingTransactionAdjustSchema = scopedSchema
|
|
651
|
+
.extend({
|
|
652
|
+
billAccountId: z.string().uuid(),
|
|
653
|
+
// SIGNED: > 0 credit, < 0 debit. Zero is rejected.
|
|
654
|
+
amount: z.number().finite(),
|
|
655
|
+
description: z.string().trim().min(1).max(500),
|
|
656
|
+
sourceRef: z.string().trim().min(1).max(255).optional(),
|
|
657
|
+
})
|
|
658
|
+
.superRefine((val, ctx) => {
|
|
659
|
+
if (val.amount === 0) {
|
|
660
|
+
ctx.addIssue({
|
|
661
|
+
code: z.ZodIssueCode.custom,
|
|
662
|
+
path: ['amount'],
|
|
663
|
+
message: 'amount must be non-zero (positive credit or negative debit)',
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
export type BillingTransactionAdjustInput = z.infer<typeof billingTransactionAdjustSchema>
|
|
669
|
+
|
|
670
|
+
export const billingTransactionListQuerySchema = z
|
|
671
|
+
.object({
|
|
672
|
+
...listPaginationBase,
|
|
673
|
+
id: z.string().uuid().optional(),
|
|
674
|
+
billAccountId: z.string().uuid().optional(),
|
|
675
|
+
type: z.enum(TRANSACTION_TYPES).optional(),
|
|
676
|
+
createdFrom: z.coerce.date().optional(),
|
|
677
|
+
createdTo: z.coerce.date().optional(),
|
|
678
|
+
})
|
|
679
|
+
.passthrough()
|
|
680
|
+
|
|
681
|
+
export type BillingTransactionListQuery = z.infer<typeof billingTransactionListQuerySchema>
|
|
682
|
+
|
|
683
|
+
// ─── Prepaid: statements (SPEC-002 P3) ───────────────────────────
|
|
684
|
+
|
|
685
|
+
export const billingStatementGenerateSchema = scopedSchema.extend({
|
|
686
|
+
billAccountId: z.string().uuid(),
|
|
687
|
+
// "today" the close evaluates the due period against. Defaults to now.
|
|
688
|
+
asOfDate: z.coerce.date().optional(),
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
export type BillingStatementGenerateInput = z.infer<typeof billingStatementGenerateSchema>
|
|
692
|
+
|
|
693
|
+
export const billingStatementListQuerySchema = z
|
|
694
|
+
.object({
|
|
695
|
+
...listPaginationBase,
|
|
696
|
+
id: z.string().uuid().optional(),
|
|
697
|
+
billAccountId: z.string().uuid().optional(),
|
|
698
|
+
status: z.enum(STATEMENT_STATUSES).optional(),
|
|
699
|
+
periodEndFrom: z.coerce.date().optional(),
|
|
700
|
+
periodEndTo: z.coerce.date().optional(),
|
|
701
|
+
})
|
|
702
|
+
.passthrough()
|
|
703
|
+
|
|
704
|
+
export type BillingStatementListQuery = z.infer<typeof billingStatementListQuerySchema>
|
|
705
|
+
|
|
562
706
|
|
|
@@ -34,6 +34,58 @@ const events = [
|
|
|
34
34
|
category: 'lifecycle',
|
|
35
35
|
clientBroadcast: true,
|
|
36
36
|
},
|
|
37
|
+
// ─── Prepaid balance billing (SPEC-002) ──────────────────────
|
|
38
|
+
{
|
|
39
|
+
id: 'billing.balance.topped_up',
|
|
40
|
+
label: 'Balance Topped Up',
|
|
41
|
+
entity: 'balance',
|
|
42
|
+
category: 'lifecycle',
|
|
43
|
+
clientBroadcast: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'billing.balance.low',
|
|
47
|
+
label: 'Balance Low',
|
|
48
|
+
entity: 'balance',
|
|
49
|
+
category: 'lifecycle',
|
|
50
|
+
clientBroadcast: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'billing.balance.exhausted',
|
|
54
|
+
label: 'Balance Exhausted',
|
|
55
|
+
entity: 'balance',
|
|
56
|
+
category: 'lifecycle',
|
|
57
|
+
clientBroadcast: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
// Per-debit signal — useful for a live balance UI but potentially
|
|
61
|
+
// high-volume, so excluded from workflow triggers (see Open Question #2).
|
|
62
|
+
id: 'billing.usage.rated',
|
|
63
|
+
label: 'Usage Rated',
|
|
64
|
+
entity: 'transaction',
|
|
65
|
+
category: 'lifecycle',
|
|
66
|
+
excludeFromTriggers: true,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'billing.statement.generated',
|
|
70
|
+
label: 'Consumption Statement Generated',
|
|
71
|
+
entity: 'statement',
|
|
72
|
+
category: 'lifecycle',
|
|
73
|
+
clientBroadcast: true,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'billing.credit.over_limit',
|
|
77
|
+
label: 'Credit Over Limit',
|
|
78
|
+
entity: 'credit',
|
|
79
|
+
category: 'lifecycle',
|
|
80
|
+
clientBroadcast: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'billing.credit.near_limit',
|
|
84
|
+
label: 'Credit Near Limit',
|
|
85
|
+
entity: 'credit',
|
|
86
|
+
category: 'lifecycle',
|
|
87
|
+
clientBroadcast: true,
|
|
88
|
+
},
|
|
37
89
|
] as const
|
|
38
90
|
|
|
39
91
|
export const eventsConfig = createModuleEvents({
|