@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,97 @@
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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'
6
+ import { billingEntityIds } from '../../../data/entityIds'
7
+ import {
8
+ billingInvoiceAddLineSchema,
9
+ billingInvoiceEditLineSchema,
10
+ billingInvoiceRemoveLineSchema,
11
+ } from '../../../data/validators'
12
+ import { createBillingCrudOpenApi } from '../../openapi'
13
+
14
+ /**
15
+ * `POST /api/billing/invoices/edit-line` — edits one line of a draft
16
+ * invoice. Refuses non-drafts (409 / `billing.invoice.not_draft`).
17
+ * Writes a `DraftInvoiceEdit` audit row per spec. Recomputes invoice
18
+ * totals from the surviving lines after the edit.
19
+ *
20
+ * Operator surface: route is gated by `billing.invoice.edit_draft`.
21
+ * Future Phase 4c admin UI calls into the same command via this route.
22
+ */
23
+
24
+ const routeMetadata = {
25
+ POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },
26
+ }
27
+
28
+ export const metadata = routeMetadata
29
+
30
+ const editLineResponseSchema = z.object({
31
+ invoiceId: z.string().uuid(),
32
+ invoiceLineId: z.string().uuid(),
33
+ auditId: z.string().uuid(),
34
+ })
35
+
36
+ const rawBodySchema = z.object({}).passthrough()
37
+
38
+ const crud = makeCrudRoute({
39
+ metadata: routeMetadata,
40
+ orm: {
41
+ entity: SalesInvoice,
42
+ idField: 'id',
43
+ orgField: 'organizationId',
44
+ tenantField: 'tenantId',
45
+ softDeleteField: 'deletedAt',
46
+ },
47
+ list: {
48
+ schema: z.object({}).passthrough(),
49
+ entityId: billingEntityIds.draftInvoiceEdit,
50
+ fields: ['id'],
51
+ buildFilters: async () => ({}),
52
+ },
53
+ actions: {
54
+ create: {
55
+ commandId: 'billing.invoices.edit_draft_line',
56
+ schema: rawBodySchema,
57
+ mapInput: async ({ raw, ctx }) => {
58
+ const { translate } = await resolveTranslations()
59
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
60
+ return billingInvoiceEditLineSchema.parse(scoped)
61
+ },
62
+ response: ({ result }) => ({
63
+ invoiceId: result?.invoiceId ?? null,
64
+ invoiceLineId: result?.invoiceLineId ?? null,
65
+ auditId: result?.auditId ?? null,
66
+ }),
67
+ status: 200,
68
+ },
69
+ },
70
+ })
71
+
72
+ export const POST = crud.POST
73
+
74
+ export const openApi = createBillingCrudOpenApi({
75
+ resourceName: 'BillingInvoiceLineEdit',
76
+ pluralName: 'BillingInvoiceLineEdits',
77
+ querySchema: z.object({}).passthrough(),
78
+ listResponseSchema: z.object({ items: z.array(z.unknown()) }),
79
+ create: {
80
+ schema: billingInvoiceEditLineSchema,
81
+ responseSchema: editLineResponseSchema,
82
+ description:
83
+ 'Edits one line of a draft invoice (description, quantity, ' +
84
+ "unit_price, or explicit `totalNetAmount` override). Writes a " +
85
+ '`DraftInvoiceEdit` audit row with before+after snapshots and ' +
86
+ 'recomputes invoice totals. Refuses non-draft invoices with ' +
87
+ 'HTTP 409 + `billing.invoice.not_draft`.',
88
+ },
89
+ })
90
+
91
+ // Re-export the add/remove route schemas so the OpenAPI bundle keeps
92
+ // them discoverable from a single import surface — the dedicated
93
+ // routes (sibling folders) own the actual handlers.
94
+ export const _phaseSchemaRefs = {
95
+ add: billingInvoiceAddLineSchema,
96
+ remove: billingInvoiceRemoveLineSchema,
97
+ }
@@ -0,0 +1,97 @@
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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'
6
+ import { billingEntityIds } from '../../../data/entityIds'
7
+ import { billingInvoicePostSchema } from '../../../data/validators'
8
+ import { createBillingCrudOpenApi } from '../../openapi'
9
+
10
+ /**
11
+ * `POST /api/billing/invoices/post`
12
+ *
13
+ * Operator action: flip a billing-drafted invoice from `draft` →
14
+ * `posted`. The command refuses non-draft inputs (409 with
15
+ * `billing.invoice.not_draft`) and test-mode invoices (409 with
16
+ * `billing.invoice.test_run`). The invoice number is NOT re-assigned
17
+ * (it was set at draft create — see Phase 2 deviation note in the
18
+ * spec).
19
+ *
20
+ * Implemented as a create-only `makeCrudRoute` so it inherits the same
21
+ * RBAC + OpenAPI plumbing as the rest of the billing surface. The
22
+ * `entity` is `SalesInvoice` only because the factory requires one;
23
+ * the GET half is never exposed on this URL.
24
+ */
25
+
26
+ const routeMetadata = {
27
+ POST: { requireAuth: true, requireFeatures: ['billing.invoice.post'] },
28
+ }
29
+
30
+ export const metadata = routeMetadata
31
+
32
+ const postResponseSchema = z.object({
33
+ invoiceId: z.string().uuid(),
34
+ invoiceNumber: z.string(),
35
+ status: z.literal('posted'),
36
+ lineCount: z.number().int().nonnegative(),
37
+ })
38
+
39
+ const rawBodySchema = z.object({}).passthrough()
40
+
41
+ const crud = makeCrudRoute({
42
+ metadata: routeMetadata,
43
+ orm: {
44
+ entity: SalesInvoice,
45
+ idField: 'id',
46
+ orgField: 'organizationId',
47
+ tenantField: 'tenantId',
48
+ softDeleteField: 'deletedAt',
49
+ },
50
+ list: {
51
+ schema: z.object({}).passthrough(),
52
+ // `entityId` is technically a core/sales concept, but the factory
53
+ // only needs it as an identifier — using a billing-namespaced one
54
+ // keeps the registry tidy (no surprise list endpoints).
55
+ entityId: billingEntityIds.draftInvoiceEdit,
56
+ fields: ['id'],
57
+ buildFilters: async () => ({}),
58
+ },
59
+ actions: {
60
+ create: {
61
+ commandId: 'billing.invoices.post',
62
+ schema: rawBodySchema,
63
+ mapInput: async ({ raw, ctx }) => {
64
+ const { translate } = await resolveTranslations()
65
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
66
+ return billingInvoicePostSchema.parse(scoped)
67
+ },
68
+ response: ({ result }) => ({
69
+ invoiceId: result?.invoiceId ?? null,
70
+ invoiceNumber: result?.invoiceNumber ?? null,
71
+ status: result?.status ?? null,
72
+ lineCount: result?.lineCount ?? 0,
73
+ }),
74
+ status: 200,
75
+ },
76
+ },
77
+ })
78
+
79
+ export const POST = crud.POST
80
+
81
+ export const openApi = createBillingCrudOpenApi({
82
+ resourceName: 'BillingInvoicePost',
83
+ pluralName: 'BillingInvoicePosts',
84
+ querySchema: z.object({}).passthrough(),
85
+ listResponseSchema: z.object({ items: z.array(z.unknown()) }),
86
+ create: {
87
+ schema: billingInvoicePostSchema,
88
+ responseSchema: postResponseSchema,
89
+ description:
90
+ 'Flips a billing-drafted `core/sales` invoice from `draft` to ' +
91
+ '`posted`. Refuses non-draft invoices (409 / ' +
92
+ '`billing.invoice.not_draft`) and test-mode invoices (409 / ' +
93
+ '`billing.invoice.test_run`). Fires `billing.invoice.posted` + ' +
94
+ 'one `billing.invoice.line_posted` per line. The invoice number ' +
95
+ 'is NOT re-assigned (set at draft create — see Phase 2 deviation).',
96
+ },
97
+ })
@@ -0,0 +1,75 @@
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 { SalesInvoice } from '@open-mercato/core/modules/sales/data/entities'
6
+ import { billingEntityIds } from '../../../data/entityIds'
7
+ import { billingInvoiceRemoveLineSchema } from '../../../data/validators'
8
+ import { createBillingCrudOpenApi } from '../../openapi'
9
+
10
+ const routeMetadata = {
11
+ POST: { requireAuth: true, requireFeatures: ['billing.invoice.edit_draft'] },
12
+ }
13
+
14
+ export const metadata = routeMetadata
15
+
16
+ const removeLineResponseSchema = z.object({
17
+ invoiceId: z.string().uuid(),
18
+ invoiceLineId: z.string().uuid(),
19
+ auditId: z.string().uuid(),
20
+ })
21
+
22
+ const rawBodySchema = z.object({}).passthrough()
23
+
24
+ const crud = makeCrudRoute({
25
+ metadata: routeMetadata,
26
+ orm: {
27
+ entity: SalesInvoice,
28
+ idField: 'id',
29
+ orgField: 'organizationId',
30
+ tenantField: 'tenantId',
31
+ softDeleteField: 'deletedAt',
32
+ },
33
+ list: {
34
+ schema: z.object({}).passthrough(),
35
+ entityId: billingEntityIds.draftInvoiceEdit,
36
+ fields: ['id'],
37
+ buildFilters: async () => ({}),
38
+ },
39
+ actions: {
40
+ create: {
41
+ commandId: 'billing.invoices.remove_draft_line',
42
+ schema: rawBodySchema,
43
+ mapInput: async ({ raw, ctx }) => {
44
+ const { translate } = await resolveTranslations()
45
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
46
+ return billingInvoiceRemoveLineSchema.parse(scoped)
47
+ },
48
+ response: ({ result }) => ({
49
+ invoiceId: result?.invoiceId ?? null,
50
+ invoiceLineId: result?.invoiceLineId ?? null,
51
+ auditId: result?.auditId ?? null,
52
+ }),
53
+ status: 200,
54
+ },
55
+ },
56
+ })
57
+
58
+ export const POST = crud.POST
59
+
60
+ export const openApi = createBillingCrudOpenApi({
61
+ resourceName: 'BillingInvoiceLineRemove',
62
+ pluralName: 'BillingInvoiceLineRemoves',
63
+ querySchema: z.object({}).passthrough(),
64
+ listResponseSchema: z.object({ items: z.array(z.unknown()) }),
65
+ create: {
66
+ schema: billingInvoiceRemoveLineSchema,
67
+ responseSchema: removeLineResponseSchema,
68
+ description:
69
+ 'Removes one line from a draft invoice (hard delete — the line ' +
70
+ 'never existed as far as future readers are concerned). Writes ' +
71
+ 'a `DraftInvoiceEdit` audit row (action=`line_removed`, ' +
72
+ '`after_json=null`) carrying the before-snapshot for forensics. ' +
73
+ 'Recomputes invoice totals. Refuses non-draft invoices.',
74
+ },
75
+ })
@@ -0,0 +1,167 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { z } from 'zod'
4
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
6
+
7
+ /**
8
+ * `GET /api/billing/invoices`
9
+ *
10
+ * Lists `core/sales` invoices that the billing engine emitted (i.e.
11
+ * carry `metadata.bill_run_id`). Hand-rolled rather than wrapped via
12
+ * `makeCrudRoute` because the filter predicate is a JSONB
13
+ * `metadata->>'bill_run_id' IS NOT NULL` check that doesn't map
14
+ * cleanly onto the factory's `where`-shaped filter API, and the
15
+ * entity itself is owned by `core/sales` — keeping the surface
16
+ * read-only and hand-rolled avoids surprising the cross-module
17
+ * reviewer.
18
+ *
19
+ * Filters:
20
+ * - `status` — draft / posted / paid / void
21
+ * - `billRunId` — exact match on `metadata->>'bill_run_id'`
22
+ * - `billAccountId` — exact match on `metadata->>'bill_account_id'`
23
+ * - `testRun=true|false` — flips the `metadata->>'test_run'` filter
24
+ * - `search` — substring on invoice_number
25
+ *
26
+ * Sort: started_at DESC (most recent first). Pagination: standard
27
+ * page / pageSize (capped at 100).
28
+ */
29
+
30
+ export const metadata = {
31
+ GET: { requireAuth: true, requireFeatures: ['billing.invoice.view'] },
32
+ }
33
+
34
+ const querySchema = z
35
+ .object({
36
+ page: z.coerce.number().int().min(1).default(1),
37
+ pageSize: z.coerce.number().int().min(1).max(100).default(50),
38
+ status: z.enum(['draft', 'posted', 'paid', 'void']).optional(),
39
+ billRunId: z.string().uuid().optional(),
40
+ billAccountId: z.string().uuid().optional(),
41
+ testRun: z.coerce.boolean().optional(),
42
+ search: z.string().optional(),
43
+ })
44
+ .passthrough()
45
+
46
+ type BillingInvoiceRow = {
47
+ id: string
48
+ invoice_number: string
49
+ status: string | null
50
+ currency_code: string
51
+ issue_date: string | null
52
+ due_date: string | null
53
+ subtotal_net_amount: string
54
+ grand_total_net_amount: string
55
+ grand_total_gross_amount: string
56
+ paid_total_amount: string
57
+ outstanding_amount: string
58
+ metadata: Record<string, unknown> | null
59
+ created_at: string
60
+ updated_at: string
61
+ }
62
+
63
+ export async function GET(req: Request) {
64
+ try {
65
+ const container = await createRequestContainer()
66
+ const auth = await getAuthFromRequest(req)
67
+ if (!auth || !auth.tenantId) {
68
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
69
+ }
70
+ const organizationId = (auth as { orgId?: string }).orgId ?? null
71
+ if (!organizationId) {
72
+ return NextResponse.json(
73
+ { error: 'Organization scope required' },
74
+ { status: 403 },
75
+ )
76
+ }
77
+
78
+ const url = new URL(req.url)
79
+ const query = querySchema.parse(
80
+ Object.fromEntries(url.searchParams.entries()),
81
+ )
82
+
83
+ const em = (container.resolve('em') as EntityManager).fork()
84
+
85
+ // Build the predicate. `jsonb_exists(metadata, 'bill_run_id')` is
86
+ // the function form of the JSONB `?` "key exists" operator — the
87
+ // operator form cannot be used here because `em.execute` treats a
88
+ // literal `?` as a positional parameter placeholder. Keeps us
89
+ // narrow to billing-managed invoices, never bleeding into invoices
90
+ // written by other modules.
91
+ const whereClauses: string[] = [
92
+ 'tenant_id = ?',
93
+ 'organization_id = ?',
94
+ 'deleted_at IS NULL',
95
+ "jsonb_exists(metadata, 'bill_run_id')",
96
+ ]
97
+ const params: unknown[] = [auth.tenantId, organizationId]
98
+
99
+ if (query.status) {
100
+ whereClauses.push('status = ?')
101
+ params.push(query.status)
102
+ }
103
+ if (query.billRunId) {
104
+ whereClauses.push("metadata->>'bill_run_id' = ?")
105
+ params.push(query.billRunId)
106
+ }
107
+ if (query.billAccountId) {
108
+ whereClauses.push("metadata->>'bill_account_id' = ?")
109
+ params.push(query.billAccountId)
110
+ }
111
+ if (typeof query.testRun === 'boolean') {
112
+ if (query.testRun) {
113
+ whereClauses.push("metadata->>'test_run' = 'true'")
114
+ } else {
115
+ whereClauses.push("(metadata->>'test_run' IS NULL OR metadata->>'test_run' != 'true')")
116
+ }
117
+ }
118
+ if (typeof query.search === 'string' && query.search.trim()) {
119
+ whereClauses.push('invoice_number ILIKE ?')
120
+ params.push(`%${query.search.trim().replace(/[%_]/g, (m) => `\\${m}`)}%`)
121
+ }
122
+
123
+ const whereSql = whereClauses.join(' AND ')
124
+ const countRows = (await em.execute(
125
+ `SELECT COUNT(*)::int AS total FROM sales_invoices WHERE ${whereSql}`,
126
+ params,
127
+ )) as unknown as Array<{ total: number }>
128
+ const total = countRows?.[0]?.total ?? 0
129
+
130
+ const offset = (query.page - 1) * query.pageSize
131
+ const itemRows = (await em.execute(
132
+ `SELECT id, invoice_number, status, currency_code, issue_date, due_date,
133
+ subtotal_net_amount, grand_total_net_amount, grand_total_gross_amount,
134
+ paid_total_amount, outstanding_amount, metadata, created_at, updated_at
135
+ FROM sales_invoices
136
+ WHERE ${whereSql}
137
+ ORDER BY created_at DESC
138
+ LIMIT ? OFFSET ?`,
139
+ [...params, query.pageSize, offset],
140
+ )) as unknown as BillingInvoiceRow[]
141
+
142
+ return NextResponse.json({
143
+ items: itemRows,
144
+ total,
145
+ page: query.page,
146
+ pageSize: query.pageSize,
147
+ totalPages: total === 0 ? 1 : Math.ceil(total / query.pageSize),
148
+ })
149
+ } catch (err) {
150
+ console.error('[billing/invoices.GET]', err)
151
+ return NextResponse.json({ error: 'Internal error' }, { status: 500 })
152
+ }
153
+ }
154
+
155
+ export const openApi = {
156
+ methods: {
157
+ GET: {
158
+ tags: ['Billing'],
159
+ summary: 'List billing-managed invoices',
160
+ description:
161
+ 'Returns a paginated list of `core/sales` invoices that the ' +
162
+ 'billing engine emitted (matched via `metadata->>\'bill_run_id\'` ' +
163
+ 'presence). Supports status / billRunId / billAccountId / testRun ' +
164
+ '/ invoice_number search filters.',
165
+ },
166
+ },
167
+ }
@@ -0,0 +1,92 @@
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 { BillingItem } from '../../../data/entities'
6
+ import { billingEntityIds } from '../../../data/entityIds'
7
+ import { billingItemBulkCreateSchema } from '../../../data/validators'
8
+ import { createBillingCrudOpenApi } from '../../openapi'
9
+
10
+ /**
11
+ * `POST /api/billing/items/bulk` — batch-create Billing Items.
12
+ *
13
+ * One scope envelope, up to 500 item payloads. Idempotent per
14
+ * `source_ref` with a *single* batched existence check, then a
15
+ * single flush. The CPQ connector's subscription-activated /
16
+ * -amended / -renewed subscribers are the primary callers — bulk
17
+ * collapses what used to be one `billing.items.create` round-trip
18
+ * per charge.
19
+ */
20
+
21
+ const routeMetadata = {
22
+ POST: { requireAuth: true, requireFeatures: ['billing.item.manage'] },
23
+ }
24
+
25
+ export const metadata = routeMetadata
26
+
27
+ const bulkResultEntrySchema = z.object({
28
+ sourceRef: z.string().nullable(),
29
+ id: z.string().uuid(),
30
+ deduplicated: z.boolean(),
31
+ })
32
+
33
+ const bulkResponseSchema = z.object({
34
+ created: z.number().int().nonnegative(),
35
+ deduplicated: z.number().int().nonnegative(),
36
+ items: z.array(bulkResultEntrySchema),
37
+ })
38
+
39
+ const rawBodySchema = z.object({}).passthrough()
40
+
41
+ const crud = makeCrudRoute({
42
+ metadata: routeMetadata,
43
+ orm: {
44
+ entity: BillingItem,
45
+ idField: 'id',
46
+ orgField: 'organizationId',
47
+ tenantField: 'tenantId',
48
+ softDeleteField: 'deletedAt',
49
+ },
50
+ list: {
51
+ schema: z.object({}).passthrough(),
52
+ entityId: billingEntityIds.item,
53
+ fields: ['id'],
54
+ buildFilters: async () => ({}),
55
+ },
56
+ actions: {
57
+ create: {
58
+ commandId: 'billing.items.bulk_create',
59
+ schema: rawBodySchema,
60
+ mapInput: async ({ raw, ctx }) => {
61
+ const { translate } = await resolveTranslations()
62
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
63
+ return billingItemBulkCreateSchema.parse(scoped)
64
+ },
65
+ response: ({ result }) => ({
66
+ created: result?.created ?? 0,
67
+ deduplicated: result?.deduplicated ?? 0,
68
+ items: result?.items ?? [],
69
+ }),
70
+ status: 200,
71
+ },
72
+ },
73
+ })
74
+
75
+ export const POST = crud.POST
76
+
77
+ export const openApi = createBillingCrudOpenApi({
78
+ resourceName: 'BillingItemBulkCreate',
79
+ pluralName: 'BillingItemBulkCreates',
80
+ querySchema: z.object({}).passthrough(),
81
+ listResponseSchema: z.object({ items: z.array(z.unknown()) }),
82
+ create: {
83
+ schema: billingItemBulkCreateSchema,
84
+ responseSchema: bulkResponseSchema,
85
+ description:
86
+ 'Batch-creates up to 500 Billing Items under one scope envelope. ' +
87
+ 'Idempotent per `sourceRef` (one batched existence check). ' +
88
+ 'Returns `{ created, deduplicated, items: [{ sourceRef, id, ' +
89
+ 'deduplicated }] }`. Always HTTP 200 — duplicates are reported ' +
90
+ 'in the body, never an error.',
91
+ },
92
+ })