@stamhoofd/backend 2.121.0 → 2.122.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 (429) hide show
  1. package/output-2.txt +1 -0
  2. package/output.txt +1 -0
  3. package/package.json +69 -52
  4. package/src/audit-logs/EventNotificationLogger.ts +40 -0
  5. package/src/audit-logs/ModelLogger.ts +19 -15
  6. package/src/audit-logs/OrganizationLogger.ts +1 -1
  7. package/src/audit-logs/RegistrationInvitationLogger.ts +24 -24
  8. package/src/audit-logs/init.ts +8 -2
  9. package/src/boot.ts +19 -18
  10. package/src/crons/amazon-ses.ts +13 -26
  11. package/src/crons/balance-emails.ts +3 -5
  12. package/src/crons/clearExcelCache.test.ts +1 -1
  13. package/src/crons/clearExcelCache.ts +1 -2
  14. package/src/crons/drip-emails.ts +42 -0
  15. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +4 -0
  16. package/src/crons/helpers/isOutside.ts +11 -0
  17. package/src/crons/helpers/useSavedIterator.ts +32 -0
  18. package/src/crons/index.ts +5 -0
  19. package/src/crons/invoices.ts +32 -38
  20. package/src/crons/members-fees.ts +50 -0
  21. package/src/crons/mollie-chargebacks.ts +14 -15
  22. package/src/crons/service-fees.ts +152 -0
  23. package/src/crons/stripe-invoices.ts +48 -0
  24. package/src/crons/transfer-fees.ts +176 -0
  25. package/src/crons/update-cached-balances.ts +4 -4
  26. package/src/crons.ts +7 -64
  27. package/src/debug.ts +4 -2
  28. package/src/email-recipient-loaders/documents.ts +1 -1
  29. package/src/email-recipient-loaders/members.ts +1 -1
  30. package/src/email-recipient-loaders/payments.ts +4 -6
  31. package/src/email-recipient-loaders/registrations.ts +1 -1
  32. package/src/email-replacements/getEmailReplacementsForPayment.ts +5 -9
  33. package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +1 -1
  34. package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +1 -1
  35. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +3 -5
  36. package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +1 -1
  37. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
  38. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +13 -7
  39. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +4 -6
  40. package/src/endpoints/admin/organizations/PatchPackagesEndpoint.test.ts +10 -6
  41. package/src/endpoints/admin/organizations/PatchPackagesEndpoint.ts +39 -21
  42. package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +1 -1
  43. package/src/endpoints/auth/CreateAdminEndpoint.ts +23 -5
  44. package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -2
  45. package/src/endpoints/auth/DeleteTokenEndpoint.ts +1 -1
  46. package/src/endpoints/auth/DeleteUserEndpoint.ts +3 -3
  47. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  48. package/src/endpoints/auth/GetOtherUserEndpoint.ts +1 -1
  49. package/src/endpoints/auth/GetUserEndpoint.ts +1 -1
  50. package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +1 -1
  51. package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +1 -1
  52. package/src/endpoints/auth/PatchUserEndpoint.ts +5 -8
  53. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
  54. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
  55. package/src/endpoints/auth/SignupEndpoint.ts +2 -3
  56. package/src/endpoints/auth/VerifyEmailEndpoint.ts +20 -3
  57. package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +2 -1
  58. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +2 -4
  59. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +1 -1
  60. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +5 -7
  61. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +2 -2
  62. package/src/endpoints/global/email/CreateEmailEndpoint.ts +3 -4
  63. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
  64. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +1 -2
  65. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +4 -6
  66. package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
  67. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
  68. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +1 -1
  69. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +6 -10
  70. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +4 -7
  71. package/src/endpoints/global/email/PatchEmailEndpoint.ts +6 -9
  72. package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +1 -1
  73. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +1 -1
  74. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +1 -1
  75. package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -2
  76. package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +1 -1
  77. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
  78. package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -3
  79. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +49 -4
  80. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +5 -8
  81. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +12 -44
  82. package/src/endpoints/global/events/PatchEventsEndpoint.ts +128 -127
  83. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -9
  84. package/src/endpoints/global/files/GetFileCache.ts +3 -4
  85. package/src/endpoints/global/files/UploadFile.ts +3 -4
  86. package/src/endpoints/global/files/UploadImage.ts +3 -4
  87. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +1 -1
  88. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +1 -1
  89. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +4 -2
  90. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +2 -3
  91. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +1 -1
  92. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +2 -2
  93. package/src/endpoints/global/members/GetMembersEndpoint.ts +10 -12
  94. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +195 -12
  95. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +16 -22
  96. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +6 -30
  97. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +1 -1
  98. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +1 -1
  99. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +40 -8
  100. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +2 -2
  101. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -2
  102. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +4 -1
  103. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +4 -5
  104. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +9 -16
  105. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -1
  106. package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -1
  107. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +1 -1
  108. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -11
  109. package/src/endpoints/global/platform-memberships/GetPlatformMembershipsCountEndpoint.ts +31 -31
  110. package/src/endpoints/global/platform-memberships/GetPlatformMembershipsEndpoint.ts +18 -20
  111. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +1 -1
  112. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1 -1
  113. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +6 -10
  114. package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +35 -6
  115. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
  116. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +1 -1
  117. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +1 -1
  118. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
  119. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -3
  120. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +90 -6
  121. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +35 -34
  122. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +1 -1
  123. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +62 -63
  124. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +6 -9
  125. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +51 -52
  126. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +6 -7
  127. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsCountEndpoint.ts +43 -0
  128. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +1 -1
  129. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +13 -5
  130. package/src/endpoints/global/sso/GetSSOEndpoint.ts +2 -3
  131. package/src/endpoints/global/sso/SetSSOEndpoint.ts +1 -1
  132. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
  133. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +1 -2
  134. package/src/endpoints/global/webshops/GetWebshopsEndpoint.ts +2 -4
  135. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +2 -2
  136. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsCountEndpoint.ts +1 -1
  137. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.ts +3 -4
  138. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +7 -9
  139. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +73 -0
  140. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.ts +89 -0
  141. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +1 -5
  142. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +11 -11
  143. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +2 -2
  144. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +8 -8
  145. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +9 -9
  146. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
  147. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableInvoicesEndpoint.ts +68 -0
  148. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +4 -4
  149. package/src/endpoints/organization/dashboard/billing/GetRegisterCodeEndpoint.ts +66 -0
  150. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.test.ts +949 -0
  151. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +60 -57
  152. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +20 -20
  153. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
  154. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +1 -1
  155. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +1 -1
  156. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
  157. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +1 -1
  158. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +10 -2
  159. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +2 -2
  160. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.ts +2 -2
  161. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +1 -1
  162. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +1 -1
  163. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +10 -8
  164. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +1 -1
  165. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +5 -6
  166. package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +1 -1
  167. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +10 -6
  168. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +11 -2
  169. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +4 -5
  170. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +5 -5
  171. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +2 -2
  172. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +7 -7
  173. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +1 -1
  174. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +2 -2
  175. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +1 -1
  176. package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +1 -1
  177. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -2
  178. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +50 -12
  179. package/src/endpoints/organization/dashboard/organization/SearchUitpasOrganizersEndpoint.ts +2 -2
  180. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +5 -10
  181. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +1 -1
  182. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +1 -1
  183. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -4
  184. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +21 -14
  185. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +35 -28
  186. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +2 -3
  187. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesCountEndpoint.ts +1 -1
  188. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -1
  189. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
  190. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +2 -3
  191. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +1 -1
  192. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
  193. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +19 -18
  194. package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +4 -5
  195. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
  196. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -3
  197. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +2 -2
  198. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +1 -1
  199. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +2 -2
  200. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +1 -1
  201. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +1 -1
  202. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +1 -1
  203. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +1 -1
  204. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +1 -1
  205. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +4 -6
  206. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +10 -15
  207. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +1 -1
  208. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +2 -2
  209. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
  210. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +1 -1
  211. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
  212. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +11 -2
  213. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +4 -6
  214. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +22 -7
  215. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +20 -27
  216. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +1 -1
  217. package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +2 -2
  218. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +2 -3
  219. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +95 -0
  220. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -2
  221. package/src/endpoints/organization/shared/GetDocumentHtml.ts +1 -1
  222. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +1 -1
  223. package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +3 -5
  224. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +1 -1
  225. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +4 -1
  226. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +14 -3
  227. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +14 -2
  228. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +37 -16
  229. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +34 -2
  230. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +4 -2
  231. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +74 -3
  232. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +16 -33
  233. package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -5
  234. package/src/endpoints/organization/webshops/WebshopAuthHelper.ts +36 -0
  235. package/src/endpoints/system/HealthEndpoint.ts +1 -1
  236. package/src/excel-loaders/balance-items.ts +1 -1
  237. package/src/excel-loaders/event-notifications.ts +1 -1
  238. package/src/excel-loaders/members.ts +1 -1
  239. package/src/excel-loaders/organizations.ts +3 -3
  240. package/src/excel-loaders/payments.ts +31 -1
  241. package/src/excel-loaders/platform-memberships.ts +13 -14
  242. package/src/excel-loaders/receivable-balances.ts +2 -2
  243. package/src/excel-loaders/registrations.ts +2 -3
  244. package/src/helpers/AddressValidator.ts +7 -14
  245. package/src/helpers/AdminPermissionChecker.ts +50 -63
  246. package/src/helpers/AuthenticatedStructures.ts +69 -55
  247. package/src/helpers/BalanceItemRelationsBackfiller.test.ts +174 -0
  248. package/src/helpers/BalanceItemRelationsBackfiller.ts +163 -0
  249. package/src/helpers/BuckarooHelper.ts +2 -4
  250. package/src/helpers/CheckSettlements.ts +12 -24
  251. package/src/helpers/Context.ts +15 -10
  252. package/src/helpers/CookieHelper.ts +1 -2
  253. package/src/helpers/EmailResumer.ts +1 -2
  254. package/src/helpers/FileCache.ts +2 -3
  255. package/src/helpers/FinancialSupportHelper.ts +2 -2
  256. package/src/helpers/FlagMomentCleanup.ts +1 -2
  257. package/src/helpers/ForwardHandler.ts +5 -10
  258. package/src/helpers/GlobalHelper.ts +3 -3
  259. package/src/helpers/LegacyBillingConverter.ts +787 -0
  260. package/src/helpers/MemberMerger.test.ts +782 -0
  261. package/src/helpers/MemberMerger.ts +600 -0
  262. package/src/helpers/MemberUserSyncer.ts +6 -12
  263. package/src/helpers/MembershipCharger.ts +2 -4
  264. package/src/helpers/PaymentCustomerResolver.test.ts +203 -0
  265. package/src/helpers/PaymentCustomerResolver.ts +144 -0
  266. package/src/helpers/PeriodHelper.ts +2 -3
  267. package/src/helpers/RecordAnswerHelper.test.ts +105 -110
  268. package/src/helpers/RecordAnswerHelper.ts +7 -7
  269. package/src/helpers/SeedTools.ts +106 -0
  270. package/src/helpers/ServiceFeeHelper.ts +125 -15
  271. package/src/helpers/SetupStepUpdater.ts +38 -32
  272. package/src/helpers/StamhoofdFilterAccessHelper.ts +52 -0
  273. package/src/helpers/StripeHelper.ts +14 -36
  274. package/src/helpers/StripeInvoicer.ts +419 -0
  275. package/src/helpers/StripePayoutChecker.ts +5 -10
  276. package/src/helpers/TagHelper.ts +7 -8
  277. package/src/helpers/TemporaryMemberAccess.ts +1 -2
  278. package/src/helpers/ThrottledQueue.ts +1 -2
  279. package/src/helpers/UitpasTokenRepository.ts +1 -2
  280. package/src/helpers/ViesHelper.ts +7 -11
  281. package/src/helpers/XlsxTransformerColumnHelper.ts +1 -1
  282. package/src/helpers/outstandingBalanceJoin.ts +2 -2
  283. package/src/helpers/updateMemberDetailsUitpasNumber.ts +3 -5
  284. package/src/migrate.ts +12 -5
  285. package/src/{seeds → migrations}/0000000004-single-organization.ts +10 -1
  286. package/src/migrations/0000000006-default-organization.ts +139 -0
  287. package/src/{seeds → migrations}/1715028563-user-permissions.ts +17 -27
  288. package/src/{seeds/1765896674-document-update-year.test.ts → migrations/1750090029-document-update-year.test.ts} +27 -26
  289. package/src/migrations/1750090029-document-update-year.ts +103 -0
  290. package/src/{seeds → migrations}/1750090030-records-configuration.ts +33 -32
  291. package/src/migrations/1750090031-records-configuration-webshops.ts +36 -0
  292. package/src/migrations/1752848540-national-numbers.ts +585 -0
  293. package/src/migrations/1752848550-records-configuration-group-filter.ts +1064 -0
  294. package/src/{seeds → migrations}/1752848561-groups-registration-periods.ts +70 -26
  295. package/src/{seeds → migrations}/1754560914-groups-prices.test.ts +1 -1
  296. package/src/{seeds → migrations}/1754560914-groups-prices.ts +9 -15
  297. package/src/{seeds → migrations}/1756293699-fill-previous-next-period-id.ts +1 -2
  298. package/src/migrations/1761665607-sync-member-users.ts +40 -0
  299. package/src/migrations/1779121240-invoice-email-template.sql +6 -0
  300. package/src/migrations/1779986893-default-payment-failed-email-templates.sql +4 -0
  301. package/src/migrations/1780578166-convert-legacy-billing.ts +13 -0
  302. package/src/{seeds/1755876819-remove-duplicate-members.ts → migrations/1780665426-remove-duplicate-members.ts} +23 -4
  303. package/src/migrations/1780933598-document-groups.ts +88 -0
  304. package/src/migrations/1781692342-balance-item-relations.ts +13 -0
  305. package/src/migrations/1781857866-platform-admins.ts +103 -0
  306. package/src/migrations/1781857867-rm-old-platform-admins.ts +25 -0
  307. package/src/seeds/0000000001-throw.ts +5 -0
  308. package/src/seeds/1760702452-update-balance-price-paid.ts +19 -0
  309. package/src/seeds/1760702453-update-balance-invoiced.ts +19 -0
  310. package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +51 -14
  311. package/src/seeds/1773754928-force-save-members.ts +41 -8
  312. package/src/seeds/1780665427-schedule-stock-updates.ts +45 -0
  313. package/src/seeds/1780665428-group-update-occupancy.ts +31 -0
  314. package/src/seeds/1780915001-fill-payment-customer.ts +70 -0
  315. package/src/seeds/1780915002-update-orders.ts +39 -0
  316. package/src/seeds/1782047708-organization-company-address.ts +44 -0
  317. package/src/seeds-temporary/1769088653-uitpas-status.ts +1 -2
  318. package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.test.ts +69 -0
  319. package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.ts +85 -0
  320. package/src/services/AuditLogService.test.ts +51 -0
  321. package/src/services/AuditLogService.ts +45 -0
  322. package/src/services/BalanceItemService.ts +141 -44
  323. package/src/services/CpuService.ts +3 -6
  324. package/src/services/DatabaseCollationService.test.ts +1 -2
  325. package/src/services/DocumentService.ts +1 -2
  326. package/src/services/EventNotificationService.ts +1 -1
  327. package/src/services/FileSignService.ts +2 -4
  328. package/src/services/InvoicePdfService.ts +286 -0
  329. package/src/services/InvoiceService.ts +204 -251
  330. package/src/services/InvoiceXMLService.ts +424 -0
  331. package/src/services/MemberNumberService.ts +6 -11
  332. package/src/services/MemberRecordStore.ts +8 -10
  333. package/src/services/MollieService.ts +114 -121
  334. package/src/services/PaymentMandateService.ts +45 -45
  335. package/src/services/PaymentReallocationService.test.ts +1 -1
  336. package/src/services/PaymentReallocationService.ts +7 -9
  337. package/src/services/PaymentService.ts +444 -308
  338. package/src/services/PlatformMembershipService.ts +4 -8
  339. package/src/services/ReferralService.ts +223 -0
  340. package/src/services/RegistrationService.ts +63 -53
  341. package/src/services/SSOService.test.ts +119 -0
  342. package/src/services/SSOService.ts +56 -49
  343. package/src/services/STPackageService.ts +97 -61
  344. package/src/services/UniqueMemberNumberService.ts +69 -0
  345. package/src/services/UniqueUserService.ts +3 -6
  346. package/src/services/VATService.ts +85 -0
  347. package/src/services/uitpas/PassholderEndpoints.ts +3 -6
  348. package/src/services/uitpas/UitpasService.ts +6 -10
  349. package/src/services/uitpas/checkUitpasNumbers.ts +2 -4
  350. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +3 -6
  351. package/src/services/uitpas/searchUitpasEvents.ts +1 -2
  352. package/src/sql-filters/audit-logs.ts +1 -1
  353. package/src/sql-filters/balance-item-payments.ts +1 -1
  354. package/src/sql-filters/balance-items.ts +1 -1
  355. package/src/sql-filters/base-registration-filter-compilers.ts +5 -33
  356. package/src/sql-filters/document-templates.ts +1 -1
  357. package/src/sql-filters/documents.ts +1 -1
  358. package/src/sql-filters/email-recipients.ts +1 -1
  359. package/src/sql-filters/emails.ts +1 -1
  360. package/src/sql-filters/event-notifications.ts +1 -1
  361. package/src/sql-filters/events.ts +6 -1
  362. package/src/sql-filters/groups.ts +5 -0
  363. package/src/sql-filters/invoiced-balance-items.ts +1 -1
  364. package/src/sql-filters/invoices.ts +1 -1
  365. package/src/sql-filters/member-responsibility-records.ts +1 -1
  366. package/src/sql-filters/members.ts +1 -1
  367. package/src/sql-filters/orders.ts +4 -4
  368. package/src/sql-filters/organization-registration-periods.ts +1 -1
  369. package/src/sql-filters/organizations.ts +1 -1
  370. package/src/sql-filters/platform-memberships.ts +3 -3
  371. package/src/sql-filters/receivable-balances.ts +1 -1
  372. package/src/sql-filters/registration-periods.ts +1 -1
  373. package/src/sql-filters/registrations.ts +1 -1
  374. package/src/sql-filters/tickets.ts +1 -1
  375. package/src/sql-filters/users.ts +1 -1
  376. package/src/sql-sorters/groups.ts +23 -0
  377. package/src/sql-sorters/platform-memberships.ts +16 -16
  378. package/src/sql-sorters/registration-invitations.ts +2 -2
  379. package/tests/actions/patchOrganizationMember.ts +1 -1
  380. package/tests/actions/patchUserMember.ts +1 -1
  381. package/tests/assertions/assertBalances.ts +6 -12
  382. package/tests/e2e/api-rate-limits.test.ts +12 -24
  383. package/tests/e2e/bundle-discounts.test.ts +4 -1
  384. package/tests/e2e/charge-members.test.ts +13 -11
  385. package/tests/e2e/documents.test.ts +4 -1
  386. package/tests/e2e/register.test.ts +7 -9
  387. package/tests/e2e/stock.test.ts +4 -3
  388. package/tests/e2e/tickets.test.ts +1 -1
  389. package/tests/helpers/MollieMocker.ts +462 -0
  390. package/tests/helpers/PayconiqMocker.ts +138 -45
  391. package/tests/helpers/StripeMocker.ts +9 -6
  392. package/tests/helpers/index.ts +4 -0
  393. package/tests/init/initAdmin.ts +2 -2
  394. package/tests/init/initBundleDiscount.ts +1 -1
  395. package/tests/init/initMembershipOrganization.ts +13 -0
  396. package/tests/init/initPermissionRole.ts +3 -4
  397. package/tests/vitest.global.setup.ts +1 -1
  398. package/vitest.config.js +6 -0
  399. package/.nvmrc +0 -1
  400. package/src/helpers/GroupBuilder.ts +0 -418
  401. package/src/seeds/0000000000-example.ts +0 -11
  402. package/src/seeds/1722256498-group-update-occupancy.ts +0 -52
  403. package/src/seeds/1726572303-schedule-stock-updates.ts +0 -52
  404. package/src/seeds/1726847064-setup-steps.ts +0 -16
  405. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +0 -31
  406. package/src/seeds/1729253172-update-orders.ts +0 -21
  407. package/src/seeds/1733319079-fill-paying-organization-ids.ts +0 -68
  408. package/src/seeds/1733748412-update-deleted-order-numbers.ts +0 -109
  409. package/src/seeds/1733994455-balance-item-status-open.ts +0 -30
  410. package/src/seeds/1740046783-update-membership.ts +0 -17
  411. package/src/seeds/1741008870-fix-auditlog-description.ts +0 -50
  412. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +0 -88
  413. package/src/seeds/1741268179-fix-group-dates-of-events.ts +0 -32
  414. package/src/seeds/1751445358-upload-email-attachments.ts +0 -106
  415. package/src/seeds/1755532883-update-email-sender-ids.ts +0 -47
  416. package/src/seeds/1755790070-fill-email-recipient-errors.ts +0 -96
  417. package/src/seeds/1756115432-remove-old-drafts.ts +0 -16
  418. package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +0 -30
  419. package/src/seeds/1756303697-update-email-counts.ts +0 -76
  420. package/src/seeds/1761665607-sync-member-users.ts +0 -59
  421. package/src/seeds/1765896674-document-update-year.ts +0 -66
  422. /package/src/{seeds → migrations}/0000000001-development-user.ts +0 -0
  423. /package/src/{seeds → migrations}/0000000002-clear-stamhoofd-email-templates.ts +0 -0
  424. /package/src/{seeds → migrations}/0000000003-default-email-templates.ts +0 -0
  425. /package/src/{seeds → migrations}/1752848561-groups-registration-periods.test.ts +0 -0
  426. /package/src/{seeds → migrations}/1766150402-document-published-at.test.ts +0 -0
  427. /package/src/{seeds → migrations}/1766150402-document-published-at.ts +0 -0
  428. /package/src/{seeds → migrations}/1779121239-default-invoice-email-template.sql +0 -0
  429. /package/src/{seeds → migrations}/data/default-email-templates.sql +0 -0
@@ -0,0 +1,787 @@
1
+ import type { Model } from '@simonbackx/simple-database';
2
+ import { BalanceItem, BalanceItemPayment, CachedBalance, Invoice, InvoicedBalanceItem, Organization, Payment, Platform, STCredit, STInvoice, STPendingInvoice, StripeAccount, UsedRegisterCode, User } from '@stamhoofd/models';
3
+ import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
4
+ import { QueryableModel } from '@stamhoofd/sql';
5
+ import type { STInvoiceItem, STInvoiceMeta } from '@stamhoofd/structures';
6
+ import {
7
+ BalanceItemRelation,
8
+ BalanceItemRelationType,
9
+ BalanceItemStatus,
10
+ BalanceItemType,
11
+ Company,
12
+ getPricingTypeName,
13
+ InvoicedBalanceItem as InvoicedBalanceItemStruct,
14
+ PaymentCustomer,
15
+ PaymentMethod,
16
+ PaymentProvider,
17
+ PaymentStatus,
18
+ PaymentType,
19
+ TranslatedString,
20
+ VATExcemptReason,
21
+ VATSubtotal,
22
+ } from '@stamhoofd/structures';
23
+ import { InvoiceStruct } from '@stamhoofd/structures/billing/Invoice.js';
24
+ import { STMath } from '@stamhoofd/utility';
25
+ import { v4 as uuidv4 } from 'uuid';
26
+ import { SeedTools } from './SeedTools.js';
27
+ import { VATService } from '../services/VATService.js';
28
+
29
+ /**
30
+ * The seller (Stamhoofd) is a Belgian company, so the standard rate that applies to taxable sales is
31
+ * 21%. Used to restore the rate on intra-community reverse-charged invoices (legacy stored 0%).
32
+ */
33
+ const STANDARD_VAT_PERCENTAGE = 21;
34
+
35
+ /**
36
+ * Converts the legacy SAAS billing models (STInvoice / STPendingInvoice / STCredit) into the new
37
+ * billing model (Invoice / InvoicedBalanceItem / BalanceItem / BalanceItemPayment).
38
+ *
39
+ * The conversion is idempotent: every new row reuses the legacy primary key as its own id, so a
40
+ * re-run skips anything that was already created.
41
+ *
42
+ * The critical correctness goal is that every legacy line tied to an STPackage becomes a BalanceItem
43
+ * with the correct packageId + amount, so STPackageService.getPaidOrPendingQuantity stays accurate and
44
+ * customers are never re-charged for something they already paid.
45
+ */
46
+
47
+ interface ConversionContext {
48
+ membershipOrganization: Organization;
49
+ /** Point-in-time snapshot of the seller (Stamhoofd) used on every generated invoice. */
50
+ seller: Company;
51
+ }
52
+
53
+ /** Round a 4-decimal price down to whole cents. BalanceItem.save() rejects sub-cent unit prices. */
54
+ function roundToCents(value: number): number {
55
+ return STMath.round(value / 100) * 100;
56
+ }
57
+
58
+ /**
59
+ * Maps the legacy single VAT rate to the new model's (VATPercentage, VATExcempt) pair.
60
+ *
61
+ * Legacy stored a 0% rate for intra-community reverse charge (a foreign EU company with a non-Belgian
62
+ * VAT number); the new model marks this explicitly with VATExcempt = IntraCommunityServices while
63
+ * keeping the seller's standard rate. The VAT amount stays 0 either way, so totals are unchanged.
64
+ */
65
+ function getVatInfo(meta: STInvoiceMeta): { VATPercentage: number; VATExcempt: VATExcemptReason | null } {
66
+ if (meta.VATPercentage === 0 && !!meta.companyVATNumber) {
67
+ return { VATPercentage: STANDARD_VAT_PERCENTAGE, VATExcempt: VATExcemptReason.IntraCommunityServices };
68
+ }
69
+ return { VATPercentage: meta.VATPercentage, VATExcempt: null };
70
+ }
71
+
72
+ function assertEqual(actual: number, expected: number, label: string, invoiceId: string) {
73
+ if (actual !== expected) {
74
+ throw new Error(
75
+ `Legacy billing migration: ${label} mismatch for invoice ${invoiceId}: got ${actual}, expected ${expected}`,
76
+ );
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Builds (but does not save) the BalanceItem for one legacy invoice/pending-invoice line.
82
+ * Mirrors STPackageService.chargePackage for the package relations/VAT mapping.
83
+ */
84
+ async function buildBalanceItem(item: STInvoiceItem, options: {
85
+ legacyInvoice: STInvoice | STPendingInvoice;
86
+ legacyMeta: STInvoiceMeta;
87
+ payingOrganizationId: string | null;
88
+ ctx: ConversionContext;
89
+ status: BalanceItemStatus;
90
+ createdAt: Date;
91
+ paidAt: Date | null;
92
+ }): Promise<BalanceItem> {
93
+ const { legacyMeta: legacyMeta, payingOrganizationId, ctx, status, createdAt, paidAt } = options;
94
+
95
+ const balanceItem = new BalanceItem();
96
+ balanceItem.id = uuidv4(); // we cannot reuse old item ids as multiple invoices could reference the same id
97
+ balanceItem.organizationId = ctx.membershipOrganization.id;
98
+ balanceItem.payingOrganizationId = payingOrganizationId;
99
+
100
+ if (item.package) {
101
+ balanceItem.packageId = item.package.id;
102
+ balanceItem.type = BalanceItemType.STPackage;
103
+ balanceItem.relations.set(BalanceItemRelationType.STPackage, BalanceItemRelation.create({
104
+ id: item.package.id,
105
+ name: TranslatedString.create(item.package.meta.name),
106
+ }));
107
+ balanceItem.relations.set(BalanceItemRelationType.STPricingType, BalanceItemRelation.create({
108
+ id: item.package.meta.pricingType,
109
+ name: TranslatedString.create(getPricingTypeName(item.package.meta.pricingType)),
110
+ }));
111
+ balanceItem.startDate = item.package.meta.startDate ?? null;
112
+ balanceItem.endDate = item.package.endDate ?? null;
113
+ } else {
114
+ balanceItem.type = BalanceItemType.Other;
115
+
116
+ // If it is credit, try to link to the credit ID instead.
117
+ if (item.name === 'Gebruikt tegoed' && options.legacyInvoice instanceof STInvoice && payingOrganizationId) {
118
+ balanceItem.type = BalanceItemType.ReferralDiscount;
119
+ // Stop here, we need to find the existing one.
120
+ const creditId = options.legacyInvoice.creditId;
121
+ if (!creditId) {
122
+ // This happens sadly because of incorrect data in the v1 database
123
+ console.error('Missing creditId on invoice ' + options.legacyInvoice.id + ' / ' + options.legacyInvoice.number);
124
+ } else {
125
+ const original = await BalanceItem.getByID(creditId);
126
+ if (!original) {
127
+ console.error('Could not find BalanceItem for creditId ' + creditId + ' at invoice ' + options.legacyInvoice.id);
128
+ } else {
129
+ if (original.status !== BalanceItemStatus.Canceled) {
130
+ throw new Error('Mismatch in creditId ' + creditId + ' at invoice ' + options.legacyInvoice.id + ' already Due');
131
+ }
132
+
133
+ // Set unitPrice = 0 of this item
134
+ if (original.unitPrice !== -item.unitPrice * 100) {
135
+ throw new Error('Mismatch in creditId ' + creditId + ' at invoice ' + options.legacyInvoice.id + ' expected ' + (-item.unitPrice * 100) + ' received ' + original.unitPrice);
136
+ }
137
+
138
+ // Normally this is svaed as a balance item with a positive value
139
+ // to reduce a negative balance item that was added earlier as a discount.
140
+ // but if we would link a balance item payment, that would remove that offset completely.
141
+ // So we need to set that the 'usage' of this balance item was not payable all along, and link the
142
+ // negative payment item with it, so we get a positive amount open, leading to the correct behaviour
143
+ original.status = status; // Also for pending payments, as we'll need to readd the missed discount when the payment would fail
144
+ original.unitPrice = item.unitPrice * 100;
145
+ original.type = BalanceItemType.ReferralDiscount;
146
+
147
+ const vatInfo = getVatInfo(legacyMeta);
148
+ original.VATIncluded = legacyMeta.areItemsIncludingVAT;
149
+ original.VATPercentage = vatInfo.VATPercentage;
150
+ original.VATExcempt = vatInfo.VATExcempt;
151
+
152
+ // Don't copy the name, as in the past we would have put a reason why the credit was used
153
+ // but that should not be reused
154
+ original.description = item.name || item.description || '';
155
+
156
+ return original;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ const vatInfo = getVatInfo(legacyMeta);
163
+
164
+ balanceItem.description = item.name || item.description || '';
165
+ balanceItem.amount = item.amount;
166
+ balanceItem.unitPrice = item.unitPrice * 100;
167
+ balanceItem.VATPercentage = vatInfo.VATPercentage;
168
+ balanceItem.VATIncluded = legacyMeta.areItemsIncludingVAT;
169
+ balanceItem.VATExcempt = vatInfo.VATExcempt;
170
+ balanceItem.status = status;
171
+ balanceItem.dueAt = null;
172
+ balanceItem.paidAt = paidAt;
173
+ balanceItem.createdAt = createdAt;
174
+
175
+ return balanceItem;
176
+ }
177
+
178
+ function buildCustomer(meta: STInvoiceMeta): PaymentCustomer {
179
+ return PaymentCustomer.create({
180
+ email: meta.companyEmail,
181
+ company: Company.create({
182
+ name: meta.companyName,
183
+ VATNumber: meta.companyVATNumber,
184
+ companyNumber: meta.companyNumber,
185
+ address: meta.companyAddress,
186
+ administrationEmail: meta.companyEmail,
187
+ }),
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Resolves the legacy Payment linked to an invoice. Legacy payments for Stamhoofd often have a null
193
+ * organizationId; we set it to the membership organization (the seller) so the new payment flow and
194
+ * cached balances work.
195
+ */
196
+ async function resolvePayment(stInvoice: STInvoice, payment: Payment, ctx: ConversionContext): Promise<Payment | null> {
197
+ let changed = false;
198
+ if (payment.organizationId === null) {
199
+ payment.organizationId = ctx.membershipOrganization.id;
200
+ changed = true;
201
+ }
202
+ if (payment.payingOrganizationId === null && stInvoice.payingOrganizationId) {
203
+ payment.payingOrganizationId = stInvoice.payingOrganizationId;
204
+ changed = true;
205
+ }
206
+
207
+ if (!payment.customer) {
208
+ payment.customer = buildCustomer(stInvoice.meta);
209
+ changed = true;
210
+ }
211
+
212
+ if (payment.price === 0 && payment.status !== PaymentStatus.Succeeded && payment.status !== PaymentStatus.Failed && payment.method === PaymentMethod.Unknown) {
213
+ payment.status = PaymentStatus.Succeeded;
214
+ changed = true;
215
+ }
216
+
217
+ if (changed) {
218
+ await payment.save();
219
+ }
220
+ return payment;
221
+ }
222
+
223
+ interface ConversionResult {
224
+ balanceItemIds: string[];
225
+ payingOrganizationIds: string[];
226
+ }
227
+
228
+ /**
229
+ * Full reconstruction of a paid/official invoice: Invoice + per-line BalanceItem (marked paid via a
230
+ * BalanceItemPayment linking the existing legacy Payment) + InvoicedBalanceItem.
231
+ */
232
+ export async function convertPaidInvoice(stInvoice: STInvoice, payment: Payment, ctx: ConversionContext): Promise<ConversionResult> {
233
+ const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
234
+ const toSaveModels: Model[] = [];
235
+
236
+ if (await Invoice.getByID(stInvoice.id)) {
237
+ // Already migrated
238
+ return result;
239
+ }
240
+
241
+ const legacyMeta = stInvoice.meta;
242
+ const payingOrganizationId = stInvoice.payingOrganizationId;
243
+ const invoicedAt = legacyMeta.date ?? stInvoice.paidAt ?? stInvoice.createdAt;
244
+
245
+ await resolvePayment(stInvoice, payment, ctx);
246
+
247
+ // Verify the legacy invoice is internally consistent before we reproduce it.
248
+ assertEqual(legacyMeta.priceWithoutVAT + legacyMeta.VAT, legacyMeta.priceWithVAT, 'priceWithVAT composition', stInvoice.id);
249
+ assertEqual(legacyMeta.priceWithVAT + legacyMeta.payableRoundingAmount, legacyMeta.totalPrice, 'totalPrice composition', stInvoice.id);
250
+
251
+ // The actual money received must match the invoice total.
252
+ if (payment && payment.status === PaymentStatus.Succeeded) {
253
+ assertEqual(payment.price, legacyMeta.totalPrice * 100, 'payment price vs invoice total', stInvoice.id);
254
+ }
255
+
256
+ // First create the balance items + their invoiced-line data (in memory). The InvoicedBalanceItem
257
+ // rows are only saved after the Invoice exists (foreign key).
258
+ const lines: { item: STInvoiceItem; balanceItem: BalanceItem; invoicedStruct: InvoicedBalanceItemStruct }[] = [];
259
+
260
+ const invoiceStruct = InvoiceStruct.create({
261
+ seller: ctx.seller,
262
+ customer: buildCustomer(legacyMeta),
263
+ });
264
+
265
+ for (const item of legacyMeta.items) {
266
+ if (item.amount === 0) {
267
+ // Not possible to be on invoice, and also cannot be represented correctly since this isn't part of the invoice (items with amount 0, means not included)
268
+ continue;
269
+ }
270
+
271
+ const balanceItem = await buildBalanceItem(item, {
272
+ legacyInvoice: stInvoice,
273
+ legacyMeta: legacyMeta,
274
+ payingOrganizationId,
275
+ ctx,
276
+ status: BalanceItemStatus.Due,
277
+ createdAt: invoicedAt,
278
+ paidAt: invoicedAt,
279
+ });
280
+
281
+ result.balanceItemIds.push(balanceItem.id);
282
+ toSaveModels.push(balanceItem);
283
+
284
+ // Build the invoiced line (computes unit price excl. VAT, quantity, PEPPOL rounding, ...)
285
+ const invoicedStruct = InvoicedBalanceItemStruct.createFor(balanceItem.getStructure(), balanceItem.priceWithVAT);
286
+ lines.push({ item, balanceItem, invoicedStruct });
287
+
288
+ if (legacyMeta.areItemsIncludingVAT) {
289
+ if (item.amount === 1) {
290
+ // There could be very rare differences.
291
+ // E.g. 10,40495 rounded to 10,4050 in the balance item, and then to 10,41 in the invoiced struct
292
+ invoicedStruct.unitPrice = stInvoice.meta.includingVATToExcludingVAT(item.price) * 100;
293
+ invoicedStruct.quantity = 1_00_00;
294
+
295
+ if (invoicedStruct.unitPrice < 0) {
296
+ // unitPrice should always be positive in peppol
297
+ invoicedStruct.unitPrice = -invoicedStruct.unitPrice;
298
+ invoicedStruct.quantity = -invoicedStruct.quantity;
299
+ }
300
+ } else {
301
+ assertEqual(invoicedStruct.totalWithoutVAT, stInvoice.meta.includingVATToExcludingVAT(item.price) * 100, 'item ' + item.id + ' price without vat should match new', stInvoice.id);
302
+ }
303
+ } else {
304
+ assertEqual(invoicedStruct.totalWithoutVAT, item.price * 100, 'item ' + item.id + ' price without vat should match new', stInvoice.id);
305
+ }
306
+
307
+ // Mark the balance item paid by linking the existing legacy payment. We pay each item in full
308
+ // (priceWithVAT) so priceOpen becomes 0 regardless of invoice-level rounding.
309
+ const balanceItemPayment = new BalanceItemPayment();
310
+ balanceItemPayment.organizationId = ctx.membershipOrganization.id;
311
+ balanceItemPayment.paymentId = payment.id;
312
+ balanceItemPayment.balanceItemId = balanceItem.id;
313
+ balanceItemPayment.price = balanceItem.priceWithVAT;
314
+ toSaveModels.push(balanceItemPayment);
315
+
316
+ invoiceStruct.addItem(invoicedStruct);
317
+ }
318
+
319
+ invoiceStruct.calculateVAT();
320
+ invoiceStruct.payableRoundingAmount = legacyMeta.payableRoundingAmount * 100;
321
+
322
+ if (legacyMeta.useLegacyRounding) {
323
+ // we can ignore the subresult because we cannot get to the same result in any way using the new invoice calculation methods
324
+ // since these are old invoices, we should accept them as the calculation was not incorrect at the time of generating the invoice
325
+ const diff = invoiceStruct.totalWithVAT - legacyMeta.totalPrice * 100;
326
+ if (Math.abs(diff) > 3_00) {
327
+ throw new Error('Unexpected large difference between legacy calculation and modern calculation at ' + stInvoice.id);
328
+ }
329
+ } else {
330
+ assertEqual(invoiceStruct.totalWithVAT, legacyMeta.totalPrice * 100, 'invoice totalWithVAT', stInvoice.id);
331
+ assertEqual(invoiceStruct.VATTotalAmount, legacyMeta.VAT * 100, 'invoice VAT', stInvoice.id);
332
+ assertEqual(invoiceStruct.totalWithoutVAT, legacyMeta.priceWithoutVAT * 100, 'invoice totalWithoutVAT', stInvoice.id);
333
+ }
334
+
335
+ const invoice = new Invoice();
336
+ invoice.id = stInvoice.id;
337
+ invoice.organizationId = ctx.membershipOrganization.id;
338
+ invoice.payingOrganizationId = payingOrganizationId;
339
+ invoice.seller = ctx.seller;
340
+ invoice.customer = buildCustomer(legacyMeta);
341
+ invoice.negativeInvoiceId = stInvoice.negativeInvoiceId;
342
+
343
+ // Carry the legacy totals over verbatim so the converted invoice is identical to the original.
344
+
345
+ invoice.totalWithoutVAT = legacyMeta.priceWithoutVAT * 100;
346
+ invoice.VATTotalAmount = legacyMeta.VAT * 100;
347
+ invoice.totalWithVAT = legacyMeta.totalPrice * 100;
348
+ invoice.payableRoundingAmount = legacyMeta.payableRoundingAmount * 100;
349
+ invoice.totalBalanceInvoicedAmount = invoiceStruct.totalBalanceInvoicedAmount;
350
+
351
+ const vatInfo = getVatInfo(legacyMeta);
352
+ invoice.VATTotal = [
353
+ VATSubtotal.create({
354
+ VATPercentage: vatInfo.VATPercentage,
355
+ VATExcempt: vatInfo.VATExcempt,
356
+ taxablePrice: legacyMeta.priceWithoutVAT * 100,
357
+ VAT: legacyMeta.VAT * 100,
358
+ }),
359
+ ];
360
+
361
+ invoice.ipAddress = legacyMeta.ipAddress;
362
+ invoice.userAgent = legacyMeta.userAgent;
363
+ invoice.stripeAccountId = legacyMeta.stripeAccountId;
364
+ invoice.reference = stInvoice.reference;
365
+ invoice.didSendPeppol = stInvoice.didSendPeppol;
366
+
367
+ // Reuse the already generated PDF/XML files instead of regenerating them.
368
+ invoice.pdf = legacyMeta.pdf ?? null;
369
+ invoice.xml = legacyMeta.xml ?? null;
370
+
371
+ invoice.invoicedAt = invoicedAt;
372
+ invoice.createdAt = stInvoice.createdAt;
373
+
374
+ // Format the legacy integer number using the seller's invoice settings so that future numbering
375
+ // (InvoiceCounter.assignNextNumber) continues the same series.
376
+ if (stInvoice.number) {
377
+ invoice.number = stInvoice.number.toString();
378
+ }
379
+
380
+ // The converted invoice must match the original exactly.
381
+ assertEqual(invoice.totalWithVAT, legacyMeta.totalPrice * 100, 'invoice totalWithVAT', stInvoice.id);
382
+ assertEqual(invoice.VATTotalAmount, legacyMeta.VAT * 100, 'invoice VAT', stInvoice.id);
383
+ assertEqual(invoice.payableRoundingAmount, legacyMeta.payableRoundingAmount * 100, 'invoice rounding', stInvoice.id);
384
+
385
+ // Only save after the last assertions
386
+ await invoice.save();
387
+ await InvoiceCounter.resetNumbers(invoice.organizationId);
388
+
389
+ for (const model of toSaveModels) {
390
+ await model.save();
391
+ }
392
+
393
+ // Now that the invoice exists, persist the invoiced balance items (FK -> invoices).
394
+ for (const { item, balanceItem, invoicedStruct } of lines) {
395
+ const invoiced = new InvoicedBalanceItem();
396
+ invoiced.organizationId = ctx.membershipOrganization.id;
397
+ invoiced.invoiceId = stInvoice.id;
398
+ invoiced.balanceItemId = balanceItem.id;
399
+ invoiced.name = item.name || invoicedStruct.name;
400
+ invoiced.description = item.description;
401
+ invoiced.balanceInvoicedAmount = invoicedStruct.balanceInvoicedAmount;
402
+ invoiced.quantity = invoicedStruct.quantity;
403
+ invoiced.unitPrice = invoicedStruct.unitPrice;
404
+ invoiced.VATPercentage = invoicedStruct.VATPercentage;
405
+ invoiced.VATIncluded = invoicedStruct.VATIncluded;
406
+ invoiced.VATExcempt = invoicedStruct.VATExcempt;
407
+ invoiced.totalWithoutVAT = invoicedStruct.totalWithoutVAT;
408
+ await invoiced.save();
409
+ }
410
+
411
+ if (payingOrganizationId) {
412
+ result.payingOrganizationIds.push(payingOrganizationId);
413
+ }
414
+
415
+ if (payment) {
416
+ payment.customer = invoice.customer;
417
+ payment.invoiceId = invoice.id;
418
+ await payment.save();
419
+ }
420
+
421
+ return result;
422
+ }
423
+
424
+ /**
425
+ * A legacy invoice whose payment is still in flight (Created/Pending). We create Hidden balance items
426
+ * linked to the still-pending payment, so when it eventually settles the existing payment-success flow
427
+ * activates the package. No Invoice is created (it is not official yet).
428
+ */
429
+ export async function convertInProgressInvoice(stInvoice: STInvoice, payment: Payment, ctx: ConversionContext): Promise<ConversionResult> {
430
+ const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
431
+
432
+ if (stInvoice.meta.items.length === 0) {
433
+ return result;
434
+ }
435
+
436
+ await resolvePayment(stInvoice, payment, ctx);
437
+
438
+ const meta = stInvoice.meta;
439
+ const payingOrganizationId = stInvoice.payingOrganizationId;
440
+ const createdAt = meta.date ?? stInvoice.createdAt;
441
+
442
+ for (const item of meta.items) {
443
+ const balanceItem = await buildBalanceItem(item, {
444
+ legacyInvoice: stInvoice,
445
+ legacyMeta: meta,
446
+ payingOrganizationId,
447
+ ctx,
448
+ status: payment.status === PaymentStatus.Succeeded ? BalanceItemStatus.Due : BalanceItemStatus.Hidden,
449
+ createdAt,
450
+ paidAt: null,
451
+ });
452
+ await balanceItem.save();
453
+ result.balanceItemIds.push(balanceItem.id);
454
+
455
+ const balanceItemPayment = new BalanceItemPayment();
456
+ balanceItemPayment.organizationId = ctx.membershipOrganization.id;
457
+ balanceItemPayment.paymentId = payment.id;
458
+ balanceItemPayment.balanceItemId = balanceItem.id;
459
+ balanceItemPayment.price = balanceItem.priceWithVAT;
460
+ await balanceItemPayment.save();
461
+ }
462
+
463
+ if (payingOrganizationId) {
464
+ result.payingOrganizationIds.push(payingOrganizationId);
465
+ }
466
+
467
+ return result;
468
+ }
469
+
470
+ /**
471
+ * The pending invoice holds amounts that were charged but not yet invoiced/paid. Each line becomes an
472
+ * unpaid Due balance item: counted by getPaidOrPendingQuantity (so it is not re-charged) and shown as
473
+ * an open balance the organization still owes.
474
+ */
475
+ export async function convertPendingInvoice(pendingInvoice: STPendingInvoice, ctx: ConversionContext): Promise<ConversionResult> {
476
+ const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
477
+
478
+ if (!pendingInvoice.organizationId) {
479
+ return result;
480
+ }
481
+
482
+ const meta = pendingInvoice.meta;
483
+ const payingOrganizationId = pendingInvoice.organizationId;
484
+
485
+ for (const item of meta.items) {
486
+ const balanceItem = await buildBalanceItem(item, {
487
+ legacyInvoice: pendingInvoice,
488
+ legacyMeta: meta,
489
+ payingOrganizationId,
490
+ ctx,
491
+ status: BalanceItemStatus.Due,
492
+ createdAt: item.date ?? pendingInvoice.createdAt,
493
+ paidAt: null,
494
+ });
495
+ await balanceItem.save();
496
+ result.balanceItemIds.push(balanceItem.id);
497
+ }
498
+
499
+ if (result.balanceItemIds.length > 0) {
500
+ result.payingOrganizationIds.push(payingOrganizationId);
501
+ }
502
+
503
+ return result;
504
+ }
505
+
506
+ /**
507
+ * Each legacy credit row becomes a balance item (preserving the ledger). A positive legacy change is
508
+ * credit we owe the organization, so it becomes a negative balance (priceOpen < 0). Expired credits are
509
+ * kept as Canceled so they preserve the history without affecting the open balance.
510
+ */
511
+ export async function convertCredit(credit: STCredit, ctx: ConversionContext): Promise<ConversionResult> {
512
+ const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
513
+
514
+ if (await BalanceItem.getByID(credit.id)) {
515
+ // Already migrated
516
+ return result;
517
+ }
518
+
519
+ const org = await Organization.getByID(credit.organizationId);
520
+ if (!org) {
521
+ // No longer existing organization (STCredit has no foreign key)
522
+ return result;
523
+ }
524
+
525
+ const balanceItem = new BalanceItem();
526
+ balanceItem.id = credit.id;
527
+ balanceItem.organizationId = ctx.membershipOrganization.id;
528
+ balanceItem.payingOrganizationId = credit.organizationId;
529
+ balanceItem.type = BalanceItemType.ReferralDiscount;
530
+ balanceItem.description = credit.description;
531
+ balanceItem.amount = 1;
532
+ balanceItem.unitPrice = -credit.change;
533
+ balanceItem.VATPercentage = 21;
534
+ balanceItem.VATIncluded = false;
535
+
536
+ // ALWAYS CANCELED.
537
+ // We'll add an extra item for the remaining to fix
538
+ // that we don't support linking these balance items with payments correctly
539
+ // and also supportint expiry dates in balance items is not possible
540
+ // since how they were built in the past
541
+ balanceItem.status = BalanceItemStatus.Canceled; // We'll set this to due when we link a payment with it later
542
+ balanceItem.dueAt = null;
543
+ balanceItem.createdAt = credit.createdAt;
544
+ await balanceItem.save();
545
+
546
+ // Relink any used register code that was rewarded with this credit to the new balance item.
547
+ const usedRegisterCodes = await UsedRegisterCode.select().where('creditId', credit.id).fetch();
548
+ for (const usedRegisterCode of usedRegisterCodes) {
549
+ usedRegisterCode.balanceItemId = balanceItem.id;
550
+ await usedRegisterCode.save();
551
+ }
552
+
553
+ result.balanceItemIds.push(balanceItem.id);
554
+ result.payingOrganizationIds.push(credit.organizationId);
555
+
556
+ return result;
557
+ }
558
+
559
+ /**
560
+ * Each legacy credit row becomes a balance item (preserving the ledger). A positive legacy change is
561
+ * credit we owe the organization, so it becomes a negative balance (priceOpen < 0). Expired credits are
562
+ * kept as Canceled so they preserve the history without affecting the open balance.
563
+ */
564
+ export async function convertRemainingCredit(organization: Organization, balance: number, ctx: ConversionContext): Promise<ConversionResult> {
565
+ const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
566
+
567
+ const balanceItem = new BalanceItem();
568
+ balanceItem.organizationId = ctx.membershipOrganization.id;
569
+ balanceItem.payingOrganizationId = organization.id;
570
+ balanceItem.type = BalanceItemType.ReferralDiscount;
571
+ balanceItem.description = 'Tegoed';
572
+ balanceItem.amount = 1;
573
+ balanceItem.unitPrice = -balance;
574
+ balanceItem.VATPercentage = 21;
575
+ balanceItem.VATIncluded = false;
576
+ balanceItem.status = BalanceItemStatus.Due;
577
+ balanceItem.dueAt = null;
578
+ balanceItem.createdAt = new Date();
579
+ balanceItem.VATExcempt = VATService.getVATExcempt({
580
+ company: organization.defaultCompanies[0] ?? null,
581
+ sellingOrganization: ctx.membershipOrganization,
582
+ type: 'services',
583
+ });
584
+
585
+ await balanceItem.save();
586
+
587
+ result.balanceItemIds.push(balanceItem.id);
588
+ result.payingOrganizationIds.push(organization.id);
589
+
590
+ return result;
591
+ }
592
+
593
+ async function refreshCaches(membershipOrganizationId: string, results: ConversionResult[]) {
594
+ const balanceItemIds = [...new Set(results.flatMap(r => r.balanceItemIds))];
595
+ const payingOrganizationIds = [...new Set(results.flatMap(r => r.payingOrganizationIds))];
596
+
597
+ if (balanceItemIds.length === 0) {
598
+ return;
599
+ }
600
+
601
+ // Recompute the cached pricePaid/pricePending/priceOpen and priceInvoiced values.
602
+ await BalanceItem.updatePricePaid(balanceItemIds);
603
+ await BalanceItem.updateInvoiced(balanceItemIds);
604
+
605
+ // Recompute the receivable balances per (paying) organization.
606
+ if (payingOrganizationIds.length > 0) {
607
+ await CachedBalance.updateForOrganizations(membershipOrganizationId, payingOrganizationIds);
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Orchestrates the full conversion. Throws when no membership organization is configured, and is a
613
+ * no-op in platform mode (platform deployments never had legacy SAAS billing).
614
+ */
615
+ export async function runConversion(): Promise<void> {
616
+ if (STAMHOOFD.userMode === 'platform') {
617
+ console.log('[LegacyBillingConverter] Skipping: running in platform mode');
618
+ return;
619
+ }
620
+
621
+ const platform = await Platform.getShared();
622
+ if (!platform.membershipOrganizationId) {
623
+ throw new Error('[LegacyBillingConverter] No membership organization configured: cannot migrate legacy billing');
624
+ }
625
+
626
+ const membershipOrganization = await Organization.getByID(platform.membershipOrganizationId, true);
627
+
628
+ const ctx: ConversionContext = {
629
+ membershipOrganization,
630
+ seller: membershipOrganization.defaultCompanies[0],
631
+ };
632
+
633
+ // First credits
634
+ let results: ConversionResult[] = [];
635
+
636
+ // All canceled credits (so we can link them with used referral codes)
637
+ await SeedTools.loop({
638
+ query: STCredit.select(),
639
+ batchSize: 100,
640
+ useTransactionPerBatch: true,
641
+ action: async (credit: STCredit) => {
642
+ results.push(await convertCredit(credit, ctx));
643
+
644
+ if (QueryableModel.shutdownMigrations) {
645
+ throw new Error('Gracefully stopping - migration supports retry');
646
+ }
647
+ },
648
+ });
649
+
650
+ // Remaining (because we don't support expiry dates)
651
+ await SeedTools.loop({
652
+ query: Organization.select(),
653
+ batchSize: 100,
654
+ useTransactionPerBatch: true,
655
+ action: async (organization: Organization) => {
656
+ const { balance } = await STCredit.getBalance(organization.id);
657
+ if (balance > 0) {
658
+ results.push(await convertRemainingCredit(organization, balance, ctx));
659
+ }
660
+
661
+ if (QueryableModel.shutdownMigrations) {
662
+ throw new Error('Gracefully stopping - migration supports retry');
663
+ }
664
+ },
665
+ });
666
+
667
+ await refreshCaches(membershipOrganization.id, results);
668
+ const systemUser = await User.getSystem();
669
+
670
+ // 1. Invoices: paid (full reconstruction) or in-progress (Hidden + linked pending payment).
671
+ async function processInvoice(stInvoice: STInvoice, results: ConversionResult[]) {
672
+ let payment = stInvoice.paymentId ? (await Payment.getByID(stInvoice.paymentId) ?? null) : null;
673
+ try {
674
+ if (stInvoice.number !== null) {
675
+ if (!payment) {
676
+ if (stInvoice.meta.stripeAccountId) {
677
+ // Create a new unpaid payment
678
+ payment = new Payment();
679
+ payment.adminUserId = systemUser.id;
680
+
681
+ // Who will receive this money?
682
+ payment.organizationId = membershipOrganization.id;
683
+
684
+ // Who paid
685
+ payment.payingOrganizationId = stInvoice.payingOrganizationId;
686
+ payment.customer = null; // will get filled in later
687
+
688
+ payment.status = PaymentStatus.Succeeded;
689
+ payment.price = stInvoice.meta.totalPrice * 100;
690
+ payment.roundingAmount = 0;
691
+ payment.method = PaymentMethod.AccountDeductions;
692
+ payment.type = PaymentType.Payment;
693
+ payment.createMandate = null;
694
+
695
+ payment.provider = PaymentProvider.Stripe;
696
+ payment.createdAt = stInvoice.createdAt;
697
+ payment.paidAt = stInvoice.createdAt;
698
+ await payment.save();
699
+ } else {
700
+ if (stInvoice.meta.totalPrice < 0) {
701
+ // Refund
702
+ payment = new Payment();
703
+ payment.adminUserId = systemUser.id;
704
+
705
+ // Who will receive this money?
706
+ payment.organizationId = membershipOrganization.id;
707
+
708
+ // Who paid
709
+ payment.payingOrganizationId = stInvoice.payingOrganizationId;
710
+ payment.customer = null; // will get filled in later
711
+
712
+ payment.status = PaymentStatus.Succeeded;
713
+ payment.price = stInvoice.meta.totalPrice * 100;
714
+ payment.roundingAmount = 0;
715
+ payment.method = PaymentMethod.Unknown;
716
+ payment.type = PaymentType.Refund;
717
+ payment.createMandate = null;
718
+ payment.createdAt = stInvoice.createdAt;
719
+ payment.paidAt = stInvoice.createdAt;
720
+
721
+ // payment.provider = PaymentProvider.Mollie;
722
+ // We currently don't have an ID stored for these refunds: to complete in the future after migration
723
+ await payment.save();
724
+ } else {
725
+ throw new Error('Unexpected missing payment for invoice that was not for stripe invoice nor refund ' + stInvoice.id);
726
+ }
727
+ }
728
+ }
729
+ results.push(await convertPaidInvoice(stInvoice, payment, ctx));
730
+ } else if (payment) {
731
+ results.push(await convertInProgressInvoice(stInvoice, payment, ctx));
732
+ }
733
+ } catch (e) {
734
+ console.error('Failure at ' + stInvoice.id, e);
735
+ throw e;
736
+ }
737
+ }
738
+
739
+ results = [];
740
+
741
+ await SeedTools.loop({
742
+ query: STInvoice.select(),
743
+ batchSize: 100,
744
+ useTransactionPerBatch: true,
745
+ action: async (invoice: STInvoice) => {
746
+ // Make sure any negative invoices exist, because otherwise the foreign key will fail
747
+ if (invoice.negativeInvoiceId) {
748
+ const negativeInvoice = await STInvoice.getByID(invoice.negativeInvoiceId);
749
+ if (negativeInvoice) {
750
+ await processInvoice(negativeInvoice, results);
751
+ }
752
+ }
753
+ await processInvoice(invoice, results);
754
+
755
+ if (QueryableModel.shutdownMigrations) {
756
+ throw new Error('Gracefully stopping - migration supports retry');
757
+ }
758
+ },
759
+ });
760
+
761
+ await refreshCaches(membershipOrganization.id, results);
762
+
763
+ // 2. Pending invoices (one per organization).
764
+ results = [];
765
+ await SeedTools.loop({
766
+ query: STPendingInvoice.select(),
767
+ batchSize: 100,
768
+ useTransactionPerBatch: true,
769
+ action: async (pendingInvoice: STPendingInvoice) => {
770
+ results.push(await convertPendingInvoice(pendingInvoice, ctx));
771
+
772
+ if (QueryableModel.shutdownMigrations) {
773
+ throw new Error('Gracefully stopping - migration supports retry');
774
+ }
775
+ },
776
+ });
777
+ await refreshCaches(membershipOrganization.id, results);
778
+
779
+ // Fix bbbaebe0-e749-4d9d-b3d8-327f43741f95 being succeeded but was refunded
780
+ const payment = await Payment.getByID('bbbaebe0-e749-4d9d-b3d8-327f43741f95');
781
+ if (payment) {
782
+ payment.status = PaymentStatus.Failed;
783
+ await payment.save();
784
+ }
785
+
786
+ console.log('[LegacyBillingConverter] Conversion finished');
787
+ }