@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,533 @@
1
+ import { OptionalProps } from '@mikro-orm/core'
2
+ import { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'
3
+
4
+ // ─── BillingAccount ──────────────────────────────────────────────
5
+ //
6
+ // A container per customer (typically one customer = one account, but the
7
+ // engine does not enforce uniqueness — multi-account-per-customer is a
8
+ // legitimate model for B2B with separate budgets / currencies / branches).
9
+ //
10
+ // The `(organization_id, tenant_id, next_bill_date)` partial index is the
11
+ // Bill Run hot path — every nightly run filters by tenant + org and orders
12
+ // by next_bill_date. Multi-org tenants share `tenant_id` across orgs, so a
13
+ // `(tenant_id, next_bill_date)`-only index would scan every other org's
14
+ // rows for each tenant — fine at 1 org / tenant, painful at 50.
15
+
16
+ @Entity({ tableName: 'billing_accounts' })
17
+ @Index({
18
+ name: 'billing_accounts_next_bill_date_idx',
19
+ properties: ['organizationId', 'tenantId', 'nextBillDate'],
20
+ expression:
21
+ 'CREATE INDEX "billing_accounts_next_bill_date_idx" ON "billing_accounts" ("organization_id", "tenant_id", "next_bill_date") WHERE "deleted_at" IS NULL',
22
+ })
23
+ @Index({
24
+ name: 'billing_accounts_customer_idx',
25
+ properties: ['organizationId', 'tenantId', 'customerId'],
26
+ })
27
+ @Index({
28
+ name: 'billing_accounts_sort_idx',
29
+ properties: ['organizationId', 'tenantId', 'deletedAt', 'createdAt'],
30
+ })
31
+ export class BillingAccount {
32
+ [OptionalProps]?:
33
+ | 'taxId'
34
+ | 'lastBillDate'
35
+ | 'isActive'
36
+ | 'createdAt'
37
+ | 'updatedAt'
38
+ | 'deletedAt'
39
+
40
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
41
+ id!: string
42
+
43
+ @Property({ name: 'organization_id', type: 'uuid' })
44
+ organizationId!: string
45
+
46
+ @Property({ name: 'tenant_id', type: 'uuid' })
47
+ tenantId!: string
48
+
49
+ // String FK to customers.person.id (or any external customer system).
50
+ // Kept as text so the package stays installable without the customers
51
+ // module — see SPEC-001 cross-package data rule.
52
+ @Property({ name: 'customer_id', type: 'text' })
53
+ customerId!: string
54
+
55
+ @Property({ type: 'text' })
56
+ name!: string
57
+
58
+ // ISO 4217 currency code. An account is single-currency by design —
59
+ // multi-currency = multiple accounts.
60
+ @Property({ name: 'currency_code', type: 'text' })
61
+ currencyCode!: string
62
+
63
+ // 'monthly' | 'quarterly' | 'annually' | 'weekly'. Validated at API ingress.
64
+ @Property({ name: 'bill_cycle', type: 'text' })
65
+ billCycle!: string
66
+
67
+ // Day-of-month for monthly/quarterly/annually (1–28), or weekday code
68
+ // ('mon'…'sun') for weekly. Text so both shapes coexist.
69
+ @Property({ name: 'bill_cycle_anchor', type: 'text' })
70
+ billCycleAnchor!: string
71
+
72
+ @Property({ name: 'invoice_email', type: 'text' })
73
+ invoiceEmail!: string
74
+
75
+ // ISO 639-1 (`en`, `pl`, …). Used by mailer / template renderer.
76
+ @Property({ name: 'invoice_language', type: 'text' })
77
+ invoiceLanguage!: string
78
+
79
+ // NIP / VATIN / TaxID. Nullable for individuals without a registered tax ID.
80
+ @Property({ name: 'tax_id', type: 'text', nullable: true })
81
+ taxId?: string | null
82
+
83
+ // { line1, line2?, city, postal_code, country } — JSONB because address
84
+ // shape differs across countries and we never query by address fields.
85
+ @Property({ name: 'invoice_address', type: 'jsonb' })
86
+ invoiceAddress!: Record<string, unknown>
87
+
88
+ // Anchor of the NEXT bill period to be processed (advanced by Bill Run
89
+ // on success). Date-only — calendar-aware cycle arithmetic.
90
+ @Property({ name: 'next_bill_date', type: 'date', columnType: 'date' })
91
+ nextBillDate!: Date
92
+
93
+ // Anchor of the most recently billed period; null for fresh accounts.
94
+ @Property({ name: 'last_bill_date', type: 'date', columnType: 'date', nullable: true })
95
+ lastBillDate?: Date | null
96
+
97
+ @Property({ name: 'is_active', type: 'boolean', default: true })
98
+ isActive: boolean = true
99
+
100
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
101
+ createdAt: Date = new Date()
102
+
103
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
104
+ updatedAt: Date = new Date()
105
+
106
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
107
+ deletedAt?: Date | null
108
+ }
109
+
110
+ // ─── BillingItem ─────────────────────────────────────────────────
111
+ //
112
+ // A single line that will appear on an invoice. Three flavours:
113
+ // `one_time` / `recurring` / `usage` — see rate_json shape per type in
114
+ // data/validators.ts.
115
+ //
116
+ // `(tenant_id, bill_account_id, source_ref)` is unique for idempotency.
117
+ // Postgres treats NULLs as distinct, so items without a source_ref do not
118
+ // conflict — that is intentional (CSV imports / manual entries).
119
+
120
+ @Entity({ tableName: 'billing_items' })
121
+ @Unique({
122
+ name: 'billing_items_source_ref_unique',
123
+ properties: ['tenantId', 'billAccountId', 'sourceRef'],
124
+ })
125
+ @Index({
126
+ name: 'billing_items_account_type_idx',
127
+ properties: ['billAccountId', 'type'],
128
+ })
129
+ @Index({
130
+ name: 'billing_items_account_active_idx',
131
+ properties: ['billAccountId', 'type', 'isActive'],
132
+ expression:
133
+ 'CREATE INDEX "billing_items_account_active_idx" ON "billing_items" ("bill_account_id", "type", "is_active") WHERE "deleted_at" IS NULL',
134
+ })
135
+ @Index({
136
+ name: 'billing_items_subscription_idx',
137
+ properties: ['organizationId', 'tenantId', 'subscriptionId'],
138
+ })
139
+ export class BillingItem {
140
+ [OptionalProps]?:
141
+ | 'billEndDate'
142
+ | 'uomCode'
143
+ | 'subscriptionId'
144
+ | 'subscriptionItemId'
145
+ | 'sourceRef'
146
+ | 'currencyMismatch'
147
+ | 'billedToDate'
148
+ | 'isActive'
149
+ | 'createdAt'
150
+ | 'updatedAt'
151
+ | 'deletedAt'
152
+
153
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
154
+ id!: string
155
+
156
+ @Property({ name: 'organization_id', type: 'uuid' })
157
+ organizationId!: string
158
+
159
+ @Property({ name: 'tenant_id', type: 'uuid' })
160
+ tenantId!: string
161
+
162
+ // FK string to BillingAccount.id (intra-module; no ORM relation kept to
163
+ // avoid coupling — we resolve at runtime).
164
+ @Property({ name: 'bill_account_id', type: 'uuid' })
165
+ billAccountId!: string
166
+
167
+ // 'one_time' | 'recurring' | 'usage'.
168
+ @Property({ type: 'text' })
169
+ type!: string
170
+
171
+ @Property({ name: 'bill_start_date', type: 'date', columnType: 'date' })
172
+ billStartDate!: Date
173
+
174
+ // Null = open-ended (typical for ongoing recurring items).
175
+ @Property({ name: 'bill_end_date', type: 'date', columnType: 'date', nullable: true })
176
+ billEndDate?: Date | null
177
+
178
+ @Property({ type: 'text' })
179
+ description!: string
180
+
181
+ // Pricing definition. Shape validated by Zod per `type`; see
182
+ // data/validators.ts.
183
+ @Property({ name: 'rate_json', type: 'jsonb' })
184
+ rateJson!: Record<string, unknown>
185
+
186
+ // Required for type='usage' (must match a Usage record's uom_code
187
+ // exactly — no conversion). Ignored for other types.
188
+ @Property({ name: 'uom_code', type: 'text', nullable: true })
189
+ uomCode?: string | null
190
+
191
+ // String FK to upstream subscription (e.g. CPQ's
192
+ // cpq_inventory_subscriptions.id). Null for CSV / manual / non-subscription
193
+ // contexts.
194
+ @Property({ name: 'subscription_id', type: 'text', nullable: true })
195
+ subscriptionId?: string | null
196
+
197
+ @Property({ name: 'subscription_item_id', type: 'text', nullable: true })
198
+ subscriptionItemId?: string | null
199
+
200
+ // Idempotency key — duplicate POST returns the existing row.
201
+ @Property({ name: 'source_ref', type: 'text', nullable: true })
202
+ sourceRef?: string | null
203
+
204
+ // Set when the integrator POSTed a value in a different currency than the
205
+ // account's currency_code. Operator sees a warning on the draft.
206
+ @Property({ name: 'currency_mismatch', type: 'boolean', default: false })
207
+ currencyMismatch: boolean = false
208
+
209
+ // Anti-double-bill guard at the item level — see spec
210
+ // "Why two anti-duplicate guards".
211
+ @Property({ name: 'billed_to_date', type: 'date', columnType: 'date', nullable: true })
212
+ billedToDate?: Date | null
213
+
214
+ @Property({ name: 'is_active', type: 'boolean', default: true })
215
+ isActive: boolean = true
216
+
217
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
218
+ createdAt: Date = new Date()
219
+
220
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
221
+ updatedAt: Date = new Date()
222
+
223
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
224
+ deletedAt?: Date | null
225
+ }
226
+
227
+ // ─── BillingAccountUsage ─────────────────────────────────────────
228
+ //
229
+ // Bucket for pre-aggregated usage uploaded by external systems. The
230
+ // selection index `(bill_account_id, uom_code, rated_in_bill_run_id,
231
+ // period_end)` matches the Bill Run "find unrated records" predicate
232
+ // exactly — keep it in sync if that predicate changes.
233
+
234
+ @Entity({ tableName: 'billing_account_usage' })
235
+ @Unique({
236
+ name: 'billing_account_usage_source_ref_unique',
237
+ properties: ['tenantId', 'billAccountId', 'sourceRef'],
238
+ })
239
+ @Index({
240
+ name: 'billing_account_usage_rating_idx',
241
+ properties: ['billAccountId', 'uomCode', 'ratedInBillRunId', 'periodEnd'],
242
+ })
243
+ export class BillingAccountUsage {
244
+ [OptionalProps]?:
245
+ | 'lineDescription'
246
+ | 'sourceRef'
247
+ | 'ratedInBillRunId'
248
+ | 'isActive'
249
+ | 'createdAt'
250
+ | 'updatedAt'
251
+ | 'deletedAt'
252
+
253
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
254
+ id!: string
255
+
256
+ @Property({ name: 'organization_id', type: 'uuid' })
257
+ organizationId!: string
258
+
259
+ @Property({ name: 'tenant_id', type: 'uuid' })
260
+ tenantId!: string
261
+
262
+ @Property({ name: 'bill_account_id', type: 'uuid' })
263
+ billAccountId!: string
264
+
265
+ // Must match the `uom_code` on a corresponding `type='usage'` Billing Item.
266
+ @Property({ name: 'uom_code', type: 'text' })
267
+ uomCode!: string
268
+
269
+ // numeric(18,4) → string in app code (standard MikroORM numeric handling).
270
+ @Property({ type: 'numeric', columnType: 'numeric(18, 4)' })
271
+ quantity!: string
272
+
273
+ @Property({ name: 'period_start', type: Date })
274
+ periodStart!: Date
275
+
276
+ @Property({ name: 'period_end', type: Date })
277
+ periodEnd!: Date
278
+
279
+ // Optional per-record description. When set, Bill Run emits one invoice
280
+ // line per Usage record with this description. When null, the run
281
+ // aggregates all matching Usage records into a single line using the
282
+ // Billing Item's `description`.
283
+ @Property({ name: 'line_description', type: 'text', nullable: true })
284
+ lineDescription?: string | null
285
+
286
+ @Property({ name: 'source_ref', type: 'text', nullable: true })
287
+ sourceRef?: string | null
288
+
289
+ // Set when consumed; prevents double-rating on retry.
290
+ @Property({ name: 'rated_in_bill_run_id', type: 'uuid', nullable: true })
291
+ ratedInBillRunId?: string | null
292
+
293
+ @Property({ name: 'is_active', type: 'boolean', default: true })
294
+ isActive: boolean = true
295
+
296
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
297
+ createdAt: Date = new Date()
298
+
299
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
300
+ updatedAt: Date = new Date()
301
+
302
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
303
+ deletedAt?: Date | null
304
+ }
305
+
306
+ // ─── BillRun ─────────────────────────────────────────────────────
307
+ //
308
+ // A single execution of the engine — one row per trigger (schedule or
309
+ // manual). The concurrency lock is a Postgres transaction-scoped advisory
310
+ // lock keyed by hash(tenant_id || 'billing-run'); it is NOT stored on this
311
+ // row. Crash recovery for zombie `status='running'` rows is handled by
312
+ // workers/reap-stale-bill-runs.ts.
313
+
314
+ @Entity({ tableName: 'billing_runs' })
315
+ @Index({
316
+ name: 'billing_runs_tenant_status_idx',
317
+ properties: ['tenantId', 'status', 'startedAt'],
318
+ })
319
+ @Index({
320
+ name: 'billing_runs_parent_idx',
321
+ properties: ['parentRunId'],
322
+ })
323
+ export class BillRun {
324
+ [OptionalProps]?:
325
+ | 'parentRunId'
326
+ | 'dryRun'
327
+ | 'testMode'
328
+ | 'catchUp'
329
+ | 'scopedAccountIds'
330
+ | 'startedAt'
331
+ | 'finishedAt'
332
+ | 'summary'
333
+ | 'isActive'
334
+ | 'createdAt'
335
+ | 'updatedAt'
336
+ | 'deletedAt'
337
+
338
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
339
+ id!: string
340
+
341
+ @Property({ name: 'organization_id', type: 'uuid' })
342
+ organizationId!: string
343
+
344
+ @Property({ name: 'tenant_id', type: 'uuid' })
345
+ tenantId!: string
346
+
347
+ // 'schedule' | 'manual'.
348
+ @Property({ name: 'triggered_by', type: 'text' })
349
+ triggeredBy!: string
350
+
351
+ // Set when this run is a "retry failed" of a previous run.
352
+ @Property({ name: 'parent_run_id', type: 'uuid', nullable: true })
353
+ parentRunId?: string | null
354
+
355
+ // True → compute everything but persist nothing (preview).
356
+ @Property({ name: 'dry_run', type: 'boolean', default: false })
357
+ dryRun: boolean = false
358
+
359
+ // True → persist drafts flagged as test, but do not advance state.
360
+ @Property({ name: 'test_mode', type: 'boolean', default: false })
361
+ testMode: boolean = false
362
+
363
+ // True → loop missed cycles per account in one run (manual only).
364
+ @Property({ name: 'catch_up', type: 'boolean', default: false })
365
+ catchUp: boolean = false
366
+
367
+ // Optional — run processes only these accounts when set. JSONB
368
+ // (Postgres arrays would work too, but jsonb keeps tooling uniform
369
+ // across this package).
370
+ @Property({ name: 'scoped_account_ids', type: 'jsonb', nullable: true })
371
+ scopedAccountIds?: string[] | null
372
+
373
+ // The "today" the run uses to evaluate `next_bill_date <= as_of_date`.
374
+ @Property({ name: 'as_of_date', type: 'date', columnType: 'date' })
375
+ asOfDate!: Date
376
+
377
+ @Property({ name: 'started_at', type: Date, nullable: true })
378
+ startedAt?: Date | null
379
+
380
+ @Property({ name: 'finished_at', type: Date, nullable: true })
381
+ finishedAt?: Date | null
382
+
383
+ // 'running' | 'completed' | 'partial_failure' | 'failed'.
384
+ @Property({ type: 'text' })
385
+ status!: string
386
+
387
+ // { accounts_processed, drafts_created, drafts_skipped_existing,
388
+ // accounts_failed, accounts_with_warnings, usage_records_rated }
389
+ @Property({ type: 'jsonb', nullable: true })
390
+ summary?: Record<string, unknown> | null
391
+
392
+ @Property({ name: 'is_active', type: 'boolean', default: true })
393
+ isActive: boolean = true
394
+
395
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
396
+ createdAt: Date = new Date()
397
+
398
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
399
+ updatedAt: Date = new Date()
400
+
401
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
402
+ deletedAt?: Date | null
403
+ }
404
+
405
+ // ─── BillRunOutcome ──────────────────────────────────────────────
406
+ //
407
+ // Per-account outcome of a Bill Run. Stored in its own table (not as a
408
+ // JSONB array on BillRun.summary) so operators can query failures across
409
+ // runs without jsonb_array_elements gymnastics. See spec rationale.
410
+
411
+ @Entity({ tableName: 'billing_run_outcomes' })
412
+ @Index({
413
+ name: 'billing_run_outcomes_run_status_idx',
414
+ properties: ['billRunId', 'status'],
415
+ })
416
+ @Index({
417
+ name: 'billing_run_outcomes_account_history_idx',
418
+ properties: ['organizationId', 'tenantId', 'billAccountId', 'createdAt'],
419
+ })
420
+ export class BillRunOutcome {
421
+ [OptionalProps]?:
422
+ | 'errorMessage'
423
+ | 'warnings'
424
+ | 'draftInvoiceId'
425
+ | 'isActive'
426
+ | 'createdAt'
427
+ | 'updatedAt'
428
+ | 'deletedAt'
429
+
430
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
431
+ id!: string
432
+
433
+ @Property({ name: 'organization_id', type: 'uuid' })
434
+ organizationId!: string
435
+
436
+ @Property({ name: 'tenant_id', type: 'uuid' })
437
+ tenantId!: string
438
+
439
+ @Property({ name: 'bill_run_id', type: 'uuid' })
440
+ billRunId!: string
441
+
442
+ @Property({ name: 'bill_account_id', type: 'uuid' })
443
+ billAccountId!: string
444
+
445
+ // 'success' | 'success_with_warnings' | 'skipped_existing_draft' | 'failed'.
446
+ @Property({ type: 'text' })
447
+ status!: string
448
+
449
+ @Property({ name: 'error_message', type: 'text', nullable: true })
450
+ errorMessage?: string | null
451
+
452
+ // E.g. `{ unmatched_usage_uoms: ["api_request"] }`.
453
+ @Property({ type: 'jsonb', nullable: true })
454
+ warnings?: Record<string, unknown> | null
455
+
456
+ // FK string to the core/sales invoice (drafted by the run). Null when
457
+ // the run skipped (status='skipped_existing_draft' or 'failed').
458
+ @Property({ name: 'draft_invoice_id', type: 'uuid', nullable: true })
459
+ draftInvoiceId?: string | null
460
+
461
+ @Property({ name: 'is_active', type: 'boolean', default: true })
462
+ isActive: boolean = true
463
+
464
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
465
+ createdAt: Date = new Date()
466
+
467
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
468
+ updatedAt: Date = new Date()
469
+
470
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
471
+ deletedAt?: Date | null
472
+ }
473
+
474
+ // ─── DraftInvoiceEdit (audit) ────────────────────────────────────
475
+ //
476
+ // Append-only audit of every operator edit on a draft invoice line.
477
+ // Pattern mirrors `cpq_subscription_change_logs` — minimal columns
478
+ // (no updated_at / is_active), append-only, soft-delete reserved for
479
+ // compliance erasure only.
480
+
481
+ @Entity({ tableName: 'billing_draft_invoice_edits' })
482
+ @Index({
483
+ name: 'billing_draft_invoice_edits_invoice_idx',
484
+ properties: ['organizationId', 'tenantId', 'invoiceId', 'createdAt'],
485
+ })
486
+ @Index({
487
+ name: 'billing_draft_invoice_edits_user_idx',
488
+ properties: ['organizationId', 'tenantId', 'userId', 'createdAt'],
489
+ })
490
+ export class DraftInvoiceEdit {
491
+ [OptionalProps]?: 'invoiceLineId' | 'beforeJson' | 'afterJson' | 'createdAt' | 'deletedAt'
492
+
493
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
494
+ id!: string
495
+
496
+ @Property({ name: 'organization_id', type: 'uuid' })
497
+ organizationId!: string
498
+
499
+ @Property({ name: 'tenant_id', type: 'uuid' })
500
+ tenantId!: string
501
+
502
+ // FK string to the core/sales invoice (no ORM relation across packages).
503
+ @Property({ name: 'invoice_id', type: 'uuid' })
504
+ invoiceId!: string
505
+
506
+ // Null for `line_added` / `line_removed` actions whose target line does
507
+ // not yet (or no longer) exist on the invoice.
508
+ @Property({ name: 'invoice_line_id', type: 'uuid', nullable: true })
509
+ invoiceLineId?: string | null
510
+
511
+ @Property({ name: 'user_id', type: 'uuid' })
512
+ userId!: string
513
+
514
+ // 'line_added' | 'line_removed' | 'line_edited'.
515
+ @Property({ type: 'text' })
516
+ action!: string
517
+
518
+ // Snapshot before the edit. Null for `line_added` (no prior state).
519
+ @Property({ name: 'before_json', type: 'jsonb', nullable: true })
520
+ beforeJson?: Record<string, unknown> | null
521
+
522
+ // Snapshot after the edit. Null for `line_removed` (no resulting state).
523
+ @Property({ name: 'after_json', type: 'jsonb', nullable: true })
524
+ afterJson?: Record<string, unknown> | null
525
+
526
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
527
+ createdAt: Date = new Date()
528
+
529
+ // Reserved for compliance erasure only; runtime never deletes audit rows
530
+ // through normal flows.
531
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
532
+ deletedAt?: Date | null
533
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Canonical entity IDs for the billing module.
3
+ *
4
+ * Mirrors what `E.billing.<entity>` would be in
5
+ * `.mercato/generated/entities.ids.generated.ts` if the standalone-app
6
+ * CLI properly discovered `@dainamite/*` workspace entities (see the
7
+ * Phase 0 deviation note in `xd-249-billing-spec.md`). Until the CLI
8
+ * gap is closed upstream, this is the single source of truth — every
9
+ * place that previously would have referenced `E.billing.*` (CRUD
10
+ * factory `entityId`, indexer `entityType`, response enricher
11
+ * `targetEntity`, etc.) imports from here instead.
12
+ *
13
+ * When the CLI is fixed, this file can be replaced by `import { E }
14
+ * from '#generated/entities.ids.generated'` in one PR.
15
+ */
16
+ export const billingEntityIds = {
17
+ account: 'billing:billing_account',
18
+ item: 'billing:billing_item',
19
+ usage: 'billing:billing_account_usage',
20
+ run: 'billing:billing_run',
21
+ runOutcome: 'billing:billing_run_outcome',
22
+ draftInvoiceEdit: 'billing:billing_draft_invoice_edit',
23
+ } as const
24
+
25
+ export type BillingEntityId = (typeof billingEntityIds)[keyof typeof billingEntityIds]