@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,401 @@
1
+ import { registerCommand } from "@open-mercato/shared/lib/commands";
2
+ import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
3
+ import {
4
+ ensureOrganizationScope,
5
+ ensureTenantScope
6
+ } from "@open-mercato/shared/lib/commands/scope";
7
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
8
+ import { SalesInvoice, SalesInvoiceLine } from "@open-mercato/core/modules/sales/data/entities";
9
+ import { DraftInvoiceEdit } from "../data/entities.js";
10
+ import { emitBillingEvent } from "../events.js";
11
+ import {
12
+ billingInvoiceAddLineSchema,
13
+ billingInvoiceEditLineSchema,
14
+ billingInvoicePostSchema,
15
+ billingInvoiceRemoveLineSchema,
16
+ billingWipeTestInvoicesSchema
17
+ } from "../data/validators.js";
18
+ import { resolveInvoiceStatusEntryId } from "../lib/invoiceStatus.js";
19
+ function getEm(ctx) {
20
+ return ctx.container.resolve("em").fork();
21
+ }
22
+ const postInvoiceCommand = {
23
+ id: "billing.invoices.post",
24
+ async execute(rawInput, ctx) {
25
+ const parsed = billingInvoicePostSchema.parse(rawInput);
26
+ ensureTenantScope(ctx, parsed.tenantId);
27
+ ensureOrganizationScope(ctx, parsed.organizationId);
28
+ const em = getEm(ctx);
29
+ const invoice = await em.findOne(SalesInvoice, {
30
+ id: parsed.invoiceId,
31
+ tenantId: parsed.tenantId,
32
+ organizationId: parsed.organizationId,
33
+ deletedAt: null
34
+ });
35
+ if (!invoice) {
36
+ throw new CrudHttpError(404, { error: "Invoice not found" });
37
+ }
38
+ if (invoice.status !== "draft") {
39
+ throw new CrudHttpError(409, {
40
+ error: "Only draft invoices can be posted",
41
+ code: "billing.invoice.not_draft",
42
+ currentStatus: invoice.status
43
+ });
44
+ }
45
+ const meta = invoice.metadata ?? {};
46
+ if (meta.test_run === true) {
47
+ throw new CrudHttpError(409, {
48
+ error: "Test-mode invoices cannot be posted",
49
+ code: "billing.invoice.test_run"
50
+ });
51
+ }
52
+ const postedStatusEntryId = await resolveInvoiceStatusEntryId(
53
+ em,
54
+ parsed.tenantId,
55
+ parsed.organizationId,
56
+ "posted"
57
+ );
58
+ await withAtomicFlush(em, [
59
+ () => {
60
+ invoice.status = "posted";
61
+ invoice.statusEntryId = postedStatusEntryId;
62
+ invoice.updatedAt = /* @__PURE__ */ new Date();
63
+ }
64
+ ]);
65
+ const lines = await em.find(SalesInvoiceLine, {
66
+ invoice,
67
+ tenantId: parsed.tenantId,
68
+ organizationId: parsed.organizationId
69
+ });
70
+ await emitBillingEvent("billing.invoice.posted", {
71
+ invoiceId: invoice.id,
72
+ invoiceNumber: invoice.invoiceNumber,
73
+ tenantId: invoice.tenantId,
74
+ organizationId: invoice.organizationId,
75
+ grandTotalNet: invoice.grandTotalNetAmount,
76
+ grandTotalGross: invoice.grandTotalGrossAmount,
77
+ currencyCode: invoice.currencyCode,
78
+ billRunId: meta.bill_run_id ?? null,
79
+ billAccountId: meta.bill_account_id ?? null,
80
+ billPeriodStart: meta.bill_period_start ?? null,
81
+ billPeriodEnd: meta.bill_period_end ?? null,
82
+ lineCount: lines.length
83
+ });
84
+ for (const line of lines) {
85
+ const lineMeta = line.metadata ?? {};
86
+ await emitBillingEvent("billing.invoice.line_posted", {
87
+ invoiceId: invoice.id,
88
+ invoiceLineId: line.id,
89
+ billingItemId: lineMeta.billing_item_id ?? null,
90
+ billingType: lineMeta.billing_type ?? null,
91
+ description: line.description ?? null,
92
+ unitPriceNet: line.unitPriceNet,
93
+ quantity: line.quantity,
94
+ totalNetAmount: line.totalNetAmount,
95
+ tenantId: invoice.tenantId,
96
+ organizationId: invoice.organizationId
97
+ });
98
+ }
99
+ return {
100
+ invoiceId: invoice.id,
101
+ invoiceNumber: invoice.invoiceNumber,
102
+ status: "posted",
103
+ lineCount: lines.length
104
+ };
105
+ }
106
+ };
107
+ function roundHalfUp2dp(value) {
108
+ return Math.round((value + Number.EPSILON) * 100) / 100;
109
+ }
110
+ function format4dp(value) {
111
+ return roundHalfUp2dp(value).toFixed(4);
112
+ }
113
+ async function assertDraftInvoice(em, params) {
114
+ const invoice = await em.findOne(SalesInvoice, {
115
+ id: params.invoiceId,
116
+ tenantId: params.tenantId,
117
+ organizationId: params.organizationId,
118
+ deletedAt: null
119
+ });
120
+ if (!invoice) {
121
+ throw new CrudHttpError(404, { error: "Invoice not found" });
122
+ }
123
+ if (invoice.status !== "draft") {
124
+ throw new CrudHttpError(409, {
125
+ error: "Only draft invoices can be edited",
126
+ code: "billing.invoice.not_draft",
127
+ currentStatus: invoice.status
128
+ });
129
+ }
130
+ return invoice;
131
+ }
132
+ function snapshotLine(line) {
133
+ return {
134
+ id: line.id,
135
+ description: line.description ?? null,
136
+ quantity: line.quantity,
137
+ unitPriceNet: line.unitPriceNet,
138
+ totalNetAmount: line.totalNetAmount,
139
+ totalGrossAmount: line.totalGrossAmount,
140
+ metadata: line.metadata ?? null
141
+ };
142
+ }
143
+ async function recomputeInvoiceTotals(em, invoice) {
144
+ const lines = await em.find(SalesInvoiceLine, {
145
+ invoice,
146
+ tenantId: invoice.tenantId,
147
+ organizationId: invoice.organizationId
148
+ });
149
+ let totalNet = 0;
150
+ for (const line of lines) {
151
+ totalNet += Number.parseFloat(line.totalNetAmount);
152
+ }
153
+ const formatted = format4dp(totalNet);
154
+ invoice.subtotalNetAmount = formatted;
155
+ invoice.subtotalGrossAmount = formatted;
156
+ invoice.grandTotalNetAmount = formatted;
157
+ invoice.grandTotalGrossAmount = formatted;
158
+ invoice.outstandingAmount = formatted;
159
+ invoice.updatedAt = /* @__PURE__ */ new Date();
160
+ }
161
+ function getUserIdFromCtx(ctx) {
162
+ const userId = ctx.auth?.sub ?? null;
163
+ return userId ?? "00000000-0000-0000-0000-000000000000";
164
+ }
165
+ const editDraftLineCommand = {
166
+ id: "billing.invoices.edit_draft_line",
167
+ async execute(rawInput, ctx) {
168
+ const parsed = billingInvoiceEditLineSchema.parse(rawInput);
169
+ ensureTenantScope(ctx, parsed.tenantId);
170
+ ensureOrganizationScope(ctx, parsed.organizationId);
171
+ const em = getEm(ctx);
172
+ const invoice = await assertDraftInvoice(em, {
173
+ invoiceId: parsed.invoiceId,
174
+ tenantId: parsed.tenantId,
175
+ organizationId: parsed.organizationId
176
+ });
177
+ const line = await em.findOne(SalesInvoiceLine, {
178
+ id: parsed.invoiceLineId,
179
+ invoice,
180
+ tenantId: parsed.tenantId,
181
+ organizationId: parsed.organizationId
182
+ });
183
+ if (!line) {
184
+ throw new CrudHttpError(404, { error: "Invoice line not found" });
185
+ }
186
+ const before = snapshotLine(line);
187
+ let audit = null;
188
+ await withAtomicFlush(em, [
189
+ () => {
190
+ if (parsed.changes.description !== void 0) {
191
+ line.description = parsed.changes.description;
192
+ }
193
+ if (parsed.changes.quantity !== void 0) {
194
+ line.quantity = format4dp(parsed.changes.quantity);
195
+ line.normalizedQuantity = line.quantity;
196
+ }
197
+ if (parsed.changes.unitPriceNet !== void 0) {
198
+ line.unitPriceNet = format4dp(parsed.changes.unitPriceNet);
199
+ line.unitPriceGross = line.unitPriceNet;
200
+ }
201
+ if (parsed.changes.totalNetAmount !== void 0) {
202
+ const total = format4dp(parsed.changes.totalNetAmount);
203
+ line.totalNetAmount = total;
204
+ line.totalGrossAmount = total;
205
+ } else {
206
+ const total = format4dp(
207
+ Number.parseFloat(line.unitPriceNet) * Number.parseFloat(line.quantity)
208
+ );
209
+ line.totalNetAmount = total;
210
+ line.totalGrossAmount = total;
211
+ }
212
+ },
213
+ async () => {
214
+ await recomputeInvoiceTotals(em, invoice);
215
+ },
216
+ () => {
217
+ const after = snapshotLine(line);
218
+ audit = em.create(DraftInvoiceEdit, {
219
+ tenantId: parsed.tenantId,
220
+ organizationId: parsed.organizationId,
221
+ invoiceId: invoice.id,
222
+ invoiceLineId: line.id,
223
+ userId: getUserIdFromCtx(ctx),
224
+ action: "line_edited",
225
+ beforeJson: before,
226
+ afterJson: after,
227
+ createdAt: /* @__PURE__ */ new Date()
228
+ });
229
+ em.persist(audit);
230
+ }
231
+ ]);
232
+ return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit.id };
233
+ }
234
+ };
235
+ const addDraftLineCommand = {
236
+ id: "billing.invoices.add_draft_line",
237
+ async execute(rawInput, ctx) {
238
+ const parsed = billingInvoiceAddLineSchema.parse(rawInput);
239
+ ensureTenantScope(ctx, parsed.tenantId);
240
+ ensureOrganizationScope(ctx, parsed.organizationId);
241
+ const em = getEm(ctx);
242
+ const invoice = await assertDraftInvoice(em, {
243
+ invoiceId: parsed.invoiceId,
244
+ tenantId: parsed.tenantId,
245
+ organizationId: parsed.organizationId
246
+ });
247
+ const existing = await em.find(SalesInvoiceLine, {
248
+ invoice,
249
+ tenantId: parsed.tenantId,
250
+ organizationId: parsed.organizationId
251
+ });
252
+ const nextLineNumber = existing.length + 1;
253
+ const total = format4dp(parsed.unitPriceNet * parsed.quantity);
254
+ const line = em.create(SalesInvoiceLine, {
255
+ invoice,
256
+ organizationId: parsed.organizationId,
257
+ tenantId: parsed.tenantId,
258
+ lineNumber: nextLineNumber,
259
+ kind: "product",
260
+ description: parsed.description,
261
+ quantity: format4dp(parsed.quantity),
262
+ normalizedQuantity: format4dp(parsed.quantity),
263
+ currencyCode: invoice.currencyCode,
264
+ unitPriceNet: format4dp(parsed.unitPriceNet),
265
+ unitPriceGross: format4dp(parsed.unitPriceNet),
266
+ discountAmount: "0",
267
+ discountPercent: "0",
268
+ taxRate: "0",
269
+ taxAmount: "0",
270
+ totalNetAmount: total,
271
+ totalGrossAmount: total,
272
+ metadata: {
273
+ billing_item_id: parsed.billingItemId ?? null,
274
+ billing_type: parsed.billingType ?? "manual",
275
+ operator_added: true
276
+ }
277
+ });
278
+ let audit = null;
279
+ await withAtomicFlush(em, [
280
+ () => {
281
+ em.persist(line);
282
+ },
283
+ async () => {
284
+ await recomputeInvoiceTotals(em, invoice);
285
+ },
286
+ () => {
287
+ const after = snapshotLine(line);
288
+ audit = em.create(DraftInvoiceEdit, {
289
+ tenantId: parsed.tenantId,
290
+ organizationId: parsed.organizationId,
291
+ invoiceId: invoice.id,
292
+ invoiceLineId: line.id,
293
+ userId: getUserIdFromCtx(ctx),
294
+ action: "line_added",
295
+ beforeJson: null,
296
+ afterJson: after,
297
+ createdAt: /* @__PURE__ */ new Date()
298
+ });
299
+ em.persist(audit);
300
+ }
301
+ ]);
302
+ return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit.id };
303
+ }
304
+ };
305
+ const removeDraftLineCommand = {
306
+ id: "billing.invoices.remove_draft_line",
307
+ async execute(rawInput, ctx) {
308
+ const parsed = billingInvoiceRemoveLineSchema.parse(rawInput);
309
+ ensureTenantScope(ctx, parsed.tenantId);
310
+ ensureOrganizationScope(ctx, parsed.organizationId);
311
+ const em = getEm(ctx);
312
+ const invoice = await assertDraftInvoice(em, {
313
+ invoiceId: parsed.invoiceId,
314
+ tenantId: parsed.tenantId,
315
+ organizationId: parsed.organizationId
316
+ });
317
+ const line = await em.findOne(SalesInvoiceLine, {
318
+ id: parsed.invoiceLineId,
319
+ invoice,
320
+ tenantId: parsed.tenantId,
321
+ organizationId: parsed.organizationId
322
+ });
323
+ if (!line) {
324
+ throw new CrudHttpError(404, { error: "Invoice line not found" });
325
+ }
326
+ const before = snapshotLine(line);
327
+ let audit = null;
328
+ await withAtomicFlush(em, [
329
+ () => {
330
+ em.remove(line);
331
+ },
332
+ async () => {
333
+ await recomputeInvoiceTotals(em, invoice);
334
+ },
335
+ () => {
336
+ audit = em.create(DraftInvoiceEdit, {
337
+ tenantId: parsed.tenantId,
338
+ organizationId: parsed.organizationId,
339
+ invoiceId: invoice.id,
340
+ invoiceLineId: null,
341
+ userId: getUserIdFromCtx(ctx),
342
+ action: "line_removed",
343
+ beforeJson: before,
344
+ afterJson: null,
345
+ createdAt: /* @__PURE__ */ new Date()
346
+ });
347
+ em.persist(audit);
348
+ }
349
+ ]);
350
+ return { invoiceId: invoice.id, invoiceLineId: parsed.invoiceLineId, auditId: audit.id };
351
+ }
352
+ };
353
+ const wipeTestInvoicesCommand = {
354
+ id: "billing.invoices.wipe_test",
355
+ async execute(rawInput, ctx) {
356
+ const parsed = billingWipeTestInvoicesSchema.parse(rawInput);
357
+ ensureTenantScope(ctx, parsed.tenantId);
358
+ ensureOrganizationScope(ctx, parsed.organizationId);
359
+ const em = getEm(ctx);
360
+ const params = [parsed.tenantId, parsed.organizationId];
361
+ let billRunClause = "";
362
+ if (parsed.billRunId) {
363
+ billRunClause = ` AND metadata->>'bill_run_id' = ?`;
364
+ params.push(parsed.billRunId);
365
+ }
366
+ const rows = await em.execute(
367
+ `SELECT id FROM sales_invoices
368
+ WHERE tenant_id = ?
369
+ AND organization_id = ?
370
+ AND deleted_at IS NULL
371
+ AND metadata->>'test_run' = 'true'${billRunClause}`,
372
+ params
373
+ );
374
+ if (!rows || rows.length === 0) {
375
+ return { invoicesRemoved: 0 };
376
+ }
377
+ const ids = rows.map((r) => r.id);
378
+ await em.execute(
379
+ `DELETE FROM sales_invoice_lines WHERE invoice_id IN (${ids.map(() => "?").join(",")})`,
380
+ ids
381
+ );
382
+ await em.execute(
383
+ `DELETE FROM sales_invoices WHERE id IN (${ids.map(() => "?").join(",")})`,
384
+ ids
385
+ );
386
+ return { invoicesRemoved: ids.length };
387
+ }
388
+ };
389
+ registerCommand(postInvoiceCommand);
390
+ registerCommand(editDraftLineCommand);
391
+ registerCommand(addDraftLineCommand);
392
+ registerCommand(removeDraftLineCommand);
393
+ registerCommand(wipeTestInvoicesCommand);
394
+ export {
395
+ addDraftLineCommand,
396
+ editDraftLineCommand,
397
+ postInvoiceCommand,
398
+ removeDraftLineCommand,
399
+ wipeTestInvoicesCommand
400
+ };
401
+ //# sourceMappingURL=invoices.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/commands/invoices.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands/types'\nimport { registerCommand } from '@open-mercato/shared/lib/commands'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n} from '@open-mercato/shared/lib/commands/scope'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { SalesInvoice, SalesInvoiceLine } from '@open-mercato/core/modules/sales/data/entities'\nimport { DraftInvoiceEdit } from '../data/entities'\nimport { emitBillingEvent } from '../events'\nimport {\n billingInvoiceAddLineSchema,\n billingInvoiceEditLineSchema,\n billingInvoicePostSchema,\n billingInvoiceRemoveLineSchema,\n billingWipeTestInvoicesSchema,\n type BillingInvoiceAddLineInput,\n type BillingInvoiceEditLineInput,\n type BillingInvoicePostInput,\n type BillingInvoiceRemoveLineInput,\n type BillingWipeTestInvoicesInput,\n} from '../data/validators'\nimport { resolveInvoiceStatusEntryId } from '../lib/invoiceStatus'\n\n/**\n * Posts a `core/sales` draft invoice that the Bill Run engine\n * created.\n *\n * Phase 2 deviation note: the invoice number is already set at\n * draft-create time (because `SalesInvoice.invoice_number` is NOT\n * NULL upstream). \"Post\" therefore only:\n * 1. Asserts the invoice is currently a draft (refuse otherwise).\n * 2. Refuses test-mode invoices (`metadata.test_run=true`) \u2014 they\n * should never reach production posting.\n * 3. Flips `status` (and `status_entry_id`) to `posted`.\n * 4. Fires `billing.invoice.posted` (one) + `billing.invoice.line_posted`\n * per line (for downstream consumers \u2014 accounting, analytics).\n *\n * Once posted, the invoice is frozen at the billing layer; corrections\n * go through `core/sales`'s manual edit flow.\n */\n\nfunction getEm(ctx: CommandRuntimeContext): EntityManager {\n return (ctx.container.resolve('em') as EntityManager).fork()\n}\n\ntype PostResult = {\n invoiceId: string\n invoiceNumber: string\n status: 'posted'\n lineCount: number\n}\n\nconst postInvoiceCommand: CommandHandler<BillingInvoicePostInput, PostResult> = {\n id: 'billing.invoices.post',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoicePostSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await em.findOne(SalesInvoice, {\n id: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n deletedAt: null,\n })\n if (!invoice) {\n throw new CrudHttpError(404, { error: 'Invoice not found' })\n }\n if (invoice.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be posted',\n code: 'billing.invoice.not_draft',\n currentStatus: invoice.status,\n })\n }\n const meta = (invoice.metadata as Record<string, unknown> | null) ?? {}\n if (meta.test_run === true) {\n // Test invoices live for inspection only \u2014 posting them would\n // leak test data into accounting. Force-delete the test invoice\n // instead (Phase 4b admin UI exposes a \"Wipe test drafts\"\n // button against `DELETE /api/billing/test-invoices`).\n throw new CrudHttpError(409, {\n error: 'Test-mode invoices cannot be posted',\n code: 'billing.invoice.test_run',\n })\n }\n\n const postedStatusEntryId = await resolveInvoiceStatusEntryId(\n em,\n parsed.tenantId,\n parsed.organizationId,\n 'posted',\n )\n\n await withAtomicFlush(em, [\n () => {\n invoice.status = 'posted'\n invoice.statusEntryId = postedStatusEntryId\n invoice.updatedAt = new Date()\n },\n ])\n\n const lines = await em.find(SalesInvoiceLine, {\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n } as never)\n\n // Side effects fire AFTER the DB commit so downstream subscribers\n // (mailer, accounting bridge, analytics) never observe a \"posted\"\n // event for an invoice the DB doesn't reflect.\n await emitBillingEvent('billing.invoice.posted', {\n invoiceId: invoice.id,\n invoiceNumber: invoice.invoiceNumber,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n grandTotalNet: invoice.grandTotalNetAmount,\n grandTotalGross: invoice.grandTotalGrossAmount,\n currencyCode: invoice.currencyCode,\n billRunId: (meta as { bill_run_id?: string }).bill_run_id ?? null,\n billAccountId: (meta as { bill_account_id?: string }).bill_account_id ?? null,\n billPeriodStart: (meta as { bill_period_start?: string }).bill_period_start ?? null,\n billPeriodEnd: (meta as { bill_period_end?: string }).bill_period_end ?? null,\n lineCount: lines.length,\n })\n for (const line of lines) {\n const lineMeta = (line.metadata as Record<string, unknown> | null) ?? {}\n await emitBillingEvent('billing.invoice.line_posted', {\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n billingItemId: lineMeta.billing_item_id ?? null,\n billingType: lineMeta.billing_type ?? null,\n description: line.description ?? null,\n unitPriceNet: line.unitPriceNet,\n quantity: line.quantity,\n totalNetAmount: line.totalNetAmount,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n })\n }\n\n return {\n invoiceId: invoice.id,\n invoiceNumber: invoice.invoiceNumber,\n status: 'posted',\n lineCount: lines.length,\n }\n },\n}\n\n// \u2500\u2500\u2500 Draft edit / add / remove (Phase 4b) \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//\n// Three commands sharing the same shape:\n// 1. Assert the invoice is still a draft (refuse otherwise).\n// 2. Mutate the line / collection.\n// 3. Write a `DraftInvoiceEdit` audit row carrying before+after\n// snapshots (`null` on the \"no prior state\" / \"no resulting state\"\n// side per the spec \u2014 see entity column docs).\n// 4. Recompute the invoice totals from the surviving lines.\n//\n// The audit row is append-only; once written, never updated. Soft-\n// deleting the invoice does NOT delete the audit rows (compliance).\n\nfunction roundHalfUp2dp(value: number): number {\n return Math.round((value + Number.EPSILON) * 100) / 100\n}\n\nfunction format4dp(value: number): string {\n return roundHalfUp2dp(value).toFixed(4)\n}\n\nasync function assertDraftInvoice(\n em: EntityManager,\n params: { invoiceId: string; tenantId: string; organizationId: string },\n): Promise<SalesInvoice> {\n const invoice = await em.findOne(SalesInvoice, {\n id: params.invoiceId,\n tenantId: params.tenantId,\n organizationId: params.organizationId,\n deletedAt: null,\n })\n if (!invoice) {\n throw new CrudHttpError(404, { error: 'Invoice not found' })\n }\n if (invoice.status !== 'draft') {\n throw new CrudHttpError(409, {\n error: 'Only draft invoices can be edited',\n code: 'billing.invoice.not_draft',\n currentStatus: invoice.status,\n })\n }\n return invoice\n}\n\ntype LineSnapshot = {\n id: string\n description: string | null\n quantity: string\n unitPriceNet: string\n totalNetAmount: string\n totalGrossAmount: string\n metadata: Record<string, unknown> | null\n}\n\nfunction snapshotLine(line: SalesInvoiceLine): LineSnapshot {\n return {\n id: line.id,\n description: line.description ?? null,\n quantity: line.quantity,\n unitPriceNet: line.unitPriceNet,\n totalNetAmount: line.totalNetAmount,\n totalGrossAmount: line.totalGrossAmount,\n metadata: (line.metadata as Record<string, unknown> | null) ?? null,\n }\n}\n\nasync function recomputeInvoiceTotals(\n em: EntityManager,\n invoice: SalesInvoice,\n): Promise<void> {\n const lines = await em.find(SalesInvoiceLine, {\n invoice,\n tenantId: invoice.tenantId,\n organizationId: invoice.organizationId,\n } as never)\n let totalNet = 0\n for (const line of lines) {\n totalNet += Number.parseFloat(line.totalNetAmount)\n }\n const formatted = format4dp(totalNet)\n invoice.subtotalNetAmount = formatted\n invoice.subtotalGrossAmount = formatted\n invoice.grandTotalNetAmount = formatted\n invoice.grandTotalGrossAmount = formatted\n invoice.outstandingAmount = formatted\n invoice.updatedAt = new Date()\n}\n\nfunction getUserIdFromCtx(ctx: CommandRuntimeContext): string {\n const userId = (ctx.auth?.sub as string | undefined) ?? null\n // The DraftInvoiceEdit.user_id column is NOT NULL. For system-driven\n // edits (e.g. catch-up flow that revises a value), we fall back to\n // a sentinel UUID. Operator-driven edits always have a real\n // ctx.auth.sub.\n return userId ?? '00000000-0000-0000-0000-000000000000'\n}\n\nconst editDraftLineCommand: CommandHandler<\n BillingInvoiceEditLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.edit_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceEditLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, {\n invoiceId: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n\n const line = await em.findOne(SalesInvoiceLine, {\n id: parsed.invoiceLineId,\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (!line) {\n throw new CrudHttpError(404, { error: 'Invoice line not found' })\n }\n\n const before = snapshotLine(line)\n let audit: DraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n if (parsed.changes.description !== undefined) {\n line.description = parsed.changes.description\n }\n if (parsed.changes.quantity !== undefined) {\n line.quantity = format4dp(parsed.changes.quantity)\n line.normalizedQuantity = line.quantity\n }\n if (parsed.changes.unitPriceNet !== undefined) {\n line.unitPriceNet = format4dp(parsed.changes.unitPriceNet)\n line.unitPriceGross = line.unitPriceNet\n }\n\n // Recompute the line total. If the operator supplied an explicit\n // `totalNetAmount` override, honour it; otherwise compute from\n // unit_price \u00D7 quantity.\n if (parsed.changes.totalNetAmount !== undefined) {\n const total = format4dp(parsed.changes.totalNetAmount)\n line.totalNetAmount = total\n line.totalGrossAmount = total\n } else {\n const total = format4dp(\n Number.parseFloat(line.unitPriceNet) * Number.parseFloat(line.quantity),\n )\n line.totalNetAmount = total\n line.totalGrossAmount = total\n }\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n const after = snapshotLine(line)\n audit = em.create(DraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n userId: getUserIdFromCtx(ctx),\n action: 'line_edited',\n beforeJson: before as Record<string, unknown>,\n afterJson: after as Record<string, unknown>,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit!.id }\n },\n}\n\nconst addDraftLineCommand: CommandHandler<\n BillingInvoiceAddLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.add_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceAddLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, {\n invoiceId: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n\n const existing = await em.find(SalesInvoiceLine, {\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n } as never)\n const nextLineNumber = existing.length + 1\n\n const total = format4dp(parsed.unitPriceNet * parsed.quantity)\n const line = em.create(SalesInvoiceLine, {\n invoice,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n lineNumber: nextLineNumber,\n kind: 'product',\n description: parsed.description,\n quantity: format4dp(parsed.quantity),\n normalizedQuantity: format4dp(parsed.quantity),\n currencyCode: invoice.currencyCode,\n unitPriceNet: format4dp(parsed.unitPriceNet),\n unitPriceGross: format4dp(parsed.unitPriceNet),\n discountAmount: '0',\n discountPercent: '0',\n taxRate: '0',\n taxAmount: '0',\n totalNetAmount: total,\n totalGrossAmount: total,\n metadata: {\n billing_item_id: parsed.billingItemId ?? null,\n billing_type: parsed.billingType ?? 'manual',\n operator_added: true,\n },\n })\n let audit: DraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n em.persist(line)\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n const after = snapshotLine(line)\n audit = em.create(DraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: line.id,\n userId: getUserIdFromCtx(ctx),\n action: 'line_added',\n beforeJson: null,\n afterJson: after as Record<string, unknown>,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: line.id, auditId: audit!.id }\n },\n}\n\nconst removeDraftLineCommand: CommandHandler<\n BillingInvoiceRemoveLineInput,\n { invoiceId: string; invoiceLineId: string; auditId: string }\n> = {\n id: 'billing.invoices.remove_draft_line',\n\n async execute(rawInput, ctx) {\n const parsed = billingInvoiceRemoveLineSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n const invoice = await assertDraftInvoice(em, {\n invoiceId: parsed.invoiceId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n const line = await em.findOne(SalesInvoiceLine, {\n id: parsed.invoiceLineId,\n invoice,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (!line) {\n throw new CrudHttpError(404, { error: 'Invoice line not found' })\n }\n\n const before = snapshotLine(line)\n let audit: DraftInvoiceEdit | null = null\n\n await withAtomicFlush(em, [\n () => {\n em.remove(line)\n },\n async () => {\n await recomputeInvoiceTotals(em, invoice)\n },\n () => {\n audit = em.create(DraftInvoiceEdit, {\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n invoiceId: invoice.id,\n invoiceLineId: null,\n userId: getUserIdFromCtx(ctx),\n action: 'line_removed',\n beforeJson: before as Record<string, unknown>,\n afterJson: null,\n createdAt: new Date(),\n })\n em.persist(audit)\n },\n ])\n\n return { invoiceId: invoice.id, invoiceLineId: parsed.invoiceLineId, auditId: audit!.id }\n },\n}\n\n// \u2500\u2500\u2500 Wipe test invoices (Phase 4b) \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//\n// Hard-deletes invoices flagged `metadata.test_run=true` (and their\n// lines via cascade \u2014 the invoice's lines collection points at it\n// with ON DELETE CASCADE). Optionally scoped to a single `bill_run_id`\n// via the param so operators can wipe a specific test run without\n// risking other test data.\n\nconst wipeTestInvoicesCommand: CommandHandler<\n BillingWipeTestInvoicesInput,\n { invoicesRemoved: number }\n> = {\n id: 'billing.invoices.wipe_test',\n\n async execute(rawInput, ctx) {\n const parsed = billingWipeTestInvoicesSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = getEm(ctx)\n // The metadata->>'test_run' = 'true' predicate uses Postgres'\n // JSONB ->> operator + the literal string 'true' (jsonb true is\n // serialized as the JSON literal `true`, which ->> returns as the\n // text \"true\").\n const params: unknown[] = [parsed.tenantId, parsed.organizationId]\n let billRunClause = ''\n if (parsed.billRunId) {\n billRunClause = ` AND metadata->>'bill_run_id' = ?`\n params.push(parsed.billRunId)\n }\n const rows = (await em.execute(\n `SELECT id FROM sales_invoices\n WHERE tenant_id = ?\n AND organization_id = ?\n AND deleted_at IS NULL\n AND metadata->>'test_run' = 'true'${billRunClause}`,\n params,\n )) as unknown as Array<{ id: string }> | undefined\n if (!rows || rows.length === 0) {\n return { invoicesRemoved: 0 }\n }\n const ids = rows.map((r) => r.id)\n // Hard-delete lines first then invoices to avoid relying on\n // cascade semantics that the migration may or may not encode.\n await em.execute(\n `DELETE FROM sales_invoice_lines WHERE invoice_id IN (${ids.map(() => '?').join(',')})`,\n ids,\n )\n await em.execute(\n `DELETE FROM sales_invoices WHERE id IN (${ids.map(() => '?').join(',')})`,\n ids,\n )\n return { invoicesRemoved: ids.length }\n },\n}\n\nregisterCommand(postInvoiceCommand)\nregisterCommand(editDraftLineCommand)\nregisterCommand(addDraftLineCommand)\nregisterCommand(removeDraftLineCommand)\nregisterCommand(wipeTestInvoicesCommand)\n\nexport {\n postInvoiceCommand,\n editDraftLineCommand,\n addDraftLineCommand,\n removeDraftLineCommand,\n wipeTestInvoicesCommand,\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }