@dainamite/billing 0.17.0 → 0.18.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.
Files changed (205) hide show
  1. package/CHANGELOG.md +81 -19
  2. package/dist/modules/billing/acl.js +9 -1
  3. package/dist/modules/billing/acl.js.map +2 -2
  4. package/dist/modules/billing/api/accounts/[id]/balance/route.js +62 -0
  5. package/dist/modules/billing/api/accounts/[id]/balance/route.js.map +7 -0
  6. package/dist/modules/billing/api/accounts/[id]/credit-status/route.js +56 -0
  7. package/dist/modules/billing/api/accounts/[id]/credit-status/route.js.map +7 -0
  8. package/dist/modules/billing/api/accounts/[id]/topups/route.js +36 -0
  9. package/dist/modules/billing/api/accounts/[id]/topups/route.js.map +7 -0
  10. package/dist/modules/billing/api/accounts/route.js +7 -0
  11. package/dist/modules/billing/api/accounts/route.js.map +2 -2
  12. package/dist/modules/billing/api/export/account/[id]/route.js +48 -3
  13. package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
  14. package/dist/modules/billing/api/statements/generate/route.js +68 -0
  15. package/dist/modules/billing/api/statements/generate/route.js.map +7 -0
  16. package/dist/modules/billing/api/statements/route.js +110 -0
  17. package/dist/modules/billing/api/statements/route.js.map +7 -0
  18. package/dist/modules/billing/api/topups/route.js +139 -0
  19. package/dist/modules/billing/api/topups/route.js.map +7 -0
  20. package/dist/modules/billing/api/transactions/adjust/route.js +71 -0
  21. package/dist/modules/billing/api/transactions/adjust/route.js.map +7 -0
  22. package/dist/modules/billing/api/transactions/route.js +109 -0
  23. package/dist/modules/billing/api/transactions/route.js.map +7 -0
  24. package/dist/modules/billing/api/usage/route.js +30 -6
  25. package/dist/modules/billing/api/usage/route.js.map +2 -2
  26. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +16 -1
  27. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
  28. package/dist/modules/billing/backend/billing/accounts/create/page.js +8 -1
  29. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
  30. package/dist/modules/billing/backend/billing/accounts/page.js +26 -1
  31. package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
  32. package/dist/modules/billing/backend/billing/statements/[id]/page.js +129 -0
  33. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +7 -0
  34. package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js +21 -0
  35. package/dist/modules/billing/backend/billing/statements/[id]/page.meta.js.map +7 -0
  36. package/dist/modules/billing/backend/billing/statements/page.js +152 -0
  37. package/dist/modules/billing/backend/billing/statements/page.js.map +7 -0
  38. package/dist/modules/billing/backend/billing/statements/page.meta.js +18 -0
  39. package/dist/modules/billing/backend/billing/statements/page.meta.js.map +7 -0
  40. package/dist/modules/billing/backend/billing/topups/[id]/page.js +90 -0
  41. package/dist/modules/billing/backend/billing/topups/[id]/page.js.map +7 -0
  42. package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js +21 -0
  43. package/dist/modules/billing/backend/billing/topups/[id]/page.meta.js.map +7 -0
  44. package/dist/modules/billing/backend/billing/topups/page.js +147 -0
  45. package/dist/modules/billing/backend/billing/topups/page.js.map +7 -0
  46. package/dist/modules/billing/backend/billing/topups/page.meta.js +18 -0
  47. package/dist/modules/billing/backend/billing/topups/page.meta.js.map +7 -0
  48. package/dist/modules/billing/backend/billing/transactions/[id]/page.js +114 -0
  49. package/dist/modules/billing/backend/billing/transactions/[id]/page.js.map +7 -0
  50. package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js +21 -0
  51. package/dist/modules/billing/backend/billing/transactions/[id]/page.meta.js.map +7 -0
  52. package/dist/modules/billing/backend/billing/transactions/page.js +149 -0
  53. package/dist/modules/billing/backend/billing/transactions/page.js.map +7 -0
  54. package/dist/modules/billing/backend/billing/transactions/page.meta.js +18 -0
  55. package/dist/modules/billing/backend/billing/transactions/page.meta.js.map +7 -0
  56. package/dist/modules/billing/commands/accounts.js +26 -1
  57. package/dist/modules/billing/commands/accounts.js.map +2 -2
  58. package/dist/modules/billing/commands/index.js +3 -0
  59. package/dist/modules/billing/commands/index.js.map +2 -2
  60. package/dist/modules/billing/commands/invoices.js +21 -1
  61. package/dist/modules/billing/commands/invoices.js.map +2 -2
  62. package/dist/modules/billing/commands/statements.js +74 -0
  63. package/dist/modules/billing/commands/statements.js.map +7 -0
  64. package/dist/modules/billing/commands/topups.js +139 -0
  65. package/dist/modules/billing/commands/topups.js.map +7 -0
  66. package/dist/modules/billing/commands/transactions.js +119 -0
  67. package/dist/modules/billing/commands/transactions.js.map +7 -0
  68. package/dist/modules/billing/commands/usage.js +72 -0
  69. package/dist/modules/billing/commands/usage.js.map +2 -2
  70. package/dist/modules/billing/components/AccountForm.js +53 -1
  71. package/dist/modules/billing/components/AccountForm.js.map +2 -2
  72. package/dist/modules/billing/components/PrepaidPanel.js +314 -0
  73. package/dist/modules/billing/components/PrepaidPanel.js.map +7 -0
  74. package/dist/modules/billing/components/accountNames.js +20 -0
  75. package/dist/modules/billing/components/accountNames.js.map +7 -0
  76. package/dist/modules/billing/data/entities.js +290 -0
  77. package/dist/modules/billing/data/entities.js.map +2 -2
  78. package/dist/modules/billing/data/entityIds.js +6 -1
  79. package/dist/modules/billing/data/entityIds.js.map +2 -2
  80. package/dist/modules/billing/data/validators.js +107 -2
  81. package/dist/modules/billing/data/validators.js.map +2 -2
  82. package/dist/modules/billing/events.js +52 -0
  83. package/dist/modules/billing/events.js.map +2 -2
  84. package/dist/modules/billing/i18n/en.json +112 -4
  85. package/dist/modules/billing/i18n/pl.json +112 -4
  86. package/dist/modules/billing/index.js +1 -1
  87. package/dist/modules/billing/index.js.map +2 -2
  88. package/dist/modules/billing/lib/accountStatus.js +106 -0
  89. package/dist/modules/billing/lib/accountStatus.js.map +7 -0
  90. package/dist/modules/billing/lib/balanceEvents.js +48 -0
  91. package/dist/modules/billing/lib/balanceEvents.js.map +7 -0
  92. package/dist/modules/billing/lib/balanceLedger.js +136 -0
  93. package/dist/modules/billing/lib/balanceLedger.js.map +7 -0
  94. package/dist/modules/billing/lib/balanceStatus.js +29 -0
  95. package/dist/modules/billing/lib/balanceStatus.js.map +7 -0
  96. package/dist/modules/billing/lib/billRunEngine.js +40 -2
  97. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  98. package/dist/modules/billing/lib/creditStatus.js +65 -0
  99. package/dist/modules/billing/lib/creditStatus.js.map +7 -0
  100. package/dist/modules/billing/lib/money.js +47 -0
  101. package/dist/modules/billing/lib/money.js.map +7 -0
  102. package/dist/modules/billing/lib/prepaidConfig.js +45 -0
  103. package/dist/modules/billing/lib/prepaidConfig.js.map +7 -0
  104. package/dist/modules/billing/lib/prepaidConsume.js +214 -0
  105. package/dist/modules/billing/lib/prepaidConsume.js.map +7 -0
  106. package/dist/modules/billing/lib/prepaidPeriodClose.js +285 -0
  107. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +7 -0
  108. package/dist/modules/billing/lib/topupCapture.js +62 -0
  109. package/dist/modules/billing/lib/topupCapture.js.map +7 -0
  110. package/dist/modules/billing/lib/topupReceipt.js +97 -0
  111. package/dist/modules/billing/lib/topupReceipt.js.map +7 -0
  112. package/dist/modules/billing/lib/topupTransition.js +19 -0
  113. package/dist/modules/billing/lib/topupTransition.js.map +7 -0
  114. package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js +110 -0
  115. package/dist/modules/billing/migrations/Migration20260619000000_billing_prepaid.js.map +7 -0
  116. package/dist/modules/billing/setup.js +32 -1
  117. package/dist/modules/billing/setup.js.map +2 -2
  118. package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js +26 -0
  119. package/dist/modules/billing/subscribers/prepaid-topup-cancelled.js.map +7 -0
  120. package/dist/modules/billing/subscribers/prepaid-topup-captured.js +87 -0
  121. package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +7 -0
  122. package/dist/modules/billing/subscribers/prepaid-topup-expired.js +26 -0
  123. package/dist/modules/billing/subscribers/prepaid-topup-expired.js.map +7 -0
  124. package/dist/modules/billing/subscribers/prepaid-topup-failed.js +26 -0
  125. package/dist/modules/billing/subscribers/prepaid-topup-failed.js.map +7 -0
  126. package/dist/modules/billing/workers/reconcile-prepaid-balances.js +50 -0
  127. package/dist/modules/billing/workers/reconcile-prepaid-balances.js.map +7 -0
  128. package/package.json +1 -1
  129. package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
  130. package/src/modules/billing/__tests__/commands/transactions.test.ts +84 -0
  131. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
  132. package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
  133. package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
  134. package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
  135. package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
  136. package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
  137. package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
  138. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
  139. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
  140. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
  141. package/src/modules/billing/__tests__/setup.test.ts +28 -2
  142. package/src/modules/billing/acl.ts +8 -0
  143. package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
  144. package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
  145. package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
  146. package/src/modules/billing/api/accounts/route.ts +7 -0
  147. package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
  148. package/src/modules/billing/api/statements/generate/route.ts +78 -0
  149. package/src/modules/billing/api/statements/route.ts +117 -0
  150. package/src/modules/billing/api/topups/route.ts +154 -0
  151. package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
  152. package/src/modules/billing/api/transactions/route.ts +116 -0
  153. package/src/modules/billing/api/usage/route.ts +36 -5
  154. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
  155. package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
  156. package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
  157. package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
  158. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
  159. package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
  160. package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
  161. package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
  162. package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
  163. package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
  164. package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
  165. package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
  166. package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
  167. package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
  168. package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
  169. package/src/modules/billing/commands/accounts.ts +42 -1
  170. package/src/modules/billing/commands/index.ts +3 -0
  171. package/src/modules/billing/commands/invoices.ts +26 -2
  172. package/src/modules/billing/commands/statements.ts +101 -0
  173. package/src/modules/billing/commands/topups.ts +181 -0
  174. package/src/modules/billing/commands/transactions.ts +152 -0
  175. package/src/modules/billing/commands/usage.ts +114 -4
  176. package/src/modules/billing/components/AccountForm.tsx +66 -0
  177. package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
  178. package/src/modules/billing/components/accountNames.ts +23 -0
  179. package/src/modules/billing/data/entities.ts +383 -0
  180. package/src/modules/billing/data/entityIds.ts +5 -0
  181. package/src/modules/billing/data/validators.ts +144 -0
  182. package/src/modules/billing/events.ts +52 -0
  183. package/src/modules/billing/i18n/en.json +112 -4
  184. package/src/modules/billing/i18n/pl.json +112 -4
  185. package/src/modules/billing/index.ts +1 -1
  186. package/src/modules/billing/lib/accountStatus.ts +155 -0
  187. package/src/modules/billing/lib/balanceEvents.ts +74 -0
  188. package/src/modules/billing/lib/balanceLedger.ts +258 -0
  189. package/src/modules/billing/lib/balanceStatus.ts +78 -0
  190. package/src/modules/billing/lib/billRunEngine.ts +66 -5
  191. package/src/modules/billing/lib/creditStatus.ts +153 -0
  192. package/src/modules/billing/lib/money.ts +73 -0
  193. package/src/modules/billing/lib/prepaidConfig.ts +58 -0
  194. package/src/modules/billing/lib/prepaidConsume.ts +299 -0
  195. package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
  196. package/src/modules/billing/lib/topupCapture.ts +117 -0
  197. package/src/modules/billing/lib/topupReceipt.ts +153 -0
  198. package/src/modules/billing/lib/topupTransition.ts +31 -0
  199. package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
  200. package/src/modules/billing/setup.ts +45 -0
  201. package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
  202. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
  203. package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
  204. package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
  205. package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @dainamite/billing
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f8210ac: Prepaid billing mode (SPEC-002 / XD-304). Adds an additive `prepaid` billing
8
+ mode alongside the existing postpaid recurring engine: top-up balance,
9
+ real-time atomic usage debit (never rejects usage), append-only transaction
10
+ ledger, prepaid period-close statements, manual balance adjustments, and the
11
+ prepaid admin UI (balance panel, Transactions / Top-ups / Statements lists +
12
+ detail pages). Postpaid behaviour is unchanged.
13
+
14
+ ## 0.18.0 — Prepaid balance billing (SPEC-002, XD-304, unreleased)
15
+
16
+ Adds a second billing **mode** to the package: `prepaid`. A customer tops up
17
+ a monetary **balance**, then consumes a metered product; each usage upload is
18
+ rated in real time, debited from the balance, and the API returns the
19
+ remaining balance. Usage is **never rejected** — low/exhausted is reported and
20
+ the client throttles itself. Postpaid accounts are completely unaffected.
21
+
22
+ - **Schema (additive).** `billing_accounts.billing_mode` (`postpaid`|`prepaid`,
23
+ immutable) + `credit_limit`. New tables: `billing_account_balances` (1:1
24
+ running-balance cache), `billing_account_transactions` (append-only ledger,
25
+ `balance = SUM(amount)`), `billing_topups` (registered top-up lifecycle),
26
+ `billing_statements` (period close). `billing_run_outcomes.statement_id`.
27
+ - **Real-time consume.** `POST /api/billing/usage` on a prepaid account rates
28
+ synchronously (reusing `lib/usageRater`), debits atomically
29
+ (`UPDATE … RETURNING` + paired transaction), and returns `ratedAmount`,
30
+ `balance`, `balanceStatus`, `creditStatus`, `tierBreakdown`. Idempotent on
31
+ `sourceRef` (no double-debit). `GET …/balance`, `GET /transactions`.
32
+ - **Top-ups.** `POST /api/billing/accounts/{id}/topups` registers a
33
+ `BillingTopup` (pending) and opens a `core/payment_gateways` session; the
34
+ `payment.captured` subscriber idempotently credits the balance, issues a
35
+ **posted+paid VAT receipt** in `core/sales` (VAT at top-up), and flips the
36
+ top-up to captured. Sibling subscribers handle failed/cancelled/expired.
37
+ - **Period close.** The Bill Run still runs for prepaid accounts, charging
38
+ `recurring`/`one_time` items to the **balance** (deterministic `source_ref`,
39
+ `billed_to_date` guard) and rolling them into a non-fiscal
40
+ **`BillingStatement`** instead of a payable invoice.
41
+ - **Credit limit (both modes).** Report-only `creditStatus` + `GET …/credit-status`
42
+ (prepaid `max(0,−balance)`; postpaid Σ posted-unpaid outstanding) +
43
+ `billing.credit.over_limit`. Billing **never blocks**.
44
+ - **Admin UI.** Account Prepaid panel (balance, status, credit, top-up,
45
+ transactions), top-ups + statements list/detail pages, account create/edit
46
+ mode + credit fields; en/pl i18n.
47
+ - **Hardening.** GDPR export extended (balance/transactions/top-ups/statements);
48
+ optional reconciliation worker; manual balance adjustments
49
+ (`POST /transactions/adjust`, audited).
50
+ - **Deviation.** The top-up receipt is created **posted+paid** (not draft) —
51
+ money already received has nothing to review (mirrors the xd-249 deviation
52
+ convention). VAT rate resolves from `prepaid.topup_tax_rate` config (the
53
+ postpaid path resolves no rate today).
54
+
3
55
  ## 0.17.0 — Bill Run + invoices live fixes, CPQ-style list views (unreleased)
4
56
 
5
57
  Continued live-database shakedown — running an actual Bill Run and
@@ -59,7 +111,7 @@ and fixed; covered by new Playwright integration tests.
59
111
  `mapInput` read `raw.id` directly, never found it, and threw
60
112
  "id is required". `mapInput` now unwraps the envelope.
61
113
  - **`?id=` list filter was a no-op.** `GET /api/billing/{accounts,
62
- items,runs}?id=<uuid>` is how the detail pages fetch one record,
114
+ items,runs}?id=<uuid>` is how the detail pages fetch one record,
63
115
  but `buildFilters` never handled `id`, so the endpoint returned an
64
116
  unrelated row. `id` is now an indexed filter on all three list
65
117
  routes (and added to the list query schemas).
@@ -136,14 +188,14 @@ write path.
136
188
  - runs **one** idempotency query over all `source_ref`s in the batch
137
189
  (`sourceRef: { $in }` + `billAccountId: { $in }`) instead of one
138
190
  per item;
139
- - dedups `source_ref`s repeated *within* the same payload (first
191
+ - dedups `source_ref`s repeated _within_ the same payload (first
140
192
  wins — the unique index would reject the rest on flush anyway);
141
193
  - `em.create` + `persist`s every new row, then issues a **single**
142
194
  `em.flush()` for the whole batch;
143
195
  - emits per-entity CRUD side effects (`markOrmEntityChange` +
144
196
  events) only after the flush commits.
145
197
  - Returns `{ created, deduplicated, items: [{ sourceRef, id,
146
- deduplicated }] }` so callers can map results back to inputs.
198
+ deduplicated }] }` so callers can map results back to inputs.
147
199
  - **`POST /api/billing/items/bulk`** — REST surface for the command,
148
200
  gated by `billing.item.manage`, `openApi` documented.
149
201
  - **Validators.** `billingItemBulkEntrySchema` (per-item, no scope,
@@ -176,16 +228,16 @@ on the way through.
176
228
  no limit, materialising every row in JS memory before summing. For
177
229
  a telco account with 1M usage records per cycle this OOMs. New
178
230
  implementation does a single `SELECT uom_code, SUM(quantity),
179
- COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
231
+ COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
180
232
  result set is `O(distinct uom_codes)` (typically 1-20) regardless
181
233
  of record count. Index `(bill_account_id, uom_code,
182
- rated_in_bill_run_id, period_end)` already exists from Phase 0.
234
+ rated_in_bill_run_id, period_end)` already exists from Phase 0.
183
235
  - **Engine rated-marking: id list → predicate update.** The
184
236
  follow-up "mark consumed records" step used to take a `consumedUsageIds`
185
237
  array and issue `UPDATE … WHERE id IN (millions of UUIDs)`. New
186
238
  shape: the runner returns `matchedUoms` (small) and the engine does
187
239
  `UPDATE … WHERE uom_code IN (matchedUoms) AND <account scope>
188
- AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
240
+ AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
189
241
  ever leaves Postgres. Memory-bounded regardless of how many records
190
242
  the predicate touches.
191
243
  - **GDPR export: time window + hard cap.** The
@@ -230,9 +282,9 @@ connector created — without dropping to `curl`.
230
282
  - `usage` (tiered): raw-JSON textarea for the advanced shape.
231
283
  Server validators (Phase 0 Zod) reject malformed structures;
232
284
  the textarea includes the canonical example as placeholder.
233
- Plus optional `subscriptionId` / `subscriptionItemId` external
234
- references. UX matches `AccountForm` (Cmd/Ctrl+Enter submit, Kbd
235
- footer hint, no raw HTML buttons).
285
+ Plus optional `subscriptionId` / `subscriptionItemId` external
286
+ references. UX matches `AccountForm` (Cmd/Ctrl+Enter submit, Kbd
287
+ footer hint, no raw HTML buttons).
236
288
  - `/backend/billing/items` — list with filters (type, account,
237
289
  subscription, active) + per-row "Open" link. **New item** CTA in
238
290
  the page header.
@@ -251,6 +303,7 @@ Validation: yarn build + generate + typecheck + test all green;
251
303
  auto-discovered (3 accounts + 3 items + 2 runs + 2 invoices).
252
304
 
253
305
  Deferred to follow-up:
306
+
254
307
  - Locale files (en, pl) — UI ships with inline English fallbacks.
255
308
  - "Add item" button on the account detail page (uses the deep-link
256
309
  parameter the create page already supports).
@@ -282,6 +335,7 @@ Validation: yarn build + generate + typecheck + test all green; 797
282
335
  repo tests, 0 regressions; all 7 backend pages auto-discovered.
283
336
 
284
337
  Deferred to follow-up:
338
+
285
339
  - Item create / edit pages (items are typically integration-driven —
286
340
  the REST API at `/api/billing/items` covers operator-side needs).
287
341
  - Locale files (en, pl).
@@ -308,7 +362,7 @@ edits no longer require `curl`.
308
362
  test-invoices flow and the line-remove flow, satisfying the
309
363
  UI AGENTS.md MUST rule.
310
364
  - All writes go through the existing `billing.invoices.{edit,add,
311
- remove}_draft_line` commands — every line mutation is still
365
+ remove}_draft_line` commands — every line mutation is still
312
366
  audited via `DraftInvoiceEdit` with before/after snapshots, and
313
367
  invoice totals are recomputed by the engine on each write.
314
368
 
@@ -317,6 +371,7 @@ Validation: yarn build + generate + typecheck + test all green;
317
371
  pages + 1 shared component.
318
372
 
319
373
  Deferred to follow-up:
374
+
320
375
  - Account / item editor pages
321
376
  - Locale files (en, pl)
322
377
 
@@ -348,7 +403,7 @@ to review or post.
348
403
  - **Wipe test invoices for this run** — visible only for
349
404
  `metadata.test_run=true`. Hard-deletes every test invoice from
350
405
  the same `bill_run_id` via `billing.invoices.wipe_test`. `window
351
- .confirm` gate (will move to `ConfirmDialog` in a follow-up to
406
+ .confirm` gate (will move to `ConfirmDialog` in a follow-up to
352
407
  match the OM UX convention).
353
408
 
354
409
  Sidebar entry under the `Billing` group at order 30.
@@ -359,6 +414,7 @@ add-line, edit-line, remove-line, invoices list, invoices detail);
359
414
  5 backend pages.
360
415
 
361
416
  Deferred to follow-up:
417
+
362
418
  - Inline line edit / add / remove dialogs on the detail page (REST
363
419
  endpoints already exist from Phase 4b).
364
420
  - ConfirmDialog instead of `window.confirm` for the wipe action.
@@ -399,6 +455,7 @@ Validation: yarn build + generate + typecheck + test all green;
399
455
  `.mercato/generated/backend-routes.generated.ts`.
400
456
 
401
457
  Deferred to a follow-up:
458
+
402
459
  - Account / item / usage / draft-invoice editor pages
403
460
  - Triggers (manual Bill Run / dry-run preview) as buttons in the
404
461
  list page
@@ -423,7 +480,7 @@ fully usable from the REST surface today.
423
480
  pre-removal snapshot preserved in `before_json`. `after_json`
424
481
  and `invoice_line_id` are `null`.
425
482
  - All three refuse non-draft invoices with `HTTP 409 +
426
- billing.invoice.not_draft` and require the
483
+ billing.invoice.not_draft` and require the
427
484
  `billing.invoice.edit_draft` feature.
428
485
  - **Test-invoice wipe command** `billing.invoices.wipe_test`:
429
486
  hard-deletes invoices flagged `metadata.test_run=true`, optionally
@@ -449,6 +506,7 @@ fully usable from the REST surface today.
449
506
  `billRunId`).
450
507
 
451
508
  Deferred to **Phase 4c** (UI dedicated effort):
509
+
452
510
  - Backend pages (DataTable + CrudForm) for accounts / items / usage /
453
511
  bill runs / draft invoice review.
454
512
  - Sidebar navigation + page metadata.
@@ -491,6 +549,7 @@ Deferred to **Phase 4c** (UI dedicated effort):
491
549
  - 6 new unit tests for the resolver (199 total for billing).
492
550
 
493
551
  Deferred to **Phase 4b**:
552
+
494
553
  - Draft-edit endpoint + `DraftInvoiceEdit` audit row writing.
495
554
  - GDPR-portability `GET /api/billing/export/account/{id}`.
496
555
  - Admin UI (backend pages for accounts / items / runs / draft review).
@@ -499,6 +558,7 @@ Deferred to **Phase 4b**:
499
558
 
500
559
  - **Usage rater** in `lib/usageRater.ts` — pure functions for all four
501
560
  spec-mandated rate shapes:
561
+
502
562
  - Simple flat (`{ unit_price }`): `quantity × unit_price`.
503
563
  - Volume tier (`{ model: 'volume', tiers }`): entire quantity at the
504
564
  `unit_price` of the tier its total falls into.
@@ -509,12 +569,13 @@ Deferred to **Phase 4b**:
509
569
 
510
570
  Returns `{ amount, breakdown }` where `breakdown` is the per-tier
511
571
  audit trail persisted into `SalesInvoiceLine.metadata
512
- .usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
572
+ .usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
513
573
  unit prices preserve sub-cent precision via the dedicated
514
574
  `formatUnitPrice` helper (e.g. `0.001 → "0.0010"` instead of being
515
575
  rounded to zero).
516
576
 
517
577
  - **Usage runner** in `lib/usageRunner.ts` — per-account processor:
578
+
518
579
  - Selects unrated usage records (`rated_in_bill_run_id IS NULL`,
519
580
  `period_end <= bill_period_end`).
520
581
  - Groups by `uom_code`, matches against the account's `type=usage`
@@ -526,9 +587,10 @@ Deferred to **Phase 4b**:
526
587
  signal to add the missing item then trigger retry-failed.
527
588
 
528
589
  - **Engine integration** in `lib/billRunEngine.ts`:
590
+
529
591
  - Per-cycle loop now combines `buildInvoiceLinesFromItems` (one_time
530
- + recurring) with `processUsageForAccount` (usage) into a single
531
- `SalesInvoice` write.
592
+ - recurring) with `processUsageForAccount` (usage) into a single
593
+ `SalesInvoice` write.
532
594
  - Real-mode commits mark consumed usage records'
533
595
  `rated_in_bill_run_id` via a single `nativeUpdate` (scales to
534
596
  accounts with thousands of records per cycle).
@@ -548,9 +610,9 @@ Deferred to **Phase 4b**:
548
610
 
549
611
  - 27 new unit tests (193 total for the package):
550
612
  - `usageRater` covers every spec example end-to-end (`25k vs
551
- [10k=0, ∞=0.001]` graduated → 15 EUR, `25k vs [10k=0,
552
- 50k=0.001, ∞=0.0005]` volume → 25 EUR, `25k vs
553
- [10k=50, 50k=200, ∞=1000]` flat → 200 EUR) plus boundary
613
+ [10k=0, ∞=0.001]` graduated → 15 EUR, `25k vs [10k=0,
614
+ 50k=0.001, ∞=0.0005]` volume → 25 EUR, `25k vs
615
+ [10k=50, 50k=200, ∞=1000]` flat → 200 EUR) plus boundary
554
616
  conditions and the `1.005 → 1.01` half-up edge case.
555
617
  - Engine integration: graduated 25k → 15 EUR end-to-end, unmatched
556
618
  UoM warning + un-rated guard, simple-flat unit_price emission,
@@ -624,7 +686,7 @@ Phase 2 deviations):
624
686
  - `GET / POST / PUT / DELETE /api/billing/accounts` (Billing Accounts CRUD)
625
687
  - `GET / POST / PUT / DELETE /api/billing/items` (Billing Items CRUD with
626
688
  `source_ref` idempotency on POST — duplicate `(tenant, account,
627
- sourceRef)` returns the existing row with `deduplicated: true`)
689
+ sourceRef)` returns the existing row with `deduplicated: true`)
628
690
  - `GET / POST /api/billing/usage` (append-only usage ingest; idempotent
629
691
  by `source_ref`; no update / delete by design)
630
692
  - Lean command pattern (no undo / snapshot yet — Phase 4 adds those):
@@ -10,7 +10,15 @@ const features = [
10
10
  { id: "billing.run.view", title: "View Bill Run History", module: "billing" },
11
11
  { id: "billing.invoice.post", title: "Post Draft Invoice", module: "billing" },
12
12
  { id: "billing.invoice.edit_draft", title: "Edit Draft Invoice", module: "billing" },
13
- { id: "billing.invoice.view", title: "View Billing Invoices", module: "billing" }
13
+ { id: "billing.invoice.view", title: "View Billing Invoices", module: "billing" },
14
+ // ─── Prepaid balance billing (SPEC-002) ──────────────────────
15
+ { id: "billing.balance.view", title: "View Balance & Transactions", module: "billing" },
16
+ { id: "billing.balance.adjust", title: "Adjust Balance (manual credit/debit)", module: "billing" },
17
+ { id: "billing.topup.create", title: "Initiate Account Top-up", module: "billing" },
18
+ { id: "billing.topup.view", title: "View Account Top-ups", module: "billing" },
19
+ { id: "billing.statement.view", title: "View Consumption Statements", module: "billing" },
20
+ { id: "billing.statement.generate", title: "Generate Consumption Statement", module: "billing" },
21
+ { id: "billing.credit.view", title: "View Account Credit Status", module: "billing" }
14
22
  ];
15
23
  var acl_default = features;
16
24
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/billing/acl.ts"],
4
- "sourcesContent": ["export const features = [\n { id: 'billing.account.manage', title: 'Manage Billing Accounts', module: 'billing' },\n { id: 'billing.account.view', title: 'View Billing Accounts', module: 'billing' },\n { id: 'billing.item.manage', title: 'Manage Billing Items', module: 'billing' },\n { id: 'billing.item.view', title: 'View Billing Items', module: 'billing' },\n { id: 'billing.usage.ingest', title: 'Ingest Usage Records', module: 'billing' },\n { id: 'billing.usage.view', title: 'View Usage Records', module: 'billing' },\n { id: 'billing.run.trigger', title: 'Trigger Bill Run', module: 'billing' },\n { id: 'billing.run.dry_run', title: 'Trigger Bill Run (dry-run)', module: 'billing' },\n { id: 'billing.run.view', title: 'View Bill Run History', module: 'billing' },\n { id: 'billing.invoice.post', title: 'Post Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.edit_draft', title: 'Edit Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.view', title: 'View Billing Invoices', module: 'billing' },\n] as const\n\nexport default features\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,0BAA0B,OAAO,2BAA2B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAChF,EAAE,IAAI,uBAAuB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC9E,EAAE,IAAI,qBAAqB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,wBAAwB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC/E,EAAE,IAAI,sBAAsB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC3E,EAAE,IAAI,uBAAuB,OAAO,oBAAoB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,uBAAuB,OAAO,8BAA8B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,oBAAoB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAC5E,EAAE,IAAI,wBAAwB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC7E,EAAE,IAAI,8BAA8B,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EACnF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAClF;AAEA,IAAO,cAAQ;",
4
+ "sourcesContent": ["export const features = [\n { id: 'billing.account.manage', title: 'Manage Billing Accounts', module: 'billing' },\n { id: 'billing.account.view', title: 'View Billing Accounts', module: 'billing' },\n { id: 'billing.item.manage', title: 'Manage Billing Items', module: 'billing' },\n { id: 'billing.item.view', title: 'View Billing Items', module: 'billing' },\n { id: 'billing.usage.ingest', title: 'Ingest Usage Records', module: 'billing' },\n { id: 'billing.usage.view', title: 'View Usage Records', module: 'billing' },\n { id: 'billing.run.trigger', title: 'Trigger Bill Run', module: 'billing' },\n { id: 'billing.run.dry_run', title: 'Trigger Bill Run (dry-run)', module: 'billing' },\n { id: 'billing.run.view', title: 'View Bill Run History', module: 'billing' },\n { id: 'billing.invoice.post', title: 'Post Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.edit_draft', title: 'Edit Draft Invoice', module: 'billing' },\n { id: 'billing.invoice.view', title: 'View Billing Invoices', module: 'billing' },\n // \u2500\u2500\u2500 Prepaid balance billing (SPEC-002) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n { id: 'billing.balance.view', title: 'View Balance & Transactions', module: 'billing' },\n { id: 'billing.balance.adjust', title: 'Adjust Balance (manual credit/debit)', module: 'billing' },\n { id: 'billing.topup.create', title: 'Initiate Account Top-up', module: 'billing' },\n { id: 'billing.topup.view', title: 'View Account Top-ups', module: 'billing' },\n { id: 'billing.statement.view', title: 'View Consumption Statements', module: 'billing' },\n { id: 'billing.statement.generate', title: 'Generate Consumption Statement', module: 'billing' },\n { id: 'billing.credit.view', title: 'View Account Credit Status', module: 'billing' },\n] as const\n\nexport default features\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,0BAA0B,OAAO,2BAA2B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAChF,EAAE,IAAI,uBAAuB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC9E,EAAE,IAAI,qBAAqB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,wBAAwB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC/E,EAAE,IAAI,sBAAsB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC3E,EAAE,IAAI,uBAAuB,OAAO,oBAAoB,QAAQ,UAAU;AAAA,EAC1E,EAAE,IAAI,uBAAuB,OAAO,8BAA8B,QAAQ,UAAU;AAAA,EACpF,EAAE,IAAI,oBAAoB,OAAO,yBAAyB,QAAQ,UAAU;AAAA,EAC5E,EAAE,IAAI,wBAAwB,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EAC7E,EAAE,IAAI,8BAA8B,OAAO,sBAAsB,QAAQ,UAAU;AAAA,EACnF,EAAE,IAAI,wBAAwB,OAAO,yBAAyB,QAAQ,UAAU;AAAA;AAAA,EAEhF,EAAE,IAAI,wBAAwB,OAAO,+BAA+B,QAAQ,UAAU;AAAA,EACtF,EAAE,IAAI,0BAA0B,OAAO,wCAAwC,QAAQ,UAAU;AAAA,EACjG,EAAE,IAAI,wBAAwB,OAAO,2BAA2B,QAAQ,UAAU;AAAA,EAClF,EAAE,IAAI,sBAAsB,OAAO,wBAAwB,QAAQ,UAAU;AAAA,EAC7E,EAAE,IAAI,0BAA0B,OAAO,+BAA+B,QAAQ,UAAU;AAAA,EACxF,EAAE,IAAI,8BAA8B,OAAO,kCAAkC,QAAQ,UAAU;AAAA,EAC/F,EAAE,IAAI,uBAAuB,OAAO,8BAA8B,QAAQ,UAAU;AACtF;AAEA,IAAO,cAAQ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,62 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { BillingAccount } from "../../../../data/entities.js";
5
+ import { getBalanceSnapshot } from "../../../../lib/accountStatus.js";
6
+ const metadata = {
7
+ GET: { requireAuth: true, requireFeatures: ["billing.balance.view"] }
8
+ };
9
+ async function GET(req, { params }) {
10
+ let container = null;
11
+ try {
12
+ container = await createRequestContainer();
13
+ const auth = await getAuthFromRequest(req);
14
+ if (!auth || !auth.tenantId) {
15
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
16
+ }
17
+ const organizationId = auth.orgId ?? null;
18
+ if (!organizationId) {
19
+ return NextResponse.json({ error: "Organization scope required" }, { status: 403 });
20
+ }
21
+ const { id: accountId } = await params;
22
+ const em = container.resolve("em").fork();
23
+ const account = await em.findOne(BillingAccount, {
24
+ id: accountId,
25
+ tenantId: auth.tenantId,
26
+ organizationId,
27
+ deletedAt: null
28
+ });
29
+ if (!account) {
30
+ return NextResponse.json({ error: "Account not found" }, { status: 404 });
31
+ }
32
+ if (account.billingMode !== "prepaid") {
33
+ return NextResponse.json(
34
+ { error: "Account is not prepaid", code: "billing.account.not_prepaid" },
35
+ { status: 409 }
36
+ );
37
+ }
38
+ const snapshot = await getBalanceSnapshot(em, account);
39
+ return NextResponse.json(snapshot);
40
+ } catch (err) {
41
+ console.error("[billing/accounts/balance.GET]", err);
42
+ return NextResponse.json({ error: "Internal error" }, { status: 500 });
43
+ }
44
+ }
45
+ const openApi = {
46
+ methods: {
47
+ GET: {
48
+ tags: ["Billing"],
49
+ summary: "Get a prepaid account balance + status",
50
+ parameters: [
51
+ { name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
52
+ ],
53
+ description: "Returns the current running balance, `balanceStatus` (ok/low/exhausted), the resolved low-balance threshold, the currency, and the last-movement timestamp for a prepaid account. 409 (`billing.account.not_prepaid`) for postpaid accounts."
54
+ }
55
+ }
56
+ };
57
+ export {
58
+ GET,
59
+ metadata,
60
+ openApi
61
+ };
62
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/billing/api/accounts/%5Bid%5D/balance/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { BillingAccount } from '../../../../data/entities'\nimport { getBalanceSnapshot } from '../../../../lib/accountStatus'\n\n/**\n * `GET /api/billing/accounts/[id]/balance` \u2014 current prepaid balance, status,\n * and resolved low-balance threshold (SPEC-002). Read-only.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.balance.view'] },\n}\n\nexport async function GET(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n) {\n let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null\n try {\n container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'Organization scope required' }, { status: 403 })\n }\n\n const { id: accountId } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n const account = await em.findOne(BillingAccount, {\n id: accountId,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!account) {\n return NextResponse.json({ error: 'Account not found' }, { status: 404 })\n }\n if (account.billingMode !== 'prepaid') {\n return NextResponse.json(\n { error: 'Account is not prepaid', code: 'billing.account.not_prepaid' },\n { status: 409 },\n )\n }\n\n const snapshot = await getBalanceSnapshot(em, account)\n return NextResponse.json(snapshot)\n } catch (err) {\n console.error('[billing/accounts/balance.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Get a prepaid account balance + status',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n ],\n description:\n 'Returns the current running balance, `balanceStatus` (ok/low/exhausted), ' +\n 'the resolved low-balance threshold, the currency, and the last-movement ' +\n 'timestamp for a prepaid account. 409 (`billing.account.not_prepaid`) for ' +\n 'postpaid accounts.',\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,0BAA0B;AAO5B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAEA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,MAAI,YAAuE;AAC3E,MAAI;AACF,gBAAY,MAAM,uBAAuB;AACzC,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,iBAAkB,KAA4B,SAAS;AAC7D,QAAI,CAAC,gBAAgB;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,8BAA8B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,EAAE,IAAI,UAAU,IAAI,MAAM;AAChC,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AACA,QAAI,QAAQ,gBAAgB,WAAW;AACrC,aAAO,aAAa;AAAA,QAClB,EAAE,OAAO,0BAA0B,MAAM,8BAA8B;AAAA,QACvE,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,mBAAmB,IAAI,OAAO;AACrD,WAAO,aAAa,KAAK,QAAQ;AAAA,EACnC,SAAS,KAAK;AACZ,YAAQ,MAAM,kCAAkC,GAAG;AACnD,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,KAAK;AAAA,MACH,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,MACvF;AAAA,MACA,aACE;AAAA,IAIJ;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,56 @@
1
+ import { NextResponse } from "next/server";
2
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { BillingAccount } from "../../../../data/entities.js";
5
+ import { getCreditSnapshot } from "../../../../lib/accountStatus.js";
6
+ const metadata = {
7
+ GET: { requireAuth: true, requireFeatures: ["billing.credit.view"] }
8
+ };
9
+ async function GET(req, { params }) {
10
+ let container = null;
11
+ try {
12
+ container = await createRequestContainer();
13
+ const auth = await getAuthFromRequest(req);
14
+ if (!auth || !auth.tenantId) {
15
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
16
+ }
17
+ const organizationId = auth.orgId ?? null;
18
+ if (!organizationId) {
19
+ return NextResponse.json({ error: "Organization scope required" }, { status: 403 });
20
+ }
21
+ const { id: accountId } = await params;
22
+ const em = container.resolve("em").fork();
23
+ const account = await em.findOne(BillingAccount, {
24
+ id: accountId,
25
+ tenantId: auth.tenantId,
26
+ organizationId,
27
+ deletedAt: null
28
+ });
29
+ if (!account) {
30
+ return NextResponse.json({ error: "Account not found" }, { status: 404 });
31
+ }
32
+ const snapshot = await getCreditSnapshot(em, account);
33
+ return NextResponse.json(snapshot);
34
+ } catch (err) {
35
+ console.error("[billing/accounts/credit-status.GET]", err);
36
+ return NextResponse.json({ error: "Internal error" }, { status: 500 });
37
+ }
38
+ }
39
+ const openApi = {
40
+ methods: {
41
+ GET: {
42
+ tags: ["Billing"],
43
+ summary: "Get an account credit status (both modes)",
44
+ parameters: [
45
+ { name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
46
+ ],
47
+ description: "Returns `{ mode, creditLimit, creditUsed, availableCredit, creditStatus, currencyCode }`. Prepaid creditUsed = max(0,\u2212balance); postpaid = \u03A3 posted-unpaid invoice outstanding. Report-only \u2014 billing never blocks."
48
+ }
49
+ }
50
+ };
51
+ export {
52
+ GET,
53
+ metadata,
54
+ openApi
55
+ };
56
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/billing/api/accounts/%5Bid%5D/credit-status/route.ts"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { BillingAccount } from '../../../../data/entities'\nimport { getCreditSnapshot } from '../../../../lib/accountStatus'\n\n/**\n * `GET /api/billing/accounts/[id]/credit-status` \u2014 credit limit / used /\n * available / status for BOTH modes (SPEC-002 P6). Report-only \u2014 billing never\n * blocks; the quote\u2192order flow (e.g. a CPQ credit guard) decides what to do.\n *\n * - prepaid: credit_used = max(0, \u2212balance)\n * - postpaid: credit_used = \u03A3 posted-unpaid invoice outstanding (core/sales)\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.credit.view'] },\n}\n\nexport async function GET(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n) {\n let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null\n try {\n container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json({ error: 'Organization scope required' }, { status: 403 })\n }\n\n const { id: accountId } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n const account = await em.findOne(BillingAccount, {\n id: accountId,\n tenantId: auth.tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!account) {\n return NextResponse.json({ error: 'Account not found' }, { status: 404 })\n }\n\n const snapshot = await getCreditSnapshot(em, account)\n return NextResponse.json(snapshot)\n } catch (err) {\n console.error('[billing/accounts/credit-status.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Get an account credit status (both modes)',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n ],\n description:\n 'Returns `{ mode, creditLimit, creditUsed, availableCredit, creditStatus, ' +\n 'currencyCode }`. Prepaid creditUsed = max(0,\u2212balance); postpaid = \u03A3 ' +\n 'posted-unpaid invoice outstanding. Report-only \u2014 billing never blocks.',\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,sBAAsB;AAC/B,SAAS,yBAAyB;AAW3B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACrE;AAEA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,MAAI,YAAuE;AAC3E,MAAI;AACF,gBAAY,MAAM,uBAAuB;AACzC,UAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,aAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrE;AACA,UAAM,iBAAkB,KAA4B,SAAS;AAC7D,QAAI,CAAC,gBAAgB;AACnB,aAAO,aAAa,KAAK,EAAE,OAAO,8BAA8B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpF;AAEA,UAAM,EAAE,IAAI,UAAU,IAAI,MAAM;AAChC,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC3D,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C,IAAI;AAAA,MACJ,UAAU,KAAK;AAAA,MACf;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,WAAW,MAAM,kBAAkB,IAAI,OAAO;AACpD,WAAO,aAAa,KAAK,QAAQ;AAAA,EACnC,SAAS,KAAK;AACZ,YAAQ,MAAM,wCAAwC,GAAG;AACzD,WAAO,aAAa,KAAK,EAAE,OAAO,iBAAiB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACvE;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,KAAK;AAAA,MACH,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,MACvF;AAAA,MACA,aACE;AAAA,IAGJ;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,36 @@
1
+ import { POST as topupsPost } from "../../../topups/route.js";
2
+ const metadata = {
3
+ POST: { requireAuth: true, requireFeatures: ["billing.topup.create"] }
4
+ };
5
+ async function POST(req, { params }) {
6
+ const { id } = await params;
7
+ const body = await req.json().catch(() => ({}));
8
+ const merged = { ...body, billAccountId: id };
9
+ const headers = new Headers(req.headers);
10
+ headers.delete("content-length");
11
+ headers.set("content-type", "application/json");
12
+ const forwarded = new Request(req.url, {
13
+ method: "POST",
14
+ headers,
15
+ body: JSON.stringify(merged)
16
+ });
17
+ return topupsPost(forwarded);
18
+ }
19
+ const openApi = {
20
+ methods: {
21
+ POST: {
22
+ tags: ["Billing"],
23
+ summary: "Initiate a prepaid top-up for an account",
24
+ parameters: [
25
+ { name: "id", in: "path", required: true, schema: { type: "string", format: "uuid" } }
26
+ ],
27
+ description: "Registers a BillingTopup (pending) and opens a payment session. The path `id` is the prepaid account; the balance is credited on capture. Body: `{ amount, providerKey?, successUrl?, cancelUrl?, sourceRef? }`."
28
+ }
29
+ }
30
+ };
31
+ export {
32
+ POST,
33
+ metadata,
34
+ openApi
35
+ };
36
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/billing/api/accounts/%5Bid%5D/topups/route.ts"],
4
+ "sourcesContent": ["import { POST as topupsPost } from '../../../topups/route'\n\n/**\n * `POST /api/billing/accounts/{id}/topups` (SPEC-002 P2).\n *\n * Spec-faithful nested path. Injects the path `id` as `billAccountId` into the\n * body and delegates to the flat `POST /api/billing/topups` handler so all the\n * RBAC / command-bus / side-effect plumbing lives in exactly one place.\n */\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.topup.create'] },\n}\n\nexport async function POST(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n): Promise<Response> {\n const { id } = await params\n const body = (await req.json().catch(() => ({}))) as Record<string, unknown>\n const merged = { ...body, billAccountId: id }\n // Rebuild headers WITHOUT the stale content-length (the body changed); keep\n // auth (cookies / Authorization) intact and force JSON content-type.\n const headers = new Headers(req.headers)\n headers.delete('content-length')\n headers.set('content-type', 'application/json')\n const forwarded = new Request(req.url, {\n method: 'POST',\n headers,\n body: JSON.stringify(merged),\n })\n return topupsPost(forwarded)\n}\n\nexport const openApi = {\n methods: {\n POST: {\n tags: ['Billing'],\n summary: 'Initiate a prepaid top-up for an account',\n parameters: [\n { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } },\n ],\n description:\n 'Registers a BillingTopup (pending) and opens a payment session. The ' +\n 'path `id` is the prepaid account; the balance is credited on capture. ' +\n 'Body: `{ amount, providerKey?, successUrl?, cancelUrl?, sourceRef? }`.',\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,QAAQ,kBAAkB;AAU5B,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACvE;AAEA,eAAsB,KACpB,KACA,EAAE,OAAO,GACU;AACnB,QAAM,EAAE,GAAG,IAAI,MAAM;AACrB,QAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,QAAM,SAAS,EAAE,GAAG,MAAM,eAAe,GAAG;AAG5C,QAAM,UAAU,IAAI,QAAQ,IAAI,OAAO;AACvC,UAAQ,OAAO,gBAAgB;AAC/B,UAAQ,IAAI,gBAAgB,kBAAkB;AAC9C,QAAM,YAAY,IAAI,QAAQ,IAAI,KAAK;AAAA,IACrC,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,UAAU,MAAM;AAAA,EAC7B,CAAC;AACD,SAAO,WAAW,SAAS;AAC7B;AAEO,MAAM,UAAU;AAAA,EACrB,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,MAAM,CAAC,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,YAAY;AAAA,QACV,EAAE,MAAM,MAAM,IAAI,QAAQ,UAAU,MAAM,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO,EAAE;AAAA,MACvF;AAAA,MACA,aACE;AAAA,IAGJ;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -35,6 +35,8 @@ const billingAccountListItemSchema = z.object({
35
35
  invoice_address: z.record(z.string(), z.unknown()),
36
36
  next_bill_date: z.string(),
37
37
  last_bill_date: z.string().nullable(),
38
+ billing_mode: z.string(),
39
+ credit_limit: z.string(),
38
40
  is_active: z.boolean(),
39
41
  organization_id: z.string().uuid(),
40
42
  tenant_id: z.string().uuid(),
@@ -67,6 +69,8 @@ const crud = makeCrudRoute({
67
69
  "invoice_address",
68
70
  "next_bill_date",
69
71
  "last_bill_date",
72
+ "billing_mode",
73
+ "credit_limit",
70
74
  "is_active",
71
75
  "organization_id",
72
76
  "tenant_id",
@@ -93,6 +97,9 @@ const crud = makeCrudRoute({
93
97
  if (typeof query.billCycle === "string" && query.billCycle) {
94
98
  filters.bill_cycle = { $eq: query.billCycle };
95
99
  }
100
+ if (typeof query.billingMode === "string" && query.billingMode) {
101
+ filters.billing_mode = { $eq: query.billingMode };
102
+ }
96
103
  if (typeof query.isActive === "boolean") {
97
104
  filters.is_active = { $eq: query.isActive };
98
105
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/billing/api/accounts/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccount } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingAccountCreateSchema,\n billingAccountListQuerySchema,\n billingAccountUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// \u2500\u2500\u2500 List response item shape (informational \u2014 used by OpenAPI) \u2500\u2500\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingAccountListItemSchema = z.object({\n id: z.string().uuid(),\n customer_id: z.string(),\n name: z.string(),\n currency_code: z.string(),\n bill_cycle: z.string(),\n bill_cycle_anchor: z.string(),\n invoice_email: z.string(),\n invoice_language: z.string(),\n tax_id: z.string().nullable(),\n invoice_address: z.record(z.string(), z.unknown()),\n next_bill_date: z.string(),\n last_bill_date: z.string().nullable(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\n// \u2500\u2500\u2500 CRUD route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccount,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingAccountListQuerySchema,\n entityId: billingEntityIds.account,\n fields: [\n 'id',\n 'customer_id',\n 'name',\n 'currency_code',\n 'bill_cycle',\n 'bill_cycle_anchor',\n 'invoice_email',\n 'invoice_language',\n 'tax_id',\n 'invoice_address',\n 'next_bill_date',\n 'last_bill_date',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n nextBillDate: 'next_bill_date',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single account through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n if (typeof query.customerId === 'string' && query.customerId) {\n filters.customer_id = { $eq: query.customerId }\n }\n if (typeof query.currencyCode === 'string' && query.currencyCode) {\n filters.currency_code = { $eq: query.currencyCode.toUpperCase() }\n }\n if (typeof query.billCycle === 'string' && query.billCycle) {\n filters.bill_cycle = { $eq: query.billCycle }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.name = { $ilike: pattern }\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.accounts.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'billing.accounts.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.accounts.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccount',\n pluralName: 'BillingAccounts',\n querySchema: billingAccountListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),\n create: {\n schema: billingAccountCreateSchema,\n responseSchema: defaultCreateResponseSchema,\n description: 'Creates a new billing account for a customer.',\n },\n update: {\n schema: billingAccountUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a billing account. `currencyCode` and `customerId` are immutable.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing account (sets `deleted_at`).',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccount } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingAccountCreateSchema,\n billingAccountListQuerySchema,\n billingAccountUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.account.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// \u2500\u2500\u2500 List response item shape (informational \u2014 used by OpenAPI) \u2500\u2500\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingAccountListItemSchema = z.object({\n id: z.string().uuid(),\n customer_id: z.string(),\n name: z.string(),\n currency_code: z.string(),\n bill_cycle: z.string(),\n bill_cycle_anchor: z.string(),\n invoice_email: z.string(),\n invoice_language: z.string(),\n tax_id: z.string().nullable(),\n invoice_address: z.record(z.string(), z.unknown()),\n next_bill_date: z.string(),\n last_bill_date: z.string().nullable(),\n billing_mode: z.string(),\n credit_limit: z.string(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\n// \u2500\u2500\u2500 CRUD route \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingAccount,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingAccountListQuerySchema,\n entityId: billingEntityIds.account,\n fields: [\n 'id',\n 'customer_id',\n 'name',\n 'currency_code',\n 'bill_cycle',\n 'bill_cycle_anchor',\n 'invoice_email',\n 'invoice_language',\n 'tax_id',\n 'invoice_address',\n 'next_bill_date',\n 'last_bill_date',\n 'billing_mode',\n 'credit_limit',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n name: 'name',\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n nextBillDate: 'next_bill_date',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single account through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n if (typeof query.customerId === 'string' && query.customerId) {\n filters.customer_id = { $eq: query.customerId }\n }\n if (typeof query.currencyCode === 'string' && query.currencyCode) {\n filters.currency_code = { $eq: query.currencyCode.toUpperCase() }\n }\n if (typeof query.billCycle === 'string' && query.billCycle) {\n filters.bill_cycle = { $eq: query.billCycle }\n }\n if (typeof query.billingMode === 'string' && query.billingMode) {\n filters.billing_mode = { $eq: query.billingMode }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.name = { $ilike: pattern }\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.accounts.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({ id: result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'billing.accounts.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingAccountUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.accounts.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingAccount',\n pluralName: 'BillingAccounts',\n querySchema: billingAccountListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),\n create: {\n schema: billingAccountCreateSchema,\n responseSchema: defaultCreateResponseSchema,\n description: 'Creates a new billing account for a customer.',\n },\n update: {\n schema: billingAccountUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a billing account. `currencyCode` and `customerId` are immutable.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing account (sets `deleted_at`).',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }