@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,224 @@
1
+ import { z } from "zod";
2
+ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
+ import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
4
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { BillingAccount, BillingItem } from "../../data/entities.js";
6
+ import { billingEntityIds } from "../../data/entityIds.js";
7
+ import {
8
+ billingItemCreateSchema,
9
+ billingItemListQuerySchema,
10
+ billingItemUpdateSchema
11
+ } from "../../data/validators.js";
12
+ import {
13
+ createBillingCrudOpenApi,
14
+ createPagedListResponseSchema,
15
+ defaultOkResponseSchema
16
+ } from "../openapi.js";
17
+ const routeMetadata = {
18
+ GET: { requireAuth: true, requireFeatures: ["billing.item.view"] },
19
+ POST: { requireAuth: true, requireFeatures: ["billing.item.manage"] },
20
+ PUT: { requireAuth: true, requireFeatures: ["billing.item.manage"] },
21
+ DELETE: { requireAuth: true, requireFeatures: ["billing.item.manage"] }
22
+ };
23
+ const metadata = routeMetadata;
24
+ const billingItemListItemSchema = z.object({
25
+ id: z.string().uuid(),
26
+ bill_account_id: z.string().uuid(),
27
+ bill_account_name: z.string().nullable(),
28
+ type: z.string(),
29
+ bill_start_date: z.string(),
30
+ bill_end_date: z.string().nullable(),
31
+ description: z.string(),
32
+ rate_json: z.record(z.string(), z.unknown()),
33
+ uom_code: z.string().nullable(),
34
+ subscription_id: z.string().nullable(),
35
+ subscription_item_id: z.string().nullable(),
36
+ source_ref: z.string().nullable(),
37
+ currency_mismatch: z.boolean(),
38
+ billed_to_date: z.string().nullable(),
39
+ is_active: z.boolean(),
40
+ organization_id: z.string().uuid(),
41
+ tenant_id: z.string().uuid(),
42
+ created_at: z.string(),
43
+ updated_at: z.string()
44
+ });
45
+ const itemCreateResponseSchema = z.object({
46
+ id: z.string().uuid().nullable(),
47
+ deduplicated: z.boolean()
48
+ });
49
+ const rawBodySchema = z.object({}).passthrough();
50
+ const crud = makeCrudRoute({
51
+ metadata: routeMetadata,
52
+ orm: {
53
+ entity: BillingItem,
54
+ idField: "id",
55
+ orgField: "organizationId",
56
+ tenantField: "tenantId",
57
+ softDeleteField: "deletedAt"
58
+ },
59
+ list: {
60
+ schema: billingItemListQuerySchema,
61
+ entityId: billingEntityIds.item,
62
+ fields: [
63
+ "id",
64
+ "bill_account_id",
65
+ "type",
66
+ "bill_start_date",
67
+ "bill_end_date",
68
+ "description",
69
+ "rate_json",
70
+ "uom_code",
71
+ "subscription_id",
72
+ "subscription_item_id",
73
+ "source_ref",
74
+ "currency_mismatch",
75
+ "billed_to_date",
76
+ "is_active",
77
+ "organization_id",
78
+ "tenant_id",
79
+ "created_at",
80
+ "updated_at"
81
+ ],
82
+ sortFieldMap: {
83
+ createdAt: "created_at",
84
+ updatedAt: "updated_at",
85
+ billStartDate: "bill_start_date",
86
+ billEndDate: "bill_end_date"
87
+ },
88
+ buildFilters: async (query) => {
89
+ const filters = {};
90
+ if (typeof query.id === "string" && query.id) {
91
+ filters.id = { $eq: query.id };
92
+ }
93
+ if (typeof query.search === "string" && query.search.trim()) {
94
+ const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\${m}`)}%`;
95
+ filters.description = { $ilike: pattern };
96
+ }
97
+ if (typeof query.billAccountId === "string" && query.billAccountId) {
98
+ filters.bill_account_id = { $eq: query.billAccountId };
99
+ }
100
+ if (typeof query.type === "string" && query.type) {
101
+ filters.type = { $eq: query.type };
102
+ }
103
+ if (typeof query.subscriptionId === "string" && query.subscriptionId) {
104
+ filters.subscription_id = { $eq: query.subscriptionId };
105
+ }
106
+ if (typeof query.isActive === "boolean") {
107
+ filters.is_active = { $eq: query.isActive };
108
+ }
109
+ if (query.activeAt instanceof Date) {
110
+ filters.bill_start_date = { $lte: query.activeAt };
111
+ filters.$or = [
112
+ { bill_end_date: null },
113
+ { bill_end_date: { $gte: query.activeAt } }
114
+ ];
115
+ }
116
+ return filters;
117
+ }
118
+ },
119
+ actions: {
120
+ create: {
121
+ commandId: "billing.items.create",
122
+ schema: rawBodySchema,
123
+ mapInput: async ({ raw, ctx }) => {
124
+ const { translate } = await resolveTranslations();
125
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate);
126
+ return billingItemCreateSchema.parse(scoped);
127
+ },
128
+ response: ({ result }) => ({
129
+ id: result?.id ?? null,
130
+ deduplicated: Boolean(result?.deduplicated)
131
+ }),
132
+ // Idempotent create returns 200 (vs 201). The spec mandates
133
+ // duplicate POST returns the existing row, never creates a
134
+ // duplicate — keeping the success status consistent makes
135
+ // client-side handling trivial.
136
+ status: 200
137
+ },
138
+ update: {
139
+ commandId: "billing.items.update",
140
+ schema: rawBodySchema,
141
+ mapInput: async ({ raw, ctx }) => {
142
+ const { translate } = await resolveTranslations();
143
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate);
144
+ return billingItemUpdateSchema.parse(scoped);
145
+ },
146
+ response: () => ({ ok: true })
147
+ },
148
+ delete: {
149
+ commandId: "billing.items.delete",
150
+ schema: rawBodySchema,
151
+ mapInput: async ({ raw, ctx }) => {
152
+ const { translate } = await resolveTranslations();
153
+ const envelope = raw ?? {};
154
+ const body = envelope.body ?? {};
155
+ const query = envelope.query ?? {};
156
+ const scoped = withScopedPayload(body, ctx, translate);
157
+ const id = typeof body.id === "string" && body.id || typeof query.id === "string" && query.id || null;
158
+ if (!id) {
159
+ throw new Error("id is required");
160
+ }
161
+ return {
162
+ id,
163
+ tenantId: scoped.tenantId,
164
+ organizationId: scoped.organizationId
165
+ };
166
+ },
167
+ response: () => ({ ok: true })
168
+ }
169
+ },
170
+ hooks: {
171
+ // Items reference their account by FK id only; the list + detail UI
172
+ // wants the human-readable account name. One bounded query
173
+ // (≤ pageSize ids) resolves it after the page is fetched.
174
+ afterList: async (payload, ctx) => {
175
+ const items = payload?.items ?? [];
176
+ const accountIds = Array.from(
177
+ new Set(
178
+ items.map((row) => row.bill_account_id).filter((v) => typeof v === "string" && v.length > 0)
179
+ )
180
+ );
181
+ if (accountIds.length === 0) return;
182
+ const em = ctx.container.resolve("em");
183
+ const accounts = await em.find(BillingAccount, { id: { $in: accountIds } });
184
+ const nameById = new Map(
185
+ accounts.map((a) => [String(a.id), a.name])
186
+ );
187
+ for (const row of items) {
188
+ row.bill_account_name = nameById.get(String(row.bill_account_id)) ?? null;
189
+ }
190
+ }
191
+ }
192
+ });
193
+ const { POST, PUT, DELETE } = crud;
194
+ const GET = crud.GET;
195
+ const openApi = createBillingCrudOpenApi({
196
+ resourceName: "BillingItem",
197
+ pluralName: "BillingItems",
198
+ querySchema: billingItemListQuerySchema,
199
+ listResponseSchema: createPagedListResponseSchema(billingItemListItemSchema),
200
+ create: {
201
+ schema: billingItemCreateSchema,
202
+ responseSchema: itemCreateResponseSchema,
203
+ description: "Creates a billing item. Idempotent via `sourceRef`: re-POSTing the same `(tenantId, billAccountId, sourceRef)` returns the existing row with `deduplicated: true` instead of creating a duplicate. The unique index on `(tenant_id, bill_account_id, source_ref)` is the correctness gate."
204
+ },
205
+ update: {
206
+ schema: billingItemUpdateSchema,
207
+ responseSchema: defaultOkResponseSchema,
208
+ description: "Updates a billing item. `type` and `sourceRef` are immutable \u2014 to change either, soft-delete the item and create a new one."
209
+ },
210
+ del: {
211
+ schema: z.object({ id: z.string().uuid() }),
212
+ responseSchema: defaultOkResponseSchema,
213
+ description: "Soft-deletes a billing item."
214
+ }
215
+ });
216
+ export {
217
+ DELETE,
218
+ GET,
219
+ POST,
220
+ PUT,
221
+ metadata,
222
+ openApi
223
+ };
224
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/billing/api/items/route.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillingAccount, BillingItem } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n billingItemCreateSchema,\n billingItemListQuerySchema,\n billingItemUpdateSchema,\n} from '../../data/validators'\nimport {\n createBillingCrudOpenApi,\n createPagedListResponseSchema,\n defaultOkResponseSchema,\n} from '../openapi'\n\n// \u2500\u2500\u2500 Auth / RBAC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.item.view'] },\n POST: { requireAuth: true, requireFeatures: ['billing.item.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['billing.item.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['billing.item.manage'] },\n}\n\nexport const metadata = routeMetadata\n\n// The list endpoint projects raw column names \u2014 snake_case.\nconst billingItemListItemSchema = z.object({\n id: z.string().uuid(),\n bill_account_id: z.string().uuid(),\n bill_account_name: z.string().nullable(),\n type: z.string(),\n bill_start_date: z.string(),\n bill_end_date: z.string().nullable(),\n description: z.string(),\n rate_json: z.record(z.string(), z.unknown()),\n uom_code: z.string().nullable(),\n subscription_id: z.string().nullable(),\n subscription_item_id: z.string().nullable(),\n source_ref: z.string().nullable(),\n currency_mismatch: z.boolean(),\n billed_to_date: z.string().nullable(),\n is_active: z.boolean(),\n organization_id: z.string().uuid(),\n tenant_id: z.string().uuid(),\n created_at: z.string(),\n updated_at: z.string(),\n})\n\n// Response for create includes `deduplicated` so integrators can tell\n// whether their POST resulted in a new row or hit the idempotency\n// short-circuit on `source_ref`.\nconst itemCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n deduplicated: z.boolean(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillingItem,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: billingItemListQuerySchema,\n entityId: billingEntityIds.item,\n fields: [\n 'id',\n 'bill_account_id',\n 'type',\n 'bill_start_date',\n 'bill_end_date',\n 'description',\n 'rate_json',\n 'uom_code',\n 'subscription_id',\n 'subscription_item_id',\n 'source_ref',\n 'currency_mismatch',\n 'billed_to_date',\n 'is_active',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n updatedAt: 'updated_at',\n billStartDate: 'bill_start_date',\n billEndDate: 'bill_end_date',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n // `?id=<uuid>` narrows the list to one row \u2014 the detail page\n // reads a single item through this filter.\n if (typeof query.id === 'string' && query.id) {\n filters.id = { $eq: query.id }\n }\n if (typeof query.search === 'string' && query.search.trim()) {\n const pattern = `%${query.search.trim().replace(/[%_]/g, (m) => `\\\\${m}`)}%`\n filters.description = { $ilike: pattern }\n }\n if (typeof query.billAccountId === 'string' && query.billAccountId) {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.type === 'string' && query.type) {\n filters.type = { $eq: query.type }\n }\n if (typeof query.subscriptionId === 'string' && query.subscriptionId) {\n filters.subscription_id = { $eq: query.subscriptionId }\n }\n if (typeof query.isActive === 'boolean') {\n filters.is_active = { $eq: query.isActive }\n }\n // `activeAt=<date>` returns items whose validity window covers\n // the supplied date (Bill Run engine uses the same predicate\n // shape internally \u2014 see spec \"Mid-cycle recurring items\").\n if (query.activeAt instanceof Date) {\n filters.bill_start_date = { $lte: query.activeAt }\n filters.$or = [\n { bill_end_date: null },\n { bill_end_date: { $gte: query.activeAt } },\n ]\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'billing.items.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingItemCreateSchema.parse(scoped)\n },\n response: ({ result }) => ({\n id: result?.id ?? null,\n deduplicated: Boolean(result?.deduplicated),\n }),\n // Idempotent create returns 200 (vs 201). The spec mandates\n // duplicate POST returns the existing row, never creates a\n // duplicate \u2014 keeping the success status consistent makes\n // client-side handling trivial.\n status: 200,\n },\n update: {\n commandId: 'billing.items.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingItemUpdateSchema.parse(scoped)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'billing.items.delete',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n // The factory hands delete a `{ body, query }` envelope \u2014 the\n // id may arrive in either, depending on the caller.\n const envelope = (raw ?? {}) as {\n body?: Record<string, unknown>\n query?: Record<string, unknown>\n }\n const body = envelope.body ?? {}\n const query = envelope.query ?? {}\n const scoped = withScopedPayload(body, ctx, translate)\n const id =\n (typeof body.id === 'string' && body.id) ||\n (typeof query.id === 'string' && query.id) ||\n null\n if (!id) {\n throw new Error('id is required')\n }\n return {\n id,\n tenantId: (scoped as { tenantId?: string }).tenantId,\n organizationId: (scoped as { organizationId?: string }).organizationId,\n }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n // Items reference their account by FK id only; the list + detail UI\n // wants the human-readable account name. One bounded query\n // (\u2264 pageSize ids) resolves it after the page is fetched.\n afterList: async (payload: unknown, ctx: unknown) => {\n const items = ((payload as { items?: unknown[] } | null)?.items ?? []) as Array<\n Record<string, unknown>\n >\n const accountIds = Array.from(\n new Set(\n items\n .map((row) => row.bill_account_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n if (accountIds.length === 0) return\n const em = (ctx as { container: AwilixContainer }).container.resolve<EntityManager>('em')\n const accounts = await em.find(BillingAccount, { id: { $in: accountIds } })\n const nameById = new Map(\n accounts.map((a) => [String(a.id), a.name] as [string, unknown]),\n )\n for (const row of items) {\n row.bill_account_name = nameById.get(String(row.bill_account_id)) ?? null\n }\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\nexport { POST, PUT, DELETE }\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillingItem',\n pluralName: 'BillingItems',\n querySchema: billingItemListQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billingItemListItemSchema),\n create: {\n schema: billingItemCreateSchema,\n responseSchema: itemCreateResponseSchema,\n description:\n 'Creates a billing item. Idempotent via `sourceRef`: re-POSTing the same ' +\n '`(tenantId, billAccountId, sourceRef)` returns the existing row with ' +\n '`deduplicated: true` instead of creating a duplicate. The unique index ' +\n 'on `(tenant_id, bill_account_id, source_ref)` is the correctness gate.',\n },\n update: {\n schema: billingItemUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description:\n 'Updates a billing item. `type` and `sourceRef` are immutable \u2014 to change ' +\n 'either, soft-delete the item and create a new one.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a billing item.',\n },\n})\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ createCrudOpenApiFactory,
3
+ createPagedListResponseSchema as createSharedPagedListResponseSchema,
4
+ defaultCreateResponseSchema,
5
+ defaultOkResponseSchema
6
+ } from "@open-mercato/shared/lib/openapi/crud";
7
+ const buildBillingCrudOpenApi = createCrudOpenApiFactory({
8
+ defaultTag: "Billing",
9
+ defaultCreateResponseSchema,
10
+ defaultOkResponseSchema,
11
+ makeListDescription: ({ pluralLower }) => `Returns a paginated collection of ${pluralLower} scoped to the authenticated organization.`
12
+ });
13
+ function createBillingCrudOpenApi(options) {
14
+ return buildBillingCrudOpenApi(options);
15
+ }
16
+ function createPagedListResponseSchema(itemSchema) {
17
+ return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true });
18
+ }
19
+ export {
20
+ createBillingCrudOpenApi,
21
+ createPagedListResponseSchema,
22
+ defaultCreateResponseSchema,
23
+ defaultOkResponseSchema
24
+ };
25
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/api/openapi.ts"],
4
+ "sourcesContent": ["import {\n createCrudOpenApiFactory,\n createPagedListResponseSchema as createSharedPagedListResponseSchema,\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n type CrudOpenApiOptions,\n} from '@open-mercato/shared/lib/openapi/crud'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi/types'\nimport type { ZodTypeAny } from 'zod'\n\n/**\n * Billing CRUD OpenAPI factory. Every billing API route imports this\n * and calls it with the resource's `create` / `update` / `del` schemas\n * \u2014 the framework picks up the resulting `openApi` export and merges\n * it into the global API documentation bundle.\n */\nconst buildBillingCrudOpenApi = createCrudOpenApiFactory({\n defaultTag: 'Billing',\n defaultCreateResponseSchema,\n defaultOkResponseSchema,\n makeListDescription: ({ pluralLower }) =>\n `Returns a paginated collection of ${pluralLower} scoped to the authenticated organization.`,\n})\n\nexport function createBillingCrudOpenApi(options: CrudOpenApiOptions): OpenApiRouteDoc {\n return buildBillingCrudOpenApi(options)\n}\n\nexport function createPagedListResponseSchema(itemSchema: ZodTypeAny) {\n return createSharedPagedListResponseSchema(itemSchema, { paginationMetaOptional: true })\n}\n\nexport { defaultCreateResponseSchema, defaultOkResponseSchema }\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EACA,iCAAiC;AAAA,EACjC;AAAA,EACA;AAAA,OAEK;AAUP,MAAM,0BAA0B,yBAAyB;AAAA,EACvD,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,qBAAqB,CAAC,EAAE,YAAY,MAClC,qCAAqC,WAAW;AACpD,CAAC;AAEM,SAAS,yBAAyB,SAA8C;AACrF,SAAO,wBAAwB,OAAO;AACxC;AAEO,SAAS,8BAA8B,YAAwB;AACpE,SAAO,oCAAoC,YAAY,EAAE,wBAAwB,KAAK,CAAC;AACzF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+ import { SalesInvoice } from "@open-mercato/core/modules/sales/data/entities";
3
+ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
4
+ import { BillingAccount, BillRunOutcome } from "../../data/entities.js";
5
+ import { billingEntityIds } from "../../data/entityIds.js";
6
+ import {
7
+ BILL_RUN_OUTCOME_STATUSES
8
+ } from "../../data/validators.js";
9
+ import { createBillingCrudOpenApi, createPagedListResponseSchema } from "../openapi.js";
10
+ const routeMetadata = {
11
+ GET: { requireAuth: true, requireFeatures: ["billing.run.view"] }
12
+ };
13
+ const metadata = routeMetadata;
14
+ const listQuerySchema = z.object({
15
+ page: z.coerce.number().int().min(1).default(1),
16
+ pageSize: z.coerce.number().int().min(1).max(100).default(50),
17
+ sortField: z.string().optional(),
18
+ sortDir: z.enum(["asc", "desc"]).optional(),
19
+ billRunId: z.string().uuid().optional(),
20
+ billAccountId: z.string().uuid().optional(),
21
+ status: z.enum(BILL_RUN_OUTCOME_STATUSES).optional()
22
+ }).passthrough();
23
+ const billRunOutcomeListItemSchema = z.object({
24
+ id: z.string().uuid(),
25
+ billRunId: z.string().uuid(),
26
+ billAccountId: z.string().uuid(),
27
+ status: z.string(),
28
+ errorMessage: z.string().nullable(),
29
+ warnings: z.record(z.string(), z.unknown()).nullable(),
30
+ draftInvoiceId: z.string().uuid().nullable(),
31
+ bill_account_name: z.string().nullable(),
32
+ invoice_number: z.string().nullable(),
33
+ organizationId: z.string().uuid(),
34
+ tenantId: z.string().uuid(),
35
+ createdAt: z.string(),
36
+ updatedAt: z.string()
37
+ });
38
+ const crud = makeCrudRoute({
39
+ metadata: routeMetadata,
40
+ orm: {
41
+ entity: BillRunOutcome,
42
+ idField: "id",
43
+ orgField: "organizationId",
44
+ tenantField: "tenantId",
45
+ softDeleteField: "deletedAt"
46
+ },
47
+ list: {
48
+ schema: listQuerySchema,
49
+ entityId: billingEntityIds.runOutcome,
50
+ fields: [
51
+ "id",
52
+ "bill_run_id",
53
+ "bill_account_id",
54
+ "status",
55
+ "error_message",
56
+ "warnings",
57
+ "draft_invoice_id",
58
+ "organization_id",
59
+ "tenant_id",
60
+ "created_at",
61
+ "updated_at"
62
+ ],
63
+ sortFieldMap: {
64
+ createdAt: "created_at"
65
+ },
66
+ buildFilters: async (query) => {
67
+ const filters = {};
68
+ if (typeof query.billRunId === "string") filters.bill_run_id = { $eq: query.billRunId };
69
+ if (typeof query.billAccountId === "string") {
70
+ filters.bill_account_id = { $eq: query.billAccountId };
71
+ }
72
+ if (typeof query.status === "string") filters.status = { $eq: query.status };
73
+ return filters;
74
+ }
75
+ },
76
+ hooks: {
77
+ // Outcomes reference account + draft invoice by FK id only. The
78
+ // run detail UI wants names — resolve both in two bounded queries.
79
+ afterList: async (payload, ctx) => {
80
+ const items = payload?.items ?? [];
81
+ if (items.length === 0) return;
82
+ const em = ctx.container.resolve("em");
83
+ const accountIds = Array.from(
84
+ new Set(
85
+ items.map((row) => row.bill_account_id).filter((v) => typeof v === "string" && v.length > 0)
86
+ )
87
+ );
88
+ const invoiceIds = Array.from(
89
+ new Set(
90
+ items.map((row) => row.draft_invoice_id).filter((v) => typeof v === "string" && v.length > 0)
91
+ )
92
+ );
93
+ const [accounts, invoices] = await Promise.all([
94
+ accountIds.length ? em.find(BillingAccount, { id: { $in: accountIds } }) : [],
95
+ invoiceIds.length ? em.find(SalesInvoice, { id: { $in: invoiceIds } }) : []
96
+ ]);
97
+ const accountName = new Map(
98
+ accounts.map((a) => [String(a.id), a.name])
99
+ );
100
+ const invoiceNumber = new Map(
101
+ invoices.map((i) => [String(i.id), i.invoiceNumber])
102
+ );
103
+ for (const row of items) {
104
+ row.bill_account_name = accountName.get(String(row.bill_account_id)) ?? null;
105
+ row.invoice_number = typeof row.draft_invoice_id === "string" ? invoiceNumber.get(row.draft_invoice_id) ?? null : null;
106
+ }
107
+ }
108
+ }
109
+ });
110
+ const GET = crud.GET;
111
+ const openApi = createBillingCrudOpenApi({
112
+ resourceName: "BillRunOutcome",
113
+ pluralName: "BillRunOutcomes",
114
+ querySchema: listQuerySchema,
115
+ listResponseSchema: createPagedListResponseSchema(billRunOutcomeListItemSchema)
116
+ });
117
+ export {
118
+ GET,
119
+ metadata,
120
+ openApi
121
+ };
122
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/billing/api/run-outcomes/route.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { BillingAccount, BillRunOutcome } from '../../data/entities'\nimport { billingEntityIds } from '../../data/entityIds'\nimport {\n BILL_RUN_OUTCOME_STATUSES,\n} from '../../data/validators'\nimport { createBillingCrudOpenApi, createPagedListResponseSchema } from '../openapi'\n\n/**\n * `GET /api/billing/run-outcomes` \u2014 list per-account outcomes,\n * typically filtered by `billRunId` to populate the \"show me what\n * happened in this run\" detail panel.\n *\n * Read-only; outcomes are written by the engine itself, never by API\n * clients.\n */\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['billing.run.view'] },\n}\n\nexport const metadata = routeMetadata\n\nconst listQuerySchema = z\n .object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(100).default(50),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n billRunId: z.string().uuid().optional(),\n billAccountId: z.string().uuid().optional(),\n status: z.enum(BILL_RUN_OUTCOME_STATUSES).optional(),\n })\n .passthrough()\n\nconst billRunOutcomeListItemSchema = z.object({\n id: z.string().uuid(),\n billRunId: z.string().uuid(),\n billAccountId: z.string().uuid(),\n status: z.string(),\n errorMessage: z.string().nullable(),\n warnings: z.record(z.string(), z.unknown()).nullable(),\n draftInvoiceId: z.string().uuid().nullable(),\n bill_account_name: z.string().nullable(),\n invoice_number: z.string().nullable(),\n organizationId: z.string().uuid(),\n tenantId: z.string().uuid(),\n createdAt: z.string(),\n updatedAt: z.string(),\n})\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillRunOutcome,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n list: {\n schema: listQuerySchema,\n entityId: billingEntityIds.runOutcome,\n fields: [\n 'id',\n 'bill_run_id',\n 'bill_account_id',\n 'status',\n 'error_message',\n 'warnings',\n 'draft_invoice_id',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n ],\n sortFieldMap: {\n createdAt: 'created_at',\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.billRunId === 'string') filters.bill_run_id = { $eq: query.billRunId }\n if (typeof query.billAccountId === 'string') {\n filters.bill_account_id = { $eq: query.billAccountId }\n }\n if (typeof query.status === 'string') filters.status = { $eq: query.status }\n return filters\n },\n },\n hooks: {\n // Outcomes reference account + draft invoice by FK id only. The\n // run detail UI wants names \u2014 resolve both in two bounded queries.\n afterList: async (payload: unknown, ctx: unknown) => {\n const items = ((payload as { items?: unknown[] } | null)?.items ?? []) as Array<\n Record<string, unknown>\n >\n if (items.length === 0) return\n const em = (ctx as { container: AwilixContainer }).container.resolve<EntityManager>('em')\n const accountIds = Array.from(\n new Set(\n items\n .map((row) => row.bill_account_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n const invoiceIds = Array.from(\n new Set(\n items\n .map((row) => row.draft_invoice_id)\n .filter((v): v is string => typeof v === 'string' && v.length > 0),\n ),\n )\n const [accounts, invoices] = await Promise.all([\n accountIds.length\n ? em.find(BillingAccount, { id: { $in: accountIds } })\n : [],\n invoiceIds.length\n ? em.find(SalesInvoice, { id: { $in: invoiceIds } })\n : [],\n ])\n const accountName = new Map(\n accounts.map((a) => [String(a.id), a.name] as [string, unknown]),\n )\n const invoiceNumber = new Map(\n invoices.map((i) => [String(i.id), i.invoiceNumber] as [string, unknown]),\n )\n for (const row of items) {\n row.bill_account_name = accountName.get(String(row.bill_account_id)) ?? null\n row.invoice_number =\n typeof row.draft_invoice_id === 'string'\n ? invoiceNumber.get(row.draft_invoice_id) ?? null\n : null\n }\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRunOutcome',\n pluralName: 'BillRunOutcomes',\n querySchema: listQuerySchema,\n listResponseSchema: createPagedListResponseSchema(billRunOutcomeListItemSchema),\n})\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ import { makeCrudRoute } from "@open-mercato/shared/lib/crud/factory";
3
+ import { withScopedPayload } from "@open-mercato/shared/lib/api/scoped";
4
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
5
+ import { BillRun } from "../../../data/entities.js";
6
+ import { billingEntityIds } from "../../../data/entityIds.js";
7
+ import { billingRunRetryFailedSchema } from "../../../data/validators.js";
8
+ import { createBillingCrudOpenApi } from "../../openapi.js";
9
+ const routeMetadata = {
10
+ POST: { requireAuth: true, requireFeatures: ["billing.run.trigger"] }
11
+ };
12
+ const metadata = routeMetadata;
13
+ const retryResponseSchema = z.object({
14
+ billRunId: z.string().uuid(),
15
+ status: z.string(),
16
+ outcomesCount: z.number().int().nonnegative()
17
+ });
18
+ const rawBodySchema = z.object({}).passthrough();
19
+ const crud = makeCrudRoute({
20
+ metadata: routeMetadata,
21
+ orm: {
22
+ entity: BillRun,
23
+ idField: "id",
24
+ orgField: "organizationId",
25
+ tenantField: "tenantId",
26
+ softDeleteField: "deletedAt"
27
+ },
28
+ // No `list` — POST-only route. Keep a stub so the factory type-checks.
29
+ list: {
30
+ schema: z.object({}).passthrough(),
31
+ entityId: billingEntityIds.run,
32
+ fields: ["id"],
33
+ buildFilters: async () => ({})
34
+ },
35
+ actions: {
36
+ create: {
37
+ commandId: "billing.runs.retry_failed",
38
+ schema: rawBodySchema,
39
+ mapInput: async ({ raw, ctx }) => {
40
+ const { translate } = await resolveTranslations();
41
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate);
42
+ return billingRunRetryFailedSchema.parse(scoped);
43
+ },
44
+ response: ({ result }) => ({
45
+ billRunId: result?.billRunId ?? null,
46
+ status: result?.status ?? null,
47
+ outcomesCount: result?.outcomesCount ?? 0
48
+ }),
49
+ status: 201
50
+ }
51
+ }
52
+ });
53
+ const POST = crud.POST;
54
+ const openApi = createBillingCrudOpenApi({
55
+ resourceName: "BillRunRetry",
56
+ pluralName: "BillRunRetries",
57
+ querySchema: z.object({}).passthrough(),
58
+ listResponseSchema: z.object({ items: z.array(z.unknown()) }),
59
+ create: {
60
+ schema: billingRunRetryFailedSchema,
61
+ responseSchema: retryResponseSchema,
62
+ description: "Re-runs the failed accounts of an earlier Bill Run. The new run inherits the parent run's `asOfDate` and `catchUp` flag; its `parent_run_id` points back at the source. Returns 409 if there are no failed outcomes or if another run is in flight."
63
+ }
64
+ });
65
+ export {
66
+ POST,
67
+ metadata,
68
+ openApi
69
+ };
70
+ //# sourceMappingURL=route.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../src/modules/billing/api/runs/retry-failed/route.ts"],
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { withScopedPayload } from '@open-mercato/shared/lib/api/scoped'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { BillRun } from '../../../data/entities'\nimport { billingEntityIds } from '../../../data/entityIds'\nimport { billingRunRetryFailedSchema } from '../../../data/validators'\nimport { createBillingCrudOpenApi } from '../../openapi'\n\n/**\n * `POST /api/billing/runs/retry-failed`\n *\n * Creates a new Bill Run scoped to the failed-account ids from a parent\n * run, with `parent_run_id` set on the new run so the retry chain is\n * traceable. Returns the new run's id + status + outcomes count.\n *\n * Implemented as a \"create-only\" `makeCrudRoute` so the framework's\n * RBAC + OpenAPI + command-bus plumbing stays consistent with the rest\n * of the billing surface. The list/get half of the factory is never\n * exercised on this URL \u2014 only POST is exported.\n */\n\nconst routeMetadata = {\n POST: { requireAuth: true, requireFeatures: ['billing.run.trigger'] },\n}\n\nexport const metadata = routeMetadata\n\nconst retryResponseSchema = z.object({\n billRunId: z.string().uuid(),\n status: z.string(),\n outcomesCount: z.number().int().nonnegative(),\n})\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: BillRun,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n // No `list` \u2014 POST-only route. Keep a stub so the factory type-checks.\n list: {\n schema: z.object({}).passthrough(),\n entityId: billingEntityIds.run,\n fields: ['id'],\n buildFilters: async () => ({}),\n },\n actions: {\n create: {\n commandId: 'billing.runs.retry_failed',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const scoped = withScopedPayload(raw ?? {}, ctx, translate)\n return billingRunRetryFailedSchema.parse(scoped)\n },\n response: ({ result }) => ({\n billRunId: result?.billRunId ?? null,\n status: result?.status ?? null,\n outcomesCount: result?.outcomesCount ?? 0,\n }),\n status: 201,\n },\n },\n})\n\nexport const POST = crud.POST\n\nexport const openApi = createBillingCrudOpenApi({\n resourceName: 'BillRunRetry',\n pluralName: 'BillRunRetries',\n querySchema: z.object({}).passthrough(),\n listResponseSchema: z.object({ items: z.array(z.unknown()) }),\n create: {\n schema: billingRunRetryFailedSchema,\n responseSchema: retryResponseSchema,\n description:\n 'Re-runs the failed accounts of an earlier Bill Run. The new run ' +\n 'inherits the parent run\\'s `asOfDate` and `catchUp` flag; its ' +\n '`parent_run_id` points back at the source. Returns 409 if there ' +\n 'are no failed outcomes or if another run is in flight.',\n },\n})\n"],
5
+ "mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAqB;AAC9B,SAAS,yBAAyB;AAClC,SAAS,2BAA2B;AACpC,SAAS,eAAe;AACxB,SAAS,wBAAwB;AACjC,SAAS,mCAAmC;AAC5C,SAAS,gCAAgC;AAezC,MAAM,gBAAgB;AAAA,EACpB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAEO,MAAM,WAAW;AAExB,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,QAAQ,EAAE,OAAO;AAAA,EACjB,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAC9C,CAAC;AAED,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,OAAO,cAAc;AAAA,EACzB,UAAU;AAAA,EACV,KAAK;AAAA,IACH,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb,iBAAiB;AAAA,EACnB;AAAA;AAAA,EAEA,MAAM;AAAA,IACJ,QAAQ,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,IACjC,UAAU,iBAAiB;AAAA,IAC3B,QAAQ,CAAC,IAAI;AAAA,IACb,cAAc,aAAa,CAAC;AAAA,EAC9B;AAAA,EACA,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,MACR,UAAU,OAAO,EAAE,KAAK,IAAI,MAAM;AAChC,cAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,cAAM,SAAS,kBAAkB,OAAO,CAAC,GAAG,KAAK,SAAS;AAC1D,eAAO,4BAA4B,MAAM,MAAM;AAAA,MACjD;AAAA,MACA,UAAU,CAAC,EAAE,OAAO,OAAO;AAAA,QACzB,WAAW,QAAQ,aAAa;AAAA,QAChC,QAAQ,QAAQ,UAAU;AAAA,QAC1B,eAAe,QAAQ,iBAAiB;AAAA,MAC1C;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,EACF;AACF,CAAC;AAEM,MAAM,OAAO,KAAK;AAElB,MAAM,UAAU,yBAAyB;AAAA,EAC9C,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,aAAa,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAAA,EACtC,oBAAoB,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;AAAA,EAC5D,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,aACE;AAAA,EAIJ;AACF,CAAC;",
6
+ "names": []
7
+ }