@dainamite/billing 0.20.0 → 0.21.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 (598) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/modules/billing/acl.js +13 -1
  3. package/dist/modules/billing/acl.js.map +2 -2
  4. package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
  5. package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
  6. package/dist/modules/billing/api/accounts/route.js +46 -4
  7. package/dist/modules/billing/api/accounts/route.js.map +2 -2
  8. package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
  9. package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
  10. package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
  11. package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
  12. package/dist/modules/billing/api/invoices/[id]/pdf/route.js +69 -0
  13. package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
  14. package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
  15. package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
  16. package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
  17. package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
  18. package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
  19. package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
  20. package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
  21. package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
  22. package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
  23. package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
  24. package/dist/modules/billing/api/invoices/post/route.js +2 -2
  25. package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
  26. package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
  27. package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
  28. package/dist/modules/billing/api/invoices/route.js +45 -46
  29. package/dist/modules/billing/api/invoices/route.js.map +2 -2
  30. package/dist/modules/billing/api/items/bulk/route.js +1 -1
  31. package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
  32. package/dist/modules/billing/api/items/change-price/route.js +44 -0
  33. package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
  34. package/dist/modules/billing/api/items/route.js +19 -7
  35. package/dist/modules/billing/api/items/route.js.map +2 -2
  36. package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
  37. package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
  38. package/dist/modules/billing/api/payment-imports/route.js +57 -0
  39. package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
  40. package/dist/modules/billing/api/payments/[id]/route.js +60 -0
  41. package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
  42. package/dist/modules/billing/api/payments/allocate/route.js +35 -0
  43. package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
  44. package/dist/modules/billing/api/payments/assign/route.js +36 -0
  45. package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
  46. package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
  47. package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
  48. package/dist/modules/billing/api/payments/ignore/route.js +35 -0
  49. package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
  50. package/dist/modules/billing/api/payments/import/route.js +63 -0
  51. package/dist/modules/billing/api/payments/import/route.js.map +7 -0
  52. package/dist/modules/billing/api/payments/restore/route.js +35 -0
  53. package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
  54. package/dist/modules/billing/api/payments/route.js +114 -0
  55. package/dist/modules/billing/api/payments/route.js.map +7 -0
  56. package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
  57. package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
  58. package/dist/modules/billing/api/payments/unassign/route.js +35 -0
  59. package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
  60. package/dist/modules/billing/api/run-outcomes/route.js +43 -6
  61. package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
  62. package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
  63. package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
  64. package/dist/modules/billing/api/runs/[id]/route.js +53 -0
  65. package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
  66. package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
  67. package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
  68. package/dist/modules/billing/api/runs/route.js +9 -3
  69. package/dist/modules/billing/api/runs/route.js.map +2 -2
  70. package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
  71. package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
  72. package/dist/modules/billing/api/settings/route.js +92 -0
  73. package/dist/modules/billing/api/settings/route.js.map +7 -0
  74. package/dist/modules/billing/api/statements/route.js +3 -0
  75. package/dist/modules/billing/api/statements/route.js.map +2 -2
  76. package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
  77. package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
  78. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
  79. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
  80. package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
  81. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
  82. package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
  83. package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
  84. package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
  85. package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
  86. package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
  87. package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
  88. package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
  89. package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
  90. package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
  91. package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
  92. package/dist/modules/billing/backend/billing/items/page.js +8 -2
  93. package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
  94. package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
  95. package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
  96. package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
  97. package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
  98. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
  99. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
  100. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
  101. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
  102. package/dist/modules/billing/backend/billing/payments/page.js +58 -0
  103. package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
  104. package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
  105. package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
  106. package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
  107. package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
  108. package/dist/modules/billing/backend/billing/runs/page.js +58 -7
  109. package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
  110. package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
  111. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
  112. package/dist/modules/billing/backend/billing/statements/page.js +1 -1
  113. package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
  114. package/dist/modules/billing/backend/billing/topups/page.js +1 -1
  115. package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
  116. package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
  117. package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
  118. package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
  119. package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
  120. package/dist/modules/billing/backend/config/billing/page.js +472 -0
  121. package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
  122. package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
  123. package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
  124. package/dist/modules/billing/cli.js +22 -1
  125. package/dist/modules/billing/cli.js.map +2 -2
  126. package/dist/modules/billing/commands/accounts.js +6 -0
  127. package/dist/modules/billing/commands/accounts.js.map +2 -2
  128. package/dist/modules/billing/commands/index.js +3 -0
  129. package/dist/modules/billing/commands/index.js.map +2 -2
  130. package/dist/modules/billing/commands/invoices.js +269 -167
  131. package/dist/modules/billing/commands/invoices.js.map +2 -2
  132. package/dist/modules/billing/commands/items.js +108 -0
  133. package/dist/modules/billing/commands/items.js.map +2 -2
  134. package/dist/modules/billing/commands/memos.js +179 -0
  135. package/dist/modules/billing/commands/memos.js.map +7 -0
  136. package/dist/modules/billing/commands/payments.js +379 -0
  137. package/dist/modules/billing/commands/payments.js.map +7 -0
  138. package/dist/modules/billing/commands/runs.js +151 -4
  139. package/dist/modules/billing/commands/runs.js.map +2 -2
  140. package/dist/modules/billing/commands/settings.js +59 -0
  141. package/dist/modules/billing/commands/settings.js.map +7 -0
  142. package/dist/modules/billing/commands/topups.js +9 -2
  143. package/dist/modules/billing/commands/topups.js.map +2 -2
  144. package/dist/modules/billing/commands/transactions.js +2 -2
  145. package/dist/modules/billing/commands/transactions.js.map +2 -2
  146. package/dist/modules/billing/components/AccountForm.js +149 -42
  147. package/dist/modules/billing/components/AccountForm.js.map +2 -2
  148. package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
  149. package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
  150. package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
  151. package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
  152. package/dist/modules/billing/components/AccountPicker.js +56 -0
  153. package/dist/modules/billing/components/AccountPicker.js.map +7 -0
  154. package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
  155. package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
  156. package/dist/modules/billing/components/BillingDataTable.js +21 -0
  157. package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
  158. package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
  159. package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
  160. package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
  161. package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
  162. package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
  163. package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
  164. package/dist/modules/billing/components/CustomerLabel.js +33 -0
  165. package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
  166. package/dist/modules/billing/components/InvoicesTable.js +270 -0
  167. package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
  168. package/dist/modules/billing/components/ItemForm.js +176 -44
  169. package/dist/modules/billing/components/ItemForm.js.map +2 -2
  170. package/dist/modules/billing/components/ItemPriceField.js +46 -0
  171. package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
  172. package/dist/modules/billing/components/LineFormDialog.js +25 -2
  173. package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
  174. package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
  175. package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
  176. package/dist/modules/billing/components/MoneyInput.js +14 -0
  177. package/dist/modules/billing/components/MoneyInput.js.map +7 -0
  178. package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
  179. package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
  180. package/dist/modules/billing/components/PaymentsTable.js +171 -0
  181. package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
  182. package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
  183. package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
  184. package/dist/modules/billing/components/PrepaidPanel.js +14 -4
  185. package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
  186. package/dist/modules/billing/components/RunCyclesSection.js +209 -0
  187. package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
  188. package/dist/modules/billing/components/TestRunActions.js +127 -0
  189. package/dist/modules/billing/components/TestRunActions.js.map +7 -0
  190. package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
  191. package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
  192. package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
  193. package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
  194. package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
  195. package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
  196. package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
  197. package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
  198. package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
  199. package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
  200. package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
  201. package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
  202. package/dist/modules/billing/components/account/AccountView.js +73 -0
  203. package/dist/modules/billing/components/account/AccountView.js.map +7 -0
  204. package/dist/modules/billing/components/account/accountRow.js +39 -0
  205. package/dist/modules/billing/components/account/accountRow.js.map +7 -0
  206. package/dist/modules/billing/components/customerNames.js +42 -0
  207. package/dist/modules/billing/components/customerNames.js.map +7 -0
  208. package/dist/modules/billing/data/entities.js +560 -4
  209. package/dist/modules/billing/data/entities.js.map +2 -2
  210. package/dist/modules/billing/data/entityIds.js +10 -1
  211. package/dist/modules/billing/data/entityIds.js.map +2 -2
  212. package/dist/modules/billing/data/validators.js +269 -15
  213. package/dist/modules/billing/data/validators.js.map +2 -2
  214. package/dist/modules/billing/events.js +50 -0
  215. package/dist/modules/billing/events.js.map +2 -2
  216. package/dist/modules/billing/i18n/en.json +457 -21
  217. package/dist/modules/billing/i18n/pl.json +457 -21
  218. package/dist/modules/billing/index.js +6 -2
  219. package/dist/modules/billing/index.js.map +2 -2
  220. package/dist/modules/billing/lib/accountFinancials.js +58 -0
  221. package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
  222. package/dist/modules/billing/lib/accountLock.js +14 -0
  223. package/dist/modules/billing/lib/accountLock.js.map +7 -0
  224. package/dist/modules/billing/lib/accountStatus.js +9 -7
  225. package/dist/modules/billing/lib/accountStatus.js.map +2 -2
  226. package/dist/modules/billing/lib/accountSummary.js +106 -0
  227. package/dist/modules/billing/lib/accountSummary.js.map +7 -0
  228. package/dist/modules/billing/lib/allocation.js +356 -0
  229. package/dist/modules/billing/lib/allocation.js.map +7 -0
  230. package/dist/modules/billing/lib/allocationEvents.js +22 -0
  231. package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
  232. package/dist/modules/billing/lib/billPeriod.js +15 -1
  233. package/dist/modules/billing/lib/billPeriod.js.map +2 -2
  234. package/dist/modules/billing/lib/billRunEngine.js +237 -102
  235. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  236. package/dist/modules/billing/lib/billingSettings.js +116 -0
  237. package/dist/modules/billing/lib/billingSettings.js.map +7 -0
  238. package/dist/modules/billing/lib/commandRoute.js +81 -0
  239. package/dist/modules/billing/lib/commandRoute.js.map +7 -0
  240. package/dist/modules/billing/lib/customerLookup.js +97 -0
  241. package/dist/modules/billing/lib/customerLookup.js.map +7 -0
  242. package/dist/modules/billing/lib/dateOnly.js +13 -0
  243. package/dist/modules/billing/lib/dateOnly.js.map +7 -0
  244. package/dist/modules/billing/lib/documentParties.js +16 -0
  245. package/dist/modules/billing/lib/documentParties.js.map +7 -0
  246. package/dist/modules/billing/lib/documentTypes.js +27 -0
  247. package/dist/modules/billing/lib/documentTypes.js.map +7 -0
  248. package/dist/modules/billing/lib/dueDate.js +18 -0
  249. package/dist/modules/billing/lib/dueDate.js.map +7 -0
  250. package/dist/modules/billing/lib/iban.js +29 -0
  251. package/dist/modules/billing/lib/iban.js.map +7 -0
  252. package/dist/modules/billing/lib/invoiceDelivery.js +131 -0
  253. package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
  254. package/dist/modules/billing/lib/invoiceLanguages.js +16 -0
  255. package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
  256. package/dist/modules/billing/lib/invoiceWriter.js +107 -99
  257. package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
  258. package/dist/modules/billing/lib/itemBillingState.js +27 -0
  259. package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
  260. package/dist/modules/billing/lib/itemPriceChange.js +18 -0
  261. package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
  262. package/dist/modules/billing/lib/itemSelector.js.map +2 -2
  263. package/dist/modules/billing/lib/lineAmounts.js +32 -0
  264. package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
  265. package/dist/modules/billing/lib/memoGuards.js +25 -0
  266. package/dist/modules/billing/lib/memoGuards.js.map +7 -0
  267. package/dist/modules/billing/lib/navigation.js +48 -0
  268. package/dist/modules/billing/lib/navigation.js.map +7 -0
  269. package/dist/modules/billing/lib/numbering.js +120 -0
  270. package/dist/modules/billing/lib/numbering.js.map +7 -0
  271. package/dist/modules/billing/lib/overdue.js +36 -0
  272. package/dist/modules/billing/lib/overdue.js.map +7 -0
  273. package/dist/modules/billing/lib/paymentCsv.js +125 -0
  274. package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
  275. package/dist/modules/billing/lib/paymentGateway.js +15 -0
  276. package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
  277. package/dist/modules/billing/lib/paymentImport.js +164 -0
  278. package/dist/modules/billing/lib/paymentImport.js.map +7 -0
  279. package/dist/modules/billing/lib/paymentMatcher.js +36 -0
  280. package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
  281. package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
  282. package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
  283. package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
  284. package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
  285. package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
  286. package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
  287. package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
  288. package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
  289. package/dist/modules/billing/lib/pdf/invoiceTemplate.js +274 -0
  290. package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
  291. package/dist/modules/billing/lib/prepaidConfig.js +17 -25
  292. package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
  293. package/dist/modules/billing/lib/prepaidConsume.js +26 -7
  294. package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
  295. package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
  296. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
  297. package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
  298. package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
  299. package/dist/modules/billing/lib/seeds/demo.js +51 -3
  300. package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
  301. package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
  302. package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
  303. package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
  304. package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
  305. package/dist/modules/billing/lib/servicePeriods.js +121 -0
  306. package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
  307. package/dist/modules/billing/lib/testRunCleanup.js +24 -0
  308. package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
  309. package/dist/modules/billing/lib/topupCapture.js +2 -2
  310. package/dist/modules/billing/lib/topupCapture.js.map +2 -2
  311. package/dist/modules/billing/lib/topupReceipt.js +58 -79
  312. package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
  313. package/dist/modules/billing/lib/usageRater.js.map +1 -1
  314. package/dist/modules/billing/lib/usageRunner.js +14 -6
  315. package/dist/modules/billing/lib/usageRunner.js.map +2 -2
  316. package/dist/modules/billing/lib/vat.js +39 -0
  317. package/dist/modules/billing/lib/vat.js.map +7 -0
  318. package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
  319. package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
  320. package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
  321. package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
  322. package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
  323. package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
  324. package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
  325. package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
  326. package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
  327. package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
  328. package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
  329. package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
  330. package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
  331. package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
  332. package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
  333. package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
  334. package/dist/modules/billing/setup.js +7 -184
  335. package/dist/modules/billing/setup.js.map +2 -2
  336. package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
  337. package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
  338. package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
  339. package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
  340. package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
  341. package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
  342. package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
  343. package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
  344. package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
  345. package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
  346. package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
  347. package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
  348. package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
  349. package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
  350. package/generated/entities/billing_account/index.ts +3 -0
  351. package/generated/entities/billing_allocation/index.ts +18 -0
  352. package/generated/entities/billing_document_sequence/index.ts +11 -0
  353. package/generated/entities/billing_invoice/index.ts +38 -0
  354. package/generated/entities/billing_invoice_line/index.ts +24 -0
  355. package/generated/entities/billing_item/index.ts +4 -0
  356. package/generated/entities/billing_payment/index.ts +25 -0
  357. package/generated/entities/billing_payment_import/index.ts +14 -0
  358. package/generated/entities/billing_run/index.ts +0 -1
  359. package/generated/entities/billing_run_outcome/index.ts +1 -0
  360. package/generated/entities/billing_settings/index.ts +26 -0
  361. package/generated/entities/billing_statement/index.ts +1 -0
  362. package/generated/entities.ids.generated.ts +7 -0
  363. package/package.json +5 -1
  364. package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +170 -0
  365. package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
  366. package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
  367. package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
  368. package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
  369. package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
  370. package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
  371. package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
  372. package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
  373. package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
  374. package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
  375. package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
  376. package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
  377. package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
  378. package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
  379. package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
  380. package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
  381. package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
  382. package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
  383. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
  384. package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
  385. package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
  386. package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
  387. package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
  388. package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
  389. package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
  390. package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
  391. package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
  392. package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +189 -0
  393. package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
  394. package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
  395. package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
  396. package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
  397. package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
  398. package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
  399. package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
  400. package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
  401. package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
  402. package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
  403. package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +199 -0
  404. package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
  405. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
  406. package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
  407. package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
  408. package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
  409. package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
  410. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
  411. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
  412. package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
  413. package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
  414. package/src/modules/billing/__tests__/setup.test.ts +13 -4
  415. package/src/modules/billing/__tests__/validators.test.ts +193 -3
  416. package/src/modules/billing/acl.ts +12 -0
  417. package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
  418. package/src/modules/billing/api/accounts/route.ts +58 -4
  419. package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
  420. package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
  421. package/src/modules/billing/api/invoices/[id]/pdf/route.ts +79 -0
  422. package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
  423. package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
  424. package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
  425. package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
  426. package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
  427. package/src/modules/billing/api/invoices/post/route.ts +7 -6
  428. package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
  429. package/src/modules/billing/api/invoices/route.ts +81 -86
  430. package/src/modules/billing/api/items/bulk/route.ts +2 -1
  431. package/src/modules/billing/api/items/change-price/route.ts +54 -0
  432. package/src/modules/billing/api/items/route.ts +23 -7
  433. package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
  434. package/src/modules/billing/api/payment-imports/route.ts +57 -0
  435. package/src/modules/billing/api/payments/[id]/route.ts +63 -0
  436. package/src/modules/billing/api/payments/allocate/route.ts +35 -0
  437. package/src/modules/billing/api/payments/assign/route.ts +36 -0
  438. package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
  439. package/src/modules/billing/api/payments/ignore/route.ts +35 -0
  440. package/src/modules/billing/api/payments/import/route.ts +72 -0
  441. package/src/modules/billing/api/payments/restore/route.ts +35 -0
  442. package/src/modules/billing/api/payments/route.ts +147 -0
  443. package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
  444. package/src/modules/billing/api/payments/unassign/route.ts +35 -0
  445. package/src/modules/billing/api/run-outcomes/route.ts +49 -6
  446. package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
  447. package/src/modules/billing/api/runs/[id]/route.ts +59 -0
  448. package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
  449. package/src/modules/billing/api/runs/route.ts +15 -7
  450. package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
  451. package/src/modules/billing/api/settings/route.ts +116 -0
  452. package/src/modules/billing/api/statements/route.ts +5 -0
  453. package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
  454. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
  455. package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
  456. package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
  457. package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
  458. package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
  459. package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
  460. package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
  461. package/src/modules/billing/backend/billing/items/page.tsx +18 -2
  462. package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
  463. package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
  464. package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
  465. package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
  466. package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
  467. package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
  468. package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
  469. package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
  470. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
  471. package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
  472. package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
  473. package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
  474. package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
  475. package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
  476. package/src/modules/billing/backend/config/billing/page.tsx +549 -0
  477. package/src/modules/billing/cli.ts +31 -1
  478. package/src/modules/billing/commands/accounts.ts +6 -0
  479. package/src/modules/billing/commands/index.ts +3 -0
  480. package/src/modules/billing/commands/invoices.ts +364 -231
  481. package/src/modules/billing/commands/items.ts +143 -0
  482. package/src/modules/billing/commands/memos.ts +218 -0
  483. package/src/modules/billing/commands/payments.ts +428 -0
  484. package/src/modules/billing/commands/runs.ts +205 -3
  485. package/src/modules/billing/commands/settings.ts +77 -0
  486. package/src/modules/billing/commands/topups.ts +12 -3
  487. package/src/modules/billing/commands/transactions.ts +2 -2
  488. package/src/modules/billing/components/AccountForm.tsx +199 -53
  489. package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
  490. package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
  491. package/src/modules/billing/components/AccountPicker.tsx +70 -0
  492. package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
  493. package/src/modules/billing/components/BillingDataTable.tsx +34 -0
  494. package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
  495. package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
  496. package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
  497. package/src/modules/billing/components/CustomerLabel.tsx +53 -0
  498. package/src/modules/billing/components/InvoicesTable.tsx +344 -0
  499. package/src/modules/billing/components/ItemForm.tsx +233 -44
  500. package/src/modules/billing/components/ItemPriceField.tsx +81 -0
  501. package/src/modules/billing/components/LineFormDialog.tsx +25 -1
  502. package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
  503. package/src/modules/billing/components/MoneyInput.tsx +26 -0
  504. package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
  505. package/src/modules/billing/components/PaymentsTable.tsx +213 -0
  506. package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
  507. package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
  508. package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
  509. package/src/modules/billing/components/TestRunActions.tsx +152 -0
  510. package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
  511. package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
  512. package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
  513. package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
  514. package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
  515. package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
  516. package/src/modules/billing/components/account/AccountView.tsx +101 -0
  517. package/src/modules/billing/components/account/accountRow.ts +70 -0
  518. package/src/modules/billing/components/customerNames.ts +62 -0
  519. package/src/modules/billing/data/entities.ts +654 -5
  520. package/src/modules/billing/data/entityIds.ts +9 -0
  521. package/src/modules/billing/data/validators.ts +388 -8
  522. package/src/modules/billing/events.ts +56 -3
  523. package/src/modules/billing/i18n/en.json +457 -21
  524. package/src/modules/billing/i18n/pl.json +457 -21
  525. package/src/modules/billing/index.ts +6 -2
  526. package/src/modules/billing/lib/accountFinancials.ts +84 -0
  527. package/src/modules/billing/lib/accountLock.ts +32 -0
  528. package/src/modules/billing/lib/accountStatus.ts +11 -9
  529. package/src/modules/billing/lib/accountSummary.ts +172 -0
  530. package/src/modules/billing/lib/allocation.ts +515 -0
  531. package/src/modules/billing/lib/allocationEvents.ts +26 -0
  532. package/src/modules/billing/lib/billPeriod.ts +19 -0
  533. package/src/modules/billing/lib/billRunEngine.ts +350 -150
  534. package/src/modules/billing/lib/billingSettings.ts +145 -0
  535. package/src/modules/billing/lib/commandRoute.ts +112 -0
  536. package/src/modules/billing/lib/customerLookup.ts +157 -0
  537. package/src/modules/billing/lib/dateOnly.ts +22 -0
  538. package/src/modules/billing/lib/documentParties.ts +23 -0
  539. package/src/modules/billing/lib/documentTypes.ts +37 -0
  540. package/src/modules/billing/lib/dueDate.ts +26 -0
  541. package/src/modules/billing/lib/iban.ts +36 -0
  542. package/src/modules/billing/lib/invoiceDelivery.ts +197 -0
  543. package/src/modules/billing/lib/invoiceLanguages.ts +34 -0
  544. package/src/modules/billing/lib/invoiceWriter.ts +162 -148
  545. package/src/modules/billing/lib/itemBillingState.ts +67 -0
  546. package/src/modules/billing/lib/itemPriceChange.ts +56 -0
  547. package/src/modules/billing/lib/itemSelector.ts +4 -1
  548. package/src/modules/billing/lib/lineAmounts.ts +61 -0
  549. package/src/modules/billing/lib/memoGuards.ts +41 -0
  550. package/src/modules/billing/lib/navigation.ts +64 -0
  551. package/src/modules/billing/lib/numbering.ts +174 -0
  552. package/src/modules/billing/lib/overdue.ts +60 -0
  553. package/src/modules/billing/lib/paymentCsv.ts +171 -0
  554. package/src/modules/billing/lib/paymentGateway.ts +52 -0
  555. package/src/modules/billing/lib/paymentImport.ts +228 -0
  556. package/src/modules/billing/lib/paymentMatcher.ts +88 -0
  557. package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
  558. package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
  559. package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
  560. package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
  561. package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
  562. package/src/modules/billing/lib/pdf/invoiceModel.ts +328 -0
  563. package/src/modules/billing/lib/pdf/invoicePdf.ts +216 -0
  564. package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
  565. package/src/modules/billing/lib/prepaidConfig.ts +27 -36
  566. package/src/modules/billing/lib/prepaidConsume.ts +29 -6
  567. package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
  568. package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
  569. package/src/modules/billing/lib/seeds/demo.ts +79 -6
  570. package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
  571. package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
  572. package/src/modules/billing/lib/servicePeriods.ts +226 -0
  573. package/src/modules/billing/lib/testRunCleanup.ts +42 -0
  574. package/src/modules/billing/lib/topupCapture.ts +3 -4
  575. package/src/modules/billing/lib/topupReceipt.ts +70 -103
  576. package/src/modules/billing/lib/usageRater.ts +1 -1
  577. package/src/modules/billing/lib/usageRunner.ts +17 -5
  578. package/src/modules/billing/lib/vat.ts +56 -0
  579. package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
  580. package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
  581. package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
  582. package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
  583. package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
  584. package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
  585. package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
  586. package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
  587. package/src/modules/billing/setup.ts +11 -237
  588. package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
  589. package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
  590. package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
  591. package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
  592. package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
  593. package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
  594. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
  595. package/dist/modules/billing/lib/invoiceStatus.js +0 -38
  596. package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
  597. package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
  598. package/src/modules/billing/lib/invoiceStatus.ts +0 -61
@@ -1,464 +1,546 @@
1
1
  /**
2
- * Unit tests for the Phase 4a + 4b invoice commands.
2
+ * Unit tests for the billing invoice commands (Spec 0 — billing-owned
3
+ * `billing_invoices`): post (number claimed at post, snapshots, refusals),
4
+ * draft line edit / add / remove (audit + totals), and the test wipe.
3
5
  *
4
- * These exercise the *command contract* — refusal on non-draft,
5
- * audit-row writing, totals recompute — via a thin mocked EM. Real
6
- * end-to-end persistence is covered by the integration suite once
7
- * Phase 4c admin UI lands and Playwright can drive the flow.
8
- *
9
- * The mock EM models the subset of operations the commands touch:
10
- * findOne / find / create / persist / flush / remove / execute.
11
- * `create` returns a shallow copy with a synthetic id so the
12
- * command's `emit / return` path can read `result.id` immediately.
6
+ * The mock EM models the subset the commands touch, keyed by entity class:
7
+ * findOne / find / create / persist / flush / remove / execute /
8
+ * transactional / fork.
13
9
  */
14
10
 
15
- import {
16
- __resetInvoiceStatusCacheForTests,
17
- } from '../../lib/invoiceStatus'
11
+ jest.mock('../../lib/pdf/invoicePdf', () => ({
12
+ freezeInvoicePdf: jest.fn(async () => ({ attachmentId: 'pdf-1', bytes: null })),
13
+ }))
14
+ jest.mock('../../events', () => ({ emitBillingEvent: jest.fn(async () => undefined) }))
15
+ jest.mock('../../lib/allocation', () => {
16
+ const actual = jest.requireActual('../../lib/allocation')
17
+ return {
18
+ ...actual,
19
+ allocate: jest.fn(async () => actual.emptyEffects()),
20
+ applyAccountCredit: jest.fn(async () => actual.emptyEffects()),
21
+ }
22
+ })
23
+ import { allocate, applyAccountCredit } from '../../lib/allocation'
24
+
25
+ import { freezeInvoicePdf } from '../../lib/pdf/invoicePdf'
26
+ import { emitBillingEvent } from '../../events'
18
27
  import {
19
28
  addDraftLineCommand,
29
+ buildBuyerSnapshot,
20
30
  editDraftLineCommand,
21
31
  postInvoiceCommand,
22
32
  removeDraftLineCommand,
23
33
  wipeTestInvoicesCommand,
24
34
  } from '../../commands/invoices'
25
35
  import {
36
+ BillingAccount,
26
37
  BillingDraftInvoiceEdit,
38
+ BillingInvoice,
39
+ BillingInvoiceLine,
40
+ BillingSettings,
27
41
  } from '../../data/entities'
28
42
 
29
- type FakeInvoice = {
30
- id: string
31
- status: string
32
- statusEntryId: string | null
33
- tenantId: string
34
- organizationId: string
35
- invoiceNumber: string
36
- currencyCode: string
37
- metadata: Record<string, unknown> | null
38
- subtotalNetAmount: string
39
- subtotalGrossAmount: string
40
- grandTotalNetAmount: string
41
- grandTotalGrossAmount: string
42
- outstandingAmount: string
43
- paidTotalAmount: string
44
- updatedAt: Date
45
- deletedAt: Date | null
46
- }
47
-
48
- type FakeLine = {
49
- id: string
50
- invoice: FakeInvoice
51
- tenantId: string
52
- organizationId: string
53
- description: string | null
54
- quantity: string
55
- normalizedQuantity: string
56
- unitPriceNet: string
57
- unitPriceGross: string
58
- totalNetAmount: string
59
- totalGrossAmount: string
60
- metadata: Record<string, unknown> | null
61
- lineNumber: number
62
- }
63
-
64
43
  const TENANT = '11111111-1111-4111-8111-111111111111'
65
44
  const ORG = '22222222-2222-4222-8222-222222222222'
66
45
  const INVOICE_ID = '33333333-3333-4333-8333-333333333333'
67
46
  const LINE_ID = '44444444-4444-4444-8444-444444444444'
47
+ const ACCOUNT_ID = '55555555-5555-4555-8555-555555555555'
48
+
49
+ type Row = Record<string, unknown>
68
50
 
69
- function makeInvoice(overrides: Partial<FakeInvoice> = {}): FakeInvoice {
51
+ function makeInvoice(overrides: Row = {}): Row {
70
52
  return {
71
- id: overrides.id ?? INVOICE_ID,
72
- status: overrides.status ?? 'draft',
73
- statusEntryId: overrides.statusEntryId ?? 'status-draft',
74
- tenantId: overrides.tenantId ?? TENANT,
75
- organizationId: overrides.organizationId ?? ORG,
76
- invoiceNumber: overrides.invoiceNumber ?? 'INV-2026-0000001',
77
- currencyCode: overrides.currencyCode ?? 'EUR',
78
- metadata: overrides.metadata ?? {},
79
- subtotalNetAmount: overrides.subtotalNetAmount ?? '100.0000',
80
- subtotalGrossAmount: overrides.subtotalGrossAmount ?? '100.0000',
81
- grandTotalNetAmount: overrides.grandTotalNetAmount ?? '100.0000',
82
- grandTotalGrossAmount: overrides.grandTotalGrossAmount ?? '100.0000',
83
- outstandingAmount: overrides.outstandingAmount ?? '100.0000',
84
- paidTotalAmount: overrides.paidTotalAmount ?? '0',
85
- updatedAt: overrides.updatedAt ?? new Date(),
86
- deletedAt: overrides.deletedAt ?? null,
53
+ id: INVOICE_ID,
54
+ tenantId: TENANT,
55
+ organizationId: ORG,
56
+ billAccountId: ACCOUNT_ID,
57
+ documentType: 'invoice',
58
+ number: 'DRAFT-ABCDEF12',
59
+ series: null,
60
+ status: 'draft',
61
+ testRun: false,
62
+ currencyCode: 'EUR',
63
+ subtotalNet: '100.0000',
64
+ taxTotal: '0.0000',
65
+ totalGross: '100.0000',
66
+ paidTotal: '0',
67
+ outstanding: '100.0000',
68
+ billDate: new Date('2026-12-01T00:00:00Z'),
69
+ periodStart: new Date('2026-12-01T00:00:00Z'),
70
+ periodEnd: new Date('2026-12-31T00:00:00Z'),
71
+ deletedAt: null,
72
+ ...overrides,
87
73
  }
88
74
  }
89
75
 
90
- function makeLine(invoice: FakeInvoice, overrides: Partial<FakeLine> = {}): FakeLine {
76
+ function makeLine(overrides: Row = {}): Row {
91
77
  return {
92
- id: overrides.id ?? LINE_ID,
93
- invoice,
94
- tenantId: invoice.tenantId,
95
- organizationId: invoice.organizationId,
96
- description: overrides.description ?? 'MRC',
97
- quantity: overrides.quantity ?? '1.0000',
98
- normalizedQuantity: overrides.normalizedQuantity ?? '1.0000',
99
- unitPriceNet: overrides.unitPriceNet ?? '100.0000',
100
- unitPriceGross: overrides.unitPriceGross ?? '100.0000',
101
- totalNetAmount: overrides.totalNetAmount ?? '100.0000',
102
- totalGrossAmount: overrides.totalGrossAmount ?? '100.0000',
103
- metadata: overrides.metadata ?? { billing_type: 'recurring' },
104
- lineNumber: overrides.lineNumber ?? 1,
78
+ id: LINE_ID,
79
+ invoiceId: INVOICE_ID,
80
+ tenantId: TENANT,
81
+ organizationId: ORG,
82
+ lineNumber: 1,
83
+ billingItemId: null,
84
+ billingType: 'recurring',
85
+ description: 'MRC',
86
+ quantity: '1.0000',
87
+ unitPriceNet: '100.0000',
88
+ taxRate: '0',
89
+ netAmount: '100.0000',
90
+ taxAmount: '0.0000',
91
+ grossAmount: '100.0000',
92
+ metadata: null,
93
+ deletedAt: null,
94
+ ...overrides,
105
95
  }
106
96
  }
107
97
 
108
- type Env = {
109
- ctx: { container: { resolve: jest.MockedFunction<(name: string) => unknown> }; auth: { sub?: string; tenantId?: string; orgId?: string } | null }
110
- em: {
111
- findOne: jest.MockedFunction<(entity: unknown, where: unknown) => Promise<unknown>>
112
- find: jest.MockedFunction<(entity: unknown, where: unknown) => Promise<unknown[]>>
113
- create: jest.MockedFunction<(entity: unknown, data: Record<string, unknown>) => unknown>
114
- persist: jest.MockedFunction<(entity: unknown) => unknown>
115
- flush: jest.MockedFunction<() => Promise<void>>
116
- remove: jest.MockedFunction<(entity: unknown) => unknown>
117
- execute: jest.MockedFunction<(sql: string, params?: unknown[]) => Promise<unknown>>
118
- fork: jest.MockedFunction<() => Env['em']>
119
- }
120
- persistedEntities: unknown[]
121
- removedEntities: unknown[]
98
+ const account = {
99
+ id: ACCOUNT_ID,
100
+ tenantId: TENANT,
101
+ organizationId: ORG,
102
+ name: 'Jan Kowalski',
103
+ customerId: 'cust-1',
104
+ customerKind: 'person',
105
+ taxId: null,
106
+ invoiceEmail: 'jan@example.test',
107
+ invoiceLanguage: 'pl',
108
+ invoiceAddress: { line1: 'ul. Testowa 1', city: 'Warszawa', postal_code: '00-001', country: 'PL' },
109
+ creditLimit: '0',
110
+ currencyCode: 'EUR',
111
+ billingMode: 'postpaid',
122
112
  }
123
113
 
124
- let auditIdCounter = 0
125
-
126
- function createEnv(
127
- options: {
128
- invoice?: FakeInvoice | null
129
- line?: FakeLine | null
130
- statusEntryDraft?: string | null
131
- statusEntryPosted?: string | null
132
- initialLines?: FakeLine[]
133
- invoiceFinalLines?: FakeLine[]
134
- auth?: { sub?: string; tenantId?: string; orgId?: string } | null
135
- testInvoiceRows?: Array<{ id: string }>
136
- } = {},
137
- ): Env {
138
- const persistedEntities: unknown[] = []
139
- const removedEntities: unknown[] = []
140
- let lineSequence = (options.initialLines ?? []).length
141
-
142
- const em: Env['em'] = {
143
- findOne: jest.fn(async (entity: unknown, _where: unknown) => {
144
- // Distinguish by entity reference. Order of priority:
145
- // SalesInvoice → Dictionary / DictionaryEntry (via invoiceStatus
146
- // resolver — returns dict then entry) → SalesInvoiceLine.
147
- const constructorName = (entity as { name?: string }).name ?? ''
148
- if (constructorName === 'SalesInvoice') return options.invoice ?? null
149
- if (constructorName === 'SalesInvoiceLine') return options.line ?? null
150
- if (constructorName === 'Dictionary') return { id: 'dict-1' }
151
- if (constructorName === 'DictionaryEntry') {
152
- // The status resolver looks up by value; assume posted for the
153
- // post test (the only one that exercises this path).
154
- return { id: options.statusEntryPosted ?? 'entry-posted' }
155
- }
114
+ // Spec B: seller details are complete, so posting is allowed.
115
+ const completeSettings: Row = {
116
+ id: 'settings-1',
117
+ organizationId: ORG,
118
+ numberMasks: null,
119
+ legalName: 'Seller sp. z o.o.',
120
+ sellerAddress: { line1: 'ul. Sprzedawcy 1', city: 'Kraków', postal_code: '30-001', country: 'PL' },
121
+ taxId: 'PL1234563218',
122
+ bankName: 'Bank',
123
+ iban: 'PL61109010140000071219812874',
124
+ bic: 'WBKPPLPP',
125
+ invoiceFooter: 'KRS 0000000000',
126
+ logoAttachmentId: null,
127
+ defaultPaymentTermsDays: 14,
128
+ defaultTaxRate: '23',
129
+ }
130
+
131
+ function createEnv(options: {
132
+ invoice?: Row | null
133
+ line?: Row | null
134
+ lines?: Row[]
135
+ testInvoiceIds?: string[]
136
+ seq?: number
137
+ claimThrows?: Error
138
+ settings?: Row
139
+ account?: Row
140
+ original?: Row
141
+ corrections?: Array<{ document_type: string; gross: string }>
142
+ } = {}) {
143
+ const settings = { ...completeSettings, ...(options.settings ?? {}) }
144
+ const accountRow = { ...account, ...(options.account ?? {}) }
145
+ const persisted: unknown[] = []
146
+ const removed: unknown[] = []
147
+ const executed: Array<{ sql: string; params: unknown[] }> = []
148
+ let created = 0
149
+ const lines = options.lines ?? (options.line ? [options.line] : [])
150
+
151
+ const em = {
152
+ findOne: jest.fn(async (entity: unknown, where?: Row) => {
153
+ if (entity === BillingInvoice && options.original && where?.id === options.original.id) return options.original
154
+ if (entity === BillingInvoice) return options.invoice ?? null
155
+ if (entity === BillingInvoiceLine) return options.line ?? null
156
+ if (entity === BillingAccount) return accountRow
157
+ if (entity === BillingSettings) return settings
156
158
  return null
157
- }) as never,
158
- find: jest.fn(async (entity: unknown, _where: unknown) => {
159
- const constructorName = (entity as { name?: string }).name ?? ''
160
- if (constructorName === 'SalesInvoiceLine') {
161
- // For add-line: return initialLines so lineNumber is computed.
162
- // For recomputeInvoiceTotals: return invoiceFinalLines after edit.
163
- if (options.invoiceFinalLines) return options.invoiceFinalLines
164
- return options.initialLines ?? []
165
- }
166
- return []
167
- }) as never,
168
- create: jest.fn((entity: unknown, data: Record<string, unknown>) => {
169
- const ctor = (entity as { name?: string }).name ?? ''
170
- if (ctor === 'BillingDraftInvoiceEdit') {
171
- auditIdCounter += 1
172
- return { ...data, id: `audit-${auditIdCounter}` }
173
- }
174
- if (ctor === 'SalesInvoiceLine') {
175
- lineSequence += 1
176
- return { ...data, id: `new-line-${lineSequence}` }
177
- }
178
- return { ...data, id: 'generated-id' }
179
- }) as never,
159
+ }),
160
+ find: jest.fn(async (entity: unknown) => (entity === BillingInvoiceLine ? lines : [])),
161
+ create: jest.fn((entity: unknown, data: Row) => {
162
+ created += 1
163
+ const prefix = entity === BillingDraftInvoiceEdit ? 'audit' : entity === BillingInvoiceLine ? 'new-line' : 'gen'
164
+ return { ...data, id: `${prefix}-${created}` }
165
+ }),
180
166
  persist: jest.fn((entity: unknown) => {
181
- persistedEntities.push(entity)
182
- }) as never,
183
- flush: jest.fn(async () => undefined) as never,
167
+ persisted.push(entity)
168
+ }),
169
+ flush: jest.fn(async () => undefined),
184
170
  remove: jest.fn((entity: unknown) => {
185
- removedEntities.push(entity)
186
- }) as never,
187
- execute: jest.fn(async () => options.testInvoiceRows ?? []) as never,
188
- fork: jest.fn() as never,
189
- }
190
- em.fork = jest.fn(() => em) as never
191
-
192
- const container = {
193
- resolve: jest.fn((name: string) => {
194
- if (name === 'em') return em
195
- return null
196
- }) as never,
171
+ removed.push(entity)
172
+ }),
173
+ execute: jest.fn(async (sql: string, params: unknown[] = []) => {
174
+ executed.push({ sql, params })
175
+ if (sql.includes('billing_document_sequences')) {
176
+ if (options.claimThrows) throw options.claimThrows
177
+ return [{ seq: options.seq ?? 1 }]
178
+ }
179
+ if (sql.includes('SUM(outstanding)')) return [{ total: '0' }]
180
+ if (sql.includes('SUM(total_gross)')) return options.corrections ?? []
181
+ if (sql.startsWith('SELECT status, outstanding FROM billing_invoices')) return [{ status: 'paid', outstanding: '0.0000' }]
182
+ if (sql.startsWith('SELECT id FROM billing_invoices')) return (options.testInvoiceIds ?? []).map((id) => ({ id }))
183
+ return []
184
+ }),
185
+ transactional: jest.fn(),
186
+ fork: jest.fn(),
197
187
  }
188
+ // Nested transactions / forks reuse the same mock EM.
189
+ em.transactional.mockImplementation(async (fn: (tem: unknown) => Promise<unknown>) => fn(em))
190
+ em.fork.mockReturnValue(em)
198
191
 
199
- return {
200
- ctx: {
201
- container,
202
- auth: options.auth ?? { sub: '99999999-9999-4999-8999-999999999999', tenantId: TENANT, orgId: ORG },
203
- },
204
- em,
205
- persistedEntities,
206
- removedEntities,
192
+ const ctx = {
193
+ container: { resolve: jest.fn((name: string) => (name === 'em' ? em : null)) },
194
+ auth: { sub: '99999999-9999-4999-8999-999999999999', tenantId: TENANT, orgId: ORG },
207
195
  }
196
+ return { em, ctx, persisted, removed, executed }
208
197
  }
209
198
 
210
- beforeEach(() => {
211
- __resetInvoiceStatusCacheForTests()
212
- auditIdCounter = 0
213
- })
214
-
215
- const baseInput = {
216
- tenantId: TENANT,
217
- organizationId: ORG,
218
- invoiceId: INVOICE_ID,
219
- }
199
+ const baseInput = { tenantId: TENANT, organizationId: ORG, invoiceId: INVOICE_ID }
220
200
 
221
201
  describe('postInvoiceCommand', () => {
222
- it('refuses non-draft invoices with 409 + not_draft code', async () => {
202
+ it('claims the series number at post and freezes the buyer snapshot', async () => {
203
+ const invoice = makeInvoice()
204
+ const env = createEnv({ invoice, lines: [makeLine()], seq: 7 })
205
+ const result = await postInvoiceCommand.execute(baseInput, env.ctx as never)
206
+
207
+ expect(result).toMatchObject({ status: 'posted', lineCount: 1 })
208
+ expect(result.invoiceNumber).toMatch(/^INV\/\d{4}\/\d{2}\/0007$/)
209
+ expect(invoice).toMatchObject({
210
+ status: 'posted',
211
+ series: 'invoice',
212
+ number: result.invoiceNumber,
213
+ outstanding: '100.0000',
214
+ paidTotal: '0',
215
+ })
216
+ expect(invoice.issueDate).toBeInstanceOf(Date)
217
+ expect(invoice.buyerSnapshot).toMatchObject({ name: 'Jan Kowalski', customer_kind: 'person', invoice_language: 'pl' })
218
+ const claim = env.executed.find((e) => e.sql.includes('billing_document_sequences'))
219
+ expect(claim?.params.slice(0, 3)).toEqual([TENANT, ORG, 'invoice'])
220
+ })
221
+
222
+ it('refuses non-draft invoices with 409 not_draft', async () => {
223
223
  const env = createEnv({ invoice: makeInvoice({ status: 'posted' }) })
224
- await expect(
225
- postInvoiceCommand.execute(baseInput, env.ctx as never),
226
- ).rejects.toMatchObject({
224
+ await expect(postInvoiceCommand.execute(baseInput, env.ctx as never)).rejects.toMatchObject({
227
225
  status: 409,
228
226
  body: expect.objectContaining({ code: 'billing.invoice.not_draft' }),
229
227
  })
230
228
  })
231
229
 
232
- it('refuses test_run=true invoices with 409 + test_run code', async () => {
233
- const env = createEnv({
234
- invoice: makeInvoice({ status: 'draft', metadata: { test_run: true } }),
235
- })
236
- await expect(
237
- postInvoiceCommand.execute(baseInput, env.ctx as never),
238
- ).rejects.toMatchObject({
230
+ it('refuses test-run drafts with 409 test_run and never claims a number', async () => {
231
+ const env = createEnv({ invoice: makeInvoice({ testRun: true, number: 'TEST-1' }) })
232
+ await expect(postInvoiceCommand.execute(baseInput, env.ctx as never)).rejects.toMatchObject({
239
233
  status: 409,
240
234
  body: expect.objectContaining({ code: 'billing.invoice.test_run' }),
241
235
  })
236
+ expect(env.executed.some((e) => e.sql.includes('billing_document_sequences'))).toBe(false)
242
237
  })
243
238
 
244
- it('happy path: draft → posted, status_entry_id resolved', async () => {
245
- const invoice = makeInvoice({ status: 'draft' })
246
- const env = createEnv({ invoice, initialLines: [makeLine(invoice)] })
239
+ it('leaves the draft untouched when the number claim fails (transaction rolls back)', async () => {
240
+ const invoice = makeInvoice()
241
+ const env = createEnv({ invoice, claimThrows: new Error('db down') })
242
+ await expect(postInvoiceCommand.execute(baseInput, env.ctx as never)).rejects.toThrow('db down')
243
+ expect(invoice).toMatchObject({ status: 'draft', number: 'DRAFT-ABCDEF12', series: null })
244
+ })
245
+
246
+ it('sets due date = issue date + organization default terms, keeps the draft number, freezes the seller (Spec B)', async () => {
247
+ const invoice = makeInvoice()
248
+ const env = createEnv({ invoice, lines: [makeLine()] })
249
+ await postInvoiceCommand.execute(baseInput, env.ctx as never)
250
+ const issue = invoice.issueDate as Date
251
+ const due = invoice.dueDate as Date
252
+ expect(Math.round((due.getTime() - issue.getTime()) / 86_400_000)).toBe(14)
253
+ expect(invoice.metadata).toMatchObject({ draft_number: 'DRAFT-ABCDEF12' })
254
+ expect(invoice.sellerSnapshot).toMatchObject({
255
+ organization_id: ORG,
256
+ legal_name: 'Seller sp. z o.o.',
257
+ tax_id: 'PL1234563218',
258
+ iban: 'PL61109010140000071219812874',
259
+ address: expect.objectContaining({ city: 'Kraków' }),
260
+ })
261
+ })
262
+
263
+ it('uses the account payment terms over the organization default', async () => {
264
+ const invoice = makeInvoice()
265
+ const env = createEnv({ invoice, lines: [makeLine()], account: { paymentTermsDays: 0 } })
266
+ await postInvoiceCommand.execute(baseInput, env.ctx as never)
267
+ expect((invoice.dueDate as Date).getTime()).toBe((invoice.issueDate as Date).getTime())
268
+ })
269
+
270
+ it('refuses with 409 settings.incomplete, listing the missing fields, and claims no number', async () => {
271
+ const invoice = makeInvoice()
272
+ const env = createEnv({ invoice, settings: { legalName: ' ', iban: null } })
273
+ await expect(postInvoiceCommand.execute(baseInput, env.ctx as never)).rejects.toMatchObject({
274
+ status: 409,
275
+ body: expect.objectContaining({ code: 'billing.settings.incomplete', missing: ['legalName', 'iban'] }),
276
+ })
277
+ expect(env.executed.some((e) => e.sql.includes('billing_document_sequences'))).toBe(false)
278
+ expect(invoice).toMatchObject({ status: 'draft', number: 'DRAFT-ABCDEF12' })
279
+ })
280
+
281
+ it('freezes the PDF after the commit and before announcing the post (Spec B)', async () => {
282
+ const order: string[] = []
283
+ ;(freezeInvoicePdf as jest.Mock).mockImplementationOnce(async () => {
284
+ order.push('freeze')
285
+ return { attachmentId: 'pdf-1', bytes: null }
286
+ })
287
+ ;(emitBillingEvent as jest.Mock).mockImplementation(async (id: string) => {
288
+ order.push(id)
289
+ })
290
+ const invoice = makeInvoice()
291
+ const env = createEnv({ invoice, lines: [makeLine()] })
292
+ await postInvoiceCommand.execute(baseInput, env.ctx as never)
293
+ expect(freezeInvoicePdf).toHaveBeenCalledWith(env.em, invoice)
294
+ expect(order.slice(0, 2)).toEqual(['freeze', 'billing.invoice.posted'])
295
+ const posted = (emitBillingEvent as jest.Mock).mock.calls.find(([id]) => id === 'billing.invoice.posted')
296
+ expect(posted?.[1]).toMatchObject({ dueDate: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/) })
297
+ })
298
+
299
+ it('keeps the post when the PDF cannot be frozen', async () => {
300
+ ;(freezeInvoicePdf as jest.Mock).mockRejectedValueOnce(new Error('storage down'))
301
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => undefined)
302
+ const invoice = makeInvoice()
303
+ const env = createEnv({ invoice, lines: [makeLine()] })
247
304
  const result = await postInvoiceCommand.execute(baseInput, env.ctx as never)
248
305
  expect(result.status).toBe('posted')
249
306
  expect(invoice.status).toBe('posted')
250
- expect(result.lineCount).toBe(1)
307
+ spy.mockRestore()
251
308
  })
252
309
 
253
310
  it('throws 404 when the invoice does not exist for this scope', async () => {
254
311
  const env = createEnv({ invoice: null })
255
- await expect(
256
- postInvoiceCommand.execute(baseInput, env.ctx as never),
257
- ).rejects.toMatchObject({ status: 404 })
312
+ await expect(postInvoiceCommand.execute(baseInput, env.ctx as never)).rejects.toMatchObject({ status: 404 })
313
+ })
314
+
315
+ it('builds a buyer snapshot from the account', () => {
316
+ expect(buildBuyerSnapshot(account as never)).toEqual({
317
+ bill_account_id: ACCOUNT_ID,
318
+ name: 'Jan Kowalski',
319
+ customer_id: 'cust-1',
320
+ customer_kind: 'person',
321
+ tax_id: null,
322
+ invoice_email: 'jan@example.test',
323
+ invoice_language: 'pl',
324
+ invoice_address: account.invoiceAddress,
325
+ })
258
326
  })
259
327
  })
260
328
 
261
329
  describe('editDraftLineCommand', () => {
330
+ const input = { ...baseInput, invoiceLineId: LINE_ID }
331
+
262
332
  it('refuses non-draft invoices', async () => {
263
- const invoice = makeInvoice({ status: 'posted' })
264
- const env = createEnv({ invoice })
333
+ const env = createEnv({ invoice: makeInvoice({ status: 'posted' }), line: makeLine() })
265
334
  await expect(
266
- editDraftLineCommand.execute(
267
- { ...baseInput, invoiceLineId: LINE_ID, changes: { description: 'x' } },
268
- env.ctx as never,
269
- ),
270
- ).rejects.toMatchObject({
271
- status: 409,
272
- body: expect.objectContaining({ code: 'billing.invoice.not_draft' }),
273
- })
335
+ editDraftLineCommand.execute({ ...input, changes: { description: 'x' } }, env.ctx as never),
336
+ ).rejects.toMatchObject({ status: 409 })
274
337
  })
275
338
 
276
- it('updates description + recomputes line total from unit_price × quantity', async () => {
277
- const invoice = makeInvoice({ status: 'draft' })
278
- const line = makeLine(invoice, {
279
- quantity: '1.0000',
280
- unitPriceNet: '50.0000',
281
- totalNetAmount: '50.0000',
282
- })
283
- const env = createEnv({
284
- invoice,
285
- line,
286
- invoiceFinalLines: [line],
287
- })
288
-
289
- await editDraftLineCommand.execute(
290
- {
291
- ...baseInput,
292
- invoiceLineId: LINE_ID,
293
- changes: { quantity: 3, unitPriceNet: 60 },
294
- },
295
- env.ctx as never,
296
- )
297
-
298
- expect(line.quantity).toBe('3.0000')
299
- expect(line.unitPriceNet).toBe('60.0000')
300
- // 3 × 60 = 180
301
- expect(line.totalNetAmount).toBe('180.0000')
302
- // Invoice totals match the new line sum.
303
- expect(invoice.grandTotalNetAmount).toBe('180.0000')
339
+ it('recomputes the line from unit price × quantity and the invoice totals', async () => {
340
+ const invoice = makeInvoice()
341
+ const line = makeLine()
342
+ const env = createEnv({ invoice, line })
343
+ await editDraftLineCommand.execute({ ...input, changes: { quantity: 3, description: 'MRC ×3' } }, env.ctx as never)
344
+ expect(line).toMatchObject({ description: 'MRC ×3', quantity: '3.0000', netAmount: '300.0000', grossAmount: '300.0000' })
345
+ expect(invoice).toMatchObject({ subtotalNet: '300.0000', totalGross: '300.0000', outstanding: '300.0000' })
304
346
  })
305
347
 
306
- it('honours explicit totalNetAmount override (operator manual fix)', async () => {
307
- const invoice = makeInvoice({ status: 'draft' })
308
- const line = makeLine(invoice, { quantity: '1.0000', unitPriceNet: '50.0000' })
309
- const env = createEnv({ invoice, line, invoiceFinalLines: [line] })
310
- await editDraftLineCommand.execute(
311
- {
312
- ...baseInput,
313
- invoiceLineId: LINE_ID,
314
- changes: { totalNetAmount: 999.99 },
315
- },
348
+ it('honours an explicit totalNetAmount override and writes an audit row', async () => {
349
+ const line = makeLine()
350
+ const env = createEnv({ invoice: makeInvoice(), line })
351
+ const result = await editDraftLineCommand.execute(
352
+ { ...input, changes: { totalNetAmount: 42.5 } },
316
353
  env.ctx as never,
317
354
  )
318
- expect(line.totalNetAmount).toBe('999.9900')
355
+ expect(line.netAmount).toBe('42.5000')
356
+ const audit = env.persisted.find((p) => (p as Row).action === 'line_edited') as Row
357
+ expect(audit).toMatchObject({ id: result.auditId, invoiceId: INVOICE_ID, invoiceLineId: LINE_ID })
358
+ expect((audit.beforeJson as Row).netAmount).toBe('100.0000')
359
+ expect((audit.afterJson as Row).netAmount).toBe('42.5000')
319
360
  })
320
361
 
321
- it('persists a BillingDraftInvoiceEdit audit row with before+after snapshots', async () => {
322
- const invoice = makeInvoice({ status: 'draft' })
323
- const line = makeLine(invoice, {
324
- description: 'Old',
325
- totalNetAmount: '50.0000',
326
- })
327
- const env = createEnv({ invoice, line, invoiceFinalLines: [line] })
328
-
329
- await editDraftLineCommand.execute(
330
- {
331
- ...baseInput,
332
- invoiceLineId: LINE_ID,
333
- changes: { description: 'New' },
334
- },
335
- env.ctx as never,
336
- )
337
-
338
- const audit = env.persistedEntities.find(
339
- (e) => (e as { action?: string }).action === 'line_edited',
340
- ) as Record<string, unknown> | undefined
341
- expect(audit).toBeDefined()
342
- expect((audit?.beforeJson as { description: string }).description).toBe('Old')
343
- expect((audit?.afterJson as { description: string }).description).toBe('New')
362
+ it('changes the VAT rate and recomputes tax and gross (Spec B)', async () => {
363
+ const invoice = makeInvoice()
364
+ const line = makeLine()
365
+ const env = createEnv({ invoice, line })
366
+ await editDraftLineCommand.execute({ ...input, changes: { taxRate: 23 } }, env.ctx as never)
367
+ expect(line).toMatchObject({ taxRate: '23', netAmount: '100.0000', taxAmount: '23.0000', grossAmount: '123.0000' })
368
+ expect(invoice).toMatchObject({ taxTotal: '23.0000', totalGross: '123.0000' })
344
369
  })
345
370
 
346
371
  it('returns 404 when the line is not on the invoice', async () => {
347
- const invoice = makeInvoice({ status: 'draft' })
348
- const env = createEnv({ invoice, line: null })
372
+ const env = createEnv({ invoice: makeInvoice(), line: null })
349
373
  await expect(
350
- editDraftLineCommand.execute(
351
- { ...baseInput, invoiceLineId: LINE_ID, changes: { description: 'x' } },
352
- env.ctx as never,
353
- ),
374
+ editDraftLineCommand.execute({ ...input, changes: { description: 'x' } }, env.ctx as never),
354
375
  ).rejects.toMatchObject({ status: 404 })
355
376
  })
356
377
  })
357
378
 
358
379
  describe('addDraftLineCommand', () => {
359
- it('appends a line with the next lineNumber and writes a line_added audit', async () => {
360
- const invoice = makeInvoice({ status: 'draft' })
361
- const existing = [makeLine(invoice), makeLine(invoice, { id: 'l2' })]
362
- const env = createEnv({
363
- invoice,
364
- initialLines: existing,
365
- invoiceFinalLines: [...existing],
380
+ it('appends a manual line after the highest line number and audits it', async () => {
381
+ const env = createEnv({ invoice: makeInvoice(), lines: [makeLine({ lineNumber: 4 })] })
382
+ const result = await addDraftLineCommand.execute(
383
+ { ...baseInput, description: 'Setup fee', quantity: 2, unitPriceNet: 25 },
384
+ env.ctx as never,
385
+ )
386
+ const line = env.persisted.find((p) => (p as Row).id === result.invoiceLineId) as Row
387
+ // Spec B: no rate given → the organization default (23 %).
388
+ expect(line).toMatchObject({
389
+ lineNumber: 5,
390
+ billingType: 'manual',
391
+ taxRate: '23',
392
+ netAmount: '50.0000',
393
+ taxAmount: '11.5000',
394
+ grossAmount: '61.5000',
366
395
  })
396
+ expect(env.persisted.some((p) => (p as Row).action === 'line_added')).toBe(true)
397
+ })
367
398
 
399
+ it('uses an explicit VAT rate when given (Spec B)', async () => {
400
+ const env = createEnv({ invoice: makeInvoice(), lines: [] })
368
401
  const result = await addDraftLineCommand.execute(
369
- {
370
- ...baseInput,
371
- description: 'Manual adjustment',
372
- quantity: 2,
373
- unitPriceNet: 25,
374
- },
402
+ { ...baseInput, description: 'Book', quantity: 1, unitPriceNet: 40, taxRate: 5 },
375
403
  env.ctx as never,
376
404
  )
377
- expect(result.invoiceLineId).toBe('new-line-3')
378
- const audit = env.persistedEntities.find(
379
- (e) => (e as { action?: string }).action === 'line_added',
380
- ) as Record<string, unknown> | undefined
381
- expect(audit).toBeDefined()
382
- expect(audit?.beforeJson).toBeNull()
383
- expect(audit?.afterJson).toBeDefined()
405
+ const line = env.persisted.find((p) => (p as Row).id === result.invoiceLineId) as Row
406
+ expect(line).toMatchObject({ taxRate: '5', taxAmount: '2.0000', grossAmount: '42.0000' })
384
407
  })
385
408
 
386
409
  it('refuses non-draft invoices', async () => {
387
410
  const env = createEnv({ invoice: makeInvoice({ status: 'paid' }) })
388
411
  await expect(
389
- addDraftLineCommand.execute(
390
- {
391
- ...baseInput,
392
- description: 'x',
393
- quantity: 1,
394
- unitPriceNet: 1,
395
- },
396
- env.ctx as never,
397
- ),
412
+ addDraftLineCommand.execute({ ...baseInput, description: 'x', quantity: 1, unitPriceNet: 1 }, env.ctx as never),
398
413
  ).rejects.toMatchObject({ status: 409 })
399
414
  })
400
415
  })
401
416
 
402
417
  describe('removeDraftLineCommand', () => {
403
- it('removes the line and writes a line_removed audit with before snapshot', async () => {
404
- const invoice = makeInvoice({ status: 'draft' })
405
- const line = makeLine(invoice, { description: 'Going away' })
406
- const env = createEnv({ invoice, line, invoiceFinalLines: [] })
407
-
408
- await removeDraftLineCommand.execute(
409
- { ...baseInput, invoiceLineId: LINE_ID },
410
- env.ctx as never,
411
- )
412
-
413
- expect(env.removedEntities).toContain(line)
414
- const audit = env.persistedEntities.find(
415
- (e) => (e as { action?: string }).action === 'line_removed',
416
- ) as Record<string, unknown> | undefined
417
- expect(audit).toBeDefined()
418
- expect((audit?.beforeJson as { description: string }).description).toBe('Going away')
419
- expect(audit?.afterJson).toBeNull()
420
- expect(audit?.invoiceLineId).toBeNull()
421
- // Invoice total reset to zero (no lines left).
422
- expect(invoice.grandTotalNetAmount).toBe('0.0000')
418
+ it('removes the line and writes a line_removed audit with the before snapshot', async () => {
419
+ const line = makeLine()
420
+ const env = createEnv({ invoice: makeInvoice(), line })
421
+ await removeDraftLineCommand.execute({ ...baseInput, invoiceLineId: LINE_ID }, env.ctx as never)
422
+ expect(env.removed).toContain(line)
423
+ const audit = env.persisted.find((p) => (p as Row).action === 'line_removed') as Row
424
+ expect(audit.afterJson).toBeNull()
425
+ expect((audit.beforeJson as Row).id).toBe(LINE_ID)
423
426
  })
424
427
  })
425
428
 
426
429
  describe('wipeTestInvoicesCommand', () => {
427
430
  it('returns 0 when no test invoices match', async () => {
428
- const env = createEnv({ testInvoiceRows: [] })
429
- const result = await wipeTestInvoicesCommand.execute(
430
- { tenantId: TENANT, organizationId: ORG },
431
- env.ctx as never,
432
- )
433
- expect(result.invoicesRemoved).toBe(0)
431
+ const env = createEnv({ testInvoiceIds: [] })
432
+ expect(await wipeTestInvoicesCommand.execute({ tenantId: TENANT, organizationId: ORG }, env.ctx as never)).toEqual({
433
+ invoicesRemoved: 0,
434
+ })
435
+ })
436
+
437
+ it('deletes lines, then invoices, from the billing tables only', async () => {
438
+ const env = createEnv({ testInvoiceIds: ['a', 'b'] })
439
+ const res = await wipeTestInvoicesCommand.execute({ tenantId: TENANT, organizationId: ORG }, env.ctx as never)
440
+ expect(res.invoicesRemoved).toBe(2)
441
+ const deletes = env.executed.filter((e) => e.sql.startsWith('DELETE'))
442
+ expect(deletes.map((d) => d.sql.split(' ')[2])).toEqual([
443
+ 'billing_draft_invoice_edits',
444
+ 'billing_invoice_lines',
445
+ 'billing_invoices',
446
+ ])
447
+ expect(env.executed.some((e) => e.sql.includes('sales_invoices'))).toBe(false)
434
448
  })
435
449
 
436
- it('deletes lines then invoices when matches exist', async () => {
450
+ it('scopes by billRunId when provided', async () => {
451
+ const runId = '66666666-6666-4666-8666-666666666666'
452
+ const env = createEnv({ testInvoiceIds: [] })
453
+ await wipeTestInvoicesCommand.execute({ tenantId: TENANT, organizationId: ORG, billRunId: runId }, env.ctx as never)
454
+ const select = env.executed[0]
455
+ expect(select.sql).toContain('bill_run_id = ?')
456
+ expect(select.params).toContain(runId)
457
+ })
458
+ })
459
+
460
+ describe('postInvoiceCommand — memos (Spec D)', () => {
461
+ beforeEach(() => jest.clearAllMocks())
462
+ const ORIGINAL_ID = '77777777-7777-4777-8777-777777777777'
463
+ const original = (overrides: Row = {}): Row => ({
464
+ id: ORIGINAL_ID,
465
+ tenantId: TENANT,
466
+ organizationId: ORG,
467
+ billAccountId: ACCOUNT_ID,
468
+ documentType: 'invoice',
469
+ status: 'posted',
470
+ number: 'INV/2026/11/0001',
471
+ totalGross: '123.0000',
472
+ outstanding: '123.0000',
473
+ currencyCode: 'EUR',
474
+ ...overrides,
475
+ })
476
+ const creditMemo = (overrides: Row = {}) =>
477
+ makeInvoice({ documentType: 'credit_memo', correctsInvoiceId: ORIGINAL_ID, totalGross: '23.0000', outstanding: '23.0000', ...overrides })
478
+
479
+ it('posts a credit memo on the credit series, without due date, allocating it to the original', async () => {
480
+ const memo = creditMemo()
481
+ const env = createEnv({ invoice: memo, original: original(), lines: [makeLine()] })
482
+ await postInvoiceCommand.execute(baseInput, env.ctx as never)
483
+ const claim = env.executed.find((e) => e.sql.includes('billing_document_sequences'))
484
+ expect(claim?.params.slice(0, 3)).toEqual([TENANT, ORG, 'credit_memo'])
485
+ expect(memo.dueDate).toBeNull()
486
+ expect(env.executed.some((e) => e.sql.includes('pg_advisory_xact_lock'))).toBe(true)
487
+ expect(allocate).toHaveBeenCalledWith(env.em, { tenantId: TENANT, organizationId: ORG }, {
488
+ source: { type: 'credit_memo', id: INVOICE_ID },
489
+ invoiceId: ORIGINAL_ID,
490
+ amountCents: 2300,
491
+ by: 'credit',
492
+ })
493
+ expect(applyAccountCredit).toHaveBeenCalledWith(env.em, { tenantId: TENANT, organizationId: ORG }, { id: ACCOUNT_ID, currencyCode: 'EUR' }, {
494
+ onlySources: [{ type: 'credit_memo', id: INVOICE_ID }],
495
+ })
496
+ const posted = (emitBillingEvent as jest.Mock).mock.calls.map(([id]) => id)
497
+ expect(posted).toContain('billing.credit_memo.posted')
498
+ expect(posted).not.toContain('billing.invoice.posted')
499
+ })
500
+
501
+ it('credits only the outstanding part of the original; a paid original gets nothing (all becomes account credit)', async () => {
502
+ const env = createEnv({ invoice: creditMemo(), original: original({ outstanding: '10.0000', status: 'partially_paid' }), lines: [makeLine()] })
503
+ await postInvoiceCommand.execute(baseInput, env.ctx as never)
504
+ expect((allocate as jest.Mock).mock.calls[0][2]).toMatchObject({ amountCents: 1000 })
505
+ jest.clearAllMocks()
506
+ const paid = createEnv({ invoice: creditMemo(), original: original({ outstanding: '0.0000', status: 'paid' }), lines: [makeLine()] })
507
+ await postInvoiceCommand.execute(baseInput, paid.ctx as never)
508
+ expect(allocate).not.toHaveBeenCalled()
509
+ expect(applyAccountCredit).toHaveBeenCalled()
510
+ })
511
+
512
+ it('refuses over-crediting (cumulative guard incl. debit memos) and claims no number', async () => {
513
+ const memo = creditMemo({ totalGross: '50.0000' })
437
514
  const env = createEnv({
438
- testInvoiceRows: [{ id: 'test-inv-1' }, { id: 'test-inv-2' }],
515
+ invoice: memo,
516
+ original: original({ totalGross: '100.0000' }),
517
+ corrections: [{ document_type: 'credit_memo', gross: '80.0000' }, { document_type: 'debit_memo', gross: '20.0000' }],
439
518
  })
440
- const result = await wipeTestInvoicesCommand.execute(
441
- { tenantId: TENANT, organizationId: ORG },
442
- env.ctx as never,
443
- )
444
- expect(result.invoicesRemoved).toBe(2)
445
- // 1 SELECT + 1 DELETE lines + 1 DELETE invoices
446
- expect(env.em.execute).toHaveBeenCalledTimes(3)
519
+ await expect(postInvoiceCommand.execute(baseInput, env.ctx as never)).rejects.toMatchObject({
520
+ status: 409,
521
+ body: expect.objectContaining({ code: 'billing.memo.exceeds_invoice' }),
522
+ })
523
+ expect(env.executed.some((e) => e.sql.includes('billing_document_sequences'))).toBe(false)
524
+ // 80 + 40 ≤ 100 + 20 → fits
525
+ const ok = createEnv({
526
+ invoice: creditMemo({ totalGross: '40.0000' }),
527
+ original: original({ totalGross: '100.0000' }),
528
+ corrections: [{ document_type: 'credit_memo', gross: '80.0000' }, { document_type: 'debit_memo', gross: '20.0000' }],
529
+ lines: [makeLine()],
530
+ })
531
+ await expect(postInvoiceCommand.execute(baseInput, ok.ctx as never)).resolves.toMatchObject({ status: 'posted' })
447
532
  })
448
533
 
449
- it('scopes by billRunId when provided', async () => {
450
- const env = createEnv({ testInvoiceRows: [{ id: 'test-1' }] })
451
- await wipeTestInvoicesCommand.execute(
452
- { tenantId: TENANT, organizationId: ORG, billRunId: INVOICE_ID },
453
- env.ctx as never,
454
- )
455
- const selectCall = env.em.execute.mock.calls[0]
456
- expect(selectCall[0]).toContain("metadata->>'bill_run_id' = ?")
457
- expect(selectCall[1]).toContain(INVOICE_ID)
534
+ it('posts a debit memo like an invoice: invoice series, due date, outstanding, its own event', async () => {
535
+ const memo = makeInvoice({ documentType: 'debit_memo', correctsInvoiceId: ORIGINAL_ID })
536
+ const env = createEnv({ invoice: memo, original: original(), lines: [makeLine()] })
537
+ await postInvoiceCommand.execute(baseInput, env.ctx as never)
538
+ const claim = env.executed.find((e) => e.sql.includes('billing_document_sequences'))
539
+ expect(claim?.params.slice(0, 3)).toEqual([TENANT, ORG, 'invoice'])
540
+ expect(memo.dueDate).toBeInstanceOf(Date)
541
+ expect(memo.outstanding).toBe('100.0000')
542
+ expect(allocate).not.toHaveBeenCalled()
543
+ expect((emitBillingEvent as jest.Mock).mock.calls.map(([id]) => id)).toContain('billing.debit_memo.posted')
458
544
  })
459
545
  })
460
546
 
461
- // Suppress unused-import warning — BillingDraftInvoiceEdit is needed by the
462
- // mock's discriminator path but not directly referenced by name in
463
- // individual assertions.
464
- void BillingDraftInvoiceEdit