@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,144 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260519000000_billing extends Migration {
3
+ async up() {
4
+ this.addSql(`create table "billing_accounts" (
5
+ "id" uuid not null default gen_random_uuid(),
6
+ "organization_id" uuid not null,
7
+ "tenant_id" uuid not null,
8
+ "customer_id" text not null,
9
+ "name" text not null,
10
+ "currency_code" text not null,
11
+ "bill_cycle" text not null,
12
+ "bill_cycle_anchor" text not null,
13
+ "invoice_email" text not null,
14
+ "invoice_language" text not null,
15
+ "tax_id" text null,
16
+ "invoice_address" jsonb not null,
17
+ "next_bill_date" date not null,
18
+ "last_bill_date" date null,
19
+ "is_active" boolean not null default true,
20
+ "created_at" timestamptz not null,
21
+ "updated_at" timestamptz not null,
22
+ "deleted_at" timestamptz null,
23
+ constraint "billing_accounts_pkey" primary key ("id")
24
+ );`);
25
+ this.addSql(`create index "billing_accounts_next_bill_date_idx" on "billing_accounts" ("organization_id", "tenant_id", "next_bill_date") where "deleted_at" is null;`);
26
+ this.addSql(`create index "billing_accounts_customer_idx" on "billing_accounts" ("organization_id", "tenant_id", "customer_id");`);
27
+ this.addSql(`create index "billing_accounts_sort_idx" on "billing_accounts" ("organization_id", "tenant_id", "deleted_at", "created_at");`);
28
+ this.addSql(`create table "billing_items" (
29
+ "id" uuid not null default gen_random_uuid(),
30
+ "organization_id" uuid not null,
31
+ "tenant_id" uuid not null,
32
+ "bill_account_id" uuid not null,
33
+ "type" text not null,
34
+ "bill_start_date" date not null,
35
+ "bill_end_date" date null,
36
+ "description" text not null,
37
+ "rate_json" jsonb not null,
38
+ "uom_code" text null,
39
+ "subscription_id" text null,
40
+ "subscription_item_id" text null,
41
+ "source_ref" text null,
42
+ "currency_mismatch" boolean not null default false,
43
+ "billed_to_date" date null,
44
+ "is_active" boolean not null default true,
45
+ "created_at" timestamptz not null,
46
+ "updated_at" timestamptz not null,
47
+ "deleted_at" timestamptz null,
48
+ constraint "billing_items_pkey" primary key ("id")
49
+ );`);
50
+ this.addSql(`alter table "billing_items" add constraint "billing_items_source_ref_unique" unique ("tenant_id", "bill_account_id", "source_ref");`);
51
+ this.addSql(`create index "billing_items_account_type_idx" on "billing_items" ("bill_account_id", "type");`);
52
+ this.addSql(`create index "billing_items_account_active_idx" on "billing_items" ("bill_account_id", "type", "is_active") where "deleted_at" is null;`);
53
+ this.addSql(`create index "billing_items_subscription_idx" on "billing_items" ("organization_id", "tenant_id", "subscription_id");`);
54
+ this.addSql(`create table "billing_account_usage" (
55
+ "id" uuid not null default gen_random_uuid(),
56
+ "organization_id" uuid not null,
57
+ "tenant_id" uuid not null,
58
+ "bill_account_id" uuid not null,
59
+ "uom_code" text not null,
60
+ "quantity" numeric(18, 4) not null,
61
+ "period_start" timestamptz not null,
62
+ "period_end" timestamptz not null,
63
+ "line_description" text null,
64
+ "source_ref" text null,
65
+ "rated_in_bill_run_id" uuid null,
66
+ "is_active" boolean not null default true,
67
+ "created_at" timestamptz not null,
68
+ "updated_at" timestamptz not null,
69
+ "deleted_at" timestamptz null,
70
+ constraint "billing_account_usage_pkey" primary key ("id")
71
+ );`);
72
+ this.addSql(`alter table "billing_account_usage" add constraint "billing_account_usage_source_ref_unique" unique ("tenant_id", "bill_account_id", "source_ref");`);
73
+ this.addSql(`create index "billing_account_usage_rating_idx" on "billing_account_usage" ("bill_account_id", "uom_code", "rated_in_bill_run_id", "period_end");`);
74
+ this.addSql(`create table "billing_runs" (
75
+ "id" uuid not null default gen_random_uuid(),
76
+ "organization_id" uuid not null,
77
+ "tenant_id" uuid not null,
78
+ "triggered_by" text not null,
79
+ "parent_run_id" uuid null,
80
+ "dry_run" boolean not null default false,
81
+ "test_mode" boolean not null default false,
82
+ "catch_up" boolean not null default false,
83
+ "scoped_account_ids" jsonb null,
84
+ "as_of_date" date not null,
85
+ "started_at" timestamptz null,
86
+ "finished_at" timestamptz null,
87
+ "status" text not null,
88
+ "summary" jsonb null,
89
+ "is_active" boolean not null default true,
90
+ "created_at" timestamptz not null,
91
+ "updated_at" timestamptz not null,
92
+ "deleted_at" timestamptz null,
93
+ constraint "billing_runs_pkey" primary key ("id")
94
+ );`);
95
+ this.addSql(`create index "billing_runs_tenant_status_idx" on "billing_runs" ("tenant_id", "status", "started_at");`);
96
+ this.addSql(`create index "billing_runs_parent_idx" on "billing_runs" ("parent_run_id");`);
97
+ this.addSql(`create table "billing_run_outcomes" (
98
+ "id" uuid not null default gen_random_uuid(),
99
+ "organization_id" uuid not null,
100
+ "tenant_id" uuid not null,
101
+ "bill_run_id" uuid not null,
102
+ "bill_account_id" uuid not null,
103
+ "status" text not null,
104
+ "error_message" text null,
105
+ "warnings" jsonb null,
106
+ "draft_invoice_id" uuid null,
107
+ "is_active" boolean not null default true,
108
+ "created_at" timestamptz not null,
109
+ "updated_at" timestamptz not null,
110
+ "deleted_at" timestamptz null,
111
+ constraint "billing_run_outcomes_pkey" primary key ("id")
112
+ );`);
113
+ this.addSql(`create index "billing_run_outcomes_run_status_idx" on "billing_run_outcomes" ("bill_run_id", "status");`);
114
+ this.addSql(`create index "billing_run_outcomes_account_history_idx" on "billing_run_outcomes" ("organization_id", "tenant_id", "bill_account_id", "created_at");`);
115
+ this.addSql(`create table "billing_draft_invoice_edits" (
116
+ "id" uuid not null default gen_random_uuid(),
117
+ "organization_id" uuid not null,
118
+ "tenant_id" uuid not null,
119
+ "invoice_id" uuid not null,
120
+ "invoice_line_id" uuid null,
121
+ "user_id" uuid not null,
122
+ "action" text not null,
123
+ "before_json" jsonb null,
124
+ "after_json" jsonb null,
125
+ "created_at" timestamptz not null,
126
+ "deleted_at" timestamptz null,
127
+ constraint "billing_draft_invoice_edits_pkey" primary key ("id")
128
+ );`);
129
+ this.addSql(`create index "billing_draft_invoice_edits_invoice_idx" on "billing_draft_invoice_edits" ("organization_id", "tenant_id", "invoice_id", "created_at");`);
130
+ this.addSql(`create index "billing_draft_invoice_edits_user_idx" on "billing_draft_invoice_edits" ("organization_id", "tenant_id", "user_id", "created_at");`);
131
+ }
132
+ async down() {
133
+ this.addSql(`drop table if exists "billing_draft_invoice_edits" cascade;`);
134
+ this.addSql(`drop table if exists "billing_run_outcomes" cascade;`);
135
+ this.addSql(`drop table if exists "billing_runs" cascade;`);
136
+ this.addSql(`drop table if exists "billing_account_usage" cascade;`);
137
+ this.addSql(`drop table if exists "billing_items" cascade;`);
138
+ this.addSql(`drop table if exists "billing_accounts" cascade;`);
139
+ }
140
+ }
141
+ export {
142
+ Migration20260519000000_billing
143
+ };
144
+ //# sourceMappingURL=Migration20260519000000_billing.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/migrations/Migration20260519000000_billing.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\n/**\n * Initial Billing module schema (XD-249 Phase 0).\n *\n * Frozen as hand-written. Why this is OK despite the \"no hand-written\n * migrations\" rule in AGENTS.md:\n *\n * - At Phase 0 (May 2026) the standalone-app build of\n * `@open-mercato/cli@0.6.0` did not auto-discover entities from\n * `@dainamite/*` workspaces, so `yarn mercato db generate` produced\n * an empty diff. The CLI patch shipped in this PR fixes the\n * resolver going forward (see `packages/cpq/scripts/cli-resolver.patch.mjs`\n * usage), but **regenerating THIS initial migration would mean\n * wiping every existing billing_* table on every tenant** to let\n * the CLI re-emit the same `create table` statements. That's\n * never safe on a live system.\n * - The 1:1 mapping against `packages/billing/src/modules/billing/data/entities.ts`\n * has been validated against production-shape data (CPQ \u2192 billing\n * end-to-end flow). Any subsequent schema change MUST go through\n * `yarn mercato db generate` \u2014 the CLI now correctly walks\n * workspace `@dainamite/*` packages, so future migration files\n * in this folder will be generated, not hand-written.\n */\nexport class Migration20260519000000_billing extends Migration {\n async up(): Promise<void> {\n // \u2500\u2500\u2500 billing_accounts \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 this.addSql(`create table \"billing_accounts\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"customer_id\" text not null,\n \"name\" text not null,\n \"currency_code\" text not null,\n \"bill_cycle\" text not null,\n \"bill_cycle_anchor\" text not null,\n \"invoice_email\" text not null,\n \"invoice_language\" text not null,\n \"tax_id\" text null,\n \"invoice_address\" jsonb not null,\n \"next_bill_date\" date not null,\n \"last_bill_date\" date null,\n \"is_active\" boolean not null default true,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_accounts_pkey\" primary key (\"id\")\n );`)\n this.addSql(`create index \"billing_accounts_next_bill_date_idx\" on \"billing_accounts\" (\"organization_id\", \"tenant_id\", \"next_bill_date\") where \"deleted_at\" is null;`)\n this.addSql(`create index \"billing_accounts_customer_idx\" on \"billing_accounts\" (\"organization_id\", \"tenant_id\", \"customer_id\");`)\n this.addSql(`create index \"billing_accounts_sort_idx\" on \"billing_accounts\" (\"organization_id\", \"tenant_id\", \"deleted_at\", \"created_at\");`)\n\n // \u2500\u2500\u2500 billing_items \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 this.addSql(`create table \"billing_items\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"type\" text not null,\n \"bill_start_date\" date not null,\n \"bill_end_date\" date null,\n \"description\" text not null,\n \"rate_json\" jsonb not null,\n \"uom_code\" text null,\n \"subscription_id\" text null,\n \"subscription_item_id\" text null,\n \"source_ref\" text null,\n \"currency_mismatch\" boolean not null default false,\n \"billed_to_date\" date null,\n \"is_active\" boolean not null default true,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_items_pkey\" primary key (\"id\")\n );`)\n this.addSql(`alter table \"billing_items\" add constraint \"billing_items_source_ref_unique\" unique (\"tenant_id\", \"bill_account_id\", \"source_ref\");`)\n this.addSql(`create index \"billing_items_account_type_idx\" on \"billing_items\" (\"bill_account_id\", \"type\");`)\n this.addSql(`create index \"billing_items_account_active_idx\" on \"billing_items\" (\"bill_account_id\", \"type\", \"is_active\") where \"deleted_at\" is null;`)\n this.addSql(`create index \"billing_items_subscription_idx\" on \"billing_items\" (\"organization_id\", \"tenant_id\", \"subscription_id\");`)\n\n // \u2500\u2500\u2500 billing_account_usage \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 this.addSql(`create table \"billing_account_usage\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"uom_code\" text not null,\n \"quantity\" numeric(18, 4) not null,\n \"period_start\" timestamptz not null,\n \"period_end\" timestamptz not null,\n \"line_description\" text null,\n \"source_ref\" text null,\n \"rated_in_bill_run_id\" uuid null,\n \"is_active\" boolean not null default true,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_account_usage_pkey\" primary key (\"id\")\n );`)\n this.addSql(`alter table \"billing_account_usage\" add constraint \"billing_account_usage_source_ref_unique\" unique (\"tenant_id\", \"bill_account_id\", \"source_ref\");`)\n this.addSql(`create index \"billing_account_usage_rating_idx\" on \"billing_account_usage\" (\"bill_account_id\", \"uom_code\", \"rated_in_bill_run_id\", \"period_end\");`)\n\n // \u2500\u2500\u2500 billing_runs \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 this.addSql(`create table \"billing_runs\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"triggered_by\" text not null,\n \"parent_run_id\" uuid null,\n \"dry_run\" boolean not null default false,\n \"test_mode\" boolean not null default false,\n \"catch_up\" boolean not null default false,\n \"scoped_account_ids\" jsonb null,\n \"as_of_date\" date not null,\n \"started_at\" timestamptz null,\n \"finished_at\" timestamptz null,\n \"status\" text not null,\n \"summary\" jsonb null,\n \"is_active\" boolean not null default true,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_runs_pkey\" primary key (\"id\")\n );`)\n this.addSql(`create index \"billing_runs_tenant_status_idx\" on \"billing_runs\" (\"tenant_id\", \"status\", \"started_at\");`)\n this.addSql(`create index \"billing_runs_parent_idx\" on \"billing_runs\" (\"parent_run_id\");`)\n\n // \u2500\u2500\u2500 billing_run_outcomes \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 this.addSql(`create table \"billing_run_outcomes\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"bill_run_id\" uuid not null,\n \"bill_account_id\" uuid not null,\n \"status\" text not null,\n \"error_message\" text null,\n \"warnings\" jsonb null,\n \"draft_invoice_id\" uuid null,\n \"is_active\" boolean not null default true,\n \"created_at\" timestamptz not null,\n \"updated_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_run_outcomes_pkey\" primary key (\"id\")\n );`)\n this.addSql(`create index \"billing_run_outcomes_run_status_idx\" on \"billing_run_outcomes\" (\"bill_run_id\", \"status\");`)\n this.addSql(`create index \"billing_run_outcomes_account_history_idx\" on \"billing_run_outcomes\" (\"organization_id\", \"tenant_id\", \"bill_account_id\", \"created_at\");`)\n\n // \u2500\u2500\u2500 billing_draft_invoice_edits \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 this.addSql(`create table \"billing_draft_invoice_edits\" (\n \"id\" uuid not null default gen_random_uuid(),\n \"organization_id\" uuid not null,\n \"tenant_id\" uuid not null,\n \"invoice_id\" uuid not null,\n \"invoice_line_id\" uuid null,\n \"user_id\" uuid not null,\n \"action\" text not null,\n \"before_json\" jsonb null,\n \"after_json\" jsonb null,\n \"created_at\" timestamptz not null,\n \"deleted_at\" timestamptz null,\n constraint \"billing_draft_invoice_edits_pkey\" primary key (\"id\")\n );`)\n this.addSql(`create index \"billing_draft_invoice_edits_invoice_idx\" on \"billing_draft_invoice_edits\" (\"organization_id\", \"tenant_id\", \"invoice_id\", \"created_at\");`)\n this.addSql(`create index \"billing_draft_invoice_edits_user_idx\" on \"billing_draft_invoice_edits\" (\"organization_id\", \"tenant_id\", \"user_id\", \"created_at\");`)\n }\n\n async down(): Promise<void> {\n this.addSql(`drop table if exists \"billing_draft_invoice_edits\" cascade;`)\n this.addSql(`drop table if exists \"billing_run_outcomes\" cascade;`)\n this.addSql(`drop table if exists \"billing_runs\" cascade;`)\n this.addSql(`drop table if exists \"billing_account_usage\" cascade;`)\n this.addSql(`drop table if exists \"billing_items\" cascade;`)\n this.addSql(`drop table if exists \"billing_accounts\" cascade;`)\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAwBnB,MAAM,wCAAwC,UAAU;AAAA,EAC7D,MAAM,KAAoB;AAExB,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAoBT;AACH,SAAK,OAAO,yJAAyJ;AACrK,SAAK,OAAO,qHAAqH;AACjI,SAAK,OAAO,8HAA8H;AAG1I,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAqBT;AACH,SAAK,OAAO,qIAAqI;AACjJ,SAAK,OAAO,+FAA+F;AAC3G,SAAK,OAAO,yIAAyI;AACrJ,SAAK,OAAO,uHAAuH;AAGnI,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAiBT;AACH,SAAK,OAAO,qJAAqJ;AACjK,SAAK,OAAO,mJAAmJ;AAG/J,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAoBT;AACH,SAAK,OAAO,wGAAwG;AACpH,SAAK,OAAO,6EAA6E;AAGzF,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAeT;AACH,SAAK,OAAO,yGAAyG;AACrH,SAAK,OAAO,sJAAsJ;AAGlK,SAAK,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAaT;AACH,SAAK,OAAO,uJAAuJ;AACnK,SAAK,OAAO,iJAAiJ;AAAA,EAC/J;AAAA,EAEA,MAAM,OAAsB;AAC1B,SAAK,OAAO,6DAA6D;AACzE,SAAK,OAAO,sDAAsD;AAClE,SAAK,OAAO,8CAA8C;AAC1D,SAAK,OAAO,uDAAuD;AACnE,SAAK,OAAO,+CAA+C;AAC3D,SAAK,OAAO,kDAAkD;AAAA,EAChE;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,190 @@
1
+ import { features as billingAclFeatures } from "./acl.js";
2
+ import {
3
+ Dictionary,
4
+ DictionaryEntry
5
+ } from "@open-mercato/core/modules/dictionaries/data/entities";
6
+ import { ModuleConfig } from "@open-mercato/core/modules/configs/data/entities";
7
+ const BILLING_UOM_DICTIONARY_KEY = "billing.uom";
8
+ const SALES_INVOICE_STATUS_DICTIONARY_KEY = "sales.invoice_status";
9
+ const BILLING_UOM_CODES = [
10
+ // time
11
+ { value: "hour", label: "Hour" },
12
+ { value: "day", label: "Day" },
13
+ { value: "week", label: "Week" },
14
+ { value: "month", label: "Month" },
15
+ // volume
16
+ { value: "gb", label: "Gigabyte" },
17
+ { value: "tb", label: "Terabyte" },
18
+ { value: "mb", label: "Megabyte" },
19
+ // requests / tokens
20
+ { value: "request", label: "Request" },
21
+ { value: "api_request", label: "API Request" },
22
+ { value: "token", label: "Token" },
23
+ { value: "input_token", label: "Input Token" },
24
+ { value: "output_token", label: "Output Token" },
25
+ // seats / devices
26
+ { value: "seat", label: "Seat" },
27
+ { value: "user", label: "User" },
28
+ { value: "device", label: "Device" },
29
+ { value: "active_user", label: "Active User" }
30
+ ];
31
+ const INVOICE_STATUS_ENTRIES = [
32
+ // `posted` is the canonical "issued" state and is also the default a
33
+ // freshly created invoice transitions to — but billing creates drafts,
34
+ // so `draft` is the default DictionaryEntry for new rows.
35
+ { value: "draft", label: "Draft", isDefault: true, color: "#9ca3af" },
36
+ { value: "posted", label: "Posted", isDefault: false, color: "#2563eb" },
37
+ { value: "paid", label: "Paid", isDefault: false, color: "#16a34a" },
38
+ { value: "void", label: "Void", isDefault: false, color: "#dc2626" }
39
+ ];
40
+ async function ensureDictionary(em, scope, params) {
41
+ const existing = await em.findOne(Dictionary, {
42
+ tenantId: scope.tenantId,
43
+ organizationId: scope.organizationId,
44
+ key: params.key,
45
+ deletedAt: null
46
+ });
47
+ if (existing) return existing;
48
+ const dictionary = em.create(Dictionary, {
49
+ tenantId: scope.tenantId,
50
+ organizationId: scope.organizationId,
51
+ key: params.key,
52
+ name: params.name,
53
+ description: params.description,
54
+ isSystem: true,
55
+ isActive: true,
56
+ managerVisibility: "hidden",
57
+ createdAt: /* @__PURE__ */ new Date(),
58
+ updatedAt: /* @__PURE__ */ new Date()
59
+ });
60
+ em.persist(dictionary);
61
+ await em.flush();
62
+ return dictionary;
63
+ }
64
+ async function ensureDictionaryEntries(em, scope, dictionary, entries) {
65
+ const existing = await em.find(DictionaryEntry, {
66
+ dictionary,
67
+ tenantId: scope.tenantId,
68
+ organizationId: scope.organizationId
69
+ });
70
+ const existingValues = new Set(existing.map((e) => e.value));
71
+ let position = existing.length;
72
+ for (const entry of entries) {
73
+ if (existingValues.has(entry.value)) continue;
74
+ em.persist(
75
+ em.create(DictionaryEntry, {
76
+ dictionary,
77
+ tenantId: scope.tenantId,
78
+ organizationId: scope.organizationId,
79
+ value: entry.value,
80
+ normalizedValue: entry.value.trim().toLowerCase(),
81
+ label: entry.label,
82
+ color: entry.color ?? null,
83
+ position,
84
+ isDefault: entry.isDefault ?? false,
85
+ createdAt: /* @__PURE__ */ new Date(),
86
+ updatedAt: /* @__PURE__ */ new Date()
87
+ })
88
+ );
89
+ position += 1;
90
+ }
91
+ await em.flush();
92
+ }
93
+ async function seedBillingUomDictionary(em, scope) {
94
+ const dict = await ensureDictionary(em, scope, {
95
+ key: BILLING_UOM_DICTIONARY_KEY,
96
+ name: "Billing units of measure",
97
+ description: "Units billing accepts on usage records (exact match \u2014 no conversion)."
98
+ });
99
+ await ensureDictionaryEntries(em, scope, dict, BILLING_UOM_CODES);
100
+ }
101
+ async function seedInvoiceStatusDictionary(em, scope) {
102
+ const dict = await ensureDictionary(em, scope, {
103
+ key: SALES_INVOICE_STATUS_DICTIONARY_KEY,
104
+ name: "Sales invoice statuses",
105
+ description: "draft \u2192 posted \u2192 paid (or void). Drives the billing draft/post lifecycle."
106
+ });
107
+ await ensureDictionaryEntries(em, scope, dict, INVOICE_STATUS_ENTRIES);
108
+ }
109
+ async function ensureModuleConfig(em, params) {
110
+ const existing = await em.findOne(ModuleConfig, {
111
+ moduleId: params.moduleId,
112
+ name: params.name
113
+ });
114
+ if (existing) return;
115
+ em.persist(
116
+ em.create(ModuleConfig, {
117
+ moduleId: params.moduleId,
118
+ name: params.name,
119
+ valueJson: params.value,
120
+ createdAt: /* @__PURE__ */ new Date(),
121
+ updatedAt: /* @__PURE__ */ new Date()
122
+ })
123
+ );
124
+ await em.flush();
125
+ }
126
+ async function seedBillingConfigs(em) {
127
+ await ensureModuleConfig(em, {
128
+ moduleId: "billing",
129
+ name: "cron_schedule",
130
+ value: "0 2 * * *"
131
+ });
132
+ await ensureModuleConfig(em, {
133
+ moduleId: "billing",
134
+ name: "cron_enabled",
135
+ value: true
136
+ });
137
+ await ensureModuleConfig(em, {
138
+ moduleId: "billing",
139
+ name: "invoice_number.format",
140
+ value: "INV-{YYYY}-{0000001}"
141
+ });
142
+ await ensureModuleConfig(em, {
143
+ moduleId: "billing",
144
+ name: "invoice_number.reset_cycle",
145
+ value: "yearly"
146
+ });
147
+ }
148
+ const ALL_BILLING_FEATURES = billingAclFeatures.map((f) => f.id);
149
+ const setup = {
150
+ defaultRoleFeatures: {
151
+ // Org admins get every billing feature — `billing.*` wildcard is matched
152
+ // by the shared feature matcher (`hasFeature` is wildcard-aware).
153
+ admin: ["billing.*"],
154
+ // Day-to-day finance operator persona.
155
+ billing_admin: [...ALL_BILLING_FEATURES],
156
+ // Read-only + dry-run preview — auditor / finance-watcher persona.
157
+ billing_finance_user: [
158
+ "billing.account.view",
159
+ "billing.item.view",
160
+ "billing.usage.view",
161
+ "billing.run.view",
162
+ "billing.run.dry_run",
163
+ "billing.invoice.view"
164
+ ],
165
+ // Narrow scope for external usage-ingestion API keys.
166
+ billing_usage_writer: ["billing.usage.ingest"]
167
+ },
168
+ /**
169
+ * Tenant init: per-tenant module configs (cron, invoice number defaults).
170
+ * Runs inside `setupInitialTenant()`, always — no `--no-examples` gate.
171
+ */
172
+ async onTenantCreated({ em }) {
173
+ await seedBillingConfigs(em);
174
+ },
175
+ /**
176
+ * Reference data: dictionaries (UoM, invoice statuses). Always runs
177
+ * during `mercato init`. Idempotent — re-seed is safe.
178
+ */
179
+ async seedDefaults({ em, tenantId, organizationId }) {
180
+ const scope = { tenantId, organizationId };
181
+ await seedBillingUomDictionary(em, scope);
182
+ await seedInvoiceStatusDictionary(em, scope);
183
+ }
184
+ };
185
+ var setup_default = setup;
186
+ export {
187
+ setup_default as default,
188
+ setup
189
+ };
190
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/billing/setup.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\nimport { features as billingAclFeatures } from './acl'\nimport {\n Dictionary,\n DictionaryEntry,\n} from '@open-mercato/core/modules/dictionaries/data/entities'\nimport { ModuleConfig } from '@open-mercato/core/modules/configs/data/entities'\n\n/**\n * Billing module setup \u2014 per-tenant defaults at module installation.\n *\n * Idempotency contract:\n * - All seed helpers below check for an existing row before inserting.\n * - Safe to re-run on every `mercato init` and on `auth sync-role-acls`.\n *\n * What this file seeds:\n * - `billing.uom` dictionary with 16 global codes (operators can add more).\n * - `sales.invoice_status` dictionary with `draft` / `posted` / `paid` /\n * `void` so the core/sales Invoice entity's dictionary-driven status\n * workflow has values to point at. (Core/sales does NOT seed these \u2014\n * billing owns them per Phase 0 validation.)\n * - `billing.cron_schedule` + `billing.cron_enabled` module configs.\n * - `billing.invoice_number.format` + `.reset_cycle` configs (per-tenant\n * defaults; the actual atomic numbering uses core/sales'\n * `SalesDocumentSequence` which is created by core/sales' own\n * `onTenantCreated`).\n *\n * Default role-feature grants:\n * - `admin` gets every `billing.*` feature.\n * - `billing_admin` is the day-to-day finance operator.\n * - `billing_finance_user` is read + dry-run (no posting / no real trigger).\n * - `billing_usage_writer` is the narrow scope for external system API keys.\n */\n\nconst BILLING_UOM_DICTIONARY_KEY = 'billing.uom'\nconst SALES_INVOICE_STATUS_DICTIONARY_KEY = 'sales.invoice_status'\n\nconst BILLING_UOM_CODES = [\n // time\n { value: 'hour', label: 'Hour' },\n { value: 'day', label: 'Day' },\n { value: 'week', label: 'Week' },\n { value: 'month', label: 'Month' },\n // volume\n { value: 'gb', label: 'Gigabyte' },\n { value: 'tb', label: 'Terabyte' },\n { value: 'mb', label: 'Megabyte' },\n // requests / tokens\n { value: 'request', label: 'Request' },\n { value: 'api_request', label: 'API Request' },\n { value: 'token', label: 'Token' },\n { value: 'input_token', label: 'Input Token' },\n { value: 'output_token', label: 'Output Token' },\n // seats / devices\n { value: 'seat', label: 'Seat' },\n { value: 'user', label: 'User' },\n { value: 'device', label: 'Device' },\n { value: 'active_user', label: 'Active User' },\n] as const\n\nconst INVOICE_STATUS_ENTRIES = [\n // `posted` is the canonical \"issued\" state and is also the default a\n // freshly created invoice transitions to \u2014 but billing creates drafts,\n // so `draft` is the default DictionaryEntry for new rows.\n { value: 'draft', label: 'Draft', isDefault: true, color: '#9ca3af' },\n { value: 'posted', label: 'Posted', isDefault: false, color: '#2563eb' },\n { value: 'paid', label: 'Paid', isDefault: false, color: '#16a34a' },\n { value: 'void', label: 'Void', isDefault: false, color: '#dc2626' },\n] as const\n\ntype SeedScope = { tenantId: string; organizationId: string }\n\nasync function ensureDictionary(\n em: EntityManager,\n scope: SeedScope,\n params: { key: string; name: string; description: string },\n): Promise<Dictionary> {\n const existing = await em.findOne(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: params.key,\n deletedAt: null,\n })\n if (existing) return existing\n const dictionary = em.create(Dictionary, {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n key: params.key,\n name: params.name,\n description: params.description,\n isSystem: true,\n isActive: true,\n managerVisibility: 'hidden',\n createdAt: new Date(),\n updatedAt: new Date(),\n })\n em.persist(dictionary)\n await em.flush()\n return dictionary\n}\n\nasync function ensureDictionaryEntries(\n em: EntityManager,\n scope: SeedScope,\n dictionary: Dictionary,\n entries: ReadonlyArray<{\n value: string\n label: string\n isDefault?: boolean\n color?: string\n }>,\n): Promise<void> {\n const existing = await em.find(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n })\n const existingValues = new Set(existing.map((e) => e.value))\n let position = existing.length\n for (const entry of entries) {\n if (existingValues.has(entry.value)) continue\n em.persist(\n em.create(DictionaryEntry, {\n dictionary,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n value: entry.value,\n normalizedValue: entry.value.trim().toLowerCase(),\n label: entry.label,\n color: entry.color ?? null,\n position,\n isDefault: entry.isDefault ?? false,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n )\n position += 1\n }\n await em.flush()\n}\n\nasync function seedBillingUomDictionary(em: EntityManager, scope: SeedScope): Promise<void> {\n const dict = await ensureDictionary(em, scope, {\n key: BILLING_UOM_DICTIONARY_KEY,\n name: 'Billing units of measure',\n description: 'Units billing accepts on usage records (exact match \u2014 no conversion).',\n })\n await ensureDictionaryEntries(em, scope, dict, BILLING_UOM_CODES)\n}\n\nasync function seedInvoiceStatusDictionary(em: EntityManager, scope: SeedScope): Promise<void> {\n const dict = await ensureDictionary(em, scope, {\n key: SALES_INVOICE_STATUS_DICTIONARY_KEY,\n name: 'Sales invoice statuses',\n description: 'draft \u2192 posted \u2192 paid (or void). Drives the billing draft/post lifecycle.',\n })\n await ensureDictionaryEntries(em, scope, dict, INVOICE_STATUS_ENTRIES)\n}\n\nasync function ensureModuleConfig(\n em: EntityManager,\n params: { moduleId: string; name: string; value: unknown },\n): Promise<void> {\n const existing = await em.findOne(ModuleConfig, {\n moduleId: params.moduleId,\n name: params.name,\n })\n if (existing) return\n em.persist(\n em.create(ModuleConfig, {\n moduleId: params.moduleId,\n name: params.name,\n valueJson: params.value as Record<string, unknown> | null,\n createdAt: new Date(),\n updatedAt: new Date(),\n }),\n )\n await em.flush()\n}\n\nasync function seedBillingConfigs(em: EntityManager): Promise<void> {\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'cron_schedule',\n value: '0 2 * * *',\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'cron_enabled',\n value: true,\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'invoice_number.format',\n value: 'INV-{YYYY}-{0000001}',\n })\n await ensureModuleConfig(em, {\n moduleId: 'billing',\n name: 'invoice_number.reset_cycle',\n value: 'yearly',\n })\n}\n\n// Derived from `acl.ts` so a new feature added there automatically lands\n// in every role that should get \"everything billing\" \u2014 no parallel list\n// to keep in sync.\nconst ALL_BILLING_FEATURES = billingAclFeatures.map((f) => f.id)\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n // Org admins get every billing feature \u2014 `billing.*` wildcard is matched\n // by the shared feature matcher (`hasFeature` is wildcard-aware).\n admin: ['billing.*'],\n\n // Day-to-day finance operator persona.\n billing_admin: [...ALL_BILLING_FEATURES],\n\n // Read-only + dry-run preview \u2014 auditor / finance-watcher persona.\n billing_finance_user: [\n 'billing.account.view',\n 'billing.item.view',\n 'billing.usage.view',\n 'billing.run.view',\n 'billing.run.dry_run',\n 'billing.invoice.view',\n ],\n\n // Narrow scope for external usage-ingestion API keys.\n billing_usage_writer: ['billing.usage.ingest'],\n },\n\n /**\n * Tenant init: per-tenant module configs (cron, invoice number defaults).\n * Runs inside `setupInitialTenant()`, always \u2014 no `--no-examples` gate.\n */\n async onTenantCreated({ em }) {\n await seedBillingConfigs(em)\n },\n\n /**\n * Reference data: dictionaries (UoM, invoice statuses). Always runs\n * during `mercato init`. Idempotent \u2014 re-seed is safe.\n */\n async seedDefaults({ em, tenantId, organizationId }) {\n const scope = { tenantId, organizationId }\n await seedBillingUomDictionary(em, scope)\n await seedInvoiceStatusDictionary(em, scope)\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,47 @@
1
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
2
+ import { SalesInvoice } from "@open-mercato/core/modules/sales/data/entities";
3
+ import { resolveInvoiceStatusEntryId } from "../lib/invoiceStatus.js";
4
+ const metadata = {
5
+ event: "payment_gateways.payment.captured",
6
+ persistent: true,
7
+ id: "billing-payment-captured-to-paid"
8
+ };
9
+ async function handle(payload) {
10
+ if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) {
11
+ return;
12
+ }
13
+ const container = await createRequestContainer();
14
+ const em = container.resolve("em").fork();
15
+ const invoice = await em.findOne(SalesInvoice, {
16
+ id: payload.paymentId,
17
+ tenantId: payload.tenantId,
18
+ organizationId: payload.organizationId,
19
+ deletedAt: null
20
+ });
21
+ if (!invoice) {
22
+ return;
23
+ }
24
+ if (invoice.status === "paid") {
25
+ return;
26
+ }
27
+ if (invoice.status !== "posted") {
28
+ return;
29
+ }
30
+ const paidStatusEntryId = await resolveInvoiceStatusEntryId(
31
+ em,
32
+ payload.tenantId,
33
+ payload.organizationId,
34
+ "paid"
35
+ );
36
+ invoice.status = "paid";
37
+ invoice.statusEntryId = paidStatusEntryId;
38
+ invoice.paidTotalAmount = invoice.grandTotalGrossAmount;
39
+ invoice.outstandingAmount = "0";
40
+ invoice.updatedAt = /* @__PURE__ */ new Date();
41
+ await em.flush();
42
+ }
43
+ export {
44
+ handle as default,
45
+ metadata
46
+ };
47
+ //# sourceMappingURL=payment-captured-to-paid.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/subscribers/payment-captured-to-paid.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { resolveInvoiceStatusEntryId } from '../lib/invoiceStatus'\n\n/**\n * Subscriber: payment captured \u2192 billing invoice paid.\n *\n * Spec wishlist was `payments.payment.completed` carrying `invoice_id`.\n * The actual upstream event is `payment_gateways.payment.captured` and\n * its payload carries `paymentId` \u2014 set by whoever created the payment\n * session. Billing flows that drive their own payment sessions can\n * pass `paymentId = invoice.id`; the subscriber matches on that.\n *\n * Behaviour:\n * 1. Try to find a `core/sales` invoice with `id = payload.paymentId`\n * scoped to the same tenant/organization.\n * 2. If found AND its `status == 'posted'`, flip it to `'paid'`\n * (resolves the `paid` `DictionaryEntry`, updates `status` +\n * `status_entry_id`). Drafts and already-paid / voided invoices\n * are left untouched.\n * 3. If `paymentId` does not point at a billing-known invoice (e.g.\n * the payment was for a `sales.order`), the subscriber is a\n * no-op \u2014 every other module's own subscriber owns its slice.\n *\n * Cross-tenant safety: we always re-scope by `tenantId` +\n * `organizationId` from the event payload. A malformed payload (e.g.\n * missing scope fields) short-circuits without touching the DB.\n *\n * Idempotency: re-firing the same captured event finds the invoice\n * already paid and skips silently.\n */\n\nexport const metadata = {\n event: 'payment_gateways.payment.captured',\n persistent: true,\n id: 'billing-payment-captured-to-paid',\n}\n\ntype CapturedPayload = {\n paymentId?: string\n transactionId?: string\n organizationId?: string\n tenantId?: string\n providerKey?: string\n}\n\nexport default async function handle(payload: CapturedPayload): Promise<void> {\n if (!payload?.paymentId || !payload.tenantId || !payload.organizationId) {\n return\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager).fork()\n\n const invoice = await em.findOne(SalesInvoice, {\n id: payload.paymentId,\n tenantId: payload.tenantId,\n organizationId: payload.organizationId,\n deletedAt: null,\n })\n if (!invoice) {\n // The captured payment isn't for a billing invoice \u2014 silent\n // no-op. Other subscribers (sales, checkout) handle their slice.\n return\n }\n if (invoice.status === 'paid') {\n // Already paid \u2014 idempotent re-firing.\n return\n }\n if (invoice.status !== 'posted') {\n // A captured payment for a draft / void invoice is suspicious but\n // not actionable here. Future iteration: log to audit_logs.\n return\n }\n\n const paidStatusEntryId = await resolveInvoiceStatusEntryId(\n em,\n payload.tenantId,\n payload.organizationId,\n 'paid',\n )\n invoice.status = 'paid'\n invoice.statusEntryId = paidStatusEntryId\n invoice.paidTotalAmount = invoice.grandTotalGrossAmount\n invoice.outstandingAmount = '0'\n invoice.updatedAt = new Date()\n await em.flush()\n}\n"],
5
+ "mappings": "AACA,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,mCAAmC;AA8BrC,MAAM,WAAW;AAAA,EACtB,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,IAAI;AACN;AAUA,eAAO,OAA8B,SAAyC;AAC5E,MAAI,CAAC,SAAS,aAAa,CAAC,QAAQ,YAAY,CAAC,QAAQ,gBAAgB;AACvE;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAM,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAE3D,QAAM,UAAU,MAAM,GAAG,QAAQ,cAAc;AAAA,IAC7C,IAAI,QAAQ;AAAA,IACZ,UAAU,QAAQ;AAAA,IAClB,gBAAgB,QAAQ;AAAA,IACxB,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,SAAS;AAGZ;AAAA,EACF;AACA,MAAI,QAAQ,WAAW,QAAQ;AAE7B;AAAA,EACF;AACA,MAAI,QAAQ,WAAW,UAAU;AAG/B;AAAA,EACF;AAEA,QAAM,oBAAoB,MAAM;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR;AAAA,EACF;AACA,UAAQ,SAAS;AACjB,UAAQ,gBAAgB;AACxB,UAAQ,kBAAkB,QAAQ;AAClC,UAAQ,oBAAoB;AAC5B,UAAQ,YAAY,oBAAI,KAAK;AAC7B,QAAM,GAAG,MAAM;AACjB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ const translatableFields = {};
2
+ export {
3
+ translatableFields
4
+ };
5
+ //# sourceMappingURL=translations.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/billing/translations.ts"],
4
+ "sourcesContent": ["/**\n * Translatable fields for billing entities.\n *\n * Phase 0 keeps this empty \u2014 the Billing Account name and invoice line\n * descriptions are user-entered free text, not localizable templates.\n * If we ever ship localized invoice templates (Phase 4 admin UI), the\n * template label fields land here.\n */\nexport const translatableFields: Record<string, string[]> = {}\n"],
5
+ "mappings": "AAQO,MAAM,qBAA+C,CAAC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,31 @@
1
+ import { BillRun } from "../data/entities.js";
2
+ const metadata = {
3
+ queue: "billing-reap-stale-runs",
4
+ id: "billing:reap-stale-bill-runs",
5
+ concurrency: 1
6
+ };
7
+ const STALE_THRESHOLD_MS = 60 * 60 * 1e3;
8
+ async function handle(_job, ctx) {
9
+ const em = ctx.resolve("em");
10
+ const threshold = new Date(Date.now() - STALE_THRESHOLD_MS);
11
+ const stale = await em.find(BillRun, {
12
+ status: "running",
13
+ startedAt: { $lt: threshold }
14
+ });
15
+ if (stale.length === 0) return;
16
+ const now = /* @__PURE__ */ new Date();
17
+ for (const run of stale) {
18
+ run.status = "failed";
19
+ run.finishedAt = now;
20
+ const summary = run.summary ?? {};
21
+ summary.reaper_message = "Stale running run reaped after crash; advisory lock released";
22
+ summary.reaped_at = now.toISOString();
23
+ run.summary = summary;
24
+ }
25
+ await em.flush();
26
+ }
27
+ export {
28
+ handle as default,
29
+ metadata
30
+ };
31
+ //# sourceMappingURL=reap-stale-bill-runs.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/workers/reap-stale-bill-runs.ts"],
4
+ "sourcesContent": ["import type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillRun } from '../data/entities'\n\nexport const metadata: WorkerMeta = {\n queue: 'billing-reap-stale-runs',\n id: 'billing:reap-stale-bill-runs',\n concurrency: 1,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\n/**\n * Reaper for \"zombie\" Bill Runs left in `status='running'` after a crash.\n *\n * Background \u2014 the Bill Run engine acquires a per-tenant Postgres\n * transaction-scoped advisory lock (`pg_try_advisory_xact_lock`). When the\n * holding transaction ends (commit / rollback / process death) the lock is\n * released automatically \u2014 so there is no lock leak \u2014 but the `BillRun`\n * row's `status='running'` stays set forever, blocking restarts and\n * confusing the dashboard.\n *\n * Phase 0 ships this as the **registered worker shell** so the module's\n * worker auto-discovery includes it from day one. The active-advisory-lock\n * check (which is what distinguishes a real zombie from a healthy\n * long-running run) lands with the Bill Run engine itself in Phase 2.\n *\n * For now the heuristic is time-only: any `running` row older than 1 hour\n * is marked failed with an explanatory message. That is conservative \u2014\n * real runs almost never take an hour even for 500k-account tenants\n * (see spec Performance Considerations).\n */\nconst STALE_THRESHOLD_MS = 60 * 60 * 1000 // 1 hour\n\nexport default async function handle(_job: QueuedJob, ctx: HandlerContext): Promise<void> {\n const em = ctx.resolve<EntityManager>('em')\n const threshold = new Date(Date.now() - STALE_THRESHOLD_MS)\n const stale = await em.find(BillRun, {\n status: 'running',\n startedAt: { $lt: threshold },\n } satisfies FilterQuery<BillRun>)\n if (stale.length === 0) return\n const now = new Date()\n for (const run of stale) {\n run.status = 'failed'\n run.finishedAt = now\n const summary = (run.summary ?? {}) as Record<string, unknown>\n summary.reaper_message =\n 'Stale running run reaped after crash; advisory lock released'\n summary.reaped_at = now.toISOString()\n run.summary = summary\n }\n await em.flush()\n}\n"],
5
+ "mappings": "AAGA,SAAS,eAAe;AAEjB,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AA0BA,MAAM,qBAAqB,KAAK,KAAK;AAErC,eAAO,OAA8B,MAAiB,KAAoC;AACxF,QAAM,KAAK,IAAI,QAAuB,IAAI;AAC1C,QAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,kBAAkB;AAC1D,QAAM,QAAQ,MAAM,GAAG,KAAK,SAAS;AAAA,IACnC,QAAQ;AAAA,IACR,WAAW,EAAE,KAAK,UAAU;AAAA,EAC9B,CAAgC;AAChC,MAAI,MAAM,WAAW,EAAG;AACxB,QAAM,MAAM,oBAAI,KAAK;AACrB,aAAW,OAAO,OAAO;AACvB,QAAI,SAAS;AACb,QAAI,aAAa;AACjB,UAAM,UAAW,IAAI,WAAW,CAAC;AACjC,YAAQ,iBACN;AACF,YAAQ,YAAY,IAAI,YAAY;AACpC,QAAI,UAAU;AAAA,EAChB;AACA,QAAM,GAAG,MAAM;AACjB;",
6
+ "names": []
7
+ }