@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
@@ -0,0 +1,594 @@
1
+ /**
2
+ * Validator tests for rate_json shapes — the contract used by the Bill Run
3
+ * engine to trust pricing definitions. Every legal shape must parse;
4
+ * every shape rejected by the spec must throw at parse time.
5
+ */
6
+
7
+ import {
8
+ BILL_CYCLES,
9
+ BILLING_ITEM_TYPES,
10
+ BILL_RUN_STATUSES,
11
+ INVOICE_STATUSES,
12
+ billCycleAnchorSchema,
13
+ invoiceAddressSchema,
14
+ rateJsonOneTime,
15
+ rateJsonRecurring,
16
+ rateJsonUsageFlat,
17
+ rateJsonUsageGraduated,
18
+ rateJsonUsageSimple,
19
+ rateJsonUsageTiered,
20
+ rateJsonUsageVolume,
21
+ validateRateJson,
22
+ } from '../data/validators'
23
+
24
+ describe('rate_json — one_time', () => {
25
+ it('accepts a positive amount', () => {
26
+ expect(rateJsonOneTime.parse({ amount: 99 })).toEqual({ amount: 99 })
27
+ })
28
+
29
+ it('accepts zero', () => {
30
+ expect(rateJsonOneTime.parse({ amount: 0 })).toEqual({ amount: 0 })
31
+ })
32
+
33
+ it('accepts negative amount (refund / credit / negative proration)', () => {
34
+ expect(rateJsonOneTime.parse({ amount: -25.5 })).toEqual({ amount: -25.5 })
35
+ })
36
+
37
+ it('rejects missing amount', () => {
38
+ expect(() => rateJsonOneTime.parse({})).toThrow()
39
+ })
40
+
41
+ it('rejects non-numeric amount', () => {
42
+ expect(() => rateJsonOneTime.parse({ amount: '99' })).toThrow()
43
+ })
44
+
45
+ it('rejects extra keys (strict)', () => {
46
+ expect(() => rateJsonOneTime.parse({ amount: 99, unit_price: 49 })).toThrow()
47
+ })
48
+ })
49
+
50
+ describe('rate_json — recurring', () => {
51
+ it('accepts positive unit_price', () => {
52
+ expect(rateJsonRecurring.parse({ unit_price: 49.99 })).toEqual({ unit_price: 49.99 })
53
+ })
54
+
55
+ it('accepts zero', () => {
56
+ expect(rateJsonRecurring.parse({ unit_price: 0 })).toEqual({ unit_price: 0 })
57
+ })
58
+
59
+ it('rejects negative unit_price (discounts must be one_time credit items)', () => {
60
+ expect(() => rateJsonRecurring.parse({ unit_price: -1 })).toThrow()
61
+ })
62
+
63
+ it('rejects missing unit_price', () => {
64
+ expect(() => rateJsonRecurring.parse({})).toThrow()
65
+ })
66
+ })
67
+
68
+ describe('rate_json — usage simple', () => {
69
+ it('accepts positive unit_price', () => {
70
+ expect(rateJsonUsageSimple.parse({ unit_price: 0.001 })).toEqual({ unit_price: 0.001 })
71
+ })
72
+
73
+ it('rejects negative unit_price', () => {
74
+ expect(() => rateJsonUsageSimple.parse({ unit_price: -0.001 })).toThrow()
75
+ })
76
+ })
77
+
78
+ describe('rate_json — usage tiered (volume)', () => {
79
+ it('accepts a well-formed volume tier ladder', () => {
80
+ const value = {
81
+ model: 'volume' as const,
82
+ tiers: [
83
+ { up_to: 10000, unit_price: 0 },
84
+ { up_to: 50000, unit_price: 0.001 },
85
+ { up_to: null, unit_price: 0.0005 },
86
+ ],
87
+ }
88
+ expect(rateJsonUsageVolume.parse(value)).toEqual(value)
89
+ })
90
+
91
+ it('rejects ladder without an open-ended top tier', () => {
92
+ expect(() =>
93
+ rateJsonUsageVolume.parse({
94
+ model: 'volume',
95
+ tiers: [
96
+ { up_to: 10000, unit_price: 0 },
97
+ { up_to: 50000, unit_price: 0.001 },
98
+ ],
99
+ }),
100
+ ).toThrow()
101
+ })
102
+
103
+ it('rejects ladder with multiple open-ended top tiers', () => {
104
+ expect(() =>
105
+ rateJsonUsageVolume.parse({
106
+ model: 'volume',
107
+ tiers: [
108
+ { up_to: null, unit_price: 0 },
109
+ { up_to: null, unit_price: 0.001 },
110
+ ],
111
+ }),
112
+ ).toThrow()
113
+ })
114
+
115
+ it('rejects ladder where up_to values are not strictly increasing', () => {
116
+ expect(() =>
117
+ rateJsonUsageVolume.parse({
118
+ model: 'volume',
119
+ tiers: [
120
+ { up_to: 50000, unit_price: 0 },
121
+ { up_to: 10000, unit_price: 0.001 },
122
+ { up_to: null, unit_price: 0.0005 },
123
+ ],
124
+ }),
125
+ ).toThrow()
126
+ })
127
+
128
+ it('rejects ladder where open-ended tier is not last', () => {
129
+ expect(() =>
130
+ rateJsonUsageVolume.parse({
131
+ model: 'volume',
132
+ tiers: [
133
+ { up_to: null, unit_price: 0.0005 },
134
+ { up_to: 10000, unit_price: 0 },
135
+ ],
136
+ }),
137
+ ).toThrow()
138
+ })
139
+
140
+ it('rejects negative unit_price in a tier', () => {
141
+ expect(() =>
142
+ rateJsonUsageVolume.parse({
143
+ model: 'volume',
144
+ tiers: [{ up_to: null, unit_price: -0.001 }],
145
+ }),
146
+ ).toThrow()
147
+ })
148
+ })
149
+
150
+ describe('rate_json — usage tiered (graduated)', () => {
151
+ it('accepts a well-formed graduated tier ladder', () => {
152
+ const value = {
153
+ model: 'graduated' as const,
154
+ tiers: [
155
+ { up_to: 10000, unit_price: 0 },
156
+ { up_to: null, unit_price: 0.001 },
157
+ ],
158
+ }
159
+ expect(rateJsonUsageGraduated.parse(value)).toEqual(value)
160
+ })
161
+
162
+ it('rejects mixing flat_amount into a graduated ladder', () => {
163
+ // Even if the value parses as `volume`/`graduated` shape with a `flat_amount`
164
+ // key, strict mode rejects it because the tier schema is strict on keys.
165
+ expect(() =>
166
+ rateJsonUsageGraduated.parse({
167
+ model: 'graduated',
168
+ tiers: [{ up_to: null, flat_amount: 100 }],
169
+ }),
170
+ ).toThrow()
171
+ })
172
+ })
173
+
174
+ describe('rate_json — usage tiered (flat)', () => {
175
+ it('accepts a well-formed flat-tier ladder', () => {
176
+ const value = {
177
+ model: 'flat' as const,
178
+ tiers: [
179
+ { up_to: 10000, flat_amount: 50 },
180
+ { up_to: null, flat_amount: 200 },
181
+ ],
182
+ }
183
+ expect(rateJsonUsageFlat.parse(value)).toEqual(value)
184
+ })
185
+
186
+ it('rejects flat ladder using unit_price instead of flat_amount', () => {
187
+ expect(() =>
188
+ rateJsonUsageFlat.parse({
189
+ model: 'flat',
190
+ tiers: [{ up_to: null, unit_price: 100 }],
191
+ }),
192
+ ).toThrow()
193
+ })
194
+ })
195
+
196
+ describe('rate_json — discriminated tiered union', () => {
197
+ it.each([
198
+ ['volume', { up_to: null, unit_price: 0 }],
199
+ ['graduated', { up_to: null, unit_price: 0 }],
200
+ ['flat', { up_to: null, flat_amount: 0 }],
201
+ ] as const)('parses a minimal %s ladder', (model, tier) => {
202
+ expect(rateJsonUsageTiered.parse({ model, tiers: [tier] })).toBeDefined()
203
+ })
204
+
205
+ it('rejects unknown tier model', () => {
206
+ expect(() =>
207
+ rateJsonUsageTiered.parse({
208
+ model: 'logarithmic' as unknown as 'volume',
209
+ tiers: [{ up_to: null, unit_price: 0 }],
210
+ }),
211
+ ).toThrow()
212
+ })
213
+ })
214
+
215
+ describe('validateRateJson dispatcher', () => {
216
+ it('routes one_time → one_time schema', () => {
217
+ expect(validateRateJson('one_time', { amount: 99 })).toEqual({ amount: 99 })
218
+ })
219
+
220
+ it('routes recurring → recurring schema', () => {
221
+ expect(validateRateJson('recurring', { unit_price: 49 })).toEqual({ unit_price: 49 })
222
+ })
223
+
224
+ it('routes usage (simple) → usage schema', () => {
225
+ expect(validateRateJson('usage', { unit_price: 0.001 })).toEqual({ unit_price: 0.001 })
226
+ })
227
+
228
+ it('routes usage (tiered) → usage schema', () => {
229
+ expect(
230
+ validateRateJson('usage', {
231
+ model: 'graduated',
232
+ tiers: [{ up_to: null, unit_price: 0.001 }],
233
+ }),
234
+ ).toBeDefined()
235
+ })
236
+
237
+ it('rejects one_time payload submitted as recurring (cross-shape mistake)', () => {
238
+ expect(() => validateRateJson('recurring', { amount: 99 })).toThrow()
239
+ })
240
+
241
+ it('rejects negative unit_price on recurring even when dispatched generically', () => {
242
+ expect(() => validateRateJson('recurring', { unit_price: -1 })).toThrow()
243
+ })
244
+ })
245
+
246
+ describe('billCycleAnchorSchema', () => {
247
+ it.each(['1', '15', '28'])('accepts day-of-month %s', (day) => {
248
+ expect(billCycleAnchorSchema.parse(day)).toBe(day)
249
+ })
250
+
251
+ it.each(['0', '29', '32', 'foo'])('rejects invalid anchor %s', (val) => {
252
+ expect(() => billCycleAnchorSchema.parse(val)).toThrow()
253
+ })
254
+
255
+ it.each(['mon', 'tue', 'sun', 'MON'])('accepts weekday anchor %s', (day) => {
256
+ expect(billCycleAnchorSchema.parse(day)).toBe(day)
257
+ })
258
+ })
259
+
260
+ describe('invoiceAddressSchema', () => {
261
+ it('accepts a minimum-viable address', () => {
262
+ const value = {
263
+ line1: 'ul. Marszałkowska 1',
264
+ city: 'Warsaw',
265
+ postal_code: '00-001',
266
+ country: 'PL',
267
+ }
268
+ expect(invoiceAddressSchema.parse(value)).toEqual(value)
269
+ })
270
+
271
+ it('rejects missing required fields', () => {
272
+ expect(() =>
273
+ invoiceAddressSchema.parse({ line1: '', city: 'Warsaw', postal_code: '00-001', country: 'PL' }),
274
+ ).toThrow()
275
+ })
276
+ })
277
+
278
+ describe('exported enums', () => {
279
+ it('BILLING_ITEM_TYPES matches spec', () => {
280
+ expect([...BILLING_ITEM_TYPES]).toEqual(['one_time', 'recurring', 'usage'])
281
+ })
282
+
283
+ it('BILL_CYCLES matches spec', () => {
284
+ expect([...BILL_CYCLES]).toEqual(['weekly', 'monthly', 'quarterly', 'annually'])
285
+ })
286
+
287
+ it('BILL_RUN_STATUSES matches spec', () => {
288
+ expect([...BILL_RUN_STATUSES]).toEqual([
289
+ 'running',
290
+ 'completed',
291
+ 'partial_failure',
292
+ 'failed',
293
+ ])
294
+ })
295
+
296
+ it('INVOICE_STATUSES matches the dictionary entries seeded by setup', () => {
297
+ expect([...INVOICE_STATUSES]).toEqual(['draft', 'posted', 'paid', 'void'])
298
+ })
299
+ })
300
+
301
+ // ─── Phase 1 — CRUD payload schemas ────────────────────────────────
302
+
303
+ import {
304
+ billingAccountCreateSchema,
305
+ billingAccountListQuerySchema,
306
+ billingAccountUpdateSchema,
307
+ billingItemCreateSchema,
308
+ billingItemListQuerySchema,
309
+ billingItemUpdateSchema,
310
+ billingUsageCreateSchema,
311
+ billingUsageListQuerySchema,
312
+ } from '../data/validators'
313
+
314
+ const TENANT = '11111111-1111-4111-8111-111111111111'
315
+ const ORG = '22222222-2222-4222-8222-222222222222'
316
+ const ACCOUNT = '33333333-3333-4333-8333-333333333333'
317
+
318
+ const validAddress = {
319
+ line1: 'ul. Marszałkowska 1',
320
+ city: 'Warsaw',
321
+ postal_code: '00-001',
322
+ country: 'PL',
323
+ }
324
+
325
+ const validAccountCreate = {
326
+ tenantId: TENANT,
327
+ organizationId: ORG,
328
+ customerId: 'cust-1',
329
+ name: 'Acme Sp. z o.o.',
330
+ currencyCode: 'eur', // lowercase; schema upcases
331
+ billCycle: 'monthly',
332
+ billCycleAnchor: '1',
333
+ invoiceEmail: 'AR@acme.test',
334
+ invoiceLanguage: 'PL', // schema downcases
335
+ invoiceAddress: validAddress,
336
+ nextBillDate: '2026-06-01',
337
+ }
338
+
339
+ describe('billingAccountCreateSchema', () => {
340
+ it('accepts a minimum-viable payload and normalizes case', () => {
341
+ const parsed = billingAccountCreateSchema.parse(validAccountCreate)
342
+ expect(parsed.currencyCode).toBe('EUR')
343
+ expect(parsed.invoiceLanguage).toBe('pl')
344
+ expect(parsed.nextBillDate).toBeInstanceOf(Date)
345
+ })
346
+
347
+ it('rejects an invalid currency length', () => {
348
+ expect(() =>
349
+ billingAccountCreateSchema.parse({ ...validAccountCreate, currencyCode: 'EURO' }),
350
+ ).toThrow()
351
+ })
352
+
353
+ it('rejects an invalid bill cycle anchor (day 32)', () => {
354
+ expect(() =>
355
+ billingAccountCreateSchema.parse({ ...validAccountCreate, billCycleAnchor: '32' }),
356
+ ).toThrow()
357
+ })
358
+
359
+ it('rejects unknown bill cycle', () => {
360
+ expect(() =>
361
+ billingAccountCreateSchema.parse({ ...validAccountCreate, billCycle: 'fortnightly' }),
362
+ ).toThrow()
363
+ })
364
+
365
+ it('rejects missing tenantId / organizationId (scope is mandatory)', () => {
366
+ const { tenantId, ...withoutTenant } = validAccountCreate
367
+ expect(() => billingAccountCreateSchema.parse(withoutTenant)).toThrow()
368
+ void tenantId
369
+ })
370
+ })
371
+
372
+ describe('billingAccountUpdateSchema', () => {
373
+ it('accepts a partial update with just id + scope', () => {
374
+ const parsed = billingAccountUpdateSchema.parse({
375
+ tenantId: TENANT,
376
+ organizationId: ORG,
377
+ id: ACCOUNT,
378
+ name: 'Acme Renamed',
379
+ })
380
+ expect(parsed.name).toBe('Acme Renamed')
381
+ })
382
+
383
+ it('rejects payload missing id', () => {
384
+ expect(() =>
385
+ billingAccountUpdateSchema.parse({
386
+ tenantId: TENANT,
387
+ organizationId: ORG,
388
+ name: 'x',
389
+ }),
390
+ ).toThrow()
391
+ })
392
+
393
+ it('does not declare currencyCode (immutable per spec)', () => {
394
+ expect('currencyCode' in billingAccountUpdateSchema.shape).toBe(false)
395
+ })
396
+
397
+ it('does not declare customerId (immutable per spec)', () => {
398
+ expect('customerId' in billingAccountUpdateSchema.shape).toBe(false)
399
+ })
400
+ })
401
+
402
+ describe('billingAccountListQuerySchema', () => {
403
+ it('coerces page/pageSize from query strings', () => {
404
+ const parsed = billingAccountListQuerySchema.parse({ page: '2', pageSize: '25' })
405
+ expect(parsed.page).toBe(2)
406
+ expect(parsed.pageSize).toBe(25)
407
+ })
408
+
409
+ it('caps pageSize at 100', () => {
410
+ expect(() => billingAccountListQuerySchema.parse({ pageSize: '500' })).toThrow()
411
+ })
412
+
413
+ it('uses page=1, pageSize=50 as defaults', () => {
414
+ const parsed = billingAccountListQuerySchema.parse({})
415
+ expect(parsed.page).toBe(1)
416
+ expect(parsed.pageSize).toBe(50)
417
+ })
418
+ })
419
+
420
+ const validItemRecurring = {
421
+ tenantId: TENANT,
422
+ organizationId: ORG,
423
+ billAccountId: ACCOUNT,
424
+ type: 'recurring' as const,
425
+ billStartDate: '2026-05-01',
426
+ description: 'MRC — Plan A',
427
+ rateJson: { unit_price: 49.99 },
428
+ }
429
+
430
+ describe('billingItemCreateSchema', () => {
431
+ it('accepts a well-formed recurring item', () => {
432
+ const parsed = billingItemCreateSchema.parse(validItemRecurring)
433
+ expect(parsed.type).toBe('recurring')
434
+ expect(parsed.rateJson).toEqual({ unit_price: 49.99 })
435
+ })
436
+
437
+ it('rejects recurring item with one_time-shaped rateJson', () => {
438
+ expect(() =>
439
+ billingItemCreateSchema.parse({ ...validItemRecurring, rateJson: { amount: 99 } }),
440
+ ).toThrow()
441
+ })
442
+
443
+ it('requires uomCode for type=usage', () => {
444
+ expect(() =>
445
+ billingItemCreateSchema.parse({
446
+ ...validItemRecurring,
447
+ type: 'usage',
448
+ rateJson: { unit_price: 0.001 },
449
+ uomCode: undefined,
450
+ }),
451
+ ).toThrow()
452
+ })
453
+
454
+ it('accepts type=usage with uomCode + simple rate', () => {
455
+ const parsed = billingItemCreateSchema.parse({
456
+ ...validItemRecurring,
457
+ type: 'usage',
458
+ rateJson: { unit_price: 0.001 },
459
+ uomCode: 'api_request',
460
+ })
461
+ expect(parsed.uomCode).toBe('api_request')
462
+ })
463
+
464
+ it('accepts a tiered graduated rate', () => {
465
+ const parsed = billingItemCreateSchema.parse({
466
+ ...validItemRecurring,
467
+ type: 'usage',
468
+ uomCode: 'api_request',
469
+ rateJson: {
470
+ model: 'graduated',
471
+ tiers: [
472
+ { up_to: 10000, unit_price: 0 },
473
+ { up_to: null, unit_price: 0.001 },
474
+ ],
475
+ },
476
+ })
477
+ expect((parsed.rateJson as { model: string }).model).toBe('graduated')
478
+ })
479
+
480
+ it('rejects a malformed tiered rate (no open top tier)', () => {
481
+ expect(() =>
482
+ billingItemCreateSchema.parse({
483
+ ...validItemRecurring,
484
+ type: 'usage',
485
+ uomCode: 'api_request',
486
+ rateJson: {
487
+ model: 'graduated',
488
+ tiers: [{ up_to: 10000, unit_price: 0 }],
489
+ },
490
+ }),
491
+ ).toThrow()
492
+ })
493
+
494
+ it('accepts an optional sourceRef (idempotency key)', () => {
495
+ const parsed = billingItemCreateSchema.parse({
496
+ ...validItemRecurring,
497
+ sourceRef: 'cpq-sub-X-mrc',
498
+ })
499
+ expect(parsed.sourceRef).toBe('cpq-sub-X-mrc')
500
+ })
501
+ })
502
+
503
+ describe('billingItemUpdateSchema', () => {
504
+ it('does NOT allow changing type (immutable per spec)', () => {
505
+ expect('type' in billingItemUpdateSchema.shape).toBe(false)
506
+ })
507
+
508
+ it('does NOT allow changing sourceRef (immutable per spec)', () => {
509
+ expect('sourceRef' in billingItemUpdateSchema.shape).toBe(false)
510
+ })
511
+
512
+ it('allows updating description + rateJson', () => {
513
+ const parsed = billingItemUpdateSchema.parse({
514
+ tenantId: TENANT,
515
+ organizationId: ORG,
516
+ id: ACCOUNT,
517
+ description: 'Updated description',
518
+ rateJson: { unit_price: 59.99 },
519
+ })
520
+ expect(parsed.description).toBe('Updated description')
521
+ })
522
+ })
523
+
524
+ describe('billingItemListQuerySchema', () => {
525
+ it('filters by type with enum validation', () => {
526
+ expect(billingItemListQuerySchema.parse({ type: 'recurring' }).type).toBe('recurring')
527
+ expect(() => billingItemListQuerySchema.parse({ type: 'invalid' })).toThrow()
528
+ })
529
+
530
+ it('coerces activeAt to a Date', () => {
531
+ const parsed = billingItemListQuerySchema.parse({ activeAt: '2026-05-15' })
532
+ expect(parsed.activeAt).toBeInstanceOf(Date)
533
+ })
534
+ })
535
+
536
+ const validUsageCreate = {
537
+ tenantId: TENANT,
538
+ organizationId: ORG,
539
+ billAccountId: ACCOUNT,
540
+ uomCode: 'api_request',
541
+ quantity: 12345,
542
+ periodStart: '2026-05-01T00:00:00Z',
543
+ periodEnd: '2026-05-31T23:59:59Z',
544
+ }
545
+
546
+ describe('billingUsageCreateSchema', () => {
547
+ it('accepts a valid usage record', () => {
548
+ const parsed = billingUsageCreateSchema.parse(validUsageCreate)
549
+ expect(parsed.quantity).toBe(12345)
550
+ expect(parsed.uomCode).toBe('api_request')
551
+ })
552
+
553
+ it('rejects negative quantity (refund modeled as one_time credit)', () => {
554
+ expect(() =>
555
+ billingUsageCreateSchema.parse({ ...validUsageCreate, quantity: -5 }),
556
+ ).toThrow()
557
+ })
558
+
559
+ it('rejects periodEnd before periodStart', () => {
560
+ expect(() =>
561
+ billingUsageCreateSchema.parse({
562
+ ...validUsageCreate,
563
+ periodStart: '2026-05-31T00:00:00Z',
564
+ periodEnd: '2026-05-01T00:00:00Z',
565
+ }),
566
+ ).toThrow()
567
+ })
568
+
569
+ it('accepts a per-record line_description (breakdown reporting hint)', () => {
570
+ const parsed = billingUsageCreateSchema.parse({
571
+ ...validUsageCreate,
572
+ lineDescription: 'API key X — 753k input tokens, 2026-02-23..25',
573
+ })
574
+ expect(parsed.lineDescription).toContain('753k')
575
+ })
576
+
577
+ it('accepts an optional sourceRef (idempotency key)', () => {
578
+ const parsed = billingUsageCreateSchema.parse({
579
+ ...validUsageCreate,
580
+ sourceRef: 'metrics-2026-05-31',
581
+ })
582
+ expect(parsed.sourceRef).toBe('metrics-2026-05-31')
583
+ })
584
+ })
585
+
586
+ describe('billingUsageListQuerySchema', () => {
587
+ it('coerces rated=true to boolean', () => {
588
+ expect(billingUsageListQuerySchema.parse({ rated: 'true' }).rated).toBe(true)
589
+ })
590
+
591
+ it('caps pageSize at 100', () => {
592
+ expect(() => billingUsageListQuerySchema.parse({ pageSize: '101' })).toThrow()
593
+ })
594
+ })
@@ -0,0 +1,16 @@
1
+ export 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
+ ] as const
15
+
16
+ export default features