@dainamite/billing 0.19.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 (614) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +14 -0
  3. package/dist/modules/billing/acl.js +13 -1
  4. package/dist/modules/billing/acl.js.map +2 -2
  5. package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
  6. package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
  7. package/dist/modules/billing/api/accounts/route.js +46 -4
  8. package/dist/modules/billing/api/accounts/route.js.map +2 -2
  9. package/dist/modules/billing/api/export/account/[id]/route.js +16 -12
  10. package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
  11. package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
  12. package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
  13. package/dist/modules/billing/api/invoices/[id]/pdf/route.js +69 -0
  14. package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
  15. package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
  16. package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
  17. package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
  18. package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
  19. package/dist/modules/billing/api/invoices/add-line/route.js +3 -3
  20. package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
  21. package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
  22. package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
  23. package/dist/modules/billing/api/invoices/edit-line/route.js +3 -3
  24. package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
  25. package/dist/modules/billing/api/invoices/post/route.js +2 -2
  26. package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
  27. package/dist/modules/billing/api/invoices/remove-line/route.js +3 -3
  28. package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
  29. package/dist/modules/billing/api/invoices/route.js +45 -46
  30. package/dist/modules/billing/api/invoices/route.js.map +2 -2
  31. package/dist/modules/billing/api/items/bulk/route.js +1 -1
  32. package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
  33. package/dist/modules/billing/api/items/change-price/route.js +44 -0
  34. package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
  35. package/dist/modules/billing/api/items/route.js +19 -7
  36. package/dist/modules/billing/api/items/route.js.map +2 -2
  37. package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
  38. package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
  39. package/dist/modules/billing/api/payment-imports/route.js +57 -0
  40. package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
  41. package/dist/modules/billing/api/payments/[id]/route.js +60 -0
  42. package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
  43. package/dist/modules/billing/api/payments/allocate/route.js +35 -0
  44. package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
  45. package/dist/modules/billing/api/payments/assign/route.js +36 -0
  46. package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
  47. package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
  48. package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
  49. package/dist/modules/billing/api/payments/ignore/route.js +35 -0
  50. package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
  51. package/dist/modules/billing/api/payments/import/route.js +63 -0
  52. package/dist/modules/billing/api/payments/import/route.js.map +7 -0
  53. package/dist/modules/billing/api/payments/restore/route.js +35 -0
  54. package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
  55. package/dist/modules/billing/api/payments/route.js +114 -0
  56. package/dist/modules/billing/api/payments/route.js.map +7 -0
  57. package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
  58. package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
  59. package/dist/modules/billing/api/payments/unassign/route.js +35 -0
  60. package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
  61. package/dist/modules/billing/api/run-outcomes/route.js +44 -7
  62. package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
  63. package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
  64. package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
  65. package/dist/modules/billing/api/runs/[id]/route.js +53 -0
  66. package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
  67. package/dist/modules/billing/api/runs/retry-failed/route.js +3 -3
  68. package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
  69. package/dist/modules/billing/api/runs/route.js +11 -5
  70. package/dist/modules/billing/api/runs/route.js.map +2 -2
  71. package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
  72. package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
  73. package/dist/modules/billing/api/settings/route.js +92 -0
  74. package/dist/modules/billing/api/settings/route.js.map +7 -0
  75. package/dist/modules/billing/api/statements/route.js +3 -0
  76. package/dist/modules/billing/api/statements/route.js.map +2 -2
  77. package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
  78. package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
  79. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
  80. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
  81. package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
  82. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
  83. package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
  84. package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
  85. package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
  86. package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
  87. package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
  88. package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
  89. package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
  90. package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
  91. package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
  92. package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
  93. package/dist/modules/billing/backend/billing/items/page.js +8 -2
  94. package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
  95. package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
  96. package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
  97. package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
  98. package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
  99. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
  100. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
  101. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
  102. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
  103. package/dist/modules/billing/backend/billing/payments/page.js +58 -0
  104. package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
  105. package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
  106. package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
  107. package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
  108. package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
  109. package/dist/modules/billing/backend/billing/runs/page.js +58 -7
  110. package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
  111. package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
  112. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
  113. package/dist/modules/billing/backend/billing/statements/page.js +1 -1
  114. package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
  115. package/dist/modules/billing/backend/billing/topups/page.js +1 -1
  116. package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
  117. package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
  118. package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
  119. package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
  120. package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
  121. package/dist/modules/billing/backend/config/billing/page.js +472 -0
  122. package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
  123. package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
  124. package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
  125. package/dist/modules/billing/ce.js.map +2 -2
  126. package/dist/modules/billing/cli.js +22 -1
  127. package/dist/modules/billing/cli.js.map +2 -2
  128. package/dist/modules/billing/commands/accounts.js +6 -0
  129. package/dist/modules/billing/commands/accounts.js.map +2 -2
  130. package/dist/modules/billing/commands/index.js +3 -0
  131. package/dist/modules/billing/commands/index.js.map +2 -2
  132. package/dist/modules/billing/commands/invoices.js +272 -170
  133. package/dist/modules/billing/commands/invoices.js.map +2 -2
  134. package/dist/modules/billing/commands/items.js +108 -0
  135. package/dist/modules/billing/commands/items.js.map +2 -2
  136. package/dist/modules/billing/commands/memos.js +179 -0
  137. package/dist/modules/billing/commands/memos.js.map +7 -0
  138. package/dist/modules/billing/commands/payments.js +379 -0
  139. package/dist/modules/billing/commands/payments.js.map +7 -0
  140. package/dist/modules/billing/commands/runs.js +154 -7
  141. package/dist/modules/billing/commands/runs.js.map +2 -2
  142. package/dist/modules/billing/commands/settings.js +59 -0
  143. package/dist/modules/billing/commands/settings.js.map +7 -0
  144. package/dist/modules/billing/commands/topups.js +9 -2
  145. package/dist/modules/billing/commands/topups.js.map +2 -2
  146. package/dist/modules/billing/commands/transactions.js +2 -2
  147. package/dist/modules/billing/commands/transactions.js.map +2 -2
  148. package/dist/modules/billing/components/AccountForm.js +149 -42
  149. package/dist/modules/billing/components/AccountForm.js.map +2 -2
  150. package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
  151. package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
  152. package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
  153. package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
  154. package/dist/modules/billing/components/AccountPicker.js +56 -0
  155. package/dist/modules/billing/components/AccountPicker.js.map +7 -0
  156. package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
  157. package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
  158. package/dist/modules/billing/components/BillingDataTable.js +21 -0
  159. package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
  160. package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
  161. package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
  162. package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
  163. package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
  164. package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
  165. package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
  166. package/dist/modules/billing/components/CustomerLabel.js +33 -0
  167. package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
  168. package/dist/modules/billing/components/InvoicesTable.js +270 -0
  169. package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
  170. package/dist/modules/billing/components/ItemForm.js +176 -44
  171. package/dist/modules/billing/components/ItemForm.js.map +2 -2
  172. package/dist/modules/billing/components/ItemPriceField.js +46 -0
  173. package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
  174. package/dist/modules/billing/components/LineFormDialog.js +25 -2
  175. package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
  176. package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
  177. package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
  178. package/dist/modules/billing/components/MoneyInput.js +14 -0
  179. package/dist/modules/billing/components/MoneyInput.js.map +7 -0
  180. package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
  181. package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
  182. package/dist/modules/billing/components/PaymentsTable.js +171 -0
  183. package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
  184. package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
  185. package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
  186. package/dist/modules/billing/components/PrepaidPanel.js +14 -4
  187. package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
  188. package/dist/modules/billing/components/RunCyclesSection.js +209 -0
  189. package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
  190. package/dist/modules/billing/components/TestRunActions.js +127 -0
  191. package/dist/modules/billing/components/TestRunActions.js.map +7 -0
  192. package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
  193. package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
  194. package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
  195. package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
  196. package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
  197. package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
  198. package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
  199. package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
  200. package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
  201. package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
  202. package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
  203. package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
  204. package/dist/modules/billing/components/account/AccountView.js +73 -0
  205. package/dist/modules/billing/components/account/AccountView.js.map +7 -0
  206. package/dist/modules/billing/components/account/accountRow.js +39 -0
  207. package/dist/modules/billing/components/account/accountRow.js.map +7 -0
  208. package/dist/modules/billing/components/customerNames.js +42 -0
  209. package/dist/modules/billing/components/customerNames.js.map +7 -0
  210. package/dist/modules/billing/data/entities.js +615 -59
  211. package/dist/modules/billing/data/entities.js.map +2 -2
  212. package/dist/modules/billing/data/entityIds.js +10 -1
  213. package/dist/modules/billing/data/entityIds.js.map +2 -2
  214. package/dist/modules/billing/data/validators.js +269 -15
  215. package/dist/modules/billing/data/validators.js.map +2 -2
  216. package/dist/modules/billing/events.js +70 -0
  217. package/dist/modules/billing/events.js.map +2 -2
  218. package/dist/modules/billing/i18n/en.json +457 -21
  219. package/dist/modules/billing/i18n/pl.json +457 -21
  220. package/dist/modules/billing/index.js +6 -2
  221. package/dist/modules/billing/index.js.map +2 -2
  222. package/dist/modules/billing/lib/accountFinancials.js +58 -0
  223. package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
  224. package/dist/modules/billing/lib/accountLock.js +14 -0
  225. package/dist/modules/billing/lib/accountLock.js.map +7 -0
  226. package/dist/modules/billing/lib/accountStatus.js +9 -7
  227. package/dist/modules/billing/lib/accountStatus.js.map +2 -2
  228. package/dist/modules/billing/lib/accountSummary.js +106 -0
  229. package/dist/modules/billing/lib/accountSummary.js.map +7 -0
  230. package/dist/modules/billing/lib/allocation.js +356 -0
  231. package/dist/modules/billing/lib/allocation.js.map +7 -0
  232. package/dist/modules/billing/lib/allocationEvents.js +22 -0
  233. package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
  234. package/dist/modules/billing/lib/billPeriod.js +15 -1
  235. package/dist/modules/billing/lib/billPeriod.js.map +2 -2
  236. package/dist/modules/billing/lib/billRunEngine.js +241 -106
  237. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  238. package/dist/modules/billing/lib/billRunLabel.js.map +1 -1
  239. package/dist/modules/billing/lib/billingSettings.js +116 -0
  240. package/dist/modules/billing/lib/billingSettings.js.map +7 -0
  241. package/dist/modules/billing/lib/commandRoute.js +81 -0
  242. package/dist/modules/billing/lib/commandRoute.js.map +7 -0
  243. package/dist/modules/billing/lib/customerLookup.js +97 -0
  244. package/dist/modules/billing/lib/customerLookup.js.map +7 -0
  245. package/dist/modules/billing/lib/dateOnly.js +13 -0
  246. package/dist/modules/billing/lib/dateOnly.js.map +7 -0
  247. package/dist/modules/billing/lib/documentParties.js +16 -0
  248. package/dist/modules/billing/lib/documentParties.js.map +7 -0
  249. package/dist/modules/billing/lib/documentTypes.js +27 -0
  250. package/dist/modules/billing/lib/documentTypes.js.map +7 -0
  251. package/dist/modules/billing/lib/dueDate.js +18 -0
  252. package/dist/modules/billing/lib/dueDate.js.map +7 -0
  253. package/dist/modules/billing/lib/iban.js +29 -0
  254. package/dist/modules/billing/lib/iban.js.map +7 -0
  255. package/dist/modules/billing/lib/invoiceDelivery.js +131 -0
  256. package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
  257. package/dist/modules/billing/lib/invoiceLanguages.js +16 -0
  258. package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
  259. package/dist/modules/billing/lib/invoiceWriter.js +107 -99
  260. package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
  261. package/dist/modules/billing/lib/itemBillingState.js +27 -0
  262. package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
  263. package/dist/modules/billing/lib/itemPriceChange.js +18 -0
  264. package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
  265. package/dist/modules/billing/lib/itemSelector.js.map +2 -2
  266. package/dist/modules/billing/lib/lineAmounts.js +32 -0
  267. package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
  268. package/dist/modules/billing/lib/memoGuards.js +25 -0
  269. package/dist/modules/billing/lib/memoGuards.js.map +7 -0
  270. package/dist/modules/billing/lib/navigation.js +48 -0
  271. package/dist/modules/billing/lib/navigation.js.map +7 -0
  272. package/dist/modules/billing/lib/numbering.js +120 -0
  273. package/dist/modules/billing/lib/numbering.js.map +7 -0
  274. package/dist/modules/billing/lib/overdue.js +36 -0
  275. package/dist/modules/billing/lib/overdue.js.map +7 -0
  276. package/dist/modules/billing/lib/paymentCsv.js +125 -0
  277. package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
  278. package/dist/modules/billing/lib/paymentGateway.js +15 -0
  279. package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
  280. package/dist/modules/billing/lib/paymentImport.js +164 -0
  281. package/dist/modules/billing/lib/paymentImport.js.map +7 -0
  282. package/dist/modules/billing/lib/paymentMatcher.js +36 -0
  283. package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
  284. package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
  285. package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
  286. package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
  287. package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
  288. package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
  289. package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
  290. package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
  291. package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
  292. package/dist/modules/billing/lib/pdf/invoiceTemplate.js +274 -0
  293. package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
  294. package/dist/modules/billing/lib/prepaidConfig.js +17 -25
  295. package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
  296. package/dist/modules/billing/lib/prepaidConsume.js +26 -7
  297. package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
  298. package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
  299. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
  300. package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
  301. package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
  302. package/dist/modules/billing/lib/seeds/demo.js +51 -3
  303. package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
  304. package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
  305. package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
  306. package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
  307. package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
  308. package/dist/modules/billing/lib/servicePeriods.js +121 -0
  309. package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
  310. package/dist/modules/billing/lib/tenantLock.js.map +1 -1
  311. package/dist/modules/billing/lib/testRunCleanup.js +24 -0
  312. package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
  313. package/dist/modules/billing/lib/topupCapture.js +2 -2
  314. package/dist/modules/billing/lib/topupCapture.js.map +2 -2
  315. package/dist/modules/billing/lib/topupReceipt.js +58 -79
  316. package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
  317. package/dist/modules/billing/lib/usageRater.js.map +1 -1
  318. package/dist/modules/billing/lib/usageRunner.js +14 -6
  319. package/dist/modules/billing/lib/usageRunner.js.map +2 -2
  320. package/dist/modules/billing/lib/vat.js +39 -0
  321. package/dist/modules/billing/lib/vat.js.map +7 -0
  322. package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
  323. package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
  324. package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
  325. package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
  326. package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
  327. package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
  328. package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
  329. package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
  330. package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
  331. package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
  332. package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
  333. package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
  334. package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
  335. package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
  336. package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
  337. package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
  338. package/dist/modules/billing/setup.js +7 -184
  339. package/dist/modules/billing/setup.js.map +2 -2
  340. package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
  341. package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
  342. package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
  343. package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
  344. package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
  345. package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
  346. package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
  347. package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
  348. package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
  349. package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
  350. package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
  351. package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
  352. package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
  353. package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
  354. package/dist/modules/billing/workers/reap-stale-bill-runs.js +2 -2
  355. package/dist/modules/billing/workers/reap-stale-bill-runs.js.map +2 -2
  356. package/generated/entities/billing_account/index.ts +26 -0
  357. package/generated/entities/billing_account_balance/index.ts +13 -0
  358. package/generated/entities/billing_account_transaction/index.ts +21 -0
  359. package/generated/entities/billing_account_usage/index.ts +18 -0
  360. package/generated/entities/billing_allocation/index.ts +18 -0
  361. package/generated/entities/billing_document_sequence/index.ts +11 -0
  362. package/generated/entities/billing_draft_invoice_edit/index.ts +14 -0
  363. package/generated/entities/billing_invoice/index.ts +38 -0
  364. package/generated/entities/billing_invoice_line/index.ts +24 -0
  365. package/generated/entities/billing_item/index.ts +26 -0
  366. package/generated/entities/billing_payment/index.ts +25 -0
  367. package/generated/entities/billing_payment_import/index.ts +14 -0
  368. package/generated/entities/billing_run/index.ts +20 -0
  369. package/generated/entities/billing_run_outcome/index.ts +18 -0
  370. package/generated/entities/billing_settings/index.ts +26 -0
  371. package/generated/entities/billing_statement/index.ts +22 -0
  372. package/generated/entities/billing_topup/index.ts +20 -0
  373. package/generated/entities.ids.generated.ts +35 -0
  374. package/package.json +15 -10
  375. package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +170 -0
  376. package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
  377. package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
  378. package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
  379. package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
  380. package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
  381. package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
  382. package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
  383. package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
  384. package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
  385. package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
  386. package/src/modules/billing/__tests__/commands/invoices.test.ts +437 -355
  387. package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
  388. package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
  389. package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
  390. package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
  391. package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
  392. package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
  393. package/src/modules/billing/__tests__/entityRegistry.test.ts +326 -0
  394. package/src/modules/billing/__tests__/events.test.ts +142 -0
  395. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
  396. package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
  397. package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
  398. package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
  399. package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +231 -20
  400. package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
  401. package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
  402. package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
  403. package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
  404. package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +189 -0
  405. package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
  406. package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
  407. package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
  408. package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
  409. package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
  410. package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
  411. package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
  412. package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
  413. package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
  414. package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
  415. package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +199 -0
  416. package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
  417. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
  418. package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
  419. package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
  420. package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
  421. package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
  422. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
  423. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
  424. package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
  425. package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
  426. package/src/modules/billing/__tests__/setup.test.ts +13 -4
  427. package/src/modules/billing/__tests__/validators.test.ts +193 -3
  428. package/src/modules/billing/acl.ts +12 -0
  429. package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
  430. package/src/modules/billing/api/accounts/route.ts +58 -4
  431. package/src/modules/billing/api/export/account/[id]/route.ts +20 -18
  432. package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
  433. package/src/modules/billing/api/invoices/[id]/pdf/route.ts +79 -0
  434. package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
  435. package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
  436. package/src/modules/billing/api/invoices/add-line/route.ts +3 -3
  437. package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
  438. package/src/modules/billing/api/invoices/edit-line/route.ts +4 -4
  439. package/src/modules/billing/api/invoices/post/route.ts +7 -6
  440. package/src/modules/billing/api/invoices/remove-line/route.ts +3 -3
  441. package/src/modules/billing/api/invoices/route.ts +81 -86
  442. package/src/modules/billing/api/items/bulk/route.ts +2 -1
  443. package/src/modules/billing/api/items/change-price/route.ts +54 -0
  444. package/src/modules/billing/api/items/route.ts +23 -7
  445. package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
  446. package/src/modules/billing/api/payment-imports/route.ts +57 -0
  447. package/src/modules/billing/api/payments/[id]/route.ts +63 -0
  448. package/src/modules/billing/api/payments/allocate/route.ts +35 -0
  449. package/src/modules/billing/api/payments/assign/route.ts +36 -0
  450. package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
  451. package/src/modules/billing/api/payments/ignore/route.ts +35 -0
  452. package/src/modules/billing/api/payments/import/route.ts +72 -0
  453. package/src/modules/billing/api/payments/restore/route.ts +35 -0
  454. package/src/modules/billing/api/payments/route.ts +147 -0
  455. package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
  456. package/src/modules/billing/api/payments/unassign/route.ts +35 -0
  457. package/src/modules/billing/api/run-outcomes/route.ts +50 -7
  458. package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
  459. package/src/modules/billing/api/runs/[id]/route.ts +59 -0
  460. package/src/modules/billing/api/runs/retry-failed/route.ts +4 -3
  461. package/src/modules/billing/api/runs/route.ts +17 -9
  462. package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
  463. package/src/modules/billing/api/settings/route.ts +116 -0
  464. package/src/modules/billing/api/statements/route.ts +5 -0
  465. package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
  466. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
  467. package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
  468. package/src/modules/billing/backend/billing/accounts/page.tsx +71 -5
  469. package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +445 -96
  470. package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
  471. package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
  472. package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
  473. package/src/modules/billing/backend/billing/items/page.tsx +19 -3
  474. package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
  475. package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
  476. package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
  477. package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
  478. package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
  479. package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
  480. package/src/modules/billing/backend/billing/runs/[id]/page.tsx +50 -19
  481. package/src/modules/billing/backend/billing/runs/page.tsx +60 -9
  482. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +87 -6
  483. package/src/modules/billing/backend/billing/statements/page.tsx +2 -2
  484. package/src/modules/billing/backend/billing/topups/page.tsx +2 -2
  485. package/src/modules/billing/backend/billing/transactions/page.tsx +2 -2
  486. package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
  487. package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
  488. package/src/modules/billing/backend/config/billing/page.tsx +549 -0
  489. package/src/modules/billing/ce.ts +8 -7
  490. package/src/modules/billing/cli.ts +31 -1
  491. package/src/modules/billing/commands/accounts.ts +6 -0
  492. package/src/modules/billing/commands/index.ts +3 -0
  493. package/src/modules/billing/commands/invoices.ts +371 -238
  494. package/src/modules/billing/commands/items.ts +143 -0
  495. package/src/modules/billing/commands/memos.ts +218 -0
  496. package/src/modules/billing/commands/payments.ts +428 -0
  497. package/src/modules/billing/commands/runs.ts +208 -6
  498. package/src/modules/billing/commands/settings.ts +77 -0
  499. package/src/modules/billing/commands/topups.ts +12 -3
  500. package/src/modules/billing/commands/transactions.ts +2 -2
  501. package/src/modules/billing/components/AccountForm.tsx +199 -53
  502. package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
  503. package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
  504. package/src/modules/billing/components/AccountPicker.tsx +70 -0
  505. package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
  506. package/src/modules/billing/components/BillingDataTable.tsx +34 -0
  507. package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
  508. package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
  509. package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
  510. package/src/modules/billing/components/CustomerLabel.tsx +53 -0
  511. package/src/modules/billing/components/InvoicesTable.tsx +344 -0
  512. package/src/modules/billing/components/ItemForm.tsx +233 -44
  513. package/src/modules/billing/components/ItemPriceField.tsx +81 -0
  514. package/src/modules/billing/components/LineFormDialog.tsx +25 -1
  515. package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
  516. package/src/modules/billing/components/MoneyInput.tsx +26 -0
  517. package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
  518. package/src/modules/billing/components/PaymentsTable.tsx +213 -0
  519. package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
  520. package/src/modules/billing/components/PrepaidPanel.tsx +20 -7
  521. package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
  522. package/src/modules/billing/components/TestRunActions.tsx +152 -0
  523. package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
  524. package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
  525. package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
  526. package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
  527. package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
  528. package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
  529. package/src/modules/billing/components/account/AccountView.tsx +101 -0
  530. package/src/modules/billing/components/account/accountRow.ts +70 -0
  531. package/src/modules/billing/components/customerNames.ts +62 -0
  532. package/src/modules/billing/data/entities.ts +685 -15
  533. package/src/modules/billing/data/entityIds.ts +25 -11
  534. package/src/modules/billing/data/validators.ts +388 -8
  535. package/src/modules/billing/events.ts +82 -3
  536. package/src/modules/billing/i18n/en.json +457 -21
  537. package/src/modules/billing/i18n/pl.json +457 -21
  538. package/src/modules/billing/index.ts +6 -2
  539. package/src/modules/billing/lib/accountFinancials.ts +84 -0
  540. package/src/modules/billing/lib/accountLock.ts +32 -0
  541. package/src/modules/billing/lib/accountStatus.ts +11 -9
  542. package/src/modules/billing/lib/accountSummary.ts +172 -0
  543. package/src/modules/billing/lib/allocation.ts +515 -0
  544. package/src/modules/billing/lib/allocationEvents.ts +26 -0
  545. package/src/modules/billing/lib/billPeriod.ts +19 -0
  546. package/src/modules/billing/lib/billRunEngine.ts +362 -162
  547. package/src/modules/billing/lib/billRunLabel.ts +1 -1
  548. package/src/modules/billing/lib/billingSettings.ts +145 -0
  549. package/src/modules/billing/lib/commandRoute.ts +112 -0
  550. package/src/modules/billing/lib/customerLookup.ts +157 -0
  551. package/src/modules/billing/lib/dateOnly.ts +22 -0
  552. package/src/modules/billing/lib/documentParties.ts +23 -0
  553. package/src/modules/billing/lib/documentTypes.ts +37 -0
  554. package/src/modules/billing/lib/dueDate.ts +26 -0
  555. package/src/modules/billing/lib/iban.ts +36 -0
  556. package/src/modules/billing/lib/invoiceDelivery.ts +197 -0
  557. package/src/modules/billing/lib/invoiceLanguages.ts +34 -0
  558. package/src/modules/billing/lib/invoiceWriter.ts +162 -148
  559. package/src/modules/billing/lib/itemBillingState.ts +67 -0
  560. package/src/modules/billing/lib/itemPriceChange.ts +56 -0
  561. package/src/modules/billing/lib/itemSelector.ts +4 -1
  562. package/src/modules/billing/lib/lineAmounts.ts +61 -0
  563. package/src/modules/billing/lib/memoGuards.ts +41 -0
  564. package/src/modules/billing/lib/navigation.ts +64 -0
  565. package/src/modules/billing/lib/numbering.ts +174 -0
  566. package/src/modules/billing/lib/overdue.ts +60 -0
  567. package/src/modules/billing/lib/paymentCsv.ts +171 -0
  568. package/src/modules/billing/lib/paymentGateway.ts +52 -0
  569. package/src/modules/billing/lib/paymentImport.ts +228 -0
  570. package/src/modules/billing/lib/paymentMatcher.ts +88 -0
  571. package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
  572. package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
  573. package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
  574. package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
  575. package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
  576. package/src/modules/billing/lib/pdf/invoiceModel.ts +328 -0
  577. package/src/modules/billing/lib/pdf/invoicePdf.ts +216 -0
  578. package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
  579. package/src/modules/billing/lib/prepaidConfig.ts +27 -36
  580. package/src/modules/billing/lib/prepaidConsume.ts +29 -6
  581. package/src/modules/billing/lib/prepaidPeriodClose.ts +410 -242
  582. package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
  583. package/src/modules/billing/lib/seeds/demo.ts +79 -6
  584. package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
  585. package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
  586. package/src/modules/billing/lib/servicePeriods.ts +226 -0
  587. package/src/modules/billing/lib/tenantLock.ts +1 -1
  588. package/src/modules/billing/lib/testRunCleanup.ts +42 -0
  589. package/src/modules/billing/lib/topupCapture.ts +3 -4
  590. package/src/modules/billing/lib/topupReceipt.ts +70 -103
  591. package/src/modules/billing/lib/usageRater.ts +1 -1
  592. package/src/modules/billing/lib/usageRunner.ts +17 -5
  593. package/src/modules/billing/lib/vat.ts +56 -0
  594. package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
  595. package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
  596. package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
  597. package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
  598. package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
  599. package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
  600. package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
  601. package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
  602. package/src/modules/billing/setup.ts +11 -237
  603. package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
  604. package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
  605. package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
  606. package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
  607. package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
  608. package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
  609. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
  610. package/src/modules/billing/workers/reap-stale-bill-runs.ts +4 -4
  611. package/dist/modules/billing/lib/invoiceStatus.js +0 -38
  612. package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
  613. package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
  614. package/src/modules/billing/lib/invoiceStatus.ts +0 -61
@@ -0,0 +1,121 @@
1
+ import { addDays, cloneAtMidnight, shiftCycles } from "./billPeriod.js";
2
+ const MAX_PERIODS_PER_ITEM = 120;
3
+ const DAY_MS = 864e5;
4
+ function toDate(value) {
5
+ return cloneAtMidnight(value);
6
+ }
7
+ function optDate(value) {
8
+ return value === null || value === void 0 || value === "" ? null : toDate(value);
9
+ }
10
+ function minDate(a, b) {
11
+ return a.getTime() <= b.getTime() ? a : b;
12
+ }
13
+ function maxDate(a, b) {
14
+ return a.getTime() >= b.getTime() ? a : b;
15
+ }
16
+ function inclusiveDays(from, to) {
17
+ return Math.round((to.getTime() - from.getTime()) / DAY_MS) + 1;
18
+ }
19
+ function roundHalfUp2dp(value) {
20
+ const sign = value < 0 ? -1 : 1;
21
+ return sign * Math.round((Math.abs(value) + Number.EPSILON) * 100) / 100;
22
+ }
23
+ function gridPeriod(anchor, cycle, index) {
24
+ const start = shiftCycles(anchor, cycle, index);
25
+ const end = addDays(shiftCycles(anchor, cycle, index + 1), -1);
26
+ return { index, start, end };
27
+ }
28
+ function gridPeriodContaining(anchor, cycle, date) {
29
+ const target = date.getTime();
30
+ const approxDays = { weekly: 7, monthly: 30.44, quarterly: 91.31, annually: 365.25 };
31
+ let index = Math.floor((target - anchor.getTime()) / DAY_MS / approxDays[cycle]);
32
+ let period = gridPeriod(anchor, cycle, index);
33
+ let guard = 0;
34
+ while (period.start.getTime() > target && guard++ < 1e3) period = gridPeriod(anchor, cycle, --index);
35
+ while (period.end.getTime() < target && guard++ < 2e3) period = gridPeriod(anchor, cycle, ++index);
36
+ return period;
37
+ }
38
+ function slice(item, anchor, cycle, from, to, kind) {
39
+ const lines = [];
40
+ const skipped = [];
41
+ let lastCovered = null;
42
+ let period = gridPeriodContaining(anchor, cycle, from);
43
+ let count = 0;
44
+ while (period.start.getTime() <= to.getTime()) {
45
+ if (count++ >= MAX_PERIODS_PER_ITEM) {
46
+ skipped.push({ billingItemId: item.id, reason: "period-cap-reached", servicePeriodStart: period.start, servicePeriodEnd: null });
47
+ break;
48
+ }
49
+ const activeStart = maxDate(period.start, from);
50
+ const activeEnd = minDate(period.end, to);
51
+ const periodDays = inclusiveDays(period.start, period.end);
52
+ const activeDays = inclusiveDays(activeStart, activeEnd);
53
+ const partial = activeDays < periodDays;
54
+ if (partial && !item.prorate) {
55
+ skipped.push({
56
+ billingItemId: item.id,
57
+ reason: "partial-period-not-prorated",
58
+ servicePeriodStart: activeStart,
59
+ servicePeriodEnd: activeEnd
60
+ });
61
+ } else {
62
+ const gross = partial ? item.unitPrice * activeDays / periodDays : item.unitPrice;
63
+ const amount = roundHalfUp2dp(gross);
64
+ lines.push({
65
+ billingItemId: item.id,
66
+ kind,
67
+ servicePeriodStart: activeStart,
68
+ servicePeriodEnd: activeEnd,
69
+ amount: kind === "credit" ? -amount : amount,
70
+ proration: partial ? { activeDays, periodDays } : null
71
+ });
72
+ }
73
+ lastCovered = activeEnd;
74
+ period = gridPeriod(anchor, cycle, period.index + 1);
75
+ }
76
+ return { lines, skipped, lastCovered };
77
+ }
78
+ function computeRecurringLines(item, params) {
79
+ const billedTo = optDate(item.billedToDate);
80
+ if (item.deletedAt) {
81
+ return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: "item-soft-deleted", servicePeriodStart: null, servicePeriodEnd: null }] };
82
+ }
83
+ if (!item.isActive) {
84
+ return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: "item-inactive", servicePeriodStart: null, servicePeriodEnd: null }] };
85
+ }
86
+ const anchor = toDate(params.billDate);
87
+ const { cycle } = params;
88
+ const start = toDate(item.billStartDate);
89
+ const end = optDate(item.billEndDate);
90
+ if (end && billedTo && end.getTime() < billedTo.getTime()) {
91
+ if (!item.prorate) {
92
+ return {
93
+ lines: [],
94
+ newBilledToDate: billedTo,
95
+ skipped: [{ billingItemId: item.id, reason: "ended-after-billing-no-credit", servicePeriodStart: addDays(end, 1), servicePeriodEnd: billedTo }]
96
+ };
97
+ }
98
+ const credit = slice(item, anchor, cycle, maxDate(addDays(end, 1), start), billedTo, "credit");
99
+ return { lines: credit.lines, newBilledToDate: end, skipped: credit.skipped };
100
+ }
101
+ const horizon = item.chargeTiming === "in_arrears" ? addDays(anchor, -1) : addDays(shiftCycles(anchor, cycle, 1), -1);
102
+ const cursor = billedTo ? maxDate(addDays(billedTo, 1), start) : start;
103
+ const windowEnd = end ? minDate(horizon, end) : horizon;
104
+ if (cursor.getTime() > windowEnd.getTime()) {
105
+ return { lines: [], newBilledToDate: billedTo, skipped: [] };
106
+ }
107
+ const charge = slice(item, anchor, cycle, cursor, windowEnd, "charge");
108
+ return {
109
+ lines: charge.lines,
110
+ newBilledToDate: charge.lastCovered ?? billedTo,
111
+ skipped: charge.skipped
112
+ };
113
+ }
114
+ export {
115
+ MAX_PERIODS_PER_ITEM,
116
+ computeRecurringLines,
117
+ gridPeriodContaining,
118
+ inclusiveDays,
119
+ roundHalfUp2dp
120
+ };
121
+ //# sourceMappingURL=servicePeriods.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/lib/servicePeriods.ts"],
4
+ "sourcesContent": ["import type { BillCycle, ChargeTiming } from '../data/validators'\nimport { addDays, cloneAtMidnight, shiftCycles } from './billPeriod'\n\n/**\n * Recurring-charge computation for one bill date (Spec A).\n *\n * Pure and storage-agnostic: given an item's state and the account's bill\n * date, decide which service periods to charge (or credit) and where the\n * item's `billed_to_date` watermark moves. The engine calls it once per item\n * per simulated bill date and owns persistence.\n *\n * Model:\n * - The account's cycle grid is anchored at the bill date `D`:\n * \u2026, D \u2212 cycle, D, D + cycle, \u2026 (each period `[g_k, g_{k+1} \u2212 1]`).\n * - Horizon \u2014 how far the item is billed on `D`:\n * in_advance \u2192 D + cycle \u2212 1 in_arrears \u2192 D \u2212 1\n * - Cursor \u2014 first unbilled day: `billed_to_date + 1`, else `bill_start_date`.\n * - The unbilled window `[cursor, min(horizon, bill_end_date)]` is cut along\n * the grid; each grid period becomes one line (full \u2192 unit price,\n * partial \u2192 prorated by calendar days, or skipped when `prorate=false`).\n * - Credit (A-D2): an item ended (`bill_end_date`) before what was already\n * billed (`billed_to_date`) gets negative lines for the unused days when\n * `prorate=true`, and the watermark is pulled back to `bill_end_date`.\n *\n * The watermark is the single source of truth against double billing: every\n * computation starts at `billed_to_date + 1`, so switching timing, re-running\n * a bill date, or catching up can never charge a day twice.\n */\n\nexport type RecurringItemState = {\n id: string\n chargeTiming: ChargeTiming\n prorate: boolean\n billStartDate: Date | string\n billEndDate?: Date | string | null\n billedToDate?: Date | string | null\n unitPrice: number\n isActive: boolean\n deletedAt?: Date | string | null\n}\n\nexport type ProrationInfo = { activeDays: number; periodDays: number }\n\nexport type RecurringLine = {\n billingItemId: string\n kind: 'charge' | 'credit'\n /** Inclusive service period the line covers. */\n servicePeriodStart: Date\n servicePeriodEnd: Date\n /** 2dp half-up; negative for credits. */\n amount: number\n /** Null when the line covers a full grid period. */\n proration: ProrationInfo | null\n}\n\nexport type RecurringSkipReason =\n | 'item-inactive'\n | 'item-soft-deleted'\n | 'partial-period-not-prorated'\n | 'ended-after-billing-no-credit'\n | 'period-cap-reached'\n\nexport type RecurringSkip = {\n billingItemId: string\n reason: RecurringSkipReason\n servicePeriodStart: Date | null\n servicePeriodEnd: Date | null\n}\n\nexport type RecurringComputation = {\n lines: RecurringLine[]\n /** Watermark after these lines; unchanged when nothing was billed. */\n newBilledToDate: Date | null\n skipped: RecurringSkip[]\n}\n\n/** Upper bound of grid periods processed per item per call (runaway guard). */\nexport const MAX_PERIODS_PER_ITEM = 120\n\nconst DAY_MS = 86_400_000\n\nfunction toDate(value: Date | string | number): Date {\n return cloneAtMidnight(value)\n}\n\nfunction optDate(value: Date | string | null | undefined): Date | null {\n return value === null || value === undefined || value === '' ? null : toDate(value)\n}\n\nfunction minDate(a: Date, b: Date): Date {\n return a.getTime() <= b.getTime() ? a : b\n}\n\nfunction maxDate(a: Date, b: Date): Date {\n return a.getTime() >= b.getTime() ? a : b\n}\n\n/** Inclusive day count of `[from, to]`. */\nexport function inclusiveDays(from: Date, to: Date): number {\n return Math.round((to.getTime() - from.getTime()) / DAY_MS) + 1\n}\n\n/** Half-up 2dp rounding that survives binary floating point (1.005 \u2192 1.01). */\nexport function roundHalfUp2dp(value: number): number {\n const sign = value < 0 ? -1 : 1\n return (sign * Math.round((Math.abs(value) + Number.EPSILON) * 100)) / 100\n}\n\ntype GridPeriod = { index: number; start: Date; end: Date }\n\nfunction gridPeriod(anchor: Date, cycle: BillCycle, index: number): GridPeriod {\n const start = shiftCycles(anchor, cycle, index)\n const end = addDays(shiftCycles(anchor, cycle, index + 1), -1)\n return { index, start, end }\n}\n\n/** Grid period (anchored at `anchor`) containing `date`. */\nexport function gridPeriodContaining(anchor: Date, cycle: BillCycle, date: Date): GridPeriod {\n const target = date.getTime()\n // Coarse estimate first so long gaps don't walk one cycle at a time.\n const approxDays: Record<BillCycle, number> = { weekly: 7, monthly: 30.44, quarterly: 91.31, annually: 365.25 }\n let index = Math.floor((target - anchor.getTime()) / DAY_MS / approxDays[cycle])\n let period = gridPeriod(anchor, cycle, index)\n let guard = 0\n while (period.start.getTime() > target && guard++ < 1000) period = gridPeriod(anchor, cycle, --index)\n while (period.end.getTime() < target && guard++ < 2000) period = gridPeriod(anchor, cycle, ++index)\n return period\n}\n\n/**\n * Slices `[from, to]` along the grid and prices each slice. Shared by the\n * charge and credit paths so both prorate identically.\n */\nfunction slice(\n item: RecurringItemState,\n anchor: Date,\n cycle: BillCycle,\n from: Date,\n to: Date,\n kind: RecurringLine['kind'],\n): { lines: RecurringLine[]; skipped: RecurringSkip[]; lastCovered: Date | null } {\n const lines: RecurringLine[] = []\n const skipped: RecurringSkip[] = []\n let lastCovered: Date | null = null\n let period = gridPeriodContaining(anchor, cycle, from)\n let count = 0\n while (period.start.getTime() <= to.getTime()) {\n if (count++ >= MAX_PERIODS_PER_ITEM) {\n skipped.push({ billingItemId: item.id, reason: 'period-cap-reached', servicePeriodStart: period.start, servicePeriodEnd: null })\n break\n }\n const activeStart = maxDate(period.start, from)\n const activeEnd = minDate(period.end, to)\n const periodDays = inclusiveDays(period.start, period.end)\n const activeDays = inclusiveDays(activeStart, activeEnd)\n const partial = activeDays < periodDays\n if (partial && !item.prorate) {\n skipped.push({\n billingItemId: item.id,\n reason: 'partial-period-not-prorated',\n servicePeriodStart: activeStart,\n servicePeriodEnd: activeEnd,\n })\n } else {\n const gross = partial ? (item.unitPrice * activeDays) / periodDays : item.unitPrice\n const amount = roundHalfUp2dp(gross)\n lines.push({\n billingItemId: item.id,\n kind,\n servicePeriodStart: activeStart,\n servicePeriodEnd: activeEnd,\n amount: kind === 'credit' ? -amount : amount,\n proration: partial ? { activeDays, periodDays } : null,\n })\n }\n lastCovered = activeEnd\n period = gridPeriod(anchor, cycle, period.index + 1)\n }\n return { lines, skipped, lastCovered }\n}\n\nexport function computeRecurringLines(\n item: RecurringItemState,\n params: { billDate: Date | string; cycle: BillCycle },\n): RecurringComputation {\n const billedTo = optDate(item.billedToDate)\n if (item.deletedAt) {\n return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: 'item-soft-deleted', servicePeriodStart: null, servicePeriodEnd: null }] }\n }\n if (!item.isActive) {\n return { lines: [], newBilledToDate: billedTo, skipped: [{ billingItemId: item.id, reason: 'item-inactive', servicePeriodStart: null, servicePeriodEnd: null }] }\n }\n\n const anchor = toDate(params.billDate)\n const { cycle } = params\n const start = toDate(item.billStartDate)\n const end = optDate(item.billEndDate)\n\n // \u2500\u2500 Credit: the item ended inside what was already billed (A-D2). \u2500\u2500\n if (end && billedTo && end.getTime() < billedTo.getTime()) {\n if (!item.prorate) {\n return {\n lines: [],\n newBilledToDate: billedTo,\n skipped: [{ billingItemId: item.id, reason: 'ended-after-billing-no-credit', servicePeriodStart: addDays(end, 1), servicePeriodEnd: billedTo }],\n }\n }\n const credit = slice(item, anchor, cycle, maxDate(addDays(end, 1), start), billedTo, 'credit')\n return { lines: credit.lines, newBilledToDate: end, skipped: credit.skipped }\n }\n\n // \u2500\u2500 Charge: bill the unbilled window up to the horizon. \u2500\u2500\n const horizon =\n item.chargeTiming === 'in_arrears' ? addDays(anchor, -1) : addDays(shiftCycles(anchor, cycle, 1), -1)\n const cursor = billedTo ? maxDate(addDays(billedTo, 1), start) : start\n const windowEnd = end ? minDate(horizon, end) : horizon\n if (cursor.getTime() > windowEnd.getTime()) {\n return { lines: [], newBilledToDate: billedTo, skipped: [] }\n }\n const charge = slice(item, anchor, cycle, cursor, windowEnd, 'charge')\n return {\n lines: charge.lines,\n newBilledToDate: charge.lastCovered ?? billedTo,\n skipped: charge.skipped,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/billing/lib/tenantLock.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\n\n/**\n * Per-tenant Postgres transaction-scoped advisory lock primitive.\n *\n * The Bill Run engine (Phase 2) uses this to guarantee at most one\n * concurrently-running run per tenant \u2014 the spec requires this so two\n * cron triggers or a \"schedule\" + \"manual\" overlap cannot produce\n * duplicate drafts.\n *\n * Why advisory locks (not row locks):\n * - `pg_try_advisory_xact_lock` is non-blocking \u2014 we get a clean \"busy\"\n * signal instead of waiting on a row.\n * - It's released automatically when the transaction ends (commit,\n * rollback, or process death) \u2014 so there is no lock-leak path even\n * on `kill -9`. The `BillRun` row's `status='running'` may be left\n * hanging, but the lock itself vanishes; the reaper worker\n * (workers/reap-stale-bill-runs.ts) handles the orphaned row.\n * - It works without a sentinel table \u2014 we hash a string key into the\n * bigint key space the advisory-lock API requires.\n *\n * Usage:\n *\n * ```ts\n * import { withTenantLock, TenantLockBusyError } from '../lib/tenantLock'\n *\n * try {\n * await withTenantLock(em, { tenantId, lockName: 'billing-run' }, async (tem) => {\n * // tem is the transactional EntityManager; lock is held here.\n * // Returns to caller after function resolves and tx commits.\n * })\n * } catch (err) {\n * if (err instanceof TenantLockBusyError) {\n * // HTTP 409 \u2014 another run is in progress.\n * }\n * throw err\n * }\n * ```\n *\n * Keep work inside the callback short. The advisory lock is released only\n * when the transaction ends; long-running work blocks other triggers for\n * the whole tenant.\n */\n\nexport class TenantLockBusyError extends Error {\n readonly tenantId: string\n readonly lockName: string\n\n constructor(tenantId: string, lockName: string) {\n super(`Tenant ${tenantId} already holds the billing lock \"${lockName}\"`)\n this.name = 'TenantLockBusyError'\n this.tenantId = tenantId\n this.lockName = lockName\n }\n}\n\nconst LOCK_KEY_PREFIX = 'dainamite-billing'\n\n/**\n * Build the deterministic string key fed into `hashtext()`. Exposed for\n * tests so they can assert lock-key independence across tenants /\n * lockNames without scraping SQL strings.\n */\nexport function buildLockKey(tenantId: string, lockName: string): string {\n return `${LOCK_KEY_PREFIX}:${tenantId}:${lockName}`\n}\n\ntype AdvisoryLockRow = { acquired: boolean }\n\nexport async function withTenantLock<T>(\n em: EntityManager,\n params: { tenantId: string; lockName: string },\n fn: (tem: EntityManager) => Promise<T>,\n): Promise<T> {\n return em.transactional(async (tem) => {\n const key = buildLockKey(params.tenantId, params.lockName)\n const rows = (await tem.execute(\n 'SELECT pg_try_advisory_xact_lock(hashtext(?)::bigint) AS acquired',\n [key],\n )) as unknown as AdvisoryLockRow[] | AdvisoryLockRow | undefined\n const first: AdvisoryLockRow | undefined = Array.isArray(rows) ? rows[0] : rows\n if (!first?.acquired) {\n throw new TenantLockBusyError(params.tenantId, params.lockName)\n }\n return fn(tem)\n })\n}\n"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\n\n/**\n * Per-tenant Postgres transaction-scoped advisory lock primitive.\n *\n * The Bill Run engine (Phase 2) uses this to guarantee at most one\n * concurrently-running run per tenant \u2014 the spec requires this so two\n * cron triggers or a \"schedule\" + \"manual\" overlap cannot produce\n * duplicate drafts.\n *\n * Why advisory locks (not row locks):\n * - `pg_try_advisory_xact_lock` is non-blocking \u2014 we get a clean \"busy\"\n * signal instead of waiting on a row.\n * - It's released automatically when the transaction ends (commit,\n * rollback, or process death) \u2014 so there is no lock-leak path even\n * on `kill -9`. The `BillingRun` row's `status='running'` may be left\n * hanging, but the lock itself vanishes; the reaper worker\n * (workers/reap-stale-bill-runs.ts) handles the orphaned row.\n * - It works without a sentinel table \u2014 we hash a string key into the\n * bigint key space the advisory-lock API requires.\n *\n * Usage:\n *\n * ```ts\n * import { withTenantLock, TenantLockBusyError } from '../lib/tenantLock'\n *\n * try {\n * await withTenantLock(em, { tenantId, lockName: 'billing-run' }, async (tem) => {\n * // tem is the transactional EntityManager; lock is held here.\n * // Returns to caller after function resolves and tx commits.\n * })\n * } catch (err) {\n * if (err instanceof TenantLockBusyError) {\n * // HTTP 409 \u2014 another run is in progress.\n * }\n * throw err\n * }\n * ```\n *\n * Keep work inside the callback short. The advisory lock is released only\n * when the transaction ends; long-running work blocks other triggers for\n * the whole tenant.\n */\n\nexport class TenantLockBusyError extends Error {\n readonly tenantId: string\n readonly lockName: string\n\n constructor(tenantId: string, lockName: string) {\n super(`Tenant ${tenantId} already holds the billing lock \"${lockName}\"`)\n this.name = 'TenantLockBusyError'\n this.tenantId = tenantId\n this.lockName = lockName\n }\n}\n\nconst LOCK_KEY_PREFIX = 'dainamite-billing'\n\n/**\n * Build the deterministic string key fed into `hashtext()`. Exposed for\n * tests so they can assert lock-key independence across tenants /\n * lockNames without scraping SQL strings.\n */\nexport function buildLockKey(tenantId: string, lockName: string): string {\n return `${LOCK_KEY_PREFIX}:${tenantId}:${lockName}`\n}\n\ntype AdvisoryLockRow = { acquired: boolean }\n\nexport async function withTenantLock<T>(\n em: EntityManager,\n params: { tenantId: string; lockName: string },\n fn: (tem: EntityManager) => Promise<T>,\n): Promise<T> {\n return em.transactional(async (tem) => {\n const key = buildLockKey(params.tenantId, params.lockName)\n const rows = (await tem.execute(\n 'SELECT pg_try_advisory_xact_lock(hashtext(?)::bigint) AS acquired',\n [key],\n )) as unknown as AdvisoryLockRow[] | AdvisoryLockRow | undefined\n const first: AdvisoryLockRow | undefined = Array.isArray(rows) ? rows[0] : rows\n if (!first?.acquired) {\n throw new TenantLockBusyError(params.tenantId, params.lockName)\n }\n return fn(tem)\n })\n}\n"],
5
5
  "mappings": "AA4CO,MAAM,4BAA4B,MAAM;AAAA,EAI7C,YAAY,UAAkB,UAAkB;AAC9C,UAAM,UAAU,QAAQ,oCAAoC,QAAQ,GAAG;AACvE,SAAK,OAAO;AACZ,SAAK,WAAW;AAChB,SAAK,WAAW;AAAA,EAClB;AACF;AAEA,MAAM,kBAAkB;AAOjB,SAAS,aAAa,UAAkB,UAA0B;AACvE,SAAO,GAAG,eAAe,IAAI,QAAQ,IAAI,QAAQ;AACnD;AAIA,eAAsB,eACpB,IACA,QACA,IACY;AACZ,SAAO,GAAG,cAAc,OAAO,QAAQ;AACrC,UAAM,MAAM,aAAa,OAAO,UAAU,OAAO,QAAQ;AACzD,UAAM,OAAQ,MAAM,IAAI;AAAA,MACtB;AAAA,MACA,CAAC,GAAG;AAAA,IACN;AACA,UAAM,QAAqC,MAAM,QAAQ,IAAI,IAAI,KAAK,CAAC,IAAI;AAC3E,QAAI,CAAC,OAAO,UAAU;AACpB,YAAM,IAAI,oBAAoB,OAAO,UAAU,OAAO,QAAQ;AAAA,IAChE;AACA,WAAO,GAAG,GAAG;AAAA,EACf,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,24 @@
1
+ async function deleteTestRunArtifacts(em, scope) {
2
+ const params = [scope.tenantId, scope.organizationId];
3
+ let billRunClause = "";
4
+ if (scope.billRunId) {
5
+ billRunClause = " AND bill_run_id = ?";
6
+ params.push(scope.billRunId);
7
+ }
8
+ const rows = await em.execute(
9
+ `SELECT id FROM billing_invoices
10
+ WHERE tenant_id = ? AND organization_id = ? AND test_run = true AND document_type = 'invoice'${billRunClause}`,
11
+ params
12
+ );
13
+ const ids = (rows ?? []).map((row) => row.id);
14
+ if (ids.length === 0) return 0;
15
+ const placeholders = ids.map(() => "?").join(",");
16
+ await em.execute(`DELETE FROM billing_draft_invoice_edits WHERE invoice_id IN (${placeholders})`, ids);
17
+ await em.execute(`DELETE FROM billing_invoice_lines WHERE invoice_id IN (${placeholders})`, ids);
18
+ await em.execute(`DELETE FROM billing_invoices WHERE id IN (${placeholders})`, ids);
19
+ return ids.length;
20
+ }
21
+ export {
22
+ deleteTestRunArtifacts
23
+ };
24
+ //# sourceMappingURL=testRunCleanup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/billing/lib/testRunCleanup.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\n\n/**\n * Test-run cleanup shared by the \"wipe test invoices\" and \"delete run\"\n * commands (Spec F, P-D1).\n *\n * Hard-deletes test drafts (`test_run = true`, `document_type = 'invoice'`)\n * together with their lines and their draft-edit audit rows. Scoped by\n * tenant + organization, and optionally by one Bill Run. Must run inside the\n * caller's transaction.\n */\n\nexport type TestRunCleanupScope = {\n tenantId: string\n organizationId: string\n /** When set, only test invoices written by this Bill Run are removed. */\n billRunId?: string | null\n}\n\nexport async function deleteTestRunArtifacts(\n em: EntityManager,\n scope: TestRunCleanupScope,\n): Promise<number> {\n const params: unknown[] = [scope.tenantId, scope.organizationId]\n let billRunClause = ''\n if (scope.billRunId) {\n billRunClause = ' AND bill_run_id = ?'\n params.push(scope.billRunId)\n }\n const rows = (await em.execute(\n `SELECT id FROM billing_invoices\n WHERE tenant_id = ? AND organization_id = ? AND test_run = true AND document_type = 'invoice'${billRunClause}`,\n params,\n )) as unknown as Array<{ id: string }> | undefined\n const ids = (rows ?? []).map((row) => row.id)\n if (ids.length === 0) return 0\n const placeholders = ids.map(() => '?').join(',')\n await em.execute(`DELETE FROM billing_draft_invoice_edits WHERE invoice_id IN (${placeholders})`, ids)\n await em.execute(`DELETE FROM billing_invoice_lines WHERE invoice_id IN (${placeholders})`, ids)\n await em.execute(`DELETE FROM billing_invoices WHERE id IN (${placeholders})`, ids)\n return ids.length\n}\n"],
5
+ "mappings": "AAmBA,eAAsB,uBACpB,IACA,OACiB;AACjB,QAAM,SAAoB,CAAC,MAAM,UAAU,MAAM,cAAc;AAC/D,MAAI,gBAAgB;AACpB,MAAI,MAAM,WAAW;AACnB,oBAAgB;AAChB,WAAO,KAAK,MAAM,SAAS;AAAA,EAC7B;AACA,QAAM,OAAQ,MAAM,GAAG;AAAA,IACrB;AAAA,oGACgG,aAAa;AAAA,IAC7G;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE;AAC5C,MAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,QAAM,eAAe,IAAI,IAAI,MAAM,GAAG,EAAE,KAAK,GAAG;AAChD,QAAM,GAAG,QAAQ,gEAAgE,YAAY,KAAK,GAAG;AACrG,QAAM,GAAG,QAAQ,0DAA0D,YAAY,KAAK,GAAG;AAC/F,QAAM,GAAG,QAAQ,6CAA6C,YAAY,KAAK,GAAG;AAClF,SAAO,IAAI;AACb;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  import { BillingTopup } from "../data/entities.js";
2
2
  import { applyBalanceMovement } from "./balanceLedger.js";
3
3
  import { createTopupReceipt } from "./topupReceipt.js";
4
- async function captureTopup(em, deps, params) {
4
+ async function captureTopup(em, params) {
5
5
  const { topup, account } = params;
6
6
  if (topup.status === "captured") {
7
7
  return { status: "already_captured" };
@@ -32,7 +32,7 @@ async function captureTopup(em, deps, params) {
32
32
  receiptInvoiceId: prior?.receiptInvoiceId ?? void 0
33
33
  };
34
34
  }
35
- const receipt = await createTopupReceipt(tem, deps, {
35
+ const receipt = await createTopupReceipt(tem, {
36
36
  account,
37
37
  topupId: topup.id,
38
38
  paymentId: topup.paymentId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/billing/lib/topupCapture.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingTopup } from '../data/entities'\nimport { applyBalanceMovement } from './balanceLedger'\nimport { createTopupReceipt, type TopupReceiptDeps } from './topupReceipt'\n\n/**\n * Idempotent top-up capture (SPEC-002 P2).\n *\n * On `payment_gateways.payment.captured` for a top-up, in ONE DB transaction:\n * - credit the balance (atomic) + append a `type='topup'` transaction with a\n * deterministic `source_ref = topup-{paymentId}` (so a re-fired capture can\n * never double-credit),\n * - create the posted+paid VAT receipt in core/sales,\n * - transition the BillingTopup \u2192 captured, backfilling `transaction_id` +\n * `receipt_invoice_id`.\n *\n * Re-firing on an already-captured top-up is a no-op (the gateway delivers\n * webhooks at-least-once).\n */\n\nexport type CaptureTopupAccount = {\n id: string\n organizationId: string\n tenantId: string\n currencyCode: string\n}\n\nexport type CaptureTopupParams = {\n topup: BillingTopup\n account: CaptureTopupAccount\n /** VAT percent applied to the receipt. */\n taxRate: number\n /** Provider transaction id for the transaction metadata, if known. */\n gatewayTransactionId?: string | null\n}\n\nexport type CaptureTopupResult = {\n status: 'captured' | 'already_captured'\n balanceAfter?: string\n transactionId?: string\n receiptInvoiceId?: string\n invoiceNumber?: string\n grossAmount?: string\n}\n\nexport async function captureTopup(\n em: EntityManager,\n deps: TopupReceiptDeps,\n params: CaptureTopupParams,\n): Promise<CaptureTopupResult> {\n const { topup, account } = params\n\n if (topup.status === 'captured') {\n return { status: 'already_captured' }\n }\n\n return em.transactional(async (tem) => {\n // 1. Atomic credit + paired transaction (idempotent on source_ref).\n const movement = await applyBalanceMovement(tem as EntityManager, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n type: 'topup',\n amount: topup.amount,\n description: `Top-up \u2014 ${topup.providerKey}`,\n topupId: topup.id,\n sourceRef: `topup-${topup.paymentId}`,\n metadata: {\n provider_key: topup.providerKey,\n payment_id: topup.paymentId,\n gateway_transaction_id: params.gatewayTransactionId ?? topup.gatewayTransactionId ?? null,\n },\n })\n\n // A deduplicated credit means a PRIOR capture already committed (the\n // gateway delivers `payment.captured` at-least-once, and the stale\n // `status` check above can race a concurrent capture). Do NOT issue a\n // second fiscal receipt \u2014 return the existing capture's links.\n if (movement.deduplicated) {\n const prior = await tem.findOne(BillingTopup, { id: topup.id })\n return {\n status: 'already_captured' as const,\n balanceAfter: movement.balanceAfter,\n transactionId: prior?.transactionId ?? movement.transaction.id,\n receiptInvoiceId: prior?.receiptInvoiceId ?? undefined,\n }\n }\n\n // 2. Fiscal VAT receipt (posted+paid).\n const receipt = await createTopupReceipt(tem as EntityManager, deps, {\n account,\n topupId: topup.id,\n paymentId: topup.paymentId,\n amount: topup.amount,\n taxRate: params.taxRate,\n })\n\n // 3. Transition the top-up.\n const managed = await tem.findOne(BillingTopup, { id: topup.id })\n const target = managed ?? topup\n target.status = 'captured'\n target.transactionId = movement.transaction.id\n target.receiptInvoiceId = receipt.invoiceId\n target.updatedAt = new Date()\n await tem.flush()\n\n return {\n status: 'captured' as const,\n balanceAfter: movement.balanceAfter,\n transactionId: movement.transaction.id,\n receiptInvoiceId: receipt.invoiceId,\n invoiceNumber: receipt.invoiceNumber,\n grossAmount: receipt.grossAmount,\n }\n })\n}\n"],
5
- "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,0BAAiD;AA0C1D,eAAsB,aACpB,IACA,MACA,QAC6B;AAC7B,QAAM,EAAE,OAAO,QAAQ,IAAI;AAE3B,MAAI,MAAM,WAAW,YAAY;AAC/B,WAAO,EAAE,QAAQ,mBAAmB;AAAA,EACtC;AAEA,SAAO,GAAG,cAAc,OAAO,QAAQ;AAErC,UAAM,WAAW,MAAM,qBAAqB,KAAsB;AAAA,MAChE,gBAAgB,QAAQ;AAAA,MACxB,UAAU,QAAQ;AAAA,MAClB,eAAe,QAAQ;AAAA,MACvB,cAAc,QAAQ;AAAA,MACtB,MAAM;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,aAAa,iBAAY,MAAM,WAAW;AAAA,MAC1C,SAAS,MAAM;AAAA,MACf,WAAW,SAAS,MAAM,SAAS;AAAA,MACnC,UAAU;AAAA,QACR,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,wBAAwB,OAAO,wBAAwB,MAAM,wBAAwB;AAAA,MACvF;AAAA,IACF,CAAC;AAMD,QAAI,SAAS,cAAc;AACzB,YAAM,QAAQ,MAAM,IAAI,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAC9D,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,cAAc,SAAS;AAAA,QACvB,eAAe,OAAO,iBAAiB,SAAS,YAAY;AAAA,QAC5D,kBAAkB,OAAO,oBAAoB;AAAA,MAC/C;AAAA,IACF;AAGA,UAAM,UAAU,MAAM,mBAAmB,KAAsB,MAAM;AAAA,MACnE;AAAA,MACA,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM;AAAA,MACd,SAAS,OAAO;AAAA,IAClB,CAAC;AAGD,UAAM,UAAU,MAAM,IAAI,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAChE,UAAM,SAAS,WAAW;AAC1B,WAAO,SAAS;AAChB,WAAO,gBAAgB,SAAS,YAAY;AAC5C,WAAO,mBAAmB,QAAQ;AAClC,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,IAAI,MAAM;AAEhB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,SAAS;AAAA,MACvB,eAAe,SAAS,YAAY;AAAA,MACpC,kBAAkB,QAAQ;AAAA,MAC1B,eAAe,QAAQ;AAAA,MACvB,aAAa,QAAQ;AAAA,IACvB;AAAA,EACF,CAAC;AACH;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingTopup } from '../data/entities'\nimport { applyBalanceMovement } from './balanceLedger'\nimport { createTopupReceipt } from './topupReceipt'\n\n/**\n * Idempotent top-up capture (SPEC-002 P2).\n *\n * On `payment_gateways.payment.captured` for a top-up, in ONE DB transaction:\n * - credit the balance (atomic) + append a `type='topup'` transaction with a\n * deterministic `source_ref = topup-{paymentId}` (so a re-fired capture can\n * never double-credit),\n * - create the paid VAT receipt as a billing document (`document_type = 'receipt'`),\n * - transition the BillingTopup \u2192 captured, backfilling `transaction_id` +\n * `receipt_invoice_id`.\n *\n * Re-firing on an already-captured top-up is a no-op (the gateway delivers\n * webhooks at-least-once).\n */\n\nexport type CaptureTopupAccount = {\n id: string\n organizationId: string\n tenantId: string\n currencyCode: string\n}\n\nexport type CaptureTopupParams = {\n topup: BillingTopup\n account: CaptureTopupAccount\n /** VAT percent applied to the receipt. */\n taxRate: number\n /** Provider transaction id for the transaction metadata, if known. */\n gatewayTransactionId?: string | null\n}\n\nexport type CaptureTopupResult = {\n status: 'captured' | 'already_captured'\n balanceAfter?: string\n transactionId?: string\n receiptInvoiceId?: string\n invoiceNumber?: string\n grossAmount?: string\n}\n\nexport async function captureTopup(\n em: EntityManager,\n params: CaptureTopupParams,\n): Promise<CaptureTopupResult> {\n const { topup, account } = params\n\n if (topup.status === 'captured') {\n return { status: 'already_captured' }\n }\n\n return em.transactional(async (tem) => {\n // 1. Atomic credit + paired transaction (idempotent on source_ref).\n const movement = await applyBalanceMovement(tem as EntityManager, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n billAccountId: account.id,\n currencyCode: account.currencyCode,\n type: 'topup',\n amount: topup.amount,\n description: `Top-up \u2014 ${topup.providerKey}`,\n topupId: topup.id,\n sourceRef: `topup-${topup.paymentId}`,\n metadata: {\n provider_key: topup.providerKey,\n payment_id: topup.paymentId,\n gateway_transaction_id: params.gatewayTransactionId ?? topup.gatewayTransactionId ?? null,\n },\n })\n\n // A deduplicated credit means a PRIOR capture already committed (the\n // gateway delivers `payment.captured` at-least-once, and the stale\n // `status` check above can race a concurrent capture). Do NOT issue a\n // second fiscal receipt \u2014 return the existing capture's links.\n if (movement.deduplicated) {\n const prior = await tem.findOne(BillingTopup, { id: topup.id })\n return {\n status: 'already_captured' as const,\n balanceAfter: movement.balanceAfter,\n transactionId: prior?.transactionId ?? movement.transaction.id,\n receiptInvoiceId: prior?.receiptInvoiceId ?? undefined,\n }\n }\n\n // 2. Fiscal VAT receipt (posted+paid).\n const receipt = await createTopupReceipt(tem as EntityManager, {\n account,\n topupId: topup.id,\n paymentId: topup.paymentId,\n amount: topup.amount,\n taxRate: params.taxRate,\n })\n\n // 3. Transition the top-up.\n const managed = await tem.findOne(BillingTopup, { id: topup.id })\n const target = managed ?? topup\n target.status = 'captured'\n target.transactionId = movement.transaction.id\n target.receiptInvoiceId = receipt.invoiceId\n target.updatedAt = new Date()\n await tem.flush()\n\n return {\n status: 'captured' as const,\n balanceAfter: movement.balanceAfter,\n transactionId: movement.transaction.id,\n receiptInvoiceId: receipt.invoiceId,\n invoiceNumber: receipt.invoiceNumber,\n grossAmount: receipt.grossAmount,\n }\n })\n}\n"],
5
+ "mappings": "AACA,SAAS,oBAAoB;AAC7B,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AA0CnC,eAAsB,aACpB,IACA,QAC6B;AAC7B,QAAM,EAAE,OAAO,QAAQ,IAAI;AAE3B,MAAI,MAAM,WAAW,YAAY;AAC/B,WAAO,EAAE,QAAQ,mBAAmB;AAAA,EACtC;AAEA,SAAO,GAAG,cAAc,OAAO,QAAQ;AAErC,UAAM,WAAW,MAAM,qBAAqB,KAAsB;AAAA,MAChE,gBAAgB,QAAQ;AAAA,MACxB,UAAU,QAAQ;AAAA,MAClB,eAAe,QAAQ;AAAA,MACvB,cAAc,QAAQ;AAAA,MACtB,MAAM;AAAA,MACN,QAAQ,MAAM;AAAA,MACd,aAAa,iBAAY,MAAM,WAAW;AAAA,MAC1C,SAAS,MAAM;AAAA,MACf,WAAW,SAAS,MAAM,SAAS;AAAA,MACnC,UAAU;AAAA,QACR,cAAc,MAAM;AAAA,QACpB,YAAY,MAAM;AAAA,QAClB,wBAAwB,OAAO,wBAAwB,MAAM,wBAAwB;AAAA,MACvF;AAAA,IACF,CAAC;AAMD,QAAI,SAAS,cAAc;AACzB,YAAM,QAAQ,MAAM,IAAI,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAC9D,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,cAAc,SAAS;AAAA,QACvB,eAAe,OAAO,iBAAiB,SAAS,YAAY;AAAA,QAC5D,kBAAkB,OAAO,oBAAoB;AAAA,MAC/C;AAAA,IACF;AAGA,UAAM,UAAU,MAAM,mBAAmB,KAAsB;AAAA,MAC7D;AAAA,MACA,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM;AAAA,MACd,SAAS,OAAO;AAAA,IAClB,CAAC;AAGD,UAAM,UAAU,MAAM,IAAI,QAAQ,cAAc,EAAE,IAAI,MAAM,GAAG,CAAC;AAChE,UAAM,SAAS,WAAW;AAC1B,WAAO,SAAS;AAChB,WAAO,gBAAgB,SAAS,YAAY;AAC5C,WAAO,mBAAmB,QAAQ;AAClC,WAAO,YAAY,oBAAI,KAAK;AAC5B,UAAM,IAAI,MAAM;AAEhB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,cAAc,SAAS;AAAA,MACvB,eAAe,SAAS,YAAY;AAAA,MACpC,kBAAkB,QAAQ;AAAA,MAC1B,eAAe,QAAQ;AAAA,MACvB,aAAa,QAAQ;AAAA,IACvB;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -1,94 +1,73 @@
1
- import {
2
- SalesInvoice,
3
- SalesInvoiceLine
4
- } from "@open-mercato/core/modules/sales/data/entities";
5
- import { resolveInvoiceStatusEntryId } from "./invoiceStatus.js";
6
- function fmt4(value) {
7
- return (Number.isFinite(value) ? value : 0).toFixed(4);
1
+ import { BillingInvoice, BillingInvoiceLine } from "../data/entities.js";
2
+ import { getBillingSettings, numberMaskFor } from "./billingSettings.js";
3
+ import { claimDocumentNumber } from "./numbering.js";
4
+ import { lineAmountsFromGross, money4 } from "./vat.js";
5
+ function todayUtc() {
6
+ const now = /* @__PURE__ */ new Date();
7
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
8
8
  }
9
- async function createTopupReceipt(em, deps, params) {
9
+ async function createTopupReceipt(em, params) {
10
10
  const { account } = params;
11
- const tax = await deps.taxService.calculateUnitAmounts({
12
- amount: Number.parseFloat(params.amount),
13
- mode: "gross",
14
- organizationId: account.organizationId,
15
- tenantId: account.tenantId,
16
- taxRate: params.taxRate
17
- });
18
- const net = fmt4(tax.netAmount);
19
- const gross = fmt4(tax.grossAmount);
20
- const taxAmount = fmt4(tax.taxAmount);
21
- const effectiveRate = tax.taxRate ?? 0;
22
- const generated = await deps.numberGenerator.generate({
23
- kind: "invoice",
24
- organizationId: account.organizationId,
25
- tenantId: account.tenantId
26
- });
27
- const paidStatusEntryId = await resolveInvoiceStatusEntryId(
28
- em,
29
- account.tenantId,
30
- account.organizationId,
31
- "paid"
32
- );
11
+ const scope = { tenantId: account.tenantId, organizationId: account.organizationId };
12
+ const taxRate = Number.isFinite(params.taxRate) && params.taxRate > 0 ? params.taxRate : 0;
13
+ const amounts = lineAmountsFromGross(Number.parseFloat(params.amount), taxRate);
14
+ const net = money4(amounts.net);
15
+ const tax = money4(amounts.tax);
16
+ const gross = money4(amounts.gross);
17
+ const settings = await getBillingSettings(em, scope);
18
+ const issueDate = todayUtc();
19
+ const claimed = await claimDocumentNumber(em, scope, "receipt", numberMaskFor(settings, "receipt"), issueDate);
33
20
  const now = /* @__PURE__ */ new Date();
34
- const invoice = em.create(SalesInvoice, {
35
- organizationId: account.organizationId,
36
- tenantId: account.tenantId,
37
- invoiceNumber: generated.number,
38
- statusEntryId: paidStatusEntryId,
21
+ const receipt = em.create(BillingInvoice, {
22
+ ...scope,
23
+ billAccountId: account.id,
24
+ documentType: "receipt",
25
+ number: claimed.number,
26
+ series: "receipt",
39
27
  status: "paid",
40
- issueDate: now,
28
+ testRun: false,
29
+ issueDate,
30
+ postedAt: now,
41
31
  currencyCode: account.currencyCode,
42
- subtotalNetAmount: net,
43
- subtotalGrossAmount: gross,
44
- discountTotalAmount: "0",
45
- taxTotalAmount: taxAmount,
46
- grandTotalNetAmount: net,
47
- grandTotalGrossAmount: gross,
48
- paidTotalAmount: gross,
49
- outstandingAmount: "0",
50
- metadata: {
51
- kind: "prepaid_topup",
52
- bill_account_id: account.id,
53
- topup_id: params.topupId,
54
- payment_id: params.paymentId
55
- },
32
+ subtotalNet: net,
33
+ taxTotal: tax,
34
+ totalGross: gross,
35
+ paidTotal: gross,
36
+ creditedTotal: "0",
37
+ outstanding: "0",
38
+ topupId: params.topupId,
39
+ metadata: { kind: "prepaid_topup", payment_id: params.paymentId },
56
40
  createdAt: now,
57
41
  updatedAt: now
58
42
  });
59
- em.persist(invoice);
60
- const line = em.create(SalesInvoiceLine, {
61
- invoice,
62
- organizationId: account.organizationId,
63
- tenantId: account.tenantId,
64
- lineNumber: 1,
65
- kind: "service",
66
- description: "Account top-up",
67
- quantity: "1",
68
- normalizedQuantity: "1",
69
- currencyCode: account.currencyCode,
70
- unitPriceNet: net,
71
- unitPriceGross: gross,
72
- discountAmount: "0",
73
- discountPercent: "0",
74
- taxRate: fmt4(effectiveRate),
75
- taxAmount,
76
- totalNetAmount: net,
77
- totalGrossAmount: gross,
78
- metadata: {
79
- kind: "prepaid_topup",
80
- topup_id: params.topupId
81
- }
82
- });
83
- em.persist(line);
43
+ em.persist(receipt);
44
+ await em.flush();
45
+ em.persist(
46
+ em.create(BillingInvoiceLine, {
47
+ ...scope,
48
+ invoiceId: receipt.id,
49
+ lineNumber: 1,
50
+ billingType: "receipt",
51
+ description: "Account top-up",
52
+ quantity: "1.0000",
53
+ unitPriceNet: net,
54
+ taxRate: taxRate.toFixed(4),
55
+ netAmount: net,
56
+ taxAmount: tax,
57
+ grossAmount: gross,
58
+ metadata: { kind: "prepaid_topup", topup_id: params.topupId },
59
+ createdAt: now,
60
+ updatedAt: now
61
+ })
62
+ );
84
63
  await em.flush();
85
64
  return {
86
- invoiceId: invoice.id,
87
- invoiceNumber: generated.number,
65
+ invoiceId: receipt.id,
66
+ invoiceNumber: claimed.number,
88
67
  netAmount: net,
89
- taxAmount,
68
+ taxAmount: tax,
90
69
  grossAmount: gross,
91
- taxRate: effectiveRate
70
+ taxRate
92
71
  };
93
72
  }
94
73
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/billing/lib/topupReceipt.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport {\n SalesInvoice,\n SalesInvoiceLine,\n} from '@open-mercato/core/modules/sales/data/entities'\nimport type { SalesDocumentNumberGenerator } from '@open-mercato/core/modules/sales/services/salesDocumentNumberGenerator'\nimport type { TaxCalculationService } from '@open-mercato/core/modules/sales/services/taxCalculationService'\nimport { resolveInvoiceStatusEntryId } from './invoiceStatus'\n\n/**\n * Creates the fiscal VAT receipt for a captured prepaid top-up (SPEC-002 P2).\n *\n * Deliberate divergence from the postpaid draft\u2192post gate: a captured top-up\n * is a completed, already-paid transaction with nothing to review, so the\n * receipt is created **posted + paid** in one shot (the gate exists for\n * outbound bills, not money already received \u2014 mirrors the xd-249 \"deviations\"\n * convention).\n *\n * **VAT at top-up.** The top-up `amount` is the GROSS the customer paid (= the\n * balance credited). The core tax service splits it into net + VAT at the\n * configured rate. Gap-free numbering reuses `salesDocumentNumberGenerator`.\n */\n\nexport type TopupReceiptParams = {\n account: {\n id: string\n organizationId: string\n tenantId: string\n currencyCode: string\n }\n topupId: string\n paymentId: string\n /** Gross amount paid (numeric(18,4) string). */\n amount: string\n /** VAT percent (e.g. 23). 0 = no VAT. */\n taxRate: number\n}\n\nexport type TopupReceiptDeps = {\n numberGenerator: Pick<SalesDocumentNumberGenerator, 'generate'>\n taxService: TaxCalculationService\n}\n\nexport type TopupReceipt = {\n invoiceId: string\n invoiceNumber: string\n netAmount: string\n taxAmount: string\n grossAmount: string\n taxRate: number\n}\n\nfunction fmt4(value: number): string {\n return (Number.isFinite(value) ? value : 0).toFixed(4)\n}\n\nexport async function createTopupReceipt(\n em: EntityManager,\n deps: TopupReceiptDeps,\n params: TopupReceiptParams,\n): Promise<TopupReceipt> {\n const { account } = params\n\n // VAT split \u2014 the top-up amount is GROSS (what was paid = balance credited).\n const tax = await deps.taxService.calculateUnitAmounts({\n amount: Number.parseFloat(params.amount),\n mode: 'gross',\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n taxRate: params.taxRate,\n })\n const net = fmt4(tax.netAmount)\n const gross = fmt4(tax.grossAmount)\n const taxAmount = fmt4(tax.taxAmount)\n const effectiveRate = tax.taxRate ?? 0\n\n const generated = await deps.numberGenerator.generate({\n kind: 'invoice',\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n })\n\n // Posted+paid in one shot \u2014 resolve the `paid` status entry directly.\n const paidStatusEntryId = await resolveInvoiceStatusEntryId(\n em,\n account.tenantId,\n account.organizationId,\n 'paid',\n )\n\n const now = new Date()\n const invoice = em.create(SalesInvoice, {\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n invoiceNumber: generated.number,\n statusEntryId: paidStatusEntryId,\n status: 'paid',\n issueDate: now,\n currencyCode: account.currencyCode,\n subtotalNetAmount: net,\n subtotalGrossAmount: gross,\n discountTotalAmount: '0',\n taxTotalAmount: taxAmount,\n grandTotalNetAmount: net,\n grandTotalGrossAmount: gross,\n paidTotalAmount: gross,\n outstandingAmount: '0',\n metadata: {\n kind: 'prepaid_topup',\n bill_account_id: account.id,\n topup_id: params.topupId,\n payment_id: params.paymentId,\n },\n createdAt: now,\n updatedAt: now,\n })\n em.persist(invoice)\n\n const line = em.create(SalesInvoiceLine, {\n invoice,\n organizationId: account.organizationId,\n tenantId: account.tenantId,\n lineNumber: 1,\n kind: 'service',\n description: 'Account top-up',\n quantity: '1',\n normalizedQuantity: '1',\n currencyCode: account.currencyCode,\n unitPriceNet: net,\n unitPriceGross: gross,\n discountAmount: '0',\n discountPercent: '0',\n taxRate: fmt4(effectiveRate),\n taxAmount,\n totalNetAmount: net,\n totalGrossAmount: gross,\n metadata: {\n kind: 'prepaid_topup',\n topup_id: params.topupId,\n },\n })\n em.persist(line)\n await em.flush()\n\n return {\n invoiceId: invoice.id,\n invoiceNumber: generated.number,\n netAmount: net,\n taxAmount,\n grossAmount: gross,\n taxRate: effectiveRate,\n }\n}\n"],
5
- "mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAGP,SAAS,mCAAmC;AA6C5C,SAAS,KAAK,OAAuB;AACnC,UAAQ,OAAO,SAAS,KAAK,IAAI,QAAQ,GAAG,QAAQ,CAAC;AACvD;AAEA,eAAsB,mBACpB,IACA,MACA,QACuB;AACvB,QAAM,EAAE,QAAQ,IAAI;AAGpB,QAAM,MAAM,MAAM,KAAK,WAAW,qBAAqB;AAAA,IACrD,QAAQ,OAAO,WAAW,OAAO,MAAM;AAAA,IACvC,MAAM;AAAA,IACN,gBAAgB,QAAQ;AAAA,IACxB,UAAU,QAAQ;AAAA,IAClB,SAAS,OAAO;AAAA,EAClB,CAAC;AACD,QAAM,MAAM,KAAK,IAAI,SAAS;AAC9B,QAAM,QAAQ,KAAK,IAAI,WAAW;AAClC,QAAM,YAAY,KAAK,IAAI,SAAS;AACpC,QAAM,gBAAgB,IAAI,WAAW;AAErC,QAAM,YAAY,MAAM,KAAK,gBAAgB,SAAS;AAAA,IACpD,MAAM;AAAA,IACN,gBAAgB,QAAQ;AAAA,IACxB,UAAU,QAAQ;AAAA,EACpB,CAAC;AAGD,QAAM,oBAAoB,MAAM;AAAA,IAC9B;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR;AAAA,EACF;AAEA,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,UAAU,GAAG,OAAO,cAAc;AAAA,IACtC,gBAAgB,QAAQ;AAAA,IACxB,UAAU,QAAQ;AAAA,IAClB,eAAe,UAAU;AAAA,IACzB,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,cAAc,QAAQ;AAAA,IACtB,mBAAmB;AAAA,IACnB,qBAAqB;AAAA,IACrB,qBAAqB;AAAA,IACrB,gBAAgB;AAAA,IAChB,qBAAqB;AAAA,IACrB,uBAAuB;AAAA,IACvB,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB,UAAU;AAAA,MACR,MAAM;AAAA,MACN,iBAAiB,QAAQ;AAAA,MACzB,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,IACrB;AAAA,IACA,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACD,KAAG,QAAQ,OAAO;AAElB,QAAM,OAAO,GAAG,OAAO,kBAAkB;AAAA,IACvC;AAAA,IACA,gBAAgB,QAAQ;AAAA,IACxB,UAAU,QAAQ;AAAA,IAClB,YAAY;AAAA,IACZ,MAAM;AAAA,IACN,aAAa;AAAA,IACb,UAAU;AAAA,IACV,oBAAoB;AAAA,IACpB,cAAc,QAAQ;AAAA,IACtB,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,SAAS,KAAK,aAAa;AAAA,IAC3B;AAAA,IACA,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,IAClB,UAAU;AAAA,MACR,MAAM;AAAA,MACN,UAAU,OAAO;AAAA,IACnB;AAAA,EACF,CAAC;AACD,KAAG,QAAQ,IAAI;AACf,QAAM,GAAG,MAAM;AAEf,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB,eAAe,UAAU;AAAA,IACzB,WAAW;AAAA,IACX;AAAA,IACA,aAAa;AAAA,IACb,SAAS;AAAA,EACX;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { BillingInvoice, BillingInvoiceLine } from '../data/entities'\nimport { getBillingSettings, numberMaskFor } from './billingSettings'\nimport { claimDocumentNumber } from './numbering'\nimport { lineAmountsFromGross, money4 } from './vat'\n\n/**\n * Creates the fiscal VAT receipt for a captured prepaid top-up (SPEC-002 P2,\n * Spec 0 E-D3).\n *\n * The receipt is a billing-owned document (`billing_invoices` with\n * `document_type = 'receipt'`, `status = 'paid'`), numbered from the\n * `receipt` series (default mask `RC/{YYYY}/{MM}/{SEQUENCE_NUM_MONTH:4}`).\n * Nothing is written to core `sales`.\n *\n * VAT at top-up: the top-up `amount` is the GROSS the customer paid (= the\n * balance credited); `vat.lineAmountsFromGross` splits it into net + tax with\n * `net + tax = gross` exactly.\n *\n * MUST run inside the capture transaction (`captureTopup`) so the receipt\n * number, the balance credit and the top-up transition commit together.\n */\n\nexport type TopupReceiptParams = {\n account: {\n id: string\n organizationId: string\n tenantId: string\n currencyCode: string\n }\n topupId: string\n paymentId: string\n /** Gross amount paid (numeric(18,4) string). */\n amount: string\n /** VAT percent (e.g. 23). 0 = no VAT. */\n taxRate: number\n}\n\nexport type TopupReceipt = {\n invoiceId: string\n invoiceNumber: string\n netAmount: string\n taxAmount: string\n grossAmount: string\n taxRate: number\n}\n\nfunction todayUtc(): Date {\n const now = new Date()\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))\n}\n\nexport async function createTopupReceipt(em: EntityManager, params: TopupReceiptParams): Promise<TopupReceipt> {\n const { account } = params\n const scope = { tenantId: account.tenantId, organizationId: account.organizationId }\n const taxRate = Number.isFinite(params.taxRate) && params.taxRate > 0 ? params.taxRate : 0\n const amounts = lineAmountsFromGross(Number.parseFloat(params.amount), taxRate)\n const net = money4(amounts.net)\n const tax = money4(amounts.tax)\n const gross = money4(amounts.gross)\n\n const settings = await getBillingSettings(em, scope)\n const issueDate = todayUtc()\n const claimed = await claimDocumentNumber(em, scope, 'receipt', numberMaskFor(settings, 'receipt'), issueDate)\n\n const now = new Date()\n const receipt = em.create(BillingInvoice, {\n ...scope,\n billAccountId: account.id,\n documentType: 'receipt',\n number: claimed.number,\n series: 'receipt',\n status: 'paid',\n testRun: false,\n issueDate,\n postedAt: now,\n currencyCode: account.currencyCode,\n subtotalNet: net,\n taxTotal: tax,\n totalGross: gross,\n paidTotal: gross,\n creditedTotal: '0',\n outstanding: '0',\n topupId: params.topupId,\n metadata: { kind: 'prepaid_topup', payment_id: params.paymentId },\n createdAt: now,\n updatedAt: now,\n })\n em.persist(receipt)\n await em.flush() // need the receipt id for the line\n\n em.persist(\n em.create(BillingInvoiceLine, {\n ...scope,\n invoiceId: receipt.id,\n lineNumber: 1,\n billingType: 'receipt',\n description: 'Account top-up',\n quantity: '1.0000',\n unitPriceNet: net,\n taxRate: taxRate.toFixed(4),\n netAmount: net,\n taxAmount: tax,\n grossAmount: gross,\n metadata: { kind: 'prepaid_topup', topup_id: params.topupId },\n createdAt: now,\n updatedAt: now,\n }),\n )\n await em.flush()\n\n return {\n invoiceId: receipt.id,\n invoiceNumber: claimed.number,\n netAmount: net,\n taxAmount: tax,\n grossAmount: gross,\n taxRate,\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,gBAAgB,0BAA0B;AACnD,SAAS,oBAAoB,qBAAqB;AAClD,SAAS,2BAA2B;AACpC,SAAS,sBAAsB,cAAc;AA2C7C,SAAS,WAAiB;AACxB,QAAM,MAAM,oBAAI,KAAK;AACrB,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,GAAG,IAAI,WAAW,CAAC,CAAC;AACrF;AAEA,eAAsB,mBAAmB,IAAmB,QAAmD;AAC7G,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,QAAQ,EAAE,UAAU,QAAQ,UAAU,gBAAgB,QAAQ,eAAe;AACnF,QAAM,UAAU,OAAO,SAAS,OAAO,OAAO,KAAK,OAAO,UAAU,IAAI,OAAO,UAAU;AACzF,QAAM,UAAU,qBAAqB,OAAO,WAAW,OAAO,MAAM,GAAG,OAAO;AAC9E,QAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAM,MAAM,OAAO,QAAQ,GAAG;AAC9B,QAAM,QAAQ,OAAO,QAAQ,KAAK;AAElC,QAAM,WAAW,MAAM,mBAAmB,IAAI,KAAK;AACnD,QAAM,YAAY,SAAS;AAC3B,QAAM,UAAU,MAAM,oBAAoB,IAAI,OAAO,WAAW,cAAc,UAAU,SAAS,GAAG,SAAS;AAE7G,QAAM,MAAM,oBAAI,KAAK;AACrB,QAAM,UAAU,GAAG,OAAO,gBAAgB;AAAA,IACxC,GAAG;AAAA,IACH,eAAe,QAAQ;AAAA,IACvB,cAAc;AAAA,IACd,QAAQ,QAAQ;AAAA,IAChB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT;AAAA,IACA,UAAU;AAAA,IACV,cAAc,QAAQ;AAAA,IACtB,aAAa;AAAA,IACb,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,eAAe;AAAA,IACf,aAAa;AAAA,IACb,SAAS,OAAO;AAAA,IAChB,UAAU,EAAE,MAAM,iBAAiB,YAAY,OAAO,UAAU;AAAA,IAChE,WAAW;AAAA,IACX,WAAW;AAAA,EACb,CAAC;AACD,KAAG,QAAQ,OAAO;AAClB,QAAM,GAAG,MAAM;AAEf,KAAG;AAAA,IACD,GAAG,OAAO,oBAAoB;AAAA,MAC5B,GAAG;AAAA,MACH,WAAW,QAAQ;AAAA,MACnB,YAAY;AAAA,MACZ,aAAa;AAAA,MACb,aAAa;AAAA,MACb,UAAU;AAAA,MACV,cAAc;AAAA,MACd,SAAS,QAAQ,QAAQ,CAAC;AAAA,MAC1B,WAAW;AAAA,MACX,WAAW;AAAA,MACX,aAAa;AAAA,MACb,UAAU,EAAE,MAAM,iBAAiB,UAAU,OAAO,QAAQ;AAAA,MAC5D,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AACA,QAAM,GAAG,MAAM;AAEf,SAAO;AAAA,IACL,WAAW,QAAQ;AAAA,IACnB,eAAe,QAAQ;AAAA,IACvB,WAAW;AAAA,IACX,WAAW;AAAA,IACX,aAAa;AAAA,IACb;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/billing/lib/usageRater.ts"],
4
- "sourcesContent": ["/**\n * Pure functions to rate usage quantity against a `rate_json` payload.\n *\n * Three tier models per the spec (`volume` / `graduated` / `flat`) plus\n * the \"simple\" flat-rate shape (`{ unit_price }` without a `model`).\n * The validator at `data/validators.ts` ensures the payload structure\n * before it reaches this rater \u2014 this module trusts the shape but\n * defends against numeric edge cases (NaN, negative quantity).\n *\n * Output:\n * - `amount` \u2014 numeric string, rounded 2dp half-up, formatted to 4\n * decimals to match the `core/sales` numeric(18,4) column shape.\n * - `breakdown` \u2014 per-tier rated quantity / amount, persisted into\n * `SalesInvoiceLine.metadata.usage_tier_breakdown` so operators\n * can see how the line was built without re-running the math.\n */\n\nimport type { BillingItem } from '../data/entities'\n\nexport type SimpleFlatRate = { unit_price: number }\nexport type VolumeTier = { up_to: number | null; unit_price: number }\nexport type GraduatedTier = { up_to: number | null; unit_price: number }\nexport type FlatTier = { up_to: number | null; flat_amount: number }\n\nexport type VolumeRate = { model: 'volume'; tiers: VolumeTier[] }\nexport type GraduatedRate = { model: 'graduated'; tiers: GraduatedTier[] }\nexport type FlatRate = { model: 'flat'; tiers: FlatTier[] }\nexport type TieredRate = VolumeRate | GraduatedRate | FlatRate\nexport type UsageRate = SimpleFlatRate | TieredRate\n\nexport type TierBreakdownEntry = {\n tierIndex: number\n upTo: number | null\n /** Quantity rated at this tier \u2014 0 for tiers above the consumed range. */\n rangeQuantity: number\n /** `unit_price` for volume/graduated; `flat_amount` for flat. */\n rate: number\n /** Per-tier amount contribution, rounded 4dp. */\n amount: string\n}\n\nexport type UsageRateResult = {\n /** Total rated amount, numeric(18,4) string. */\n amount: string\n /** Per-tier or single-rate breakdown for invoice-line metadata. */\n breakdown: TierBreakdownEntry[]\n}\n\n/**\n * Half-up rounding to 2dp survives the JS floating-point edge case\n * (`1.005 \u2192 1.01`, `100 \u00D7 0.001 + 100 \u00D7 0.001 + \u2026 \u2192 exactly N\u00D70.001`).\n * Match `invoiceWriter`'s rounding policy so totals are consistent\n * across line types.\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\n/**\n * Format a unit price to 4dp WITHOUT 2dp rounding \u2014 sub-cent rates\n * like `0.001` (per-API-request pricing) are legitimate. The spec's\n * 2dp half-up rule applies only to line *amounts*, not per-unit\n * rates.\n */\nfunction formatUnitPrice(value: number): string {\n if (!Number.isFinite(value)) return '0.0000'\n return value.toFixed(4)\n}\n\nfunction isTieredRate(rate: UsageRate): rate is TieredRate {\n return typeof (rate as { model?: unknown }).model === 'string'\n}\n\nfunction safeQuantity(quantity: number): number {\n if (!Number.isFinite(quantity) || quantity < 0) return 0\n return quantity\n}\n\n// \u2500\u2500\u2500 Simple flat-rate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction rateSimpleFlat(quantity: number, rate: SimpleFlatRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const total = q * rate.unit_price\n return {\n amount: format4dp(total),\n breakdown: [\n {\n tierIndex: 0,\n upTo: null,\n rangeQuantity: q,\n rate: rate.unit_price,\n amount: format4dp(total),\n },\n ],\n }\n}\n\n// \u2500\u2500\u2500 Volume tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"Entire quantity priced at the unit_price of the tier its total falls\n// into.\" So we walk tiers in order, find the first tier whose `up_to`\n// is `null` (open-ended) or `>= quantity`, and charge `quantity \u00D7\n// that tier's unit_price`. The unconsumed tiers report 0.\n\nfunction rateVolume(quantity: number, rate: VolumeRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const breakdown: TierBreakdownEntry[] = rate.tiers.map((tier, idx) => ({\n tierIndex: idx,\n upTo: tier.up_to,\n rangeQuantity: 0,\n rate: tier.unit_price,\n amount: format4dp(0),\n }))\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n if (tier.up_to === null || q <= tier.up_to) {\n const total = q * tier.unit_price\n breakdown[i] = {\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: q,\n rate: tier.unit_price,\n amount: format4dp(total),\n }\n return { amount: format4dp(total), breakdown }\n }\n }\n // Should be unreachable: validator enforces an open-ended top tier.\n return { amount: format4dp(0), breakdown }\n}\n\n// \u2500\u2500\u2500 Graduated tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"Each unit priced at its tier.\" Walk tiers in order, charging\n// `min(q_remaining, tier_capacity) \u00D7 unit_price` for each tier the\n// quantity touches.\n\nfunction rateGraduated(quantity: number, rate: GraduatedRate): UsageRateResult {\n const q = safeQuantity(quantity)\n let remaining = q\n let previousUpTo = 0\n let total = 0\n const breakdown: TierBreakdownEntry[] = []\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n const tierCapacity =\n tier.up_to === null ? Infinity : Math.max(0, tier.up_to - previousUpTo)\n const consumed = Math.min(remaining, tierCapacity)\n const tierAmount = consumed * tier.unit_price\n total += tierAmount\n breakdown.push({\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: consumed,\n rate: tier.unit_price,\n amount: format4dp(tierAmount),\n })\n remaining -= consumed\n previousUpTo = tier.up_to ?? previousUpTo\n if (remaining <= 0) {\n // Pad remaining tiers with zeros for breakdown completeness.\n for (let j = i + 1; j < rate.tiers.length; j++) {\n breakdown.push({\n tierIndex: j,\n upTo: rate.tiers[j].up_to,\n rangeQuantity: 0,\n rate: rate.tiers[j].unit_price,\n amount: format4dp(0),\n })\n }\n break\n }\n }\n return { amount: format4dp(total), breakdown }\n}\n\n// \u2500\u2500\u2500 Flat tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"One flat fee for the highest tier reached.\" Find the first tier\n// whose `up_to` is `null` or `>= quantity`. Charge that tier's\n// `flat_amount`. Other tiers report 0.\n\nfunction rateFlat(quantity: number, rate: FlatRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const breakdown: TierBreakdownEntry[] = rate.tiers.map((tier, idx) => ({\n tierIndex: idx,\n upTo: tier.up_to,\n rangeQuantity: 0,\n rate: tier.flat_amount,\n amount: format4dp(0),\n }))\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n if (tier.up_to === null || q <= tier.up_to) {\n breakdown[i] = {\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: q,\n rate: tier.flat_amount,\n amount: format4dp(tier.flat_amount),\n }\n return { amount: format4dp(tier.flat_amount), breakdown }\n }\n }\n return { amount: format4dp(0), breakdown }\n}\n\n// \u2500\u2500\u2500 Public dispatcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function rateUsageQuantity(rate: UsageRate, totalQuantity: number): UsageRateResult {\n if (!isTieredRate(rate)) {\n return rateSimpleFlat(totalQuantity, rate)\n }\n switch (rate.model) {\n case 'volume':\n return rateVolume(totalQuantity, rate)\n case 'graduated':\n return rateGraduated(totalQuantity, rate)\n case 'flat':\n return rateFlat(totalQuantity, rate)\n }\n}\n\n/**\n * Convenience wrapper: takes a BillingItem's `rate_json` (typed as\n * `unknown` from JSONB) and the total quantity, returns the rated\n * amount + breakdown. Returns `null` when the rate shape is invalid \u2014\n * caller falls back to \"no charge\" + warns instead of crashing the\n * whole account.\n */\nexport function rateUsageForItem(\n item: Pick<BillingItem, 'rateJson' | 'type'>,\n totalQuantity: number,\n): UsageRateResult | null {\n if (item.type !== 'usage') return null\n const rate = item.rateJson as UsageRate | null | undefined\n if (!rate || typeof rate !== 'object') return null\n if (isTieredRate(rate)) {\n if (!Array.isArray(rate.tiers) || rate.tiers.length === 0) return null\n } else if (typeof (rate as SimpleFlatRate).unit_price !== 'number') {\n return null\n }\n return rateUsageQuantity(rate, totalQuantity)\n}\n\nexport { roundHalfUp2dp, format4dp, formatUnitPrice }\n"],
4
+ "sourcesContent": ["/**\n * Pure functions to rate usage quantity against a `rate_json` payload.\n *\n * Three tier models per the spec (`volume` / `graduated` / `flat`) plus\n * the \"simple\" flat-rate shape (`{ unit_price }` without a `model`).\n * The validator at `data/validators.ts` ensures the payload structure\n * before it reaches this rater \u2014 this module trusts the shape but\n * defends against numeric edge cases (NaN, negative quantity).\n *\n * Output:\n * - `amount` \u2014 numeric string, rounded 2dp half-up, formatted to 4\n * decimals to match the `core/sales` numeric(18,4) column shape.\n * - `breakdown` \u2014 per-tier rated quantity / amount, persisted into\n * `billing_invoice_lines.metadata.usage_tier_breakdown` so operators\n * can see how the line was built without re-running the math.\n */\n\nimport type { BillingItem } from '../data/entities'\n\nexport type SimpleFlatRate = { unit_price: number }\nexport type VolumeTier = { up_to: number | null; unit_price: number }\nexport type GraduatedTier = { up_to: number | null; unit_price: number }\nexport type FlatTier = { up_to: number | null; flat_amount: number }\n\nexport type VolumeRate = { model: 'volume'; tiers: VolumeTier[] }\nexport type GraduatedRate = { model: 'graduated'; tiers: GraduatedTier[] }\nexport type FlatRate = { model: 'flat'; tiers: FlatTier[] }\nexport type TieredRate = VolumeRate | GraduatedRate | FlatRate\nexport type UsageRate = SimpleFlatRate | TieredRate\n\nexport type TierBreakdownEntry = {\n tierIndex: number\n upTo: number | null\n /** Quantity rated at this tier \u2014 0 for tiers above the consumed range. */\n rangeQuantity: number\n /** `unit_price` for volume/graduated; `flat_amount` for flat. */\n rate: number\n /** Per-tier amount contribution, rounded 4dp. */\n amount: string\n}\n\nexport type UsageRateResult = {\n /** Total rated amount, numeric(18,4) string. */\n amount: string\n /** Per-tier or single-rate breakdown for invoice-line metadata. */\n breakdown: TierBreakdownEntry[]\n}\n\n/**\n * Half-up rounding to 2dp survives the JS floating-point edge case\n * (`1.005 \u2192 1.01`, `100 \u00D7 0.001 + 100 \u00D7 0.001 + \u2026 \u2192 exactly N\u00D70.001`).\n * Match `invoiceWriter`'s rounding policy so totals are consistent\n * across line types.\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\n/**\n * Format a unit price to 4dp WITHOUT 2dp rounding \u2014 sub-cent rates\n * like `0.001` (per-API-request pricing) are legitimate. The spec's\n * 2dp half-up rule applies only to line *amounts*, not per-unit\n * rates.\n */\nfunction formatUnitPrice(value: number): string {\n if (!Number.isFinite(value)) return '0.0000'\n return value.toFixed(4)\n}\n\nfunction isTieredRate(rate: UsageRate): rate is TieredRate {\n return typeof (rate as { model?: unknown }).model === 'string'\n}\n\nfunction safeQuantity(quantity: number): number {\n if (!Number.isFinite(quantity) || quantity < 0) return 0\n return quantity\n}\n\n// \u2500\u2500\u2500 Simple flat-rate \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction rateSimpleFlat(quantity: number, rate: SimpleFlatRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const total = q * rate.unit_price\n return {\n amount: format4dp(total),\n breakdown: [\n {\n tierIndex: 0,\n upTo: null,\n rangeQuantity: q,\n rate: rate.unit_price,\n amount: format4dp(total),\n },\n ],\n }\n}\n\n// \u2500\u2500\u2500 Volume tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"Entire quantity priced at the unit_price of the tier its total falls\n// into.\" So we walk tiers in order, find the first tier whose `up_to`\n// is `null` (open-ended) or `>= quantity`, and charge `quantity \u00D7\n// that tier's unit_price`. The unconsumed tiers report 0.\n\nfunction rateVolume(quantity: number, rate: VolumeRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const breakdown: TierBreakdownEntry[] = rate.tiers.map((tier, idx) => ({\n tierIndex: idx,\n upTo: tier.up_to,\n rangeQuantity: 0,\n rate: tier.unit_price,\n amount: format4dp(0),\n }))\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n if (tier.up_to === null || q <= tier.up_to) {\n const total = q * tier.unit_price\n breakdown[i] = {\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: q,\n rate: tier.unit_price,\n amount: format4dp(total),\n }\n return { amount: format4dp(total), breakdown }\n }\n }\n // Should be unreachable: validator enforces an open-ended top tier.\n return { amount: format4dp(0), breakdown }\n}\n\n// \u2500\u2500\u2500 Graduated tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"Each unit priced at its tier.\" Walk tiers in order, charging\n// `min(q_remaining, tier_capacity) \u00D7 unit_price` for each tier the\n// quantity touches.\n\nfunction rateGraduated(quantity: number, rate: GraduatedRate): UsageRateResult {\n const q = safeQuantity(quantity)\n let remaining = q\n let previousUpTo = 0\n let total = 0\n const breakdown: TierBreakdownEntry[] = []\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n const tierCapacity =\n tier.up_to === null ? Infinity : Math.max(0, tier.up_to - previousUpTo)\n const consumed = Math.min(remaining, tierCapacity)\n const tierAmount = consumed * tier.unit_price\n total += tierAmount\n breakdown.push({\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: consumed,\n rate: tier.unit_price,\n amount: format4dp(tierAmount),\n })\n remaining -= consumed\n previousUpTo = tier.up_to ?? previousUpTo\n if (remaining <= 0) {\n // Pad remaining tiers with zeros for breakdown completeness.\n for (let j = i + 1; j < rate.tiers.length; j++) {\n breakdown.push({\n tierIndex: j,\n upTo: rate.tiers[j].up_to,\n rangeQuantity: 0,\n rate: rate.tiers[j].unit_price,\n amount: format4dp(0),\n })\n }\n break\n }\n }\n return { amount: format4dp(total), breakdown }\n}\n\n// \u2500\u2500\u2500 Flat tier \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// \"One flat fee for the highest tier reached.\" Find the first tier\n// whose `up_to` is `null` or `>= quantity`. Charge that tier's\n// `flat_amount`. Other tiers report 0.\n\nfunction rateFlat(quantity: number, rate: FlatRate): UsageRateResult {\n const q = safeQuantity(quantity)\n const breakdown: TierBreakdownEntry[] = rate.tiers.map((tier, idx) => ({\n tierIndex: idx,\n upTo: tier.up_to,\n rangeQuantity: 0,\n rate: tier.flat_amount,\n amount: format4dp(0),\n }))\n for (let i = 0; i < rate.tiers.length; i++) {\n const tier = rate.tiers[i]\n if (tier.up_to === null || q <= tier.up_to) {\n breakdown[i] = {\n tierIndex: i,\n upTo: tier.up_to,\n rangeQuantity: q,\n rate: tier.flat_amount,\n amount: format4dp(tier.flat_amount),\n }\n return { amount: format4dp(tier.flat_amount), breakdown }\n }\n }\n return { amount: format4dp(0), breakdown }\n}\n\n// \u2500\u2500\u2500 Public dispatcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function rateUsageQuantity(rate: UsageRate, totalQuantity: number): UsageRateResult {\n if (!isTieredRate(rate)) {\n return rateSimpleFlat(totalQuantity, rate)\n }\n switch (rate.model) {\n case 'volume':\n return rateVolume(totalQuantity, rate)\n case 'graduated':\n return rateGraduated(totalQuantity, rate)\n case 'flat':\n return rateFlat(totalQuantity, rate)\n }\n}\n\n/**\n * Convenience wrapper: takes a BillingItem's `rate_json` (typed as\n * `unknown` from JSONB) and the total quantity, returns the rated\n * amount + breakdown. Returns `null` when the rate shape is invalid \u2014\n * caller falls back to \"no charge\" + warns instead of crashing the\n * whole account.\n */\nexport function rateUsageForItem(\n item: Pick<BillingItem, 'rateJson' | 'type'>,\n totalQuantity: number,\n): UsageRateResult | null {\n if (item.type !== 'usage') return null\n const rate = item.rateJson as UsageRate | null | undefined\n if (!rate || typeof rate !== 'object') return null\n if (isTieredRate(rate)) {\n if (!Array.isArray(rate.tiers) || rate.tiers.length === 0) return null\n } else if (typeof (rate as SimpleFlatRate).unit_price !== 'number') {\n return null\n }\n return rateUsageQuantity(rate, totalQuantity)\n}\n\nexport { roundHalfUp2dp, format4dp, formatUnitPrice }\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,6 @@
1
+ import { itemTaxRate } from "./invoiceWriter.js";
1
2
  import { format4dp, formatUnitPrice, rateUsageForItem } from "./usageRater.js";
3
+ import { priceBasisOf } from "./lineAmounts.js";
2
4
  const EMPTY_RESULT = {
3
5
  lines: [],
4
6
  matchedUoms: [],
@@ -15,7 +17,9 @@ function nonNullUomItems(items) {
15
17
  }
16
18
  return out;
17
19
  }
18
- async function fetchUsageAggregate(em, account, period) {
20
+ async function fetchUsageAggregate(em, account, period, after) {
21
+ const params = [account.tenantId, account.organizationId, account.id, period.periodEnd];
22
+ if (after) params.push(after);
19
23
  const rows = await em.execute(
20
24
  `SELECT uom_code,
21
25
  SUM(quantity)::text AS total_quantity,
@@ -27,13 +31,14 @@ async function fetchUsageAggregate(em, account, period) {
27
31
  AND deleted_at IS NULL
28
32
  AND rated_in_bill_run_id IS NULL
29
33
  AND period_end <= ?
34
+ ${after ? "AND period_end > ?" : ""}
30
35
  GROUP BY uom_code`,
31
- [account.tenantId, account.organizationId, account.id, period.periodEnd]
36
+ params
32
37
  );
33
38
  return rows ?? [];
34
39
  }
35
- async function processUsageForAccount(em, account, period, usageItems) {
36
- const aggregateRows = await fetchUsageAggregate(em, account, period);
40
+ async function processUsageForAccount(em, account, period, usageItems, options = {}) {
41
+ const aggregateRows = await fetchUsageAggregate(em, account, period, options.after ?? null);
37
42
  if (aggregateRows.length === 0) return EMPTY_RESULT;
38
43
  const itemByUom = nonNullUomItems(usageItems);
39
44
  const lines = [];
@@ -61,9 +66,12 @@ async function processUsageForAccount(em, account, period, usageItems) {
61
66
  billingItemId: item.id,
62
67
  billingType: "usage",
63
68
  description: item.description,
64
- unitPriceNet,
69
+ unitPrice: unitPriceNet,
65
70
  quantity: format4dp(totalQuantity),
66
- totalNetAmount: rated.amount,
71
+ lineAmount: rated.amount,
72
+ // Tiered rates are always net (P-D4); a simple rate keeps the item basis.
73
+ priceBasis: isSimpleFlat ? priceBasisOf(item.priceBasis) : "net",
74
+ taxRate: itemTaxRate(item),
67
75
  metadataExtra: {
68
76
  uom_code: uom,
69
77
  usage_record_count: recordCount,