@dainamite/billing 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/dist/modules/billing/acl.js +13 -1
  3. package/dist/modules/billing/acl.js.map +2 -2
  4. package/dist/modules/billing/api/accounts/[id]/summary/route.js +52 -0
  5. package/dist/modules/billing/api/accounts/[id]/summary/route.js.map +7 -0
  6. package/dist/modules/billing/api/accounts/route.js +46 -4
  7. package/dist/modules/billing/api/accounts/route.js.map +2 -2
  8. package/dist/modules/billing/api/export/account/[id]/route.js +11 -7
  9. package/dist/modules/billing/api/export/account/[id]/route.js.map +2 -2
  10. package/dist/modules/billing/api/invoices/[id]/memos/route.js +54 -0
  11. package/dist/modules/billing/api/invoices/[id]/memos/route.js.map +7 -0
  12. package/dist/modules/billing/api/invoices/[id]/pdf/route.js +69 -0
  13. package/dist/modules/billing/api/invoices/[id]/pdf/route.js.map +7 -0
  14. package/dist/modules/billing/api/invoices/[id]/route.js +44 -24
  15. package/dist/modules/billing/api/invoices/[id]/route.js.map +2 -2
  16. package/dist/modules/billing/api/invoices/[id]/send/route.js +107 -0
  17. package/dist/modules/billing/api/invoices/[id]/send/route.js.map +7 -0
  18. package/dist/modules/billing/api/invoices/add-line/route.js +2 -2
  19. package/dist/modules/billing/api/invoices/add-line/route.js.map +2 -2
  20. package/dist/modules/billing/api/invoices/delete-draft/route.js +33 -0
  21. package/dist/modules/billing/api/invoices/delete-draft/route.js.map +7 -0
  22. package/dist/modules/billing/api/invoices/edit-line/route.js +2 -2
  23. package/dist/modules/billing/api/invoices/edit-line/route.js.map +2 -2
  24. package/dist/modules/billing/api/invoices/post/route.js +2 -2
  25. package/dist/modules/billing/api/invoices/post/route.js.map +2 -2
  26. package/dist/modules/billing/api/invoices/remove-line/route.js +2 -2
  27. package/dist/modules/billing/api/invoices/remove-line/route.js.map +2 -2
  28. package/dist/modules/billing/api/invoices/route.js +45 -46
  29. package/dist/modules/billing/api/invoices/route.js.map +2 -2
  30. package/dist/modules/billing/api/items/bulk/route.js +1 -1
  31. package/dist/modules/billing/api/items/bulk/route.js.map +2 -2
  32. package/dist/modules/billing/api/items/change-price/route.js +44 -0
  33. package/dist/modules/billing/api/items/change-price/route.js.map +7 -0
  34. package/dist/modules/billing/api/items/route.js +19 -7
  35. package/dist/modules/billing/api/items/route.js.map +2 -2
  36. package/dist/modules/billing/api/payment-imports/[id]/route.js +49 -0
  37. package/dist/modules/billing/api/payment-imports/[id]/route.js.map +7 -0
  38. package/dist/modules/billing/api/payment-imports/route.js +57 -0
  39. package/dist/modules/billing/api/payment-imports/route.js.map +7 -0
  40. package/dist/modules/billing/api/payments/[id]/route.js +60 -0
  41. package/dist/modules/billing/api/payments/[id]/route.js.map +7 -0
  42. package/dist/modules/billing/api/payments/allocate/route.js +35 -0
  43. package/dist/modules/billing/api/payments/allocate/route.js.map +7 -0
  44. package/dist/modules/billing/api/payments/assign/route.js +36 -0
  45. package/dist/modules/billing/api/payments/assign/route.js.map +7 -0
  46. package/dist/modules/billing/api/payments/convert-to-topup/route.js +35 -0
  47. package/dist/modules/billing/api/payments/convert-to-topup/route.js.map +7 -0
  48. package/dist/modules/billing/api/payments/ignore/route.js +35 -0
  49. package/dist/modules/billing/api/payments/ignore/route.js.map +7 -0
  50. package/dist/modules/billing/api/payments/import/route.js +63 -0
  51. package/dist/modules/billing/api/payments/import/route.js.map +7 -0
  52. package/dist/modules/billing/api/payments/restore/route.js +35 -0
  53. package/dist/modules/billing/api/payments/restore/route.js.map +7 -0
  54. package/dist/modules/billing/api/payments/route.js +114 -0
  55. package/dist/modules/billing/api/payments/route.js.map +7 -0
  56. package/dist/modules/billing/api/payments/unallocate/route.js +35 -0
  57. package/dist/modules/billing/api/payments/unallocate/route.js.map +7 -0
  58. package/dist/modules/billing/api/payments/unassign/route.js +35 -0
  59. package/dist/modules/billing/api/payments/unassign/route.js.map +7 -0
  60. package/dist/modules/billing/api/run-outcomes/route.js +43 -6
  61. package/dist/modules/billing/api/run-outcomes/route.js.map +2 -2
  62. package/dist/modules/billing/api/runs/[id]/post-drafts/route.js +61 -0
  63. package/dist/modules/billing/api/runs/[id]/post-drafts/route.js.map +7 -0
  64. package/dist/modules/billing/api/runs/[id]/route.js +53 -0
  65. package/dist/modules/billing/api/runs/[id]/route.js.map +7 -0
  66. package/dist/modules/billing/api/runs/retry-failed/route.js +1 -1
  67. package/dist/modules/billing/api/runs/retry-failed/route.js.map +2 -2
  68. package/dist/modules/billing/api/runs/route.js +9 -3
  69. package/dist/modules/billing/api/runs/route.js.map +2 -2
  70. package/dist/modules/billing/api/settings/number-preview/route.js +42 -0
  71. package/dist/modules/billing/api/settings/number-preview/route.js.map +7 -0
  72. package/dist/modules/billing/api/settings/route.js +92 -0
  73. package/dist/modules/billing/api/settings/route.js.map +7 -0
  74. package/dist/modules/billing/api/statements/route.js +3 -0
  75. package/dist/modules/billing/api/statements/route.js.map +2 -2
  76. package/dist/modules/billing/api/test-invoices/wipe/route.js +2 -2
  77. package/dist/modules/billing/api/test-invoices/wipe/route.js.map +2 -2
  78. package/dist/modules/billing/backend/billing/accounts/[id]/page.js +121 -232
  79. package/dist/modules/billing/backend/billing/accounts/[id]/page.js.map +2 -2
  80. package/dist/modules/billing/backend/billing/accounts/create/page.js +6 -0
  81. package/dist/modules/billing/backend/billing/accounts/create/page.js.map +2 -2
  82. package/dist/modules/billing/backend/billing/accounts/page.js +63 -4
  83. package/dist/modules/billing/backend/billing/accounts/page.js.map +2 -2
  84. package/dist/modules/billing/backend/billing/invoices/[id]/page.js +297 -65
  85. package/dist/modules/billing/backend/billing/invoices/[id]/page.js.map +3 -3
  86. package/dist/modules/billing/backend/billing/invoices/page.js +6 -172
  87. package/dist/modules/billing/backend/billing/invoices/page.js.map +2 -2
  88. package/dist/modules/billing/backend/billing/items/[id]/page.js +51 -7
  89. package/dist/modules/billing/backend/billing/items/[id]/page.js.map +2 -2
  90. package/dist/modules/billing/backend/billing/items/create/page.js +9 -6
  91. package/dist/modules/billing/backend/billing/items/create/page.js.map +2 -2
  92. package/dist/modules/billing/backend/billing/items/page.js +8 -2
  93. package/dist/modules/billing/backend/billing/items/page.js.map +2 -2
  94. package/dist/modules/billing/backend/billing/payments/[id]/page.js +297 -0
  95. package/dist/modules/billing/backend/billing/payments/[id]/page.js.map +7 -0
  96. package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js +21 -0
  97. package/dist/modules/billing/backend/billing/payments/[id]/page.meta.js.map +7 -0
  98. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js +74 -0
  99. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.js.map +7 -0
  100. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js +21 -0
  101. package/dist/modules/billing/backend/billing/payments/imports/[id]/page.meta.js.map +7 -0
  102. package/dist/modules/billing/backend/billing/payments/page.js +58 -0
  103. package/dist/modules/billing/backend/billing/payments/page.js.map +7 -0
  104. package/dist/modules/billing/backend/billing/payments/page.meta.js +18 -0
  105. package/dist/modules/billing/backend/billing/payments/page.meta.js.map +7 -0
  106. package/dist/modules/billing/backend/billing/runs/[id]/page.js +49 -14
  107. package/dist/modules/billing/backend/billing/runs/[id]/page.js.map +2 -2
  108. package/dist/modules/billing/backend/billing/runs/page.js +58 -7
  109. package/dist/modules/billing/backend/billing/runs/page.js.map +2 -2
  110. package/dist/modules/billing/backend/billing/statements/[id]/page.js +61 -6
  111. package/dist/modules/billing/backend/billing/statements/[id]/page.js.map +2 -2
  112. package/dist/modules/billing/backend/billing/statements/page.js +1 -1
  113. package/dist/modules/billing/backend/billing/statements/page.js.map +2 -2
  114. package/dist/modules/billing/backend/billing/topups/page.js +1 -1
  115. package/dist/modules/billing/backend/billing/topups/page.js.map +2 -2
  116. package/dist/modules/billing/backend/billing/transactions/page.js +1 -1
  117. package/dist/modules/billing/backend/billing/transactions/page.js.map +2 -2
  118. package/dist/modules/billing/backend/billing/usage/create/page.js +3 -2
  119. package/dist/modules/billing/backend/billing/usage/create/page.js.map +2 -2
  120. package/dist/modules/billing/backend/config/billing/page.js +472 -0
  121. package/dist/modules/billing/backend/config/billing/page.js.map +7 -0
  122. package/dist/modules/billing/backend/config/billing/page.meta.js +19 -0
  123. package/dist/modules/billing/backend/config/billing/page.meta.js.map +7 -0
  124. package/dist/modules/billing/cli.js +22 -1
  125. package/dist/modules/billing/cli.js.map +2 -2
  126. package/dist/modules/billing/commands/accounts.js +6 -0
  127. package/dist/modules/billing/commands/accounts.js.map +2 -2
  128. package/dist/modules/billing/commands/index.js +3 -0
  129. package/dist/modules/billing/commands/index.js.map +2 -2
  130. package/dist/modules/billing/commands/invoices.js +269 -167
  131. package/dist/modules/billing/commands/invoices.js.map +2 -2
  132. package/dist/modules/billing/commands/items.js +108 -0
  133. package/dist/modules/billing/commands/items.js.map +2 -2
  134. package/dist/modules/billing/commands/memos.js +179 -0
  135. package/dist/modules/billing/commands/memos.js.map +7 -0
  136. package/dist/modules/billing/commands/payments.js +379 -0
  137. package/dist/modules/billing/commands/payments.js.map +7 -0
  138. package/dist/modules/billing/commands/runs.js +151 -4
  139. package/dist/modules/billing/commands/runs.js.map +2 -2
  140. package/dist/modules/billing/commands/settings.js +59 -0
  141. package/dist/modules/billing/commands/settings.js.map +7 -0
  142. package/dist/modules/billing/commands/topups.js +9 -2
  143. package/dist/modules/billing/commands/topups.js.map +2 -2
  144. package/dist/modules/billing/commands/transactions.js +2 -2
  145. package/dist/modules/billing/commands/transactions.js.map +2 -2
  146. package/dist/modules/billing/components/AccountForm.js +149 -42
  147. package/dist/modules/billing/components/AccountForm.js.map +2 -2
  148. package/dist/modules/billing/components/AccountInvoicesSection.js +64 -0
  149. package/dist/modules/billing/components/AccountInvoicesSection.js.map +7 -0
  150. package/dist/modules/billing/components/AccountPaymentsSection.js +43 -0
  151. package/dist/modules/billing/components/AccountPaymentsSection.js.map +7 -0
  152. package/dist/modules/billing/components/AccountPicker.js +56 -0
  153. package/dist/modules/billing/components/AccountPicker.js.map +7 -0
  154. package/dist/modules/billing/components/AllocatePaymentDialog.js +134 -0
  155. package/dist/modules/billing/components/AllocatePaymentDialog.js.map +7 -0
  156. package/dist/modules/billing/components/BillingDataTable.js +21 -0
  157. package/dist/modules/billing/components/BillingDataTable.js.map +7 -0
  158. package/dist/modules/billing/components/BillingSettingsLink.js +19 -0
  159. package/dist/modules/billing/components/BillingSettingsLink.js.map +7 -0
  160. package/dist/modules/billing/components/ChangeItemPriceDialog.js +158 -0
  161. package/dist/modules/billing/components/ChangeItemPriceDialog.js.map +7 -0
  162. package/dist/modules/billing/components/CreateMemoDialog.js +200 -0
  163. package/dist/modules/billing/components/CreateMemoDialog.js.map +7 -0
  164. package/dist/modules/billing/components/CustomerLabel.js +33 -0
  165. package/dist/modules/billing/components/CustomerLabel.js.map +7 -0
  166. package/dist/modules/billing/components/InvoicesTable.js +270 -0
  167. package/dist/modules/billing/components/InvoicesTable.js.map +7 -0
  168. package/dist/modules/billing/components/ItemForm.js +176 -44
  169. package/dist/modules/billing/components/ItemForm.js.map +2 -2
  170. package/dist/modules/billing/components/ItemPriceField.js +46 -0
  171. package/dist/modules/billing/components/ItemPriceField.js.map +7 -0
  172. package/dist/modules/billing/components/LineFormDialog.js +25 -2
  173. package/dist/modules/billing/components/LineFormDialog.js.map +2 -2
  174. package/dist/modules/billing/components/ManualPaymentDialog.js +122 -0
  175. package/dist/modules/billing/components/ManualPaymentDialog.js.map +7 -0
  176. package/dist/modules/billing/components/MoneyInput.js +14 -0
  177. package/dist/modules/billing/components/MoneyInput.js.map +7 -0
  178. package/dist/modules/billing/components/PaymentUploadDialog.js +115 -0
  179. package/dist/modules/billing/components/PaymentUploadDialog.js.map +7 -0
  180. package/dist/modules/billing/components/PaymentsTable.js +171 -0
  181. package/dist/modules/billing/components/PaymentsTable.js.map +7 -0
  182. package/dist/modules/billing/components/PostRunDraftsButton.js +105 -0
  183. package/dist/modules/billing/components/PostRunDraftsButton.js.map +7 -0
  184. package/dist/modules/billing/components/PrepaidPanel.js +14 -4
  185. package/dist/modules/billing/components/PrepaidPanel.js.map +2 -2
  186. package/dist/modules/billing/components/RunCyclesSection.js +209 -0
  187. package/dist/modules/billing/components/RunCyclesSection.js.map +7 -0
  188. package/dist/modules/billing/components/TestRunActions.js +127 -0
  189. package/dist/modules/billing/components/TestRunActions.js.map +7 -0
  190. package/dist/modules/billing/components/account/AccountItemsTab.js +157 -0
  191. package/dist/modules/billing/components/account/AccountItemsTab.js.map +7 -0
  192. package/dist/modules/billing/components/account/AccountRunBillingDialog.js +122 -0
  193. package/dist/modules/billing/components/account/AccountRunBillingDialog.js.map +7 -0
  194. package/dist/modules/billing/components/account/AccountRunsTab.js +152 -0
  195. package/dist/modules/billing/components/account/AccountRunsTab.js.map +7 -0
  196. package/dist/modules/billing/components/account/AccountStatementsTab.js +98 -0
  197. package/dist/modules/billing/components/account/AccountStatementsTab.js.map +7 -0
  198. package/dist/modules/billing/components/account/AccountSummaryCards.js +125 -0
  199. package/dist/modules/billing/components/account/AccountSummaryCards.js.map +7 -0
  200. package/dist/modules/billing/components/account/AccountUsageTab.js +91 -0
  201. package/dist/modules/billing/components/account/AccountUsageTab.js.map +7 -0
  202. package/dist/modules/billing/components/account/AccountView.js +73 -0
  203. package/dist/modules/billing/components/account/AccountView.js.map +7 -0
  204. package/dist/modules/billing/components/account/accountRow.js +39 -0
  205. package/dist/modules/billing/components/account/accountRow.js.map +7 -0
  206. package/dist/modules/billing/components/customerNames.js +42 -0
  207. package/dist/modules/billing/components/customerNames.js.map +7 -0
  208. package/dist/modules/billing/data/entities.js +560 -4
  209. package/dist/modules/billing/data/entities.js.map +2 -2
  210. package/dist/modules/billing/data/entityIds.js +10 -1
  211. package/dist/modules/billing/data/entityIds.js.map +2 -2
  212. package/dist/modules/billing/data/validators.js +269 -15
  213. package/dist/modules/billing/data/validators.js.map +2 -2
  214. package/dist/modules/billing/events.js +50 -0
  215. package/dist/modules/billing/events.js.map +2 -2
  216. package/dist/modules/billing/i18n/en.json +457 -21
  217. package/dist/modules/billing/i18n/pl.json +457 -21
  218. package/dist/modules/billing/index.js +6 -2
  219. package/dist/modules/billing/index.js.map +2 -2
  220. package/dist/modules/billing/lib/accountFinancials.js +58 -0
  221. package/dist/modules/billing/lib/accountFinancials.js.map +7 -0
  222. package/dist/modules/billing/lib/accountLock.js +14 -0
  223. package/dist/modules/billing/lib/accountLock.js.map +7 -0
  224. package/dist/modules/billing/lib/accountStatus.js +9 -7
  225. package/dist/modules/billing/lib/accountStatus.js.map +2 -2
  226. package/dist/modules/billing/lib/accountSummary.js +106 -0
  227. package/dist/modules/billing/lib/accountSummary.js.map +7 -0
  228. package/dist/modules/billing/lib/allocation.js +356 -0
  229. package/dist/modules/billing/lib/allocation.js.map +7 -0
  230. package/dist/modules/billing/lib/allocationEvents.js +22 -0
  231. package/dist/modules/billing/lib/allocationEvents.js.map +7 -0
  232. package/dist/modules/billing/lib/billPeriod.js +15 -1
  233. package/dist/modules/billing/lib/billPeriod.js.map +2 -2
  234. package/dist/modules/billing/lib/billRunEngine.js +237 -102
  235. package/dist/modules/billing/lib/billRunEngine.js.map +2 -2
  236. package/dist/modules/billing/lib/billingSettings.js +116 -0
  237. package/dist/modules/billing/lib/billingSettings.js.map +7 -0
  238. package/dist/modules/billing/lib/commandRoute.js +81 -0
  239. package/dist/modules/billing/lib/commandRoute.js.map +7 -0
  240. package/dist/modules/billing/lib/customerLookup.js +97 -0
  241. package/dist/modules/billing/lib/customerLookup.js.map +7 -0
  242. package/dist/modules/billing/lib/dateOnly.js +13 -0
  243. package/dist/modules/billing/lib/dateOnly.js.map +7 -0
  244. package/dist/modules/billing/lib/documentParties.js +16 -0
  245. package/dist/modules/billing/lib/documentParties.js.map +7 -0
  246. package/dist/modules/billing/lib/documentTypes.js +27 -0
  247. package/dist/modules/billing/lib/documentTypes.js.map +7 -0
  248. package/dist/modules/billing/lib/dueDate.js +18 -0
  249. package/dist/modules/billing/lib/dueDate.js.map +7 -0
  250. package/dist/modules/billing/lib/iban.js +29 -0
  251. package/dist/modules/billing/lib/iban.js.map +7 -0
  252. package/dist/modules/billing/lib/invoiceDelivery.js +131 -0
  253. package/dist/modules/billing/lib/invoiceDelivery.js.map +7 -0
  254. package/dist/modules/billing/lib/invoiceLanguages.js +16 -0
  255. package/dist/modules/billing/lib/invoiceLanguages.js.map +7 -0
  256. package/dist/modules/billing/lib/invoiceWriter.js +107 -99
  257. package/dist/modules/billing/lib/invoiceWriter.js.map +2 -2
  258. package/dist/modules/billing/lib/itemBillingState.js +27 -0
  259. package/dist/modules/billing/lib/itemBillingState.js.map +7 -0
  260. package/dist/modules/billing/lib/itemPriceChange.js +18 -0
  261. package/dist/modules/billing/lib/itemPriceChange.js.map +7 -0
  262. package/dist/modules/billing/lib/itemSelector.js.map +2 -2
  263. package/dist/modules/billing/lib/lineAmounts.js +32 -0
  264. package/dist/modules/billing/lib/lineAmounts.js.map +7 -0
  265. package/dist/modules/billing/lib/memoGuards.js +25 -0
  266. package/dist/modules/billing/lib/memoGuards.js.map +7 -0
  267. package/dist/modules/billing/lib/navigation.js +48 -0
  268. package/dist/modules/billing/lib/navigation.js.map +7 -0
  269. package/dist/modules/billing/lib/numbering.js +120 -0
  270. package/dist/modules/billing/lib/numbering.js.map +7 -0
  271. package/dist/modules/billing/lib/overdue.js +36 -0
  272. package/dist/modules/billing/lib/overdue.js.map +7 -0
  273. package/dist/modules/billing/lib/paymentCsv.js +125 -0
  274. package/dist/modules/billing/lib/paymentCsv.js.map +7 -0
  275. package/dist/modules/billing/lib/paymentGateway.js +15 -0
  276. package/dist/modules/billing/lib/paymentGateway.js.map +7 -0
  277. package/dist/modules/billing/lib/paymentImport.js +164 -0
  278. package/dist/modules/billing/lib/paymentImport.js.map +7 -0
  279. package/dist/modules/billing/lib/paymentMatcher.js +36 -0
  280. package/dist/modules/billing/lib/paymentMatcher.js.map +7 -0
  281. package/dist/modules/billing/lib/pdf/attachmentStore.js +46 -0
  282. package/dist/modules/billing/lib/pdf/attachmentStore.js.map +7 -0
  283. package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js +7 -0
  284. package/dist/modules/billing/lib/pdf/fonts/embeddedFonts.js.map +7 -0
  285. package/dist/modules/billing/lib/pdf/invoiceModel.js +194 -0
  286. package/dist/modules/billing/lib/pdf/invoiceModel.js.map +7 -0
  287. package/dist/modules/billing/lib/pdf/invoicePdf.js +165 -0
  288. package/dist/modules/billing/lib/pdf/invoicePdf.js.map +7 -0
  289. package/dist/modules/billing/lib/pdf/invoiceTemplate.js +274 -0
  290. package/dist/modules/billing/lib/pdf/invoiceTemplate.js.map +7 -0
  291. package/dist/modules/billing/lib/prepaidConfig.js +17 -25
  292. package/dist/modules/billing/lib/prepaidConfig.js.map +2 -2
  293. package/dist/modules/billing/lib/prepaidConsume.js +26 -7
  294. package/dist/modules/billing/lib/prepaidConsume.js.map +2 -2
  295. package/dist/modules/billing/lib/prepaidPeriodClose.js +277 -158
  296. package/dist/modules/billing/lib/prepaidPeriodClose.js.map +2 -2
  297. package/dist/modules/billing/lib/prepaidTaxPoint.js +37 -0
  298. package/dist/modules/billing/lib/prepaidTaxPoint.js.map +7 -0
  299. package/dist/modules/billing/lib/seeds/demo.js +51 -3
  300. package/dist/modules/billing/lib/seeds/demo.js.map +2 -2
  301. package/dist/modules/billing/lib/seeds/paymentsSample.js +93 -0
  302. package/dist/modules/billing/lib/seeds/paymentsSample.js.map +7 -0
  303. package/dist/modules/billing/lib/seeds/sellerSettings.js +41 -0
  304. package/dist/modules/billing/lib/seeds/sellerSettings.js.map +7 -0
  305. package/dist/modules/billing/lib/servicePeriods.js +121 -0
  306. package/dist/modules/billing/lib/servicePeriods.js.map +7 -0
  307. package/dist/modules/billing/lib/testRunCleanup.js +24 -0
  308. package/dist/modules/billing/lib/testRunCleanup.js.map +7 -0
  309. package/dist/modules/billing/lib/topupCapture.js +2 -2
  310. package/dist/modules/billing/lib/topupCapture.js.map +2 -2
  311. package/dist/modules/billing/lib/topupReceipt.js +58 -79
  312. package/dist/modules/billing/lib/topupReceipt.js.map +2 -2
  313. package/dist/modules/billing/lib/usageRater.js.map +1 -1
  314. package/dist/modules/billing/lib/usageRunner.js +14 -6
  315. package/dist/modules/billing/lib/usageRunner.js.map +2 -2
  316. package/dist/modules/billing/lib/vat.js +39 -0
  317. package/dist/modules/billing/lib/vat.js.map +7 -0
  318. package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js +15 -0
  319. package/dist/modules/billing/migrations/Migration20261114000000_billing_customer_kind.js.map +7 -0
  320. package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js +25 -0
  321. package/dist/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.js.map +7 -0
  322. package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js +115 -0
  323. package/dist/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.js.map +7 -0
  324. package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js +47 -0
  325. package/dist/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.js.map +7 -0
  326. package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js +89 -0
  327. package/dist/modules/billing/migrations/Migration20261117000000_billing_payments.js.map +7 -0
  328. package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js +17 -0
  329. package/dist/modules/billing/migrations/Migration20261118000000_billing_memos.js.map +7 -0
  330. package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js +27 -0
  331. package/dist/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.js.map +7 -0
  332. package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js +29 -0
  333. package/dist/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.js.map +7 -0
  334. package/dist/modules/billing/setup.js +7 -184
  335. package/dist/modules/billing/setup.js.map +2 -2
  336. package/dist/modules/billing/subscribers/credit-memo-auto-send.js +16 -0
  337. package/dist/modules/billing/subscribers/credit-memo-auto-send.js.map +7 -0
  338. package/dist/modules/billing/subscribers/debit-memo-apply-credit.js +16 -0
  339. package/dist/modules/billing/subscribers/debit-memo-apply-credit.js.map +7 -0
  340. package/dist/modules/billing/subscribers/debit-memo-auto-send.js +16 -0
  341. package/dist/modules/billing/subscribers/debit-memo-auto-send.js.map +7 -0
  342. package/dist/modules/billing/subscribers/invoice-apply-credit.js +34 -0
  343. package/dist/modules/billing/subscribers/invoice-apply-credit.js.map +7 -0
  344. package/dist/modules/billing/subscribers/invoice-auto-send.js +30 -0
  345. package/dist/modules/billing/subscribers/invoice-auto-send.js.map +7 -0
  346. package/dist/modules/billing/subscribers/payment-captured-to-paid.js +69 -33
  347. package/dist/modules/billing/subscribers/payment-captured-to-paid.js.map +2 -2
  348. package/dist/modules/billing/subscribers/prepaid-topup-captured.js +4 -6
  349. package/dist/modules/billing/subscribers/prepaid-topup-captured.js.map +2 -2
  350. package/generated/entities/billing_account/index.ts +3 -0
  351. package/generated/entities/billing_allocation/index.ts +18 -0
  352. package/generated/entities/billing_document_sequence/index.ts +11 -0
  353. package/generated/entities/billing_invoice/index.ts +38 -0
  354. package/generated/entities/billing_invoice_line/index.ts +24 -0
  355. package/generated/entities/billing_item/index.ts +4 -0
  356. package/generated/entities/billing_payment/index.ts +25 -0
  357. package/generated/entities/billing_payment_import/index.ts +14 -0
  358. package/generated/entities/billing_run/index.ts +0 -1
  359. package/generated/entities/billing_run_outcome/index.ts +1 -0
  360. package/generated/entities/billing_settings/index.ts +26 -0
  361. package/generated/entities/billing_statement/index.ts +1 -0
  362. package/generated/entities.ids.generated.ts +7 -0
  363. package/package.json +5 -1
  364. package/src/modules/billing/__integration__/TC-BILL-004.spec.ts +170 -0
  365. package/src/modules/billing/__integration__/TC-BILL-005.spec.ts +134 -0
  366. package/src/modules/billing/__integration__/TC-BILL-006.spec.ts +101 -0
  367. package/src/modules/billing/__integration__/TC-BILL-007.spec.ts +140 -0
  368. package/src/modules/billing/__integration__/TC-BILL-008.spec.ts +108 -0
  369. package/src/modules/billing/__integration__/TC-BILL-009.spec.ts +113 -0
  370. package/src/modules/billing/__integration__/TC-BILL-010.spec.ts +118 -0
  371. package/src/modules/billing/__integration__/TC-BILL-011.spec.ts +142 -0
  372. package/src/modules/billing/__integration__/TC-BILL-012.spec.ts +115 -0
  373. package/src/modules/billing/__integration__/TC-BILL-013.spec.ts +118 -0
  374. package/src/modules/billing/__integration__/sellerSettings.ts +24 -0
  375. package/src/modules/billing/__tests__/commands/invoices.test.ts +436 -354
  376. package/src/modules/billing/__tests__/commands/itemsBulk.test.ts +33 -0
  377. package/src/modules/billing/__tests__/commands/memos.test.ts +205 -0
  378. package/src/modules/billing/__tests__/commands/payments.test.ts +241 -0
  379. package/src/modules/billing/__tests__/commands/runsDelete.test.ts +146 -0
  380. package/src/modules/billing/__tests__/commands/runsPostDrafts.test.ts +106 -0
  381. package/src/modules/billing/__tests__/commands/topups.test.ts +11 -0
  382. package/src/modules/billing/__tests__/entityRegistry.test.ts +8 -1
  383. package/src/modules/billing/__tests__/helpers/prepaidMockEm.ts +28 -11
  384. package/src/modules/billing/__tests__/lib/accountFinancials.test.ts +53 -0
  385. package/src/modules/billing/__tests__/lib/accountSummary.test.ts +97 -0
  386. package/src/modules/billing/__tests__/lib/allocation.test.ts +319 -0
  387. package/src/modules/billing/__tests__/lib/billRunEngine.test.ts +228 -17
  388. package/src/modules/billing/__tests__/lib/billingSettings.test.ts +109 -0
  389. package/src/modules/billing/__tests__/lib/customerLookup.test.ts +156 -0
  390. package/src/modules/billing/__tests__/lib/dueDate.test.ts +35 -0
  391. package/src/modules/billing/__tests__/lib/iban.test.ts +38 -0
  392. package/src/modules/billing/__tests__/lib/invoiceDelivery.test.ts +189 -0
  393. package/src/modules/billing/__tests__/lib/invoiceWriter.test.ts +212 -10
  394. package/src/modules/billing/__tests__/lib/itemBillingState.test.ts +42 -0
  395. package/src/modules/billing/__tests__/lib/itemPriceChange.test.ts +165 -0
  396. package/src/modules/billing/__tests__/lib/lineAmounts.test.ts +86 -0
  397. package/src/modules/billing/__tests__/lib/navigation.test.ts +71 -0
  398. package/src/modules/billing/__tests__/lib/numbering.test.ts +91 -0
  399. package/src/modules/billing/__tests__/lib/overdue.test.ts +36 -0
  400. package/src/modules/billing/__tests__/lib/paymentCsv.test.ts +148 -0
  401. package/src/modules/billing/__tests__/lib/paymentImport.test.ts +113 -0
  402. package/src/modules/billing/__tests__/lib/paymentsSample.test.ts +73 -0
  403. package/src/modules/billing/__tests__/lib/pdf/invoicePdf.test.ts +199 -0
  404. package/src/modules/billing/__tests__/lib/pdf/invoicePdfService.test.ts +147 -0
  405. package/src/modules/billing/__tests__/lib/prepaidPeriodClose.test.ts +374 -98
  406. package/src/modules/billing/__tests__/lib/prepaidTaxPoint.test.ts +118 -0
  407. package/src/modules/billing/__tests__/lib/seedsDemo.test.ts +53 -2
  408. package/src/modules/billing/__tests__/lib/sellerSettings.test.ts +28 -0
  409. package/src/modules/billing/__tests__/lib/servicePeriods.test.ts +190 -0
  410. package/src/modules/billing/__tests__/lib/topupCapture.test.ts +6 -28
  411. package/src/modules/billing/__tests__/lib/topupReceipt.test.ts +21 -52
  412. package/src/modules/billing/__tests__/lib/vat.test.ts +51 -0
  413. package/src/modules/billing/__tests__/package-isolation.test.ts +22 -8
  414. package/src/modules/billing/__tests__/setup.test.ts +13 -4
  415. package/src/modules/billing/__tests__/validators.test.ts +193 -3
  416. package/src/modules/billing/acl.ts +12 -0
  417. package/src/modules/billing/api/accounts/[id]/summary/route.ts +62 -0
  418. package/src/modules/billing/api/accounts/route.ts +58 -4
  419. package/src/modules/billing/api/export/account/[id]/route.ts +13 -11
  420. package/src/modules/billing/api/invoices/[id]/memos/route.ts +63 -0
  421. package/src/modules/billing/api/invoices/[id]/pdf/route.ts +79 -0
  422. package/src/modules/billing/api/invoices/[id]/route.ts +57 -70
  423. package/src/modules/billing/api/invoices/[id]/send/route.ts +127 -0
  424. package/src/modules/billing/api/invoices/add-line/route.ts +2 -2
  425. package/src/modules/billing/api/invoices/delete-draft/route.ts +33 -0
  426. package/src/modules/billing/api/invoices/edit-line/route.ts +2 -2
  427. package/src/modules/billing/api/invoices/post/route.ts +7 -6
  428. package/src/modules/billing/api/invoices/remove-line/route.ts +2 -2
  429. package/src/modules/billing/api/invoices/route.ts +81 -86
  430. package/src/modules/billing/api/items/bulk/route.ts +2 -1
  431. package/src/modules/billing/api/items/change-price/route.ts +54 -0
  432. package/src/modules/billing/api/items/route.ts +23 -7
  433. package/src/modules/billing/api/payment-imports/[id]/route.ts +49 -0
  434. package/src/modules/billing/api/payment-imports/route.ts +57 -0
  435. package/src/modules/billing/api/payments/[id]/route.ts +63 -0
  436. package/src/modules/billing/api/payments/allocate/route.ts +35 -0
  437. package/src/modules/billing/api/payments/assign/route.ts +36 -0
  438. package/src/modules/billing/api/payments/convert-to-topup/route.ts +35 -0
  439. package/src/modules/billing/api/payments/ignore/route.ts +35 -0
  440. package/src/modules/billing/api/payments/import/route.ts +72 -0
  441. package/src/modules/billing/api/payments/restore/route.ts +35 -0
  442. package/src/modules/billing/api/payments/route.ts +147 -0
  443. package/src/modules/billing/api/payments/unallocate/route.ts +35 -0
  444. package/src/modules/billing/api/payments/unassign/route.ts +35 -0
  445. package/src/modules/billing/api/run-outcomes/route.ts +49 -6
  446. package/src/modules/billing/api/runs/[id]/post-drafts/route.ts +71 -0
  447. package/src/modules/billing/api/runs/[id]/route.ts +59 -0
  448. package/src/modules/billing/api/runs/retry-failed/route.ts +2 -1
  449. package/src/modules/billing/api/runs/route.ts +15 -7
  450. package/src/modules/billing/api/settings/number-preview/route.ts +46 -0
  451. package/src/modules/billing/api/settings/route.ts +116 -0
  452. package/src/modules/billing/api/statements/route.ts +5 -0
  453. package/src/modules/billing/api/test-invoices/wipe/route.ts +2 -2
  454. package/src/modules/billing/backend/billing/accounts/[id]/page.tsx +151 -372
  455. package/src/modules/billing/backend/billing/accounts/create/page.tsx +6 -0
  456. package/src/modules/billing/backend/billing/accounts/page.tsx +70 -4
  457. package/src/modules/billing/backend/billing/invoices/[id]/page.tsx +444 -95
  458. package/src/modules/billing/backend/billing/invoices/page.tsx +8 -227
  459. package/src/modules/billing/backend/billing/items/[id]/page.tsx +75 -6
  460. package/src/modules/billing/backend/billing/items/create/page.tsx +13 -8
  461. package/src/modules/billing/backend/billing/items/page.tsx +18 -2
  462. package/src/modules/billing/backend/billing/payments/[id]/page.meta.ts +19 -0
  463. package/src/modules/billing/backend/billing/payments/[id]/page.tsx +415 -0
  464. package/src/modules/billing/backend/billing/payments/imports/[id]/page.meta.ts +19 -0
  465. package/src/modules/billing/backend/billing/payments/imports/[id]/page.tsx +106 -0
  466. package/src/modules/billing/backend/billing/payments/page.meta.ts +16 -0
  467. package/src/modules/billing/backend/billing/payments/page.tsx +56 -0
  468. package/src/modules/billing/backend/billing/runs/[id]/page.tsx +49 -18
  469. package/src/modules/billing/backend/billing/runs/page.tsx +59 -8
  470. package/src/modules/billing/backend/billing/statements/[id]/page.tsx +86 -5
  471. package/src/modules/billing/backend/billing/statements/page.tsx +1 -1
  472. package/src/modules/billing/backend/billing/topups/page.tsx +1 -1
  473. package/src/modules/billing/backend/billing/transactions/page.tsx +1 -1
  474. package/src/modules/billing/backend/billing/usage/create/page.tsx +3 -2
  475. package/src/modules/billing/backend/config/billing/page.meta.ts +21 -0
  476. package/src/modules/billing/backend/config/billing/page.tsx +549 -0
  477. package/src/modules/billing/cli.ts +31 -1
  478. package/src/modules/billing/commands/accounts.ts +6 -0
  479. package/src/modules/billing/commands/index.ts +3 -0
  480. package/src/modules/billing/commands/invoices.ts +364 -231
  481. package/src/modules/billing/commands/items.ts +143 -0
  482. package/src/modules/billing/commands/memos.ts +218 -0
  483. package/src/modules/billing/commands/payments.ts +428 -0
  484. package/src/modules/billing/commands/runs.ts +205 -3
  485. package/src/modules/billing/commands/settings.ts +77 -0
  486. package/src/modules/billing/commands/topups.ts +12 -3
  487. package/src/modules/billing/commands/transactions.ts +2 -2
  488. package/src/modules/billing/components/AccountForm.tsx +199 -53
  489. package/src/modules/billing/components/AccountInvoicesSection.tsx +75 -0
  490. package/src/modules/billing/components/AccountPaymentsSection.tsx +42 -0
  491. package/src/modules/billing/components/AccountPicker.tsx +70 -0
  492. package/src/modules/billing/components/AllocatePaymentDialog.tsx +167 -0
  493. package/src/modules/billing/components/BillingDataTable.tsx +34 -0
  494. package/src/modules/billing/components/BillingSettingsLink.tsx +25 -0
  495. package/src/modules/billing/components/ChangeItemPriceDialog.tsx +180 -0
  496. package/src/modules/billing/components/CreateMemoDialog.tsx +232 -0
  497. package/src/modules/billing/components/CustomerLabel.tsx +53 -0
  498. package/src/modules/billing/components/InvoicesTable.tsx +344 -0
  499. package/src/modules/billing/components/ItemForm.tsx +233 -44
  500. package/src/modules/billing/components/ItemPriceField.tsx +81 -0
  501. package/src/modules/billing/components/LineFormDialog.tsx +25 -1
  502. package/src/modules/billing/components/ManualPaymentDialog.tsx +140 -0
  503. package/src/modules/billing/components/MoneyInput.tsx +26 -0
  504. package/src/modules/billing/components/PaymentUploadDialog.tsx +148 -0
  505. package/src/modules/billing/components/PaymentsTable.tsx +213 -0
  506. package/src/modules/billing/components/PostRunDraftsButton.tsx +129 -0
  507. package/src/modules/billing/components/PrepaidPanel.tsx +19 -6
  508. package/src/modules/billing/components/RunCyclesSection.tsx +298 -0
  509. package/src/modules/billing/components/TestRunActions.tsx +152 -0
  510. package/src/modules/billing/components/account/AccountItemsTab.tsx +192 -0
  511. package/src/modules/billing/components/account/AccountRunBillingDialog.tsx +147 -0
  512. package/src/modules/billing/components/account/AccountRunsTab.tsx +201 -0
  513. package/src/modules/billing/components/account/AccountStatementsTab.tsx +120 -0
  514. package/src/modules/billing/components/account/AccountSummaryCards.tsx +153 -0
  515. package/src/modules/billing/components/account/AccountUsageTab.tsx +122 -0
  516. package/src/modules/billing/components/account/AccountView.tsx +101 -0
  517. package/src/modules/billing/components/account/accountRow.ts +70 -0
  518. package/src/modules/billing/components/customerNames.ts +62 -0
  519. package/src/modules/billing/data/entities.ts +654 -5
  520. package/src/modules/billing/data/entityIds.ts +9 -0
  521. package/src/modules/billing/data/validators.ts +388 -8
  522. package/src/modules/billing/events.ts +56 -3
  523. package/src/modules/billing/i18n/en.json +457 -21
  524. package/src/modules/billing/i18n/pl.json +457 -21
  525. package/src/modules/billing/index.ts +6 -2
  526. package/src/modules/billing/lib/accountFinancials.ts +84 -0
  527. package/src/modules/billing/lib/accountLock.ts +32 -0
  528. package/src/modules/billing/lib/accountStatus.ts +11 -9
  529. package/src/modules/billing/lib/accountSummary.ts +172 -0
  530. package/src/modules/billing/lib/allocation.ts +515 -0
  531. package/src/modules/billing/lib/allocationEvents.ts +26 -0
  532. package/src/modules/billing/lib/billPeriod.ts +19 -0
  533. package/src/modules/billing/lib/billRunEngine.ts +350 -150
  534. package/src/modules/billing/lib/billingSettings.ts +145 -0
  535. package/src/modules/billing/lib/commandRoute.ts +112 -0
  536. package/src/modules/billing/lib/customerLookup.ts +157 -0
  537. package/src/modules/billing/lib/dateOnly.ts +22 -0
  538. package/src/modules/billing/lib/documentParties.ts +23 -0
  539. package/src/modules/billing/lib/documentTypes.ts +37 -0
  540. package/src/modules/billing/lib/dueDate.ts +26 -0
  541. package/src/modules/billing/lib/iban.ts +36 -0
  542. package/src/modules/billing/lib/invoiceDelivery.ts +197 -0
  543. package/src/modules/billing/lib/invoiceLanguages.ts +34 -0
  544. package/src/modules/billing/lib/invoiceWriter.ts +162 -148
  545. package/src/modules/billing/lib/itemBillingState.ts +67 -0
  546. package/src/modules/billing/lib/itemPriceChange.ts +56 -0
  547. package/src/modules/billing/lib/itemSelector.ts +4 -1
  548. package/src/modules/billing/lib/lineAmounts.ts +61 -0
  549. package/src/modules/billing/lib/memoGuards.ts +41 -0
  550. package/src/modules/billing/lib/navigation.ts +64 -0
  551. package/src/modules/billing/lib/numbering.ts +174 -0
  552. package/src/modules/billing/lib/overdue.ts +60 -0
  553. package/src/modules/billing/lib/paymentCsv.ts +171 -0
  554. package/src/modules/billing/lib/paymentGateway.ts +52 -0
  555. package/src/modules/billing/lib/paymentImport.ts +228 -0
  556. package/src/modules/billing/lib/paymentMatcher.ts +88 -0
  557. package/src/modules/billing/lib/pdf/attachmentStore.ts +60 -0
  558. package/src/modules/billing/lib/pdf/fonts/NotoSans-Bold.ttf +0 -0
  559. package/src/modules/billing/lib/pdf/fonts/NotoSans-Regular.ttf +0 -0
  560. package/src/modules/billing/lib/pdf/fonts/OFL.txt +93 -0
  561. package/src/modules/billing/lib/pdf/fonts/embeddedFonts.ts +9 -0
  562. package/src/modules/billing/lib/pdf/invoiceModel.ts +328 -0
  563. package/src/modules/billing/lib/pdf/invoicePdf.ts +216 -0
  564. package/src/modules/billing/lib/pdf/invoiceTemplate.ts +338 -0
  565. package/src/modules/billing/lib/prepaidConfig.ts +27 -36
  566. package/src/modules/billing/lib/prepaidConsume.ts +29 -6
  567. package/src/modules/billing/lib/prepaidPeriodClose.ts +407 -239
  568. package/src/modules/billing/lib/prepaidTaxPoint.ts +76 -0
  569. package/src/modules/billing/lib/seeds/demo.ts +79 -6
  570. package/src/modules/billing/lib/seeds/paymentsSample.ts +136 -0
  571. package/src/modules/billing/lib/seeds/sellerSettings.ts +78 -0
  572. package/src/modules/billing/lib/servicePeriods.ts +226 -0
  573. package/src/modules/billing/lib/testRunCleanup.ts +42 -0
  574. package/src/modules/billing/lib/topupCapture.ts +3 -4
  575. package/src/modules/billing/lib/topupReceipt.ts +70 -103
  576. package/src/modules/billing/lib/usageRater.ts +1 -1
  577. package/src/modules/billing/lib/usageRunner.ts +17 -5
  578. package/src/modules/billing/lib/vat.ts +56 -0
  579. package/src/modules/billing/migrations/Migration20261114000000_billing_customer_kind.ts +25 -0
  580. package/src/modules/billing/migrations/Migration20261114100000_billing_cycle_semantics.ts +37 -0
  581. package/src/modules/billing/migrations/Migration20261115000000_billing_invoice_ledger.ts +132 -0
  582. package/src/modules/billing/migrations/Migration20261116000000_billing_invoice_documents.ts +58 -0
  583. package/src/modules/billing/migrations/Migration20261117000000_billing_payments.ts +97 -0
  584. package/src/modules/billing/migrations/Migration20261118000000_billing_memos.ts +21 -0
  585. package/src/modules/billing/migrations/Migration20261119000000_billing_operator_feedback.ts +39 -0
  586. package/src/modules/billing/migrations/Migration20261120000000_billing_allocation_scope_indexes.ts +35 -0
  587. package/src/modules/billing/setup.ts +11 -237
  588. package/src/modules/billing/subscribers/credit-memo-auto-send.ts +15 -0
  589. package/src/modules/billing/subscribers/debit-memo-apply-credit.ts +15 -0
  590. package/src/modules/billing/subscribers/debit-memo-auto-send.ts +15 -0
  591. package/src/modules/billing/subscribers/invoice-apply-credit.ts +39 -0
  592. package/src/modules/billing/subscribers/invoice-auto-send.ts +46 -0
  593. package/src/modules/billing/subscribers/payment-captured-to-paid.ts +84 -62
  594. package/src/modules/billing/subscribers/prepaid-topup-captured.ts +4 -8
  595. package/dist/modules/billing/lib/invoiceStatus.js +0 -38
  596. package/dist/modules/billing/lib/invoiceStatus.js.map +0 -7
  597. package/src/modules/billing/__tests__/lib/invoiceStatus.test.ts +0 -97
  598. package/src/modules/billing/lib/invoiceStatus.ts +0 -61
@@ -1,23 +1,33 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import type { AwilixContainer } from 'awilix'
3
+ import { UniqueConstraintViolationException } from '@mikro-orm/core'
3
4
  import {
4
5
  BillingAccount,
5
6
  BillingAccountUsage,
7
+ BillingInvoice,
6
8
  BillingItem,
7
9
  BillingRun,
8
10
  BillingRunOutcome,
9
11
  } from '../data/entities'
10
- import { advanceNextBillDate, deriveBillPeriod, isCycleDue } from './billPeriod'
11
12
  import {
12
- selectItemsForPeriod,
13
- type SelectableItem,
14
- } from './itemSelector'
13
+ addDays,
14
+ advanceNextBillDate,
15
+ cloneAtMidnight,
16
+ deriveBillPeriod,
17
+ isCycleDue,
18
+ shiftCycles,
19
+ type BillPeriod,
20
+ } from './billPeriod'
21
+ import { shouldIncludeItem, type SelectableItem } from './itemSelector'
15
22
  import {
16
23
  buildInvoiceLinesFromItems,
24
+ buildRecurringLineDescriptors,
25
+ displayPeriod,
17
26
  writeDraftInvoice,
18
27
  type InvoiceLineDescriptor,
19
28
  type WriteMode,
20
29
  } from './invoiceWriter'
30
+ import { computeRecurringLines, type RecurringSkip } from './servicePeriods'
21
31
  import { withTenantLock } from './tenantLock'
22
32
  import { processUsageForAccount } from './usageRunner'
23
33
  import { processPrepaidAccount } from './prepaidPeriodClose'
@@ -39,23 +49,24 @@ import type { BillCycle } from '../data/validators'
39
49
  * nested `em.transactional(...)` (PG SAVEPOINT). A per-account failure
40
50
  * rolls back only that account's work, leaving siblings unaffected.
41
51
  *
42
- * Run modes:
43
- * - `real` (default) — persist drafts, advance `next_bill_date`, mark
44
- * items `billed_to_date`. Production path.
45
- * - `test` — persist drafts flagged with `metadata.test_run=true`, but
46
- * do NOT advance state. Repeatable.
47
- * - `dry` — compute everything; persist nothing. Preview only.
52
+ * Cycles (Spec A): every run — manual or scheduled, any mode — bills ALL
53
+ * due bill dates up to `asOfDate`, one draft per bill date (never
54
+ * combined), capped at `CATCH_UP_HARD_CAP` per account. The account's
55
+ * `next_bill_date` and each recurring item's `billed_to_date` watermark are
56
+ * advanced in memory cycle by cycle; only `real` persists them:
57
+ * - `real` — persist drafts, advance `next_bill_date` / watermarks, mark
58
+ * usage rated. Production path.
59
+ * - `test` — persist drafts flagged `metadata.test_run=true` for every
60
+ * cycle, but do NOT advance state. Repeatable.
61
+ * - `dry` — compute every cycle; persist nothing. Preview only.
48
62
  *
49
- * Catch-up:
50
- * - Manual triggers may set `catchUp=true`. The engine then loops cycles
51
- * per account until `next_bill_date > as_of_date`, emitting one draft
52
- * per missed cycle (never combined).
53
- * - Scheduled triggers always process exactly ONE cycle per account per
54
- * run, regardless of how far behind the account is.
63
+ * Recurring items are priced by `servicePeriods.computeRecurringLines`
64
+ * (in advance / in arrears, proration, credits). `one_time` items use the
65
+ * selector; usage is always billed in arrears for `[D − cycle, D − 1]`.
55
66
  *
56
- * Anti-duplicate guards (both run in every mode except `dry`):
57
- * 1. Open-draft check on `core/sales` invoice (per period boundary).
58
- * 2. `billed_to_date` per BillingItem.
67
+ * Anti-duplicate guards (every mode except `dry`):
68
+ * 1. Open draft for the same `(account, bill_date, test_run)`.
69
+ * 2. `billed_to_date` per BillingItem (the watermark).
59
70
  */
60
71
 
61
72
  export type RunMode = WriteMode
@@ -68,7 +79,6 @@ export type RunBillRunParams = {
68
79
  mode: RunMode
69
80
  scopedAccountIds?: string[]
70
81
  parentRunId?: string | null
71
- catchUp?: boolean
72
82
  }
73
83
 
74
84
  export type RunBillRunResult = {
@@ -81,9 +91,38 @@ type RunOutput = RunBillRunResult & {
81
91
  pendingEvents: DomainEvent[]
82
92
  }
83
93
 
94
+ type SkippedItemWarning = {
95
+ item_id: string
96
+ reason: string
97
+ service_period_start: string | null
98
+ service_period_end: string | null
99
+ }
100
+
84
101
  type AccountWarnings = {
85
102
  currency_mismatch_items?: string[]
86
103
  unmatched_usage_uoms?: string[]
104
+ /** Spec A: bill dates whose draft has a negative total (credits > charges). */
105
+ negative_total_bill_dates?: string[]
106
+ /** Spec A: the account hit CATCH_UP_HARD_CAP; later cycles wait for the next run. */
107
+ catch_up_cap_reached?: boolean
108
+ /** Spec A: diagnostic — partial periods not prorated, no-credit ends, caps. */
109
+ skipped_items?: SkippedItemWarning[]
110
+ }
111
+
112
+ /** `charged` is the prepaid analogue of `invoiced` (Spec F, P-D6). */
113
+ export type CycleStatus = 'invoiced' | 'charged' | 'previewed' | 'empty' | 'skipped_existing'
114
+
115
+ export type CycleRecord = {
116
+ bill_date: string
117
+ period_start: string | null
118
+ period_end: string | null
119
+ status: CycleStatus
120
+ line_count: number
121
+ total_net: string
122
+ invoice_id: string | null
123
+ /** Prepaid only: what left the balance, and the statement for the period. */
124
+ total_debit?: string
125
+ statement_id?: string | null
87
126
  }
88
127
 
89
128
  type AccountProcessSuccess = {
@@ -99,6 +138,8 @@ type AccountProcessSuccess = {
99
138
  statementEvents?: DomainEvent[]
100
139
  cyclesEmitted: number
101
140
  usageRecordsRated: number
141
+ /** Spec A: per-bill-date breakdown (postpaid). */
142
+ cycles?: CycleRecord[]
102
143
  }
103
144
 
104
145
  type AccountProcessFailed = {
@@ -108,8 +149,10 @@ type AccountProcessFailed = {
108
149
 
109
150
  type AccountProcessResult = AccountProcessSuccess | AccountProcessFailed
110
151
 
111
- const SCHEDULED_CYCLE_LIMIT = 1
112
- const CATCH_UP_HARD_CAP = 60 // safety net so a misconfigured account can't loop forever
152
+ export const CATCH_UP_HARD_CAP = 60 // safety net so a misconfigured account can't loop forever
153
+ const SKIPPED_ITEMS_WARNING_CAP = 200
154
+ /** Skip reasons that are normal state, not worth surfacing on the outcome. */
155
+ const QUIET_SKIP_REASONS = new Set(['item-inactive', 'item-soft-deleted'])
113
156
 
114
157
  export async function runBillRun(
115
158
  em: EntityManager,
@@ -143,7 +186,6 @@ async function executeRunWithinLock(
143
186
  parentRunId: params.parentRunId ?? null,
144
187
  dryRun: params.mode === 'dry',
145
188
  testMode: params.mode === 'test',
146
- catchUp: Boolean(params.catchUp),
147
189
  scopedAccountIds: params.scopedAccountIds ?? null,
148
190
  asOfDate: params.asOfDate,
149
191
  startedAt: now,
@@ -179,6 +221,7 @@ async function executeRunWithinLock(
179
221
  : (processResult.warnings as Record<string, unknown> | null),
180
222
  draftInvoiceId: processResult.status === 'failed' ? null : processResult.draftInvoiceId,
181
223
  statementId: processResult.status === 'failed' ? null : (processResult.statementId ?? null),
224
+ cycles: processResult.status === 'failed' ? null : (processResult.cycles ?? null),
182
225
  isActive: true,
183
226
  createdAt: new Date(),
184
227
  updatedAt: new Date(),
@@ -253,7 +296,6 @@ async function processAccount(
253
296
  asOfDate: params.asOfDate,
254
297
  mode: params.mode,
255
298
  triggeredBy: params.triggeredBy,
256
- catchUp: params.catchUp,
257
299
  })
258
300
  if (result.status === 'failed') {
259
301
  return { status: 'failed', errorMessage: result.errorMessage }
@@ -265,111 +307,236 @@ async function processAccount(
265
307
  statementId: result.statementId,
266
308
  statementEvents: result.statementEvents,
267
309
  cyclesEmitted: result.cyclesEmitted,
310
+ cycles: result.cycles,
268
311
  usageRecordsRated: 0,
269
312
  }
270
313
  }
271
314
 
272
- const maxCycles =
273
- params.triggeredBy === 'schedule' || !params.catchUp
274
- ? SCHEDULED_CYCLE_LIMIT
275
- : CATCH_UP_HARD_CAP
315
+ return processPostpaidAccount(em, container, billRun, account, params)
316
+ }
317
+
318
+ function isoDate(date: Date): string {
319
+ return cloneAtMidnight(date).toISOString().slice(0, 10)
320
+ }
321
+
322
+ function toSkipWarning(skip: RecurringSkip): SkippedItemWarning {
323
+ return {
324
+ item_id: skip.billingItemId,
325
+ reason: skip.reason,
326
+ service_period_start: skip.servicePeriodStart ? isoDate(skip.servicePeriodStart) : null,
327
+ service_period_end: skip.servicePeriodEnd ? isoDate(skip.servicePeriodEnd) : null,
328
+ }
329
+ }
276
330
 
277
- let cyclesEmitted = 0
331
+ /**
332
+ * Postpaid cycle loop (Spec A). Items are loaded once; the bill date and
333
+ * the per-item watermarks advance in memory so test / dry can simulate every
334
+ * due cycle exactly like real does — only persistence differs by mode.
335
+ */
336
+ async function processPostpaidAccount(
337
+ em: EntityManager,
338
+ container: AwilixContainer,
339
+ billRun: BillingRun,
340
+ account: BillingAccount,
341
+ params: RunBillRunParams,
342
+ ): Promise<AccountProcessResult> {
343
+ const cycle = account.billCycle as BillCycle
344
+ const testRun = params.mode === 'test'
345
+ const items = (await em.find(BillingItem, {
346
+ tenantId: account.tenantId,
347
+ organizationId: account.organizationId,
348
+ billAccountId: account.id,
349
+ deletedAt: null,
350
+ } as never)) as BillingItem[]
351
+ const recurringItems = items.filter((it) => it.type === 'recurring')
352
+ const oneTimeItems = items.filter((it) => it.type === 'one_time')
353
+ const usageItems = items.filter((it) => it.type === 'usage')
354
+
355
+ // In-memory cycle state — persisted per cycle only in real mode.
356
+ const watermarks = new Map<string, Date | null>(
357
+ recurringItems.map((it) => [it.id, it.billedToDate ? cloneAtMidnight(it.billedToDate) : null]),
358
+ )
359
+ const billedOneTimes = new Set<string>(oneTimeItems.filter((it) => it.billedToDate).map((it) => it.id))
360
+ let billDate = cloneAtMidnight(account.nextBillDate)
361
+ // Usage already counted by an earlier cycle of THIS run (test / dry never
362
+ // mark records, so later simulated cycles must not re-count them).
363
+ let usageCountedThrough: Date | null = null
364
+
365
+ const cycles: CycleRecord[] = []
366
+ const warnings: AccountWarnings = {}
367
+ const skippedItems: SkippedItemWarning[] = []
278
368
  let lastDraftInvoiceId: string | null = null
279
- let totalUsageRatedThisAccount = 0
280
- const aggregatedWarnings: AccountWarnings = {}
281
-
282
- while (cyclesEmitted < maxCycles && isCycleDue(account.nextBillDate, params.asOfDate)) {
283
- const period = deriveBillPeriod(account.nextBillDate, account.billCycle as BillCycle)
284
-
285
- // Open-draft anti-duplicate — only applies when we'd actually persist.
286
- if (params.mode !== 'dry') {
287
- const existing = await findExistingDraftForPeriod(em, account, period)
288
- if (existing) {
289
- return {
290
- status: 'skipped_existing_draft',
291
- warnings: null,
292
- draftInvoiceId: existing,
293
- cyclesEmitted,
294
- usageRecordsRated: totalUsageRatedThisAccount,
295
- }
369
+ let usageRecordsRated = 0
370
+
371
+ const pushSkips = (skips: readonly RecurringSkip[]) => {
372
+ for (const skip of skips) {
373
+ if (QUIET_SKIP_REASONS.has(skip.reason)) continue
374
+ if (skippedItems.length < SKIPPED_ITEMS_WARNING_CAP) skippedItems.push(toSkipWarning(skip))
375
+ }
376
+ }
377
+ const addWarning = (key: 'currency_mismatch_items' | 'unmatched_usage_uoms' | 'negative_total_bill_dates', values: string[]) => {
378
+ if (values.length === 0) return
379
+ warnings[key] = Array.from(new Set([...(warnings[key] ?? []), ...values]))
380
+ }
381
+
382
+ while (isCycleDue(billDate, params.asOfDate)) {
383
+ if (cycles.length >= CATCH_UP_HARD_CAP) {
384
+ warnings.catch_up_cap_reached = true
385
+ break
386
+ }
387
+ const nextBillDate = advanceNextBillDate(billDate, cycle)
388
+ const arrearsPeriod: BillPeriod = deriveBillPeriod(billDate, cycle)
389
+
390
+ // ── Recurring: service periods against the in-memory watermarks ──
391
+ const lines: InvoiceLineDescriptor[] = []
392
+ const watermarkUpdates = new Map<string, Date | null>()
393
+ const chargedItems: BillingItem[] = []
394
+ for (const item of recurringItems) {
395
+ const unitPrice = (item.rateJson as { unit_price?: number } | null)?.unit_price
396
+ if (typeof unitPrice !== 'number') continue
397
+ const computed = computeRecurringLines(
398
+ {
399
+ id: item.id,
400
+ chargeTiming: item.chargeTiming ?? 'in_advance',
401
+ prorate: item.prorate ?? true,
402
+ billStartDate: item.billStartDate,
403
+ billEndDate: item.billEndDate ?? null,
404
+ billedToDate: watermarks.get(item.id) ?? null,
405
+ unitPrice,
406
+ isActive: item.isActive,
407
+ deletedAt: item.deletedAt ?? null,
408
+ },
409
+ { billDate, cycle },
410
+ )
411
+ pushSkips(computed.skipped)
412
+ if (computed.newBilledToDate?.getTime() !== watermarks.get(item.id)?.getTime()) {
413
+ watermarkUpdates.set(item.id, computed.newBilledToDate)
414
+ }
415
+ if (computed.lines.length > 0) {
416
+ lines.push(...buildRecurringLineDescriptors(item, computed.lines))
417
+ chargedItems.push(item)
296
418
  }
297
419
  }
298
420
 
299
- const items = await em.find(BillingItem, {
300
- tenantId: account.tenantId,
301
- organizationId: account.organizationId,
302
- billAccountId: account.id,
303
- deletedAt: null,
304
- } as never)
305
-
306
- const usageItems = (items as BillingItem[]).filter((it) => it.type === 'usage')
307
-
308
- // Non-usage path: one_time + recurring filtered by the selector.
309
- const { included } = selectItemsForPeriod(items as SelectableItem[], period)
310
- const billableItems = included as unknown as BillingItem[]
311
-
312
- const currencyMismatchIds = billableItems
313
- .filter((it) => it.currencyMismatch)
314
- .map((it) => it.id)
315
- if (currencyMismatchIds.length > 0) {
316
- aggregatedWarnings.currency_mismatch_items = [
317
- ...(aggregatedWarnings.currency_mismatch_items ?? []),
318
- ...currencyMismatchIds,
319
- ]
421
+ // ── One-time: first bill date whose [D − cycle, D + cycle − 1] window overlaps ──
422
+ const oneTimeWindow: BillPeriod = {
423
+ periodStart: shiftCycles(billDate, cycle, -1),
424
+ periodEnd: addDays(nextBillDate, -1),
425
+ }
426
+ const oneTimesThisCycle = oneTimeItems.filter(
427
+ (it) => !billedOneTimes.has(it.id) && shouldIncludeItem(it as SelectableItem, oneTimeWindow).include,
428
+ )
429
+ lines.push(...buildInvoiceLinesFromItems(oneTimesThisCycle))
430
+ chargedItems.push(...oneTimesThisCycle)
431
+
432
+ // ── Usage: always in arrears ──
433
+ const usageResult = await processUsageForAccount(em, account, arrearsPeriod, usageItems, {
434
+ after: usageCountedThrough,
435
+ })
436
+ addWarning('unmatched_usage_uoms', usageResult.unmatchedUoms)
437
+ lines.push(
438
+ ...usageResult.lines.map((line) => ({
439
+ ...line,
440
+ servicePeriodStart: arrearsPeriod.periodStart,
441
+ servicePeriodEnd: arrearsPeriod.periodEnd,
442
+ })),
443
+ )
444
+ addWarning(
445
+ 'currency_mismatch_items',
446
+ chargedItems.filter((it) => it.currencyMismatch).map((it) => it.id),
447
+ )
448
+
449
+ const period = displayPeriod(lines, arrearsPeriod)
450
+ const record: CycleRecord = {
451
+ bill_date: isoDate(billDate),
452
+ period_start: lines.length ? isoDate(period.periodStart) : null,
453
+ period_end: lines.length ? isoDate(period.periodEnd) : null,
454
+ status: 'empty',
455
+ line_count: lines.length,
456
+ total_net: '0.0000',
457
+ invoice_id: null,
320
458
  }
321
- const itemLines = buildInvoiceLinesFromItems(billableItems)
322
-
323
- // Usage path: aggregate matching usage records per uom_code, rate,
324
- // emit one line per matched usage item, warn about unmatched UoMs.
325
- const usageResult = await processUsageForAccount(em, account, period, usageItems)
326
- if (usageResult.unmatchedUoms.length > 0) {
327
- aggregatedWarnings.unmatched_usage_uoms = [
328
- ...(aggregatedWarnings.unmatched_usage_uoms ?? []),
329
- ...usageResult.unmatchedUoms,
330
- ]
459
+
460
+ // ── Dedupe: an open draft already exists for this bill date ──
461
+ const existing =
462
+ params.mode === 'dry' ? null : await findExistingDraftForBillDate(em, account, billDate, testRun)
463
+ if (existing && params.mode === 'real') {
464
+ // Real state is inconsistent (draft written, account not advanced) —
465
+ // stop here without advancing, as before Spec A.
466
+ cycles.push({ ...record, status: 'skipped_existing', invoice_id: existing })
467
+ return {
468
+ status: 'skipped_existing_draft',
469
+ warnings: finalizeWarnings(warnings, skippedItems),
470
+ draftInvoiceId: existing,
471
+ cyclesEmitted: cycles.filter((c) => c.status === 'invoiced').length,
472
+ usageRecordsRated,
473
+ cycles,
474
+ }
331
475
  }
332
476
 
333
- const allLines: InvoiceLineDescriptor[] = [...itemLines, ...usageResult.lines]
334
- if (allLines.length === 0) {
335
- // Nothing to bill this cycle — still advance so the account moves
336
- // forward in real mode. In test / dry, no advance, so we'd loop
337
- // forever; break out.
338
- if (params.mode !== 'real') break
339
- account.nextBillDate = advanceNextBillDate(
340
- account.nextBillDate,
341
- account.billCycle as BillCycle,
342
- )
343
- account.lastBillDate = period.periodEnd
344
- account.updatedAt = new Date()
345
- cyclesEmitted += 1
346
- continue
477
+ if (existing) {
478
+ cycles.push({ ...record, status: 'skipped_existing', invoice_id: existing })
479
+ } else if (lines.length > 0) {
480
+ const written = await writeDraftOnce(em, {
481
+ mode: params.mode,
482
+ account,
483
+ billDate,
484
+ period,
485
+ billRunId: billRun.id,
486
+ lines,
487
+ })
488
+ if (written === 'conflict') {
489
+ // Unique (account, bill_date, test_run) backstop fired: a concurrent
490
+ // writer got there first. Same semantics as the dedupe check above.
491
+ cycles.push({ ...record, status: 'skipped_existing' })
492
+ if (params.mode === 'real') {
493
+ return {
494
+ status: 'skipped_existing_draft',
495
+ warnings: finalizeWarnings(warnings, skippedItems),
496
+ draftInvoiceId: lastDraftInvoiceId,
497
+ cyclesEmitted: cycles.filter((c) => c.status === 'invoiced').length,
498
+ usageRecordsRated,
499
+ cycles,
500
+ }
501
+ }
502
+ billDate = nextBillDate
503
+ for (const [itemId, date] of watermarkUpdates) watermarks.set(itemId, date)
504
+ for (const it of oneTimesThisCycle) billedOneTimes.add(it.id)
505
+ usageCountedThrough = arrearsPeriod.periodEnd
506
+ continue
507
+ }
508
+ if (written.invoiceId) lastDraftInvoiceId = written.invoiceId
509
+ if (Number.parseFloat(written.totalNet) < 0) addWarning('negative_total_bill_dates', [record.bill_date])
510
+ cycles.push({
511
+ ...record,
512
+ status: params.mode === 'dry' ? 'previewed' : 'invoiced',
513
+ total_net: written.totalNet,
514
+ invoice_id: written.invoiceId,
515
+ })
516
+ } else {
517
+ cycles.push(record)
347
518
  }
348
519
 
349
- const written = await writeDraftInvoice(em, container, {
350
- mode: params.mode,
351
- account,
352
- period,
353
- billRunId: billRun.id,
354
- lines: allLines,
355
- })
356
- lastDraftInvoiceId = written.invoiceId
520
+ // ── Advance state: in memory always, persisted in real mode ──
521
+ for (const [itemId, date] of watermarkUpdates) watermarks.set(itemId, date)
522
+ for (const it of oneTimesThisCycle) billedOneTimes.add(it.id)
523
+ usageCountedThrough = arrearsPeriod.periodEnd
357
524
 
358
525
  if (params.mode === 'real') {
359
526
  const now = new Date()
360
- for (const item of billableItems) {
361
- item.billedToDate = period.periodEnd
527
+ for (const item of recurringItems) {
528
+ if (!watermarkUpdates.has(item.id)) continue
529
+ item.billedToDate = watermarkUpdates.get(item.id) ?? null
530
+ item.updatedAt = now
531
+ }
532
+ for (const item of oneTimesThisCycle) {
533
+ item.billedToDate = oneTimeWindow.periodEnd
362
534
  item.updatedAt = now
363
535
  }
364
- // Mark consumed usage records — single bulk update so we don't
365
- // round-trip per record on accounts with thousands of usage rows.
366
536
  if (usageResult.matchedUoms.length > 0) {
367
- // Predicate-based bulk update: marks every still-unrated record
368
- // for the matched uoms in the account's period with a single
369
- // SQL UPDATE — no id list ever leaves Postgres. Stays
370
- // memory-bounded even for telco accounts with millions of
371
- // usage records per cycle (spec Performance Considerations
372
- // "Shape B").
537
+ // Predicate-based bulk update: marks every still-unrated record for
538
+ // the matched uoms up to the period end with a single SQL UPDATE —
539
+ // no id list ever leaves Postgres (spec Performance "Shape B").
373
540
  await em.nativeUpdate(
374
541
  BillingAccountUsage,
375
542
  {
@@ -378,63 +545,95 @@ async function processAccount(
378
545
  billAccountId: account.id,
379
546
  uomCode: { $in: usageResult.matchedUoms },
380
547
  ratedInBillRunId: null,
381
- periodEnd: { $lte: period.periodEnd },
548
+ periodEnd: { $lte: arrearsPeriod.periodEnd },
382
549
  deletedAt: null,
383
550
  } as never,
384
551
  { ratedInBillRunId: billRun.id, updatedAt: now } as never,
385
552
  )
553
+ usageRecordsRated += usageResult.ratedCount
386
554
  }
387
- totalUsageRatedThisAccount += usageResult.ratedCount
388
- account.nextBillDate = advanceNextBillDate(
389
- account.nextBillDate,
390
- account.billCycle as BillCycle,
391
- )
392
- account.lastBillDate = period.periodEnd
555
+ account.nextBillDate = nextBillDate
556
+ account.lastBillDate = arrearsPeriod.periodEnd
393
557
  account.updatedAt = now
394
558
  await em.flush()
395
- } else {
396
- // Test / dry: do not advance state. One iteration only.
397
- break
398
559
  }
399
560
 
400
- cyclesEmitted += 1
561
+ billDate = nextBillDate
401
562
  }
402
563
 
403
- const hasCurrencyWarnings =
404
- (aggregatedWarnings.currency_mismatch_items ?? []).length > 0
405
- const hasUnmatchedUsage =
406
- (aggregatedWarnings.unmatched_usage_uoms ?? []).length > 0
407
- const hasWarnings = hasCurrencyWarnings || hasUnmatchedUsage
564
+ const finalWarnings = finalizeWarnings(warnings, skippedItems)
408
565
  return {
409
- status: hasWarnings ? 'success_with_warnings' : 'success',
410
- warnings: hasWarnings ? aggregatedWarnings : null,
566
+ status: hasRealWarnings(warnings) ? 'success_with_warnings' : 'success',
567
+ warnings: finalWarnings,
411
568
  draftInvoiceId: lastDraftInvoiceId,
412
- cyclesEmitted,
413
- usageRecordsRated: totalUsageRatedThisAccount,
569
+ cyclesEmitted: cycles.filter((c) => c.status === 'invoiced').length,
570
+ usageRecordsRated,
571
+ cycles,
414
572
  }
415
573
  }
416
574
 
417
- async function findExistingDraftForPeriod(
575
+ function finalizeWarnings(
576
+ warnings: AccountWarnings,
577
+ skippedItems: SkippedItemWarning[],
578
+ ): AccountWarnings | null {
579
+ const merged: AccountWarnings = { ...warnings, ...(skippedItems.length ? { skipped_items: skippedItems } : {}) }
580
+ return Object.keys(merged).length > 0 ? merged : null
581
+ }
582
+
583
+ /** Skipped items are diagnostics; they don't downgrade the outcome status. */
584
+ function hasRealWarnings(warnings: AccountWarnings): boolean {
585
+ return Boolean(
586
+ warnings.currency_mismatch_items?.length ||
587
+ warnings.unmatched_usage_uoms?.length ||
588
+ warnings.negative_total_bill_dates?.length ||
589
+ warnings.catch_up_cap_reached,
590
+ )
591
+ }
592
+
593
+ /**
594
+ * Existing invoice for `(account, bill_date, test_run)` in ANY status — a
595
+ * posted invoice for the same bill date also blocks re-billing (e.g. after
596
+ * an operator moved `next_bill_date` back). Test drafts never block real
597
+ * drafts and vice versa. The partial unique index
598
+ * `billing_invoices_bill_date_uq` is the backstop (see `writeDraftOnce`).
599
+ */
600
+ async function findExistingDraftForBillDate(
418
601
  em: EntityManager,
419
602
  account: BillingAccount,
420
- period: { periodStart: Date; periodEnd: Date },
603
+ billDate: Date,
604
+ testRun: boolean,
421
605
  ): Promise<string | null> {
422
- const periodStartISO = period.periodStart.toISOString().slice(0, 10)
423
- const periodEndISO = period.periodEnd.toISOString().slice(0, 10)
424
- const rows = (await em.execute(
425
- `SELECT id FROM sales_invoices
426
- WHERE tenant_id = ?
427
- AND organization_id = ?
428
- AND deleted_at IS NULL
429
- AND status = 'draft'
430
- AND metadata->>'bill_account_id' = ?
431
- AND metadata->>'bill_period_start' = ?
432
- AND metadata->>'bill_period_end' = ?
433
- LIMIT 1`,
434
- [account.tenantId, account.organizationId, account.id, periodStartISO, periodEndISO],
435
- )) as unknown as Array<{ id: string }> | undefined
436
- if (!rows || rows.length === 0) return null
437
- return rows[0].id ?? null
606
+ const existing = await em.findOne(
607
+ BillingInvoice,
608
+ {
609
+ tenantId: account.tenantId,
610
+ organizationId: account.organizationId,
611
+ billAccountId: account.id,
612
+ billDate,
613
+ testRun,
614
+ documentType: 'invoice',
615
+ deletedAt: null,
616
+ } as never,
617
+ )
618
+ return existing?.id ?? null
619
+ }
620
+
621
+ /**
622
+ * Writes the draft inside its own savepoint so a unique-index conflict
623
+ * (concurrent writer for the same bill date) doesn't abort the account's
624
+ * transaction — it is reported as 'conflict' and handled like a dedupe hit.
625
+ */
626
+ async function writeDraftOnce(
627
+ em: EntityManager,
628
+ params: Parameters<typeof writeDraftInvoice>[1],
629
+ ): Promise<Awaited<ReturnType<typeof writeDraftInvoice>> | 'conflict'> {
630
+ if (params.mode === 'dry') return writeDraftInvoice(em, params)
631
+ try {
632
+ return await em.transactional((tem) => writeDraftInvoice(tem as EntityManager, params))
633
+ } catch (err) {
634
+ if (err instanceof UniqueConstraintViolationException) return 'conflict'
635
+ throw err
636
+ }
438
637
  }
439
638
 
440
639
  function computeFinalRunStatus(
@@ -455,11 +654,12 @@ function buildRunSummary(
455
654
  totalAccounts: number,
456
655
  usageRecordsRated: number,
457
656
  ): Record<string, unknown> {
657
+ const allCycles = outcomes.flatMap((o) => (Array.isArray(o.cycles) ? (o.cycles as CycleRecord[]) : []))
458
658
  return {
459
659
  accounts_processed: totalAccounts,
460
- drafts_created: outcomes.filter(
461
- (o) => o.draftInvoiceId !== null && o.status !== 'skipped_existing_draft',
462
- ).length,
660
+ // Spec A: one draft per invoiced cycle (catch-up writes several per account).
661
+ drafts_created: allCycles.filter((c) => c.status === 'invoiced' && c.invoice_id).length,
662
+ cycles_processed: allCycles.length,
463
663
  drafts_skipped_existing: outcomes.filter((o) => o.status === 'skipped_existing_draft')
464
664
  .length,
465
665
  // SPEC-002 prepaid period close.