@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.
Files changed (206) hide show
  1. package/CHANGELOG.md +99 -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 +9 -9
  129. package/src/modules/billing/__tests__/commands/invoices.test.ts +3 -3
  130. package/src/modules/billing/__tests__/commands/topups.test.ts +104 -0
  131. package/src/modules/billing/__tests__/commands/transactions.test.ts +107 -0
  132. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +235 -0
  133. package/src/modules/billing/__tests__/lib/accountStatus.test.ts +93 -0
  134. package/src/modules/billing/__tests__/lib/balanceLedger.test.ts +125 -0
  135. package/src/modules/billing/__tests__/lib/balanceStatus.test.ts +65 -0
  136. package/src/modules/billing/__tests__/lib/creditStatus.test.ts +111 -0
  137. package/src/modules/billing/__tests__/lib/money.test.ts +56 -0
  138. package/src/modules/billing/__tests__/lib/prepaidConsume.test.ts +179 -0
  139. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +232 -0
  140. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +123 -0
  141. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +84 -0
  142. package/src/modules/billing/__tests__/setup.test.ts +28 -2
  143. package/src/modules/billing/acl.ts +8 -0
  144. package/src/modules/billing/api/accounts/[id]/balance/route.ts +74 -0
  145. package/src/modules/billing/api/accounts/[id]/credit-status/route.ts +71 -0
  146. package/src/modules/billing/api/accounts/[id]/topups/route.ts +49 -0
  147. package/src/modules/billing/api/accounts/route.ts +7 -0
  148. package/src/modules/billing/api/export/account/[id]/route.ts +47 -2
  149. package/src/modules/billing/api/statements/generate/route.ts +78 -0
  150. package/src/modules/billing/api/statements/route.ts +117 -0
  151. package/src/modules/billing/api/topups/route.ts +154 -0
  152. package/src/modules/billing/api/transactions/adjust/route.ts +80 -0
  153. package/src/modules/billing/api/transactions/route.ts +116 -0
  154. package/src/modules/billing/api/usage/route.ts +36 -5
  155. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +28 -0
  156. package/src/modules/billing/backend/billing/accounts/create/page.tsx +7 -0
  157. package/src/modules/billing/backend/billing/accounts/page.tsx +32 -1
  158. package/src/modules/billing/backend/billing/statements/[id]/page.meta.ts +19 -0
  159. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +198 -0
  160. package/src/modules/billing/backend/billing/statements/page.meta.ts +16 -0
  161. package/src/modules/billing/backend/billing/statements/page.tsx +190 -0
  162. package/src/modules/billing/backend/billing/topups/[id]/page.meta.ts +19 -0
  163. package/src/modules/billing/backend/billing/topups/[id]/page.tsx +151 -0
  164. package/src/modules/billing/backend/billing/topups/page.meta.ts +16 -0
  165. package/src/modules/billing/backend/billing/topups/page.tsx +182 -0
  166. package/src/modules/billing/backend/billing/transactions/[id]/page.meta.ts +19 -0
  167. package/src/modules/billing/backend/billing/transactions/[id]/page.tsx +179 -0
  168. package/src/modules/billing/backend/billing/transactions/page.meta.ts +16 -0
  169. package/src/modules/billing/backend/billing/transactions/page.tsx +187 -0
  170. package/src/modules/billing/commands/accounts.ts +42 -1
  171. package/src/modules/billing/commands/index.ts +3 -0
  172. package/src/modules/billing/commands/invoices.ts +26 -2
  173. package/src/modules/billing/commands/statements.ts +101 -0
  174. package/src/modules/billing/commands/topups.ts +181 -0
  175. package/src/modules/billing/commands/transactions.ts +152 -0
  176. package/src/modules/billing/commands/usage.ts +114 -4
  177. package/src/modules/billing/components/AccountForm.tsx +66 -0
  178. package/src/modules/billing/components/PrepaidPanel.tsx +382 -0
  179. package/src/modules/billing/components/accountNames.ts +23 -0
  180. package/src/modules/billing/data/entities.ts +383 -0
  181. package/src/modules/billing/data/entityIds.ts +5 -0
  182. package/src/modules/billing/data/validators.ts +144 -0
  183. package/src/modules/billing/events.ts +52 -0
  184. package/src/modules/billing/i18n/en.json +112 -4
  185. package/src/modules/billing/i18n/pl.json +112 -4
  186. package/src/modules/billing/index.ts +1 -1
  187. package/src/modules/billing/lib/accountStatus.ts +155 -0
  188. package/src/modules/billing/lib/balanceEvents.ts +74 -0
  189. package/src/modules/billing/lib/balanceLedger.ts +258 -0
  190. package/src/modules/billing/lib/balanceStatus.ts +78 -0
  191. package/src/modules/billing/lib/billRunEngine.ts +66 -5
  192. package/src/modules/billing/lib/creditStatus.ts +153 -0
  193. package/src/modules/billing/lib/money.ts +73 -0
  194. package/src/modules/billing/lib/prepaidConfig.ts +58 -0
  195. package/src/modules/billing/lib/prepaidConsume.ts +299 -0
  196. package/src/modules/billing/lib/prepaidPeriodClose.ts +453 -0
  197. package/src/modules/billing/lib/topupCapture.ts +117 -0
  198. package/src/modules/billing/lib/topupReceipt.ts +153 -0
  199. package/src/modules/billing/lib/topupTransition.ts +31 -0
  200. package/src/modules/billing/migrations/Migration20260619000000_billing_prepaid.ts +131 -0
  201. package/src/modules/billing/setup.ts +45 -0
  202. package/src/modules/billing/subscribers/prepaid-topup-cancelled.ts +31 -0
  203. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +120 -0
  204. package/src/modules/billing/subscribers/prepaid-topup-expired.ts +31 -0
  205. package/src/modules/billing/subscribers/prepaid-topup-failed.ts +32 -0
  206. package/src/modules/billing/workers/reconcile-prepaid-balances.ts +68 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # @dainamite/billing
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8834b8b: Require Open Mercato `0.6.7`. Peer ranges move from `^0.6.0` to `^0.6.7` for
8
+ `@open-mercato/core`, `shared`, `ui`, `events` and `queue`, and the MikroORM
9
+ peers from `^7.0.0` to `^7.1.5` (7.1 split the SQL layer into
10
+ `@mikro-orm/sql`, so a shared 7.1 instance is required).
11
+
12
+ No package source changes — this only narrows the supported framework range.
13
+ Consumers on Open Mercato `0.6.0`–`0.6.6` must upgrade before taking these
14
+ versions.
15
+
16
+ Note for consumers running commands with a system identity (`ctx.auth: null`):
17
+ `0.6.7` hardened `ensureTenantScope` / `ensureOrganizationScope`. Those calls
18
+ are still no-ops with `auth: null`, but an unscoped organization command now
19
+ logs a warning and will throw `403` if `OM_ENFORCE_ORG_SCOPE_STRICT=true`.
20
+
21
+ ## 0.18.0
22
+
23
+ ### Minor Changes
24
+
25
+ - f8210ac: Prepaid billing mode (SPEC-002 / XD-304). Adds an additive `prepaid` billing
26
+ mode alongside the existing postpaid recurring engine: top-up balance,
27
+ real-time atomic usage debit (never rejects usage), append-only transaction
28
+ ledger, prepaid period-close statements, manual balance adjustments, and the
29
+ prepaid admin UI (balance panel, Transactions / Top-ups / Statements lists +
30
+ detail pages). Postpaid behaviour is unchanged.
31
+
32
+ ## 0.18.0 — Prepaid balance billing (SPEC-002, XD-304, unreleased)
33
+
34
+ Adds a second billing **mode** to the package: `prepaid`. A customer tops up
35
+ a monetary **balance**, then consumes a metered product; each usage upload is
36
+ rated in real time, debited from the balance, and the API returns the
37
+ remaining balance. Usage is **never rejected** — low/exhausted is reported and
38
+ the client throttles itself. Postpaid accounts are completely unaffected.
39
+
40
+ - **Schema (additive).** `billing_accounts.billing_mode` (`postpaid`|`prepaid`,
41
+ immutable) + `credit_limit`. New tables: `billing_account_balances` (1:1
42
+ running-balance cache), `billing_account_transactions` (append-only ledger,
43
+ `balance = SUM(amount)`), `billing_topups` (registered top-up lifecycle),
44
+ `billing_statements` (period close). `billing_run_outcomes.statement_id`.
45
+ - **Real-time consume.** `POST /api/billing/usage` on a prepaid account rates
46
+ synchronously (reusing `lib/usageRater`), debits atomically
47
+ (`UPDATE … RETURNING` + paired transaction), and returns `ratedAmount`,
48
+ `balance`, `balanceStatus`, `creditStatus`, `tierBreakdown`. Idempotent on
49
+ `sourceRef` (no double-debit). `GET …/balance`, `GET /transactions`.
50
+ - **Top-ups.** `POST /api/billing/accounts/{id}/topups` registers a
51
+ `BillingTopup` (pending) and opens a `core/payment_gateways` session; the
52
+ `payment.captured` subscriber idempotently credits the balance, issues a
53
+ **posted+paid VAT receipt** in `core/sales` (VAT at top-up), and flips the
54
+ top-up to captured. Sibling subscribers handle failed/cancelled/expired.
55
+ - **Period close.** The Bill Run still runs for prepaid accounts, charging
56
+ `recurring`/`one_time` items to the **balance** (deterministic `source_ref`,
57
+ `billed_to_date` guard) and rolling them into a non-fiscal
58
+ **`BillingStatement`** instead of a payable invoice.
59
+ - **Credit limit (both modes).** Report-only `creditStatus` + `GET …/credit-status`
60
+ (prepaid `max(0,−balance)`; postpaid Σ posted-unpaid outstanding) +
61
+ `billing.credit.over_limit`. Billing **never blocks**.
62
+ - **Admin UI.** Account Prepaid panel (balance, status, credit, top-up,
63
+ transactions), top-ups + statements list/detail pages, account create/edit
64
+ mode + credit fields; en/pl i18n.
65
+ - **Hardening.** GDPR export extended (balance/transactions/top-ups/statements);
66
+ optional reconciliation worker; manual balance adjustments
67
+ (`POST /transactions/adjust`, audited).
68
+ - **Deviation.** The top-up receipt is created **posted+paid** (not draft) —
69
+ money already received has nothing to review (mirrors the xd-249 deviation
70
+ convention). VAT rate resolves from `prepaid.topup_tax_rate` config (the
71
+ postpaid path resolves no rate today).
72
+
3
73
  ## 0.17.0 — Bill Run + invoices live fixes, CPQ-style list views (unreleased)
4
74
 
5
75
  Continued live-database shakedown — running an actual Bill Run and
@@ -59,7 +129,7 @@ and fixed; covered by new Playwright integration tests.
59
129
  `mapInput` read `raw.id` directly, never found it, and threw
60
130
  "id is required". `mapInput` now unwraps the envelope.
61
131
  - **`?id=` list filter was a no-op.** `GET /api/billing/{accounts,
62
- items,runs}?id=<uuid>` is how the detail pages fetch one record,
132
+ items,runs}?id=<uuid>` is how the detail pages fetch one record,
63
133
  but `buildFilters` never handled `id`, so the endpoint returned an
64
134
  unrelated row. `id` is now an indexed filter on all three list
65
135
  routes (and added to the list query schemas).
@@ -136,14 +206,14 @@ write path.
136
206
  - runs **one** idempotency query over all `source_ref`s in the batch
137
207
  (`sourceRef: { $in }` + `billAccountId: { $in }`) instead of one
138
208
  per item;
139
- - dedups `source_ref`s repeated *within* the same payload (first
209
+ - dedups `source_ref`s repeated _within_ the same payload (first
140
210
  wins — the unique index would reject the rest on flush anyway);
141
211
  - `em.create` + `persist`s every new row, then issues a **single**
142
212
  `em.flush()` for the whole batch;
143
213
  - emits per-entity CRUD side effects (`markOrmEntityChange` +
144
214
  events) only after the flush commits.
145
215
  - Returns `{ created, deduplicated, items: [{ sourceRef, id,
146
- deduplicated }] }` so callers can map results back to inputs.
216
+ deduplicated }] }` so callers can map results back to inputs.
147
217
  - **`POST /api/billing/items/bulk`** — REST surface for the command,
148
218
  gated by `billing.item.manage`, `openApi` documented.
149
219
  - **Validators.** `billingItemBulkEntrySchema` (per-item, no scope,
@@ -176,16 +246,16 @@ on the way through.
176
246
  no limit, materialising every row in JS memory before summing. For
177
247
  a telco account with 1M usage records per cycle this OOMs. New
178
248
  implementation does a single `SELECT uom_code, SUM(quantity),
179
- COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
249
+ COUNT(*) FROM billing_account_usage WHERE … GROUP BY uom_code` —
180
250
  result set is `O(distinct uom_codes)` (typically 1-20) regardless
181
251
  of record count. Index `(bill_account_id, uom_code,
182
- rated_in_bill_run_id, period_end)` already exists from Phase 0.
252
+ rated_in_bill_run_id, period_end)` already exists from Phase 0.
183
253
  - **Engine rated-marking: id list → predicate update.** The
184
254
  follow-up "mark consumed records" step used to take a `consumedUsageIds`
185
255
  array and issue `UPDATE … WHERE id IN (millions of UUIDs)`. New
186
256
  shape: the runner returns `matchedUoms` (small) and the engine does
187
257
  `UPDATE … WHERE uom_code IN (matchedUoms) AND <account scope>
188
- AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
258
+ AND rated_in_bill_run_id IS NULL AND period_end <= ?` — no ID list
189
259
  ever leaves Postgres. Memory-bounded regardless of how many records
190
260
  the predicate touches.
191
261
  - **GDPR export: time window + hard cap.** The
@@ -230,9 +300,9 @@ connector created — without dropping to `curl`.
230
300
  - `usage` (tiered): raw-JSON textarea for the advanced shape.
231
301
  Server validators (Phase 0 Zod) reject malformed structures;
232
302
  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).
303
+ Plus optional `subscriptionId` / `subscriptionItemId` external
304
+ references. UX matches `AccountForm` (Cmd/Ctrl+Enter submit, Kbd
305
+ footer hint, no raw HTML buttons).
236
306
  - `/backend/billing/items` — list with filters (type, account,
237
307
  subscription, active) + per-row "Open" link. **New item** CTA in
238
308
  the page header.
@@ -251,6 +321,7 @@ Validation: yarn build + generate + typecheck + test all green;
251
321
  auto-discovered (3 accounts + 3 items + 2 runs + 2 invoices).
252
322
 
253
323
  Deferred to follow-up:
324
+
254
325
  - Locale files (en, pl) — UI ships with inline English fallbacks.
255
326
  - "Add item" button on the account detail page (uses the deep-link
256
327
  parameter the create page already supports).
@@ -282,6 +353,7 @@ Validation: yarn build + generate + typecheck + test all green; 797
282
353
  repo tests, 0 regressions; all 7 backend pages auto-discovered.
283
354
 
284
355
  Deferred to follow-up:
356
+
285
357
  - Item create / edit pages (items are typically integration-driven —
286
358
  the REST API at `/api/billing/items` covers operator-side needs).
287
359
  - Locale files (en, pl).
@@ -308,7 +380,7 @@ edits no longer require `curl`.
308
380
  test-invoices flow and the line-remove flow, satisfying the
309
381
  UI AGENTS.md MUST rule.
310
382
  - All writes go through the existing `billing.invoices.{edit,add,
311
- remove}_draft_line` commands — every line mutation is still
383
+ remove}_draft_line` commands — every line mutation is still
312
384
  audited via `DraftInvoiceEdit` with before/after snapshots, and
313
385
  invoice totals are recomputed by the engine on each write.
314
386
 
@@ -317,6 +389,7 @@ Validation: yarn build + generate + typecheck + test all green;
317
389
  pages + 1 shared component.
318
390
 
319
391
  Deferred to follow-up:
392
+
320
393
  - Account / item editor pages
321
394
  - Locale files (en, pl)
322
395
 
@@ -348,7 +421,7 @@ to review or post.
348
421
  - **Wipe test invoices for this run** — visible only for
349
422
  `metadata.test_run=true`. Hard-deletes every test invoice from
350
423
  the same `bill_run_id` via `billing.invoices.wipe_test`. `window
351
- .confirm` gate (will move to `ConfirmDialog` in a follow-up to
424
+ .confirm` gate (will move to `ConfirmDialog` in a follow-up to
352
425
  match the OM UX convention).
353
426
 
354
427
  Sidebar entry under the `Billing` group at order 30.
@@ -359,6 +432,7 @@ add-line, edit-line, remove-line, invoices list, invoices detail);
359
432
  5 backend pages.
360
433
 
361
434
  Deferred to follow-up:
435
+
362
436
  - Inline line edit / add / remove dialogs on the detail page (REST
363
437
  endpoints already exist from Phase 4b).
364
438
  - ConfirmDialog instead of `window.confirm` for the wipe action.
@@ -399,6 +473,7 @@ Validation: yarn build + generate + typecheck + test all green;
399
473
  `.mercato/generated/backend-routes.generated.ts`.
400
474
 
401
475
  Deferred to a follow-up:
476
+
402
477
  - Account / item / usage / draft-invoice editor pages
403
478
  - Triggers (manual Bill Run / dry-run preview) as buttons in the
404
479
  list page
@@ -423,7 +498,7 @@ fully usable from the REST surface today.
423
498
  pre-removal snapshot preserved in `before_json`. `after_json`
424
499
  and `invoice_line_id` are `null`.
425
500
  - All three refuse non-draft invoices with `HTTP 409 +
426
- billing.invoice.not_draft` and require the
501
+ billing.invoice.not_draft` and require the
427
502
  `billing.invoice.edit_draft` feature.
428
503
  - **Test-invoice wipe command** `billing.invoices.wipe_test`:
429
504
  hard-deletes invoices flagged `metadata.test_run=true`, optionally
@@ -449,6 +524,7 @@ fully usable from the REST surface today.
449
524
  `billRunId`).
450
525
 
451
526
  Deferred to **Phase 4c** (UI dedicated effort):
527
+
452
528
  - Backend pages (DataTable + CrudForm) for accounts / items / usage /
453
529
  bill runs / draft invoice review.
454
530
  - Sidebar navigation + page metadata.
@@ -491,6 +567,7 @@ Deferred to **Phase 4c** (UI dedicated effort):
491
567
  - 6 new unit tests for the resolver (199 total for billing).
492
568
 
493
569
  Deferred to **Phase 4b**:
570
+
494
571
  - Draft-edit endpoint + `DraftInvoiceEdit` audit row writing.
495
572
  - GDPR-portability `GET /api/billing/export/account/{id}`.
496
573
  - Admin UI (backend pages for accounts / items / runs / draft review).
@@ -499,6 +576,7 @@ Deferred to **Phase 4b**:
499
576
 
500
577
  - **Usage rater** in `lib/usageRater.ts` — pure functions for all four
501
578
  spec-mandated rate shapes:
579
+
502
580
  - Simple flat (`{ unit_price }`): `quantity × unit_price`.
503
581
  - Volume tier (`{ model: 'volume', tiers }`): entire quantity at the
504
582
  `unit_price` of the tier its total falls into.
@@ -509,12 +587,13 @@ Deferred to **Phase 4b**:
509
587
 
510
588
  Returns `{ amount, breakdown }` where `breakdown` is the per-tier
511
589
  audit trail persisted into `SalesInvoiceLine.metadata
512
- .usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
590
+ .usage_tier_breakdown`. Half-up rounding to 2dp on the amount;
513
591
  unit prices preserve sub-cent precision via the dedicated
514
592
  `formatUnitPrice` helper (e.g. `0.001 → "0.0010"` instead of being
515
593
  rounded to zero).
516
594
 
517
595
  - **Usage runner** in `lib/usageRunner.ts` — per-account processor:
596
+
518
597
  - Selects unrated usage records (`rated_in_bill_run_id IS NULL`,
519
598
  `period_end <= bill_period_end`).
520
599
  - Groups by `uom_code`, matches against the account's `type=usage`
@@ -526,9 +605,10 @@ Deferred to **Phase 4b**:
526
605
  signal to add the missing item then trigger retry-failed.
527
606
 
528
607
  - **Engine integration** in `lib/billRunEngine.ts`:
608
+
529
609
  - Per-cycle loop now combines `buildInvoiceLinesFromItems` (one_time
530
- + recurring) with `processUsageForAccount` (usage) into a single
531
- `SalesInvoice` write.
610
+ - recurring) with `processUsageForAccount` (usage) into a single
611
+ `SalesInvoice` write.
532
612
  - Real-mode commits mark consumed usage records'
533
613
  `rated_in_bill_run_id` via a single `nativeUpdate` (scales to
534
614
  accounts with thousands of records per cycle).
@@ -548,9 +628,9 @@ Deferred to **Phase 4b**:
548
628
 
549
629
  - 27 new unit tests (193 total for the package):
550
630
  - `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
631
+ [10k=0, ∞=0.001]` graduated → 15 EUR, `25k vs [10k=0,
632
+ 50k=0.001, ∞=0.0005]` volume → 25 EUR, `25k vs
633
+ [10k=50, 50k=200, ∞=1000]` flat → 200 EUR) plus boundary
554
634
  conditions and the `1.005 → 1.01` half-up edge case.
555
635
  - Engine integration: graduated 25k → 15 EUR end-to-end, unmatched
556
636
  UoM warning + un-rated guard, simple-flat unit_price emission,
@@ -624,7 +704,7 @@ Phase 2 deviations):
624
704
  - `GET / POST / PUT / DELETE /api/billing/accounts` (Billing Accounts CRUD)
625
705
  - `GET / POST / PUT / DELETE /api/billing/items` (Billing Items CRUD with
626
706
  `source_ref` idempotency on POST — duplicate `(tenant, account,
627
- sourceRef)` returns the existing row with `deduplicated: true`)
707
+ sourceRef)` returns the existing row with `deduplicated: true`)
628
708
  - `GET / POST /api/billing/usage` (append-only usage ingest; idempotent
629
709
  by `source_ref`; no update / delete by design)
630
710
  - 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
  }