@dainamite/billing 0.17.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 (248) hide show
  1. package/CHANGELOG.md +658 -0
  2. package/LICENSE +21 -0
  3. package/README.md +77 -0
  4. package/dist/modules/billing/acl.js +20 -0
  5. package/dist/modules/billing/acl.js.map +7 -0
  6. package/dist/modules/billing/api/accounts/route.js +182 -0
  7. package/dist/modules/billing/api/accounts/route.js.map +7 -0
  8. package/dist/modules/billing/api/export/account/[id]/route.js +170 -0
  9. package/dist/modules/billing/api/export/account/[id]/route.js.map +7 -0
  10. package/dist/modules/billing/api/invoices/[id]/route.js +85 -0
  11. package/dist/modules/billing/api/invoices/[id]/route.js.map +7 -0
  12. package/dist/modules/billing/api/invoices/add-line/route.js +69 -0
  13. package/dist/modules/billing/api/invoices/add-line/route.js.map +7 -0
  14. package/dist/modules/billing/api/invoices/edit-line/route.js +78 -0
  15. package/dist/modules/billing/api/invoices/edit-line/route.js.map +7 -0
  16. package/dist/modules/billing/api/invoices/post/route.js +74 -0
  17. package/dist/modules/billing/api/invoices/post/route.js.map +7 -0
  18. package/dist/modules/billing/api/invoices/remove-line/route.js +69 -0
  19. package/dist/modules/billing/api/invoices/remove-line/route.js.map +7 -0
  20. package/dist/modules/billing/api/invoices/route.js +109 -0
  21. package/dist/modules/billing/api/invoices/route.js.map +7 -0
  22. package/dist/modules/billing/api/items/bulk/route.js +74 -0
  23. package/dist/modules/billing/api/items/bulk/route.js.map +7 -0
  24. package/dist/modules/billing/api/items/route.js +224 -0
  25. package/dist/modules/billing/api/items/route.js.map +7 -0
  26. package/dist/modules/billing/api/openapi.js +25 -0
  27. package/dist/modules/billing/api/openapi.js.map +7 -0
  28. package/dist/modules/billing/api/run-outcomes/route.js +122 -0
  29. package/dist/modules/billing/api/run-outcomes/route.js.map +7 -0
  30. package/dist/modules/billing/api/runs/retry-failed/route.js +70 -0
  31. package/dist/modules/billing/api/runs/retry-failed/route.js.map +7 -0
  32. package/dist/modules/billing/api/runs/route.js +143 -0
  33. package/dist/modules/billing/api/runs/route.js.map +7 -0
  34. package/dist/modules/billing/api/test-invoices/wipe/route.js +65 -0
  35. package/dist/modules/billing/api/test-invoices/wipe/route.js.map +7 -0
  36. package/dist/modules/billing/api/usage/route.js +138 -0
  37. package/dist/modules/billing/api/usage/route.js.map +7 -0
  38. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +402 -0
  39. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +7 -0
  40. package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js +25 -0
  41. package/dist/modules/billing/backend/billing/accounts/[id]/page.meta.js.map +7 -0
  42. package/dist/modules/billing/backend/billing/accounts/create/page.js +82 -0
  43. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +7 -0
  44. package/dist/modules/billing/backend/billing/accounts/create/page.meta.js +25 -0
  45. package/dist/modules/billing/backend/billing/accounts/create/page.meta.js.map +7 -0
  46. package/dist/modules/billing/backend/billing/accounts/page.js +173 -0
  47. package/dist/modules/billing/backend/billing/accounts/page.js.map +7 -0
  48. package/dist/modules/billing/backend/billing/accounts/page.meta.js +18 -0
  49. package/dist/modules/billing/backend/billing/accounts/page.meta.js.map +7 -0
  50. package/dist/modules/billing/backend/billing/invoices/[id]/page.js +500 -0
  51. package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +7 -0
  52. package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js +25 -0
  53. package/dist/modules/billing/backend/billing/invoices/[id]/page.meta.js.map +7 -0
  54. package/dist/modules/billing/backend/billing/invoices/page.js +187 -0
  55. package/dist/modules/billing/backend/billing/invoices/page.js.map +7 -0
  56. package/dist/modules/billing/backend/billing/invoices/page.meta.js +18 -0
  57. package/dist/modules/billing/backend/billing/invoices/page.meta.js.map +7 -0
  58. package/dist/modules/billing/backend/billing/items/[id]/page.js +265 -0
  59. package/dist/modules/billing/backend/billing/items/[id]/page.js.map +7 -0
  60. package/dist/modules/billing/backend/billing/items/[id]/page.meta.js +25 -0
  61. package/dist/modules/billing/backend/billing/items/[id]/page.meta.js.map +7 -0
  62. package/dist/modules/billing/backend/billing/items/create/page.js +90 -0
  63. package/dist/modules/billing/backend/billing/items/create/page.js.map +7 -0
  64. package/dist/modules/billing/backend/billing/items/create/page.meta.js +25 -0
  65. package/dist/modules/billing/backend/billing/items/create/page.meta.js.map +7 -0
  66. package/dist/modules/billing/backend/billing/items/page.js +208 -0
  67. package/dist/modules/billing/backend/billing/items/page.js.map +7 -0
  68. package/dist/modules/billing/backend/billing/items/page.meta.js +18 -0
  69. package/dist/modules/billing/backend/billing/items/page.meta.js.map +7 -0
  70. package/dist/modules/billing/backend/billing/runs/[id]/page.js +293 -0
  71. package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +7 -0
  72. package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js +21 -0
  73. package/dist/modules/billing/backend/billing/runs/[id]/page.meta.js.map +7 -0
  74. package/dist/modules/billing/backend/billing/runs/page.js +341 -0
  75. package/dist/modules/billing/backend/billing/runs/page.js.map +7 -0
  76. package/dist/modules/billing/backend/billing/runs/page.meta.js +18 -0
  77. package/dist/modules/billing/backend/billing/runs/page.meta.js.map +7 -0
  78. package/dist/modules/billing/backend/billing/usage/create/page.js +100 -0
  79. package/dist/modules/billing/backend/billing/usage/create/page.js.map +7 -0
  80. package/dist/modules/billing/backend/billing/usage/create/page.meta.js +25 -0
  81. package/dist/modules/billing/backend/billing/usage/create/page.meta.js.map +7 -0
  82. package/dist/modules/billing/ce.js +56 -0
  83. package/dist/modules/billing/ce.js.map +7 -0
  84. package/dist/modules/billing/cli.js +67 -0
  85. package/dist/modules/billing/cli.js.map +7 -0
  86. package/dist/modules/billing/commands/accounts.js +157 -0
  87. package/dist/modules/billing/commands/accounts.js.map +7 -0
  88. package/dist/modules/billing/commands/index.js +6 -0
  89. package/dist/modules/billing/commands/index.js.map +7 -0
  90. package/dist/modules/billing/commands/invoices.js +401 -0
  91. package/dist/modules/billing/commands/invoices.js.map +7 -0
  92. package/dist/modules/billing/commands/items.js +317 -0
  93. package/dist/modules/billing/commands/items.js.map +7 -0
  94. package/dist/modules/billing/commands/runs.js +123 -0
  95. package/dist/modules/billing/commands/runs.js.map +7 -0
  96. package/dist/modules/billing/commands/usage.js +88 -0
  97. package/dist/modules/billing/commands/usage.js.map +7 -0
  98. package/dist/modules/billing/components/AccountForm.js +420 -0
  99. package/dist/modules/billing/components/AccountForm.js.map +7 -0
  100. package/dist/modules/billing/components/DetailFields.js +37 -0
  101. package/dist/modules/billing/components/DetailFields.js.map +7 -0
  102. package/dist/modules/billing/components/ItemForm.js +440 -0
  103. package/dist/modules/billing/components/ItemForm.js.map +7 -0
  104. package/dist/modules/billing/components/LineFormDialog.js +195 -0
  105. package/dist/modules/billing/components/LineFormDialog.js.map +7 -0
  106. package/dist/modules/billing/components/UsageForm.js +262 -0
  107. package/dist/modules/billing/components/UsageForm.js.map +7 -0
  108. package/dist/modules/billing/data/entities.js +433 -0
  109. package/dist/modules/billing/data/entities.js.map +7 -0
  110. package/dist/modules/billing/data/entityIds.js +12 -0
  111. package/dist/modules/billing/data/entityIds.js.map +7 -0
  112. package/dist/modules/billing/data/validators.js +396 -0
  113. package/dist/modules/billing/data/validators.js.map +7 -0
  114. package/dist/modules/billing/di.js +6 -0
  115. package/dist/modules/billing/di.js.map +7 -0
  116. package/dist/modules/billing/events.js +35 -0
  117. package/dist/modules/billing/events.js.map +7 -0
  118. package/dist/modules/billing/i18n/en.json +336 -0
  119. package/dist/modules/billing/i18n/pl.json +336 -0
  120. package/dist/modules/billing/index.js +17 -0
  121. package/dist/modules/billing/index.js.map +7 -0
  122. package/dist/modules/billing/lib/billPeriod.js +101 -0
  123. package/dist/modules/billing/lib/billPeriod.js.map +7 -0
  124. package/dist/modules/billing/lib/billRunEngine.js +256 -0
  125. package/dist/modules/billing/lib/billRunEngine.js.map +7 -0
  126. package/dist/modules/billing/lib/billRunLabel.js +8 -0
  127. package/dist/modules/billing/lib/billRunLabel.js.map +7 -0
  128. package/dist/modules/billing/lib/idempotency.js +13 -0
  129. package/dist/modules/billing/lib/idempotency.js.map +7 -0
  130. package/dist/modules/billing/lib/invoiceStatus.js +38 -0
  131. package/dist/modules/billing/lib/invoiceStatus.js.map +7 -0
  132. package/dist/modules/billing/lib/invoiceWriter.js +153 -0
  133. package/dist/modules/billing/lib/invoiceWriter.js.map +7 -0
  134. package/dist/modules/billing/lib/itemSelector.js +77 -0
  135. package/dist/modules/billing/lib/itemSelector.js.map +7 -0
  136. package/dist/modules/billing/lib/seeds/demo.js +130 -0
  137. package/dist/modules/billing/lib/seeds/demo.js.map +7 -0
  138. package/dist/modules/billing/lib/tenantLock.js +32 -0
  139. package/dist/modules/billing/lib/tenantLock.js.map +7 -0
  140. package/dist/modules/billing/lib/usageRater.js +150 -0
  141. package/dist/modules/billing/lib/usageRater.js.map +7 -0
  142. package/dist/modules/billing/lib/usageRunner.js +82 -0
  143. package/dist/modules/billing/lib/usageRunner.js.map +7 -0
  144. package/dist/modules/billing/migrations/Migration20260519000000_billing.js +144 -0
  145. package/dist/modules/billing/migrations/Migration20260519000000_billing.js.map +7 -0
  146. package/dist/modules/billing/setup.js +190 -0
  147. package/dist/modules/billing/setup.js.map +7 -0
  148. package/dist/modules/billing/subscribers/payment-captured-to-paid.js +47 -0
  149. package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +7 -0
  150. package/dist/modules/billing/translations.js +5 -0
  151. package/dist/modules/billing/translations.js.map +7 -0
  152. package/dist/modules/billing/workers/reap-stale-bill-runs.js +31 -0
  153. package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +7 -0
  154. package/package.json +148 -0
  155. package/src/modules/billing/__integration__/TC-BILL-001.spec.ts +116 -0
  156. package/src/modules/billing/__integration__/TC-BILL-002.spec.ts +169 -0
  157. package/src/modules/billing/__integration__/TC-BILL-003.spec.ts +28 -0
  158. package/src/modules/billing/__integration__/meta.ts +4 -0
  159. package/src/modules/billing/__tests__/commands/invoices.test.ts +464 -0
  160. package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +284 -0
  161. package/src/modules/billing/__tests__/lib/billPeriod.test.ts +131 -0
  162. package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +561 -0
  163. package/src/modules/billing/__tests__/lib/idempotency.test.ts +81 -0
  164. package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +97 -0
  165. package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +119 -0
  166. package/src/modules/billing/__tests__/lib/itemSelector.test.ts +192 -0
  167. package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +86 -0
  168. package/src/modules/billing/__tests__/lib/tenantLock.test.ts +126 -0
  169. package/src/modules/billing/__tests__/lib/usageRater.test.ts +190 -0
  170. package/src/modules/billing/__tests__/package-isolation.test.ts +223 -0
  171. package/src/modules/billing/__tests__/setup.test.ts +107 -0
  172. package/src/modules/billing/__tests__/validators.test.ts +594 -0
  173. package/src/modules/billing/acl.ts +16 -0
  174. package/src/modules/billing/api/accounts/route.ts +198 -0
  175. package/src/modules/billing/api/export/account/[id]/route.ts +233 -0
  176. package/src/modules/billing/api/invoices/[id]/route.ts +143 -0
  177. package/src/modules/billing/api/invoices/add-line/route.ts +74 -0
  178. package/src/modules/billing/api/invoices/edit-line/route.ts +97 -0
  179. package/src/modules/billing/api/invoices/post/route.ts +97 -0
  180. package/src/modules/billing/api/invoices/remove-line/route.ts +75 -0
  181. package/src/modules/billing/api/invoices/route.ts +167 -0
  182. package/src/modules/billing/api/items/bulk/route.ts +92 -0
  183. package/src/modules/billing/api/items/route.ts +255 -0
  184. package/src/modules/billing/api/openapi.ts +33 -0
  185. package/src/modules/billing/api/run-outcomes/route.ts +149 -0
  186. package/src/modules/billing/api/runs/retry-failed/route.ts +88 -0
  187. package/src/modules/billing/api/runs/route.ts +170 -0
  188. package/src/modules/billing/api/test-invoices/wipe/route.ts +87 -0
  189. package/src/modules/billing/api/usage/route.ts +148 -0
  190. package/src/modules/billing/backend/billing/accounts/[id]/page.meta.ts +23 -0
  191. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +574 -0
  192. package/src/modules/billing/backend/billing/accounts/create/page.meta.ts +23 -0
  193. package/src/modules/billing/backend/billing/accounts/create/page.tsx +101 -0
  194. package/src/modules/billing/backend/billing/accounts/page.meta.ts +16 -0
  195. package/src/modules/billing/backend/billing/accounts/page.tsx +222 -0
  196. package/src/modules/billing/backend/billing/invoices/[id]/page.meta.ts +23 -0
  197. package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +613 -0
  198. package/src/modules/billing/backend/billing/invoices/page.meta.ts +16 -0
  199. package/src/modules/billing/backend/billing/invoices/page.tsx +245 -0
  200. package/src/modules/billing/backend/billing/items/[id]/page.meta.ts +23 -0
  201. package/src/modules/billing/backend/billing/items/[id]/page.tsx +366 -0
  202. package/src/modules/billing/backend/billing/items/create/page.meta.ts +23 -0
  203. package/src/modules/billing/backend/billing/items/create/page.tsx +110 -0
  204. package/src/modules/billing/backend/billing/items/page.meta.ts +16 -0
  205. package/src/modules/billing/backend/billing/items/page.tsx +272 -0
  206. package/src/modules/billing/backend/billing/runs/[id]/page.meta.ts +19 -0
  207. package/src/modules/billing/backend/billing/runs/[id]/page.tsx +404 -0
  208. package/src/modules/billing/backend/billing/runs/page.meta.ts +16 -0
  209. package/src/modules/billing/backend/billing/runs/page.tsx +426 -0
  210. package/src/modules/billing/backend/billing/usage/create/page.meta.ts +23 -0
  211. package/src/modules/billing/backend/billing/usage/create/page.tsx +120 -0
  212. package/src/modules/billing/ce.ts +74 -0
  213. package/src/modules/billing/cli.ts +89 -0
  214. package/src/modules/billing/commands/accounts.ts +205 -0
  215. package/src/modules/billing/commands/index.ts +13 -0
  216. package/src/modules/billing/commands/invoices.ts +542 -0
  217. package/src/modules/billing/commands/items.ts +438 -0
  218. package/src/modules/billing/commands/runs.ts +166 -0
  219. package/src/modules/billing/commands/usage.ts +118 -0
  220. package/src/modules/billing/components/AccountForm.tsx +517 -0
  221. package/src/modules/billing/components/DetailFields.tsx +60 -0
  222. package/src/modules/billing/components/ItemForm.tsx +558 -0
  223. package/src/modules/billing/components/LineFormDialog.tsx +253 -0
  224. package/src/modules/billing/components/UsageForm.tsx +326 -0
  225. package/src/modules/billing/data/entities.ts +533 -0
  226. package/src/modules/billing/data/entityIds.ts +25 -0
  227. package/src/modules/billing/data/validators.ts +562 -0
  228. package/src/modules/billing/di.ts +14 -0
  229. package/src/modules/billing/events.ts +47 -0
  230. package/src/modules/billing/i18n/en.json +336 -0
  231. package/src/modules/billing/i18n/pl.json +336 -0
  232. package/src/modules/billing/index.ts +22 -0
  233. package/src/modules/billing/lib/billPeriod.ts +181 -0
  234. package/src/modules/billing/lib/billRunEngine.ts +416 -0
  235. package/src/modules/billing/lib/billRunLabel.ts +14 -0
  236. package/src/modules/billing/lib/idempotency.ts +46 -0
  237. package/src/modules/billing/lib/invoiceStatus.ts +61 -0
  238. package/src/modules/billing/lib/invoiceWriter.ts +260 -0
  239. package/src/modules/billing/lib/itemSelector.ts +154 -0
  240. package/src/modules/billing/lib/seeds/demo.ts +199 -0
  241. package/src/modules/billing/lib/tenantLock.ts +87 -0
  242. package/src/modules/billing/lib/usageRater.ts +250 -0
  243. package/src/modules/billing/lib/usageRunner.ts +189 -0
  244. package/src/modules/billing/migrations/Migration20260519000000_billing.ts +175 -0
  245. package/src/modules/billing/setup.ts +252 -0
  246. package/src/modules/billing/subscribers/payment-captured-to-paid.ts +89 -0
  247. package/src/modules/billing/translations.ts +9 -0
  248. package/src/modules/billing/workers/reap-stale-bill-runs.ts +57 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dainamite Technologies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @dainamite/billing
2
+
3
+ Lightweight recurring-billing engine for Open Mercato applications.
4
+
5
+ Collects items to charge (`one_time`, `recurring`, `usage`), runs a scheduled
6
+ **Bill Run** that calculates totals and produces **draft invoices** in
7
+ `@open-mercato/core/sales`, and waits for human approval before posting. It
8
+ does **not** replace your accounting system — it prepares billing data and
9
+ hands drafts to a human.
10
+
11
+ ## Status
12
+
13
+ **Built — publishing soon.** Schema, ACL, tenant setup, the Bill Run engine,
14
+ usage rating, the full admin UI, and the `@dainamite/cpq-billing-connector`
15
+ companion are all in place. The package is not yet on npm; until it publishes
16
+ it is available inside the `dainamite-core` monorepo.
17
+
18
+ See [`specs/implementation/xd-249-billing-spec.md`](../../specs/implementation/xd-249-billing-spec.md)
19
+ for the full implementation plan.
20
+
21
+ ## What's inside
22
+
23
+ - **Billing Items** — the charge backlog (`one_time`, `recurring`, `usage`),
24
+ each scoped to a Billing Account with a bill start/end window.
25
+ - **Bill Runs** — a background worker (`@open-mercato/queue`) that, on a
26
+ schedule or on demand, rates usage, sums the due items for the period, and
27
+ writes **draft invoices into `@open-mercato/core/sales`**. Runs are
28
+ idempotent and tenant-locked; stale runs are reaped by a worker.
29
+ - **Draft invoice approval** — invoices land in `draft`; a human reviews and
30
+ **posts** them (a guarded ACL action) before they leave draft.
31
+ - **Usage rating** — metered items are rated at run time from ingested usage.
32
+ - **Admin UI** under `/backend/billing/*` — Accounts, Items, Bill Runs,
33
+ Invoices (list + detail pages).
34
+ - **Commands** for every write (accounts / items / runs / invoices / usage),
35
+ validated with Zod.
36
+
37
+ ## Installation
38
+
39
+ `@dainamite/billing` is an Open Mercato module. Install it and register it in
40
+ your host app's `src/modules.ts`:
41
+
42
+ ```bash
43
+ yarn mercato module add @dainamite/billing # installs + registers (once published to npm)
44
+ # …or: yarn add @dainamite/billing then add { id: 'billing', from: '@dainamite/billing' }
45
+ ```
46
+
47
+ ```bash
48
+ yarn generate
49
+ yarn mercato db migrate
50
+ ```
51
+
52
+ ### Required Open Mercato modules
53
+
54
+ `sales` (draft invoices are written there), `@open-mercato/queue` (Bill Run
55
+ worker), and `@open-mercato/events` (lifecycle events consumed by the
56
+ connector). Engines: **Node ≥ 24**.
57
+
58
+ ## CPQ integration
59
+
60
+ To bill subscriptions sold through CPQ, add
61
+ [`@dainamite/cpq-billing-connector`](../cpq-billing-connector) — it subscribes
62
+ to CPQ subscription lifecycle events and creates/updates Billing Accounts and
63
+ Items automatically. Register it **after** both `cpq` and `billing`.
64
+
65
+ ## Distribution rules (per SPEC-001)
66
+
67
+ - **`peerDependencies`, never `dependencies`** for `@open-mercato/*` and
68
+ sibling `@dainamite/*` — one shared instance in `node_modules`.
69
+ - **No ORM relations across packages** — cross-module references are FK
70
+ strings; draft invoices are created through the sales service / events, not
71
+ a direct relation.
72
+ - **Ships its own migrations** under `src/modules/billing/migrations/`.
73
+ - Ships `src/` (types) + `dist/` (runtime JS); no `.d.ts` emitted.
74
+
75
+ ## License
76
+
77
+ MIT.
@@ -0,0 +1,20 @@
1
+ const features = [
2
+ { id: "billing.account.manage", title: "Manage Billing Accounts", module: "billing" },
3
+ { id: "billing.account.view", title: "View Billing Accounts", module: "billing" },
4
+ { id: "billing.item.manage", title: "Manage Billing Items", module: "billing" },
5
+ { id: "billing.item.view", title: "View Billing Items", module: "billing" },
6
+ { id: "billing.usage.ingest", title: "Ingest Usage Records", module: "billing" },
7
+ { id: "billing.usage.view", title: "View Usage Records", module: "billing" },
8
+ { id: "billing.run.trigger", title: "Trigger Bill Run", module: "billing" },
9
+ { id: "billing.run.dry_run", title: "Trigger Bill Run (dry-run)", module: "billing" },
10
+ { id: "billing.run.view", title: "View Bill Run History", module: "billing" },
11
+ { id: "billing.invoice.post", title: "Post Draft Invoice", module: "billing" },
12
+ { id: "billing.invoice.edit_draft", title: "Edit Draft Invoice", module: "billing" },
13
+ { id: "billing.invoice.view", title: "View Billing Invoices", module: "billing" }
14
+ ];
15
+ var acl_default = features;
16
+ export {
17
+ acl_default as default,
18
+ features
19
+ };
20
+ //# sourceMappingURL=acl.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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;",
6
+ "names": []
7
+ }
@@ -0,0 +1,182 @@
1
+ import { z } from "zod";
2
+ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
+ import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
4
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { BillingAccount } from "../../data/entities.js";
6
+ import { billingEntityIds } from "../../data/entityIds.js";
7
+ import {
8
+ billingAccountCreateSchema,
9
+ billingAccountListQuerySchema,
10
+ billingAccountUpdateSchema
11
+ } from "../../data/validators.js";
12
+ import {
13
+ createBillingCrudOpenApi,
14
+ createPagedListResponseSchema,
15
+ defaultCreateResponseSchema,
16
+ defaultOkResponseSchema
17
+ } from "../openapi.js";
18
+ const routeMetadata = {
19
+ GET: { requireAuth: true, requireFeatures: ["billing.account.view"] },
20
+ POST: { requireAuth: true, requireFeatures: ["billing.account.manage"] },
21
+ PUT: { requireAuth: true, requireFeatures: ["billing.account.manage"] },
22
+ DELETE: { requireAuth: true, requireFeatures: ["billing.account.manage"] }
23
+ };
24
+ const metadata = routeMetadata;
25
+ const billingAccountListItemSchema = z.object({
26
+ id: z.string().uuid(),
27
+ customer_id: z.string(),
28
+ name: z.string(),
29
+ currency_code: z.string(),
30
+ bill_cycle: z.string(),
31
+ bill_cycle_anchor: z.string(),
32
+ invoice_email: z.string(),
33
+ invoice_language: z.string(),
34
+ tax_id: z.string().nullable(),
35
+ invoice_address: z.record(z.string(), z.unknown()),
36
+ next_bill_date: z.string(),
37
+ last_bill_date: z.string().nullable(),
38
+ is_active: z.boolean(),
39
+ organization_id: z.string().uuid(),
40
+ tenant_id: z.string().uuid(),
41
+ created_at: z.string(),
42
+ updated_at: z.string()
43
+ });
44
+ const rawBodySchema = z.object({}).passthrough();
45
+ const crud = makeCrudRoute({
46
+ metadata: routeMetadata,
47
+ orm: {
48
+ entity: BillingAccount,
49
+ idField: "id",
50
+ orgField: "organizationId",
51
+ tenantField: "tenantId",
52
+ softDeleteField: "deletedAt"
53
+ },
54
+ list: {
55
+ schema: billingAccountListQuerySchema,
56
+ entityId: billingEntityIds.account,
57
+ fields: [
58
+ "id",
59
+ "customer_id",
60
+ "name",
61
+ "currency_code",
62
+ "bill_cycle",
63
+ "bill_cycle_anchor",
64
+ "invoice_email",
65
+ "invoice_language",
66
+ "tax_id",
67
+ "invoice_address",
68
+ "next_bill_date",
69
+ "last_bill_date",
70
+ "is_active",
71
+ "organization_id",
72
+ "tenant_id",
73
+ "created_at",
74
+ "updated_at"
75
+ ],
76
+ sortFieldMap: {
77
+ name: "name",
78
+ createdAt: "created_at",
79
+ updatedAt: "updated_at",
80
+ nextBillDate: "next_bill_date"
81
+ },
82
+ buildFilters: async (query) => {
83
+ const filters = {};
84
+ if (typeof query.id === "string" && query.id) {
85
+ filters.id = { $eq: query.id };
86
+ }
87
+ if (typeof query.customerId === "string" && query.customerId) {
88
+ filters.customer_id = { $eq: query.customerId };
89
+ }
90
+ if (typeof query.currencyCode === "string" && query.currencyCode) {
91
+ filters.currency_code = { $eq: query.currencyCode.toUpperCase() };
92
+ }
93
+ if (typeof query.billCycle === "string" && query.billCycle) {
94
+ filters.bill_cycle = { $eq: query.billCycle };
95
+ }
96
+ if (typeof query.isActive === "boolean") {
97
+ filters.is_active = { $eq: query.isActive };
98
+ }
99
+ if (typeof query.search === "string" && query.search.trim()) {
100
+ const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\${m}`)}%`;
101
+ filters.name = { $ilike: pattern };
102
+ }
103
+ return filters;
104
+ }
105
+ },
106
+ actions: {
107
+ create: {
108
+ commandId: "billing.accounts.create",
109
+ schema: rawBodySchema,
110
+ mapInput: async ({ raw, ctx }) => {
111
+ const { translate } = await resolveTranslations();
112
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate);
113
+ return billingAccountCreateSchema.parse(scoped);
114
+ },
115
+ response: ({ result }) => ({ id: result?.id ?? null }),
116
+ status: 201
117
+ },
118
+ update: {
119
+ commandId: "billing.accounts.update",
120
+ schema: rawBodySchema,
121
+ mapInput: async ({ raw, ctx }) => {
122
+ const { translate } = await resolveTranslations();
123
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate);
124
+ return billingAccountUpdateSchema.parse(scoped);
125
+ },
126
+ response: () => ({ ok: true })
127
+ },
128
+ delete: {
129
+ commandId: "billing.accounts.delete",
130
+ schema: rawBodySchema,
131
+ mapInput: async ({ raw, ctx }) => {
132
+ const { translate } = await resolveTranslations();
133
+ const envelope = raw ?? {};
134
+ const body = envelope.body ?? {};
135
+ const query = envelope.query ?? {};
136
+ const scoped = withScopedPayload(body, ctx, translate);
137
+ const id = typeof body.id === "string" && body.id || typeof query.id === "string" && query.id || null;
138
+ if (!id) {
139
+ throw new Error("id is required");
140
+ }
141
+ return {
142
+ id,
143
+ tenantId: scoped.tenantId,
144
+ organizationId: scoped.organizationId
145
+ };
146
+ },
147
+ response: () => ({ ok: true })
148
+ }
149
+ }
150
+ });
151
+ const { POST, PUT, DELETE } = crud;
152
+ const GET = crud.GET;
153
+ const openApi = createBillingCrudOpenApi({
154
+ resourceName: "BillingAccount",
155
+ pluralName: "BillingAccounts",
156
+ querySchema: billingAccountListQuerySchema,
157
+ listResponseSchema: createPagedListResponseSchema(billingAccountListItemSchema),
158
+ create: {
159
+ schema: billingAccountCreateSchema,
160
+ responseSchema: defaultCreateResponseSchema,
161
+ description: "Creates a new billing account for a customer."
162
+ },
163
+ update: {
164
+ schema: billingAccountUpdateSchema,
165
+ responseSchema: defaultOkResponseSchema,
166
+ description: "Updates a billing account. `currencyCode` and `customerId` are immutable."
167
+ },
168
+ del: {
169
+ schema: z.object({ id: z.string().uuid() }),
170
+ responseSchema: defaultOkResponseSchema,
171
+ description: "Soft-deletes a billing account (sets `deleted_at`)."
172
+ }
173
+ });
174
+ export {
175
+ DELETE,
176
+ GET,
177
+ POST,
178
+ PUT,
179
+ metadata,
180
+ openApi
181
+ };
182
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,170 @@
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 {
5
+ BillingAccount,
6
+ BillingAccountUsage,
7
+ BillingItem,
8
+ BillRunOutcome,
9
+ DraftInvoiceEdit
10
+ } from "../../../../data/entities.js";
11
+ const metadata = {
12
+ GET: { requireAuth: true, requireFeatures: ["billing.account.view"] }
13
+ };
14
+ async function GET(req, { params }) {
15
+ let container = null;
16
+ try {
17
+ container = await createRequestContainer();
18
+ const auth = await getAuthFromRequest(req);
19
+ if (!auth || !auth.tenantId) {
20
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
21
+ }
22
+ const tenantId = auth.tenantId;
23
+ const organizationId = auth.orgId ?? null;
24
+ if (!organizationId) {
25
+ return NextResponse.json(
26
+ { error: "Organization scope required" },
27
+ { status: 403 }
28
+ );
29
+ }
30
+ const { id: accountId } = await params;
31
+ const em = container.resolve("em").fork();
32
+ const account = await em.findOne(BillingAccount, {
33
+ id: accountId,
34
+ tenantId,
35
+ organizationId,
36
+ deletedAt: null
37
+ });
38
+ if (!account) {
39
+ return NextResponse.json({ error: "Account not found" }, { status: 404 });
40
+ }
41
+ const url = new URL(req.url);
42
+ const since = parseDate(url.searchParams.get("since")) ?? defaultSince();
43
+ const until = parseDate(url.searchParams.get("until")) ?? /* @__PURE__ */ new Date();
44
+ const [items, usage, outcomes] = await Promise.all([
45
+ // Items are bounded by integration volume (typically <100 per
46
+ // account); no time-window needed — they often outlive a single
47
+ // export window and the operator needs to see active items
48
+ // regardless of when they were created.
49
+ em.find(
50
+ BillingItem,
51
+ { tenantId, organizationId, billAccountId: account.id },
52
+ { limit: EXPORT_HARD_LIMIT }
53
+ ),
54
+ em.find(
55
+ BillingAccountUsage,
56
+ {
57
+ tenantId,
58
+ organizationId,
59
+ billAccountId: account.id,
60
+ periodEnd: { $gte: since, $lte: until }
61
+ },
62
+ { limit: EXPORT_HARD_LIMIT, orderBy: { periodEnd: "DESC" } }
63
+ ),
64
+ em.find(
65
+ BillRunOutcome,
66
+ {
67
+ tenantId,
68
+ organizationId,
69
+ billAccountId: account.id,
70
+ createdAt: { $gte: since, $lte: until }
71
+ },
72
+ { limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: "DESC" } }
73
+ )
74
+ ]);
75
+ const invoiceRows = await em.execute(
76
+ `SELECT id, invoice_number, status, status_entry_id, issue_date, due_date,
77
+ currency_code, subtotal_net_amount, grand_total_net_amount,
78
+ grand_total_gross_amount, paid_total_amount, outstanding_amount,
79
+ metadata, created_at, updated_at, deleted_at
80
+ FROM sales_invoices
81
+ WHERE tenant_id = ?
82
+ AND organization_id = ?
83
+ AND deleted_at IS NULL
84
+ AND metadata->>'bill_account_id' = ?
85
+ AND created_at >= ?
86
+ AND created_at <= ?
87
+ ORDER BY created_at DESC
88
+ LIMIT ?`,
89
+ [tenantId, organizationId, account.id, since, until, EXPORT_HARD_LIMIT]
90
+ );
91
+ const invoiceIds = invoiceRows.map((r) => r.id);
92
+ const draftEdits = invoiceIds.length > 0 ? await em.find(
93
+ DraftInvoiceEdit,
94
+ {
95
+ tenantId,
96
+ organizationId,
97
+ invoiceId: { $in: invoiceIds }
98
+ },
99
+ { limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: "DESC" } }
100
+ ) : [];
101
+ return NextResponse.json({
102
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
103
+ schemaVersion: 1,
104
+ tenantId,
105
+ organizationId,
106
+ window: {
107
+ since: since.toISOString(),
108
+ until: until.toISOString(),
109
+ hardLimit: EXPORT_HARD_LIMIT
110
+ },
111
+ account,
112
+ items,
113
+ usage,
114
+ runOutcomes: outcomes,
115
+ invoices: invoiceRows,
116
+ draftInvoiceEdits: draftEdits
117
+ });
118
+ } catch (err) {
119
+ console.error("[billing/export/account.GET]", err);
120
+ return NextResponse.json({ error: "Internal error" }, { status: 500 });
121
+ }
122
+ }
123
+ const EXPORT_HARD_LIMIT = 5e4;
124
+ function defaultSince() {
125
+ const d = /* @__PURE__ */ new Date();
126
+ d.setUTCFullYear(d.getUTCFullYear() - 1);
127
+ return d;
128
+ }
129
+ function parseDate(value) {
130
+ if (!value) return null;
131
+ const d = new Date(value);
132
+ return Number.isNaN(d.getTime()) ? null : d;
133
+ }
134
+ const openApi = {
135
+ methods: {
136
+ GET: {
137
+ tags: ["Billing"],
138
+ summary: "Export a billing account (GDPR portability)",
139
+ parameters: [
140
+ {
141
+ name: "id",
142
+ in: "path",
143
+ required: true,
144
+ schema: { type: "string", format: "uuid" }
145
+ },
146
+ {
147
+ name: "since",
148
+ in: "query",
149
+ required: false,
150
+ description: "ISO timestamp; lower bound for usage / outcomes / invoices windows. Defaults to today \u2212 12 months.",
151
+ schema: { type: "string", format: "date-time" }
152
+ },
153
+ {
154
+ name: "until",
155
+ in: "query",
156
+ required: false,
157
+ description: "ISO timestamp; upper bound. Defaults to now.",
158
+ schema: { type: "string", format: "date-time" }
159
+ }
160
+ ],
161
+ description: "Returns a JSON dump of a single Billing Account: the account itself, every Billing Item (no time cap \u2014 items often outlive a single export window), and the Usage records / BillRunOutcomes / emitted core/sales invoices / DraftInvoiceEdit rows whose timestamps fall in the [since, until] window (defaults to the last 12 months). Each collection is independently capped by EXPORT_HARD_LIMIT = 50,000 rows. Lifetime dumps for outlier accounts need a follow-up chunked background-job approach \u2014 out of scope for this v1 endpoint."
162
+ }
163
+ }
164
+ };
165
+ export {
166
+ GET,
167
+ metadata,
168
+ openApi
169
+ };
170
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/billing/api/export/account/%5Bid%5D/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 {\n BillingAccount,\n BillingAccountUsage,\n BillingItem,\n BillRunOutcome,\n DraftInvoiceEdit,\n} from '../../../../data/entities'\n\n/**\n * `GET /api/billing/export/account/[id]` \u2014 GDPR portability dump.\n *\n * Returns a full JSON serialization of a single Billing Account\n * including:\n * - The account row itself.\n * - Every active + soft-deleted `BillingItem` for the account.\n * - Every `BillingAccountUsage` record.\n * - Every `BillRunOutcome` row referencing the account.\n * - Every `core/sales` invoice the engine emitted for the account\n * (matched via `metadata->>'bill_account_id'`).\n * - Every `DraftInvoiceEdit` audit row for those invoices.\n *\n * Scope: the caller's auth tenant/organization is the authority \u2014 a\n * 404 is returned even for valid UUIDs that belong to a different\n * tenant (no cross-tenant leak).\n *\n * Soft-deleted rows are included on purpose: GDPR portability is\n * about the data we still hold, not just the data the UI shows.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.account.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 tenantId = auth.tenantId\n const organizationId = (auth as { orgId?: string }).orgId ?? null\n if (!organizationId) {\n return NextResponse.json(\n { error: 'Organization scope required' },\n { status: 403 },\n )\n }\n\n const { id: accountId } = await params\n\n const em = (container.resolve('em') as EntityManager).fork()\n const account = await em.findOne(BillingAccount, {\n id: accountId,\n tenantId,\n organizationId,\n deletedAt: null,\n })\n if (!account) {\n return NextResponse.json({ error: 'Account not found' }, { status: 404 })\n }\n\n // Time-window cap for usage records, run outcomes and invoices.\n //\n // Scale note: a telco account can carry millions of usage rows.\n // Returning everything would OOM on the export path. We default\n // to the last 12 months and let the caller override via `since` /\n // `until` for full GDPR-compliance dumps (a regulator request that\n // legitimately needs the lifetime export should set `since=1970-01-01`).\n // Each query is independently capped by `EXPORT_HARD_LIMIT` so a\n // single year of pathological volume still returns rather than\n // hanging.\n const url = new URL(req.url)\n const since = parseDate(url.searchParams.get('since'))\n ?? defaultSince()\n const until = parseDate(url.searchParams.get('until')) ?? new Date()\n\n const [items, usage, outcomes] = await Promise.all([\n // Items are bounded by integration volume (typically <100 per\n // account); no time-window needed \u2014 they often outlive a single\n // export window and the operator needs to see active items\n // regardless of when they were created.\n em.find(\n BillingItem,\n { tenantId, organizationId, billAccountId: account.id } as never,\n { limit: EXPORT_HARD_LIMIT },\n ),\n em.find(\n BillingAccountUsage,\n {\n tenantId,\n organizationId,\n billAccountId: account.id,\n periodEnd: { $gte: since, $lte: until },\n } as never,\n { limit: EXPORT_HARD_LIMIT, orderBy: { periodEnd: 'DESC' } },\n ),\n em.find(\n BillRunOutcome,\n {\n tenantId,\n organizationId,\n billAccountId: account.id,\n createdAt: { $gte: since, $lte: until },\n } as never,\n { limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: 'DESC' } },\n ),\n ])\n\n // Invoices created in the window. The `metadata->>'bill_account_id'`\n // predicate keeps non-billing core/sales invoices out.\n const invoiceRows = (await em.execute(\n `SELECT id, invoice_number, status, status_entry_id, issue_date, due_date,\n currency_code, subtotal_net_amount, grand_total_net_amount,\n grand_total_gross_amount, paid_total_amount, outstanding_amount,\n metadata, created_at, updated_at, deleted_at\n FROM sales_invoices\n WHERE tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND metadata->>'bill_account_id' = ?\n AND created_at >= ?\n AND created_at <= ?\n ORDER BY created_at DESC\n LIMIT ?`,\n [tenantId, organizationId, account.id, since, until, EXPORT_HARD_LIMIT],\n )) as unknown as Array<{ id: string }>\n\n const invoiceIds = invoiceRows.map((r) => r.id)\n const draftEdits =\n invoiceIds.length > 0\n ? await em.find(\n DraftInvoiceEdit,\n {\n tenantId,\n organizationId,\n invoiceId: { $in: invoiceIds },\n } as never,\n { limit: EXPORT_HARD_LIMIT, orderBy: { createdAt: 'DESC' } },\n )\n : []\n\n return NextResponse.json({\n exportedAt: new Date().toISOString(),\n schemaVersion: 1,\n tenantId,\n organizationId,\n window: {\n since: since.toISOString(),\n until: until.toISOString(),\n hardLimit: EXPORT_HARD_LIMIT,\n },\n account,\n items,\n usage,\n runOutcomes: outcomes,\n invoices: invoiceRows,\n draftInvoiceEdits: draftEdits,\n })\n } catch (err) {\n console.error('[billing/export/account.GET]', err)\n return NextResponse.json({ error: 'Internal error' }, { status: 500 })\n }\n}\n\n/**\n * Per-collection cap to keep a single export bounded in size and time\n * even when the caller asks for `since=1970-01-01`. The full GDPR\n * lifetime dump for an outlier account requires a follow-up\n * background-job approach (chunked NDJSON to object storage) \u2014 out of\n * scope for the v1 admin export endpoint.\n */\nconst EXPORT_HARD_LIMIT = 50_000\n\n/**\n * Default time window for the export \u2014 last 12 months. Operators\n * pulling for routine GDPR requests almost always want a finite\n * window; the regulator-compliance \"lifetime\" case overrides via\n * `?since=1970-01-01`.\n */\nfunction defaultSince(): Date {\n const d = new Date()\n d.setUTCFullYear(d.getUTCFullYear() - 1)\n return d\n}\n\nfunction parseDate(value: string | null | undefined): Date | null {\n if (!value) return null\n const d = new Date(value)\n return Number.isNaN(d.getTime()) ? null : d\n}\n\nexport const openApi = {\n methods: {\n GET: {\n tags: ['Billing'],\n summary: 'Export a billing account (GDPR portability)',\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n {\n name: 'since',\n in: 'query',\n required: false,\n description:\n 'ISO timestamp; lower bound for usage / outcomes / invoices windows. Defaults to today \u2212 12 months.',\n schema: { type: 'string', format: 'date-time' },\n },\n {\n name: 'until',\n in: 'query',\n required: false,\n description: 'ISO timestamp; upper bound. Defaults to now.',\n schema: { type: 'string', format: 'date-time' },\n },\n ],\n description:\n \"Returns a JSON dump of a single Billing Account: the account itself, every Billing Item (no time cap \u2014 items often outlive a single export window), and the Usage records / BillRunOutcomes / emitted core/sales invoices / DraftInvoiceEdit rows whose timestamps fall in the [since, until] window (defaults to the last 12 months). Each collection is independently capped by EXPORT_HARD_LIMIT = 50,000 rows. Lifetime dumps for outlier accounts need a follow-up chunked background-job approach \u2014 out of scope for this v1 endpoint.\",\n },\n },\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,85 @@
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
+ const metadata = {
5
+ GET: { requireAuth: true, requireFeatures: ["billing.invoice.view"] }
6
+ };
7
+ async function GET(req, { params }) {
8
+ try {
9
+ const container = await createRequestContainer();
10
+ const auth = await getAuthFromRequest(req);
11
+ if (!auth || !auth.tenantId) {
12
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
13
+ }
14
+ const organizationId = auth.orgId ?? null;
15
+ if (!organizationId) {
16
+ return NextResponse.json(
17
+ { error: "Organization scope required" },
18
+ { status: 403 }
19
+ );
20
+ }
21
+ const { id: invoiceId } = await params;
22
+ const em = container.resolve("em").fork();
23
+ const invoiceRows = await em.execute(
24
+ `SELECT id, invoice_number, status, status_entry_id, currency_code,
25
+ issue_date, due_date,
26
+ subtotal_net_amount, subtotal_gross_amount,
27
+ discount_total_amount, tax_total_amount,
28
+ grand_total_net_amount, grand_total_gross_amount,
29
+ paid_total_amount, outstanding_amount,
30
+ metadata, created_at, updated_at
31
+ FROM sales_invoices
32
+ WHERE id = ?
33
+ AND tenant_id = ?
34
+ AND organization_id = ?
35
+ AND deleted_at IS NULL
36
+ AND jsonb_exists(metadata, 'bill_run_id')`,
37
+ [invoiceId, auth.tenantId, organizationId]
38
+ );
39
+ if (!invoiceRows || invoiceRows.length === 0) {
40
+ return NextResponse.json({ error: "Invoice not found" }, { status: 404 });
41
+ }
42
+ const invoice = invoiceRows[0];
43
+ const lineRows = await em.execute(
44
+ `SELECT id, line_number, kind, description, quantity, currency_code,
45
+ unit_price_net, unit_price_gross, tax_rate, tax_amount,
46
+ total_net_amount, total_gross_amount, metadata
47
+ FROM sales_invoice_lines
48
+ WHERE invoice_id = ?
49
+ AND tenant_id = ?
50
+ AND organization_id = ?
51
+ ORDER BY line_number ASC`,
52
+ [invoiceId, auth.tenantId, organizationId]
53
+ );
54
+ return NextResponse.json({
55
+ invoice,
56
+ lines: lineRows
57
+ });
58
+ } catch (err) {
59
+ console.error("[billing/invoices/[id].GET]", err);
60
+ return NextResponse.json({ error: "Internal error" }, { status: 500 });
61
+ }
62
+ }
63
+ const openApi = {
64
+ methods: {
65
+ GET: {
66
+ tags: ["Billing"],
67
+ summary: "Get a billing-managed invoice (with lines)",
68
+ description: "Returns the `core/sales` invoice + every line, scoped to billing-managed invoices (`metadata.bill_run_id` set). Non-billing invoices return 404 even when the UUID resolves at `/api/sales/invoices/[id]`.",
69
+ parameters: [
70
+ {
71
+ name: "id",
72
+ in: "path",
73
+ required: true,
74
+ schema: { type: "string", format: "uuid" }
75
+ }
76
+ ]
77
+ }
78
+ }
79
+ };
80
+ export {
81
+ GET,
82
+ metadata,
83
+ openApi
84
+ };
85
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/billing/api/invoices/%5Bid%5D/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'\n\n/**\n * `GET /api/billing/invoices/[id]` \u2014 billing-invoice detail.\n *\n * Returns the `core/sales` invoice row + its lines, but only if the\n * invoice is billing-managed (carries `metadata.bill_run_id`). Other\n * `core/sales` invoices return 404 here even though the same UUID\n * resolves at `/api/sales/invoices/[id]` \u2014 this endpoint is the\n * billing operator's surface, scoped to the billing universe.\n */\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },\n}\n\ntype InvoiceRow = {\n id: string\n invoice_number: string\n status: string | null\n status_entry_id: string | null\n currency_code: string\n issue_date: string | null\n due_date: string | null\n subtotal_net_amount: string\n subtotal_gross_amount: string\n discount_total_amount: string\n tax_total_amount: string\n grand_total_net_amount: string\n grand_total_gross_amount: string\n paid_total_amount: string\n outstanding_amount: string\n metadata: Record<string, unknown> | null\n created_at: string\n updated_at: string\n}\n\ntype InvoiceLineRow = {\n id: string\n line_number: number\n kind: string\n description: string | null\n quantity: string\n currency_code: string\n unit_price_net: string\n unit_price_gross: string\n tax_rate: string\n tax_amount: string\n total_net_amount: string\n total_gross_amount: string\n metadata: Record<string, unknown> | null\n}\n\nexport async function GET(\n req: Request,\n { params }: { params: Promise<{ id: string }> },\n) {\n try {\n const 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(\n { error: 'Organization scope required' },\n { status: 403 },\n )\n }\n\n const { id: invoiceId } = await params\n const em = (container.resolve('em') as EntityManager).fork()\n\n const invoiceRows = (await em.execute(\n `SELECT id, invoice_number, status, status_entry_id, currency_code,\n issue_date, due_date,\n subtotal_net_amount, subtotal_gross_amount,\n discount_total_amount, tax_total_amount,\n grand_total_net_amount, grand_total_gross_amount,\n paid_total_amount, outstanding_amount,\n metadata, created_at, updated_at\n FROM sales_invoices\n WHERE id = ?\n AND tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND jsonb_exists(metadata, 'bill_run_id')`,\n [invoiceId, auth.tenantId, organizationId],\n )) as unknown as InvoiceRow[]\n\n if (!invoiceRows || invoiceRows.length === 0) {\n return NextResponse.json({ error: 'Invoice not found' }, { status: 404 })\n }\n\n const invoice = invoiceRows[0]\n\n const lineRows = (await em.execute(\n `SELECT id, line_number, kind, description, quantity, currency_code,\n unit_price_net, unit_price_gross, tax_rate, tax_amount,\n total_net_amount, total_gross_amount, metadata\n FROM sales_invoice_lines\n WHERE invoice_id = ?\n AND tenant_id = ?\n AND organization_id = ?\n ORDER BY line_number ASC`,\n [invoiceId, auth.tenantId, organizationId],\n )) as unknown as InvoiceLineRow[]\n\n return NextResponse.json({\n invoice,\n lines: lineRows,\n })\n } catch (err) {\n console.error('[billing/invoices/[id].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 billing-managed invoice (with lines)',\n description:\n 'Returns the `core/sales` invoice + every line, scoped to ' +\n \"billing-managed invoices (`metadata.bill_run_id` set). Non-\" +\n 'billing invoices return 404 even when the UUID resolves at ' +\n '`/api/sales/invoices/[id]`.',\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n },\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AAY5B,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,sBAAsB,EAAE;AACtE;AAuCA,eAAsB,IACpB,KACA,EAAE,OAAO,GACT;AACA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,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;AAAA,QAClB,EAAE,OAAO,8BAA8B;AAAA,QACvC,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAEA,UAAM,EAAE,IAAI,UAAU,IAAI,MAAM;AAChC,UAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAE3D,UAAM,cAAe,MAAM,GAAG;AAAA,MAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAaA,CAAC,WAAW,KAAK,UAAU,cAAc;AAAA,IAC3C;AAEA,QAAI,CAAC,eAAe,YAAY,WAAW,GAAG;AAC5C,aAAO,aAAa,KAAK,EAAE,OAAO,oBAAoB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC1E;AAEA,UAAM,UAAU,YAAY,CAAC;AAE7B,UAAM,WAAY,MAAM,GAAG;AAAA,MACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,CAAC,WAAW,KAAK,UAAU,cAAc;AAAA,IAC3C;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,YAAQ,MAAM,+BAA+B,GAAG;AAChD,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,aACE;AAAA,MAIF,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,UAAU;AAAA,UACV,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }