@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
@@ -1,11 +1,10 @@
1
- import { createMollieClient, PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
2
1
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
2
  import type { Member, User } from '@stamhoofd/models';
4
- import { BalanceItem } from '@stamhoofd/models';
5
- import { BalanceItemPayment, Group, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, sendEmailTemplate } from '@stamhoofd/models';
3
+ import { BalanceItem, BalanceItemPayment, Group, Organization, PayconiqPayment, Payment, Platform, sendEmailTemplate, UsedRegisterCode } from '@stamhoofd/models';
6
4
  import { QueueHandler } from '@stamhoofd/queues';
7
5
  import type { Checkoutable, PaymentConfiguration, PrivatePaymentConfiguration } from '@stamhoofd/structures';
8
- import { AuditLogSource, BalanceItemPaymentDetailed, BalanceItemType, EmailTemplateType, Invoice, PaymentCustomer, PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, PaymentType, Recipient, VATExcemptReason, Version } from '@stamhoofd/structures';
6
+ import { AuditLogSource, BalanceItemPaymentDetailed, BalanceItemType, EmailTemplateType, Invoice, PaymentCustomer, PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, PaymentType, Recipient, Replacement, VATExcemptReason, Version } from '@stamhoofd/structures';
7
+ import type { CreateMandateSettings } from '@stamhoofd/structures/checkout/CreateMandateSettings.js';
9
8
  import type { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
10
9
  import { PaymentMandateStatus, PaymentMandateType } from '@stamhoofd/structures/PaymentMandate.js';
11
10
  import { Formatter } from '@stamhoofd/utility';
@@ -20,10 +19,22 @@ import { BalanceItemPaymentService } from './BalanceItemPaymentService.js';
20
19
  import { BalanceItemService } from './BalanceItemService.js';
21
20
  import { MollieService } from './MollieService.js';
22
21
  import { PaymentMandateService } from './PaymentMandateService.js';
23
- import type { CreateMandateSettings } from '@stamhoofd/structures/checkout/CreateMandateSettings.js';
22
+ import { ReferralService } from './ReferralService.js';
23
+ import { VATService } from './VATService.js';
24
+ import { STPackageService } from './STPackageService.js';
24
25
 
25
26
  export class PaymentService {
26
- static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
27
+ static async updateReversedPaymentsFor(payment: Payment) {
28
+ if (payment.reversingPaymentId) {
29
+ // Update refund amount of that payment
30
+ const reversingPayment = await Payment.getByID(payment.reversingPaymentId);
31
+ if (reversingPayment) {
32
+ await reversingPayment.updateRefundedAmount();
33
+ }
34
+ }
35
+ }
36
+
37
+ static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus, paidAt?: Date) {
27
38
  if (payment.status === status) {
28
39
  return;
29
40
  }
@@ -32,14 +43,19 @@ export class PaymentService {
32
43
  throw new SimpleError({
33
44
  code: 'cannot_fail',
34
45
  message: 'A payment that has been invoiced cannot be marked as failed. Instead create a separate chargeback payment with a negative amount.',
35
- human: $t('%1RI')
36
- })
46
+ human: $t('%1RI'),
47
+ });
48
+ }
49
+
50
+ if (organization.id !== payment.organizationId) {
51
+ throw new Error('Unexpected organization, expected ' + payment.organizationId + ', received ' + organization.id);
37
52
  }
38
53
 
39
54
  await AuditLogService.setContext({ fallbackUserId: payment.payingUserId, source: AuditLogSource.Payment, fallbackOrganizationId: payment.organizationId }, async () => {
40
55
  if (status === PaymentStatus.Succeeded) {
56
+ console.log('Marking as succeeded', payment.id);
41
57
  payment.status = PaymentStatus.Succeeded;
42
- payment.paidAt = new Date();
58
+ payment.paidAt = paidAt ?? new Date();
43
59
  await payment.save();
44
60
 
45
61
  // Prevent concurrency issues
@@ -57,15 +73,30 @@ export class PaymentService {
57
73
 
58
74
  // Flush caches so data is up to date in response
59
75
  await BalanceItemService.flushCaches(organization.id);
76
+
77
+ await this.updateReversedPaymentsFor(payment);
60
78
  });
61
79
 
80
+ if (payment.price >= 100_00 && payment.organizationId && payment.payingOrganizationId && payment.organizationId === ((await Platform.getShared()).membershipOrganizationId)) {
81
+ // We spend some money
82
+ const code = await UsedRegisterCode.getFor(payment.payingOrganizationId);
83
+ if (code && !code.balanceItemId) {
84
+ console.log('Rewarding code ' + code.id + ' for payment');
85
+
86
+ // Deze code werd nog niet beloond
87
+ await ReferralService.reward(code);
88
+ }
89
+ }
90
+
62
91
  // It is possible the mandate succeeds immediately, in which case we might
63
92
  // need to save it as the default payment method
64
- if (payment.status === PaymentStatus.Succeeded) {
65
- await this.saveMandateIfNeeded({
66
- payment,
67
- sellingOrganization: organization,
68
- })
93
+ await this.saveMandateIfNeeded({
94
+ payment,
95
+ sellingOrganizationId: organization.id,
96
+ });
97
+
98
+ if (payment.type === PaymentType.Chargeback) {
99
+ await this.handleChargebackCreated(payment, organization);
69
100
  }
70
101
  return;
71
102
  }
@@ -93,11 +124,14 @@ export class PaymentService {
93
124
 
94
125
  // Flush caches so data is up to date in response
95
126
  await BalanceItemService.flushCaches(organization.id);
127
+
128
+ await this.updateReversedPaymentsFor(payment);
96
129
  });
97
130
  }
98
131
 
99
132
  // Moved to failed
100
133
  if (status === PaymentStatus.Failed) {
134
+ console.log('Marking as failed', payment.id);
101
135
  await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
102
136
  const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
103
137
  (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
@@ -112,6 +146,10 @@ export class PaymentService {
112
146
  // Flush caches so data is up to date in response
113
147
  await BalanceItemService.flushCaches(organization.id);
114
148
  });
149
+
150
+ if (payment.type === PaymentType.Payment) {
151
+ await this.handlePaymentFailed(payment, organization);
152
+ }
115
153
  }
116
154
 
117
155
  // If OLD status was FAILED, we need to revert the actions
@@ -134,7 +172,84 @@ export class PaymentService {
134
172
  });
135
173
  }
136
174
 
137
- static async saveMandateIfNeeded({payment, sellingOrganization}: {payment: Payment, sellingOrganization: Organization}) {
175
+ private static async handleChargebackCreated(payment: Payment, organization: Organization) {
176
+ if (!payment.payingOrganizationId) {
177
+ // For now only for B2B
178
+ return;
179
+ }
180
+
181
+ const payingOrganization = await Organization.getByID(payment.payingOrganizationId, true);
182
+
183
+ await sendEmailTemplate(organization, {
184
+ recipients: await payingOrganization.getFinanceAdminRecipients(),
185
+ defaultReplacements: [
186
+ Replacement.create({
187
+ token: 'payingOrganizationName',
188
+ value: payingOrganization.name,
189
+ }),
190
+ Replacement.create({
191
+ token: 'paymentUrl',
192
+ value: 'https://' + payingOrganization.getDashboardHost() + '/boekhouding/openstaand/' + organization.uri,
193
+ }),
194
+ ],
195
+ template: {
196
+ type: EmailTemplateType.ChargebackPayingOrganization,
197
+ },
198
+ type: 'transactional',
199
+ });
200
+
201
+ await this.deactivatePackagesIfNeeded(payment, organization);
202
+ }
203
+
204
+ private static async handlePaymentFailed(payment: Payment, organization: Organization) {
205
+ if (!payment.payingOrganizationId) {
206
+ // For now only for B2B
207
+ return;
208
+ }
209
+
210
+ if (!payment.mandateId) {
211
+ return;
212
+ }
213
+
214
+ if (!payment.adminUserId && payment.method !== PaymentMethod.DirectDebit) {
215
+ return;
216
+ }
217
+
218
+ const payingOrganization = await Organization.getByID(payment.payingOrganizationId, true);
219
+
220
+ await sendEmailTemplate(organization, {
221
+ recipients: await payingOrganization.getFinanceAdminRecipients(),
222
+ defaultReplacements: [
223
+ Replacement.create({
224
+ token: 'payingOrganizationName',
225
+ value: payingOrganization.name,
226
+ }),
227
+ Replacement.create({
228
+ token: 'paymentUrl',
229
+ value: 'https://' + payingOrganization.getDashboardHost() + '/boekhouding/openstaand/' + organization.uri,
230
+ }),
231
+ ],
232
+ template: {
233
+ type: EmailTemplateType.AutomaticChargeFailedPayingOrganization,
234
+ },
235
+ type: 'transactional',
236
+ });
237
+
238
+ await this.deactivatePackagesIfNeeded(payment, organization);
239
+ }
240
+
241
+ private static async deactivatePackagesIfNeeded(payment: Payment, organization: Organization) {
242
+ // If admin initiated mandate, then mark packages as failed only
243
+ if ((payment.adminUserId || payment.type === PaymentType.Chargeback) && payment.mandateId && STAMHOOFD.userMode === 'organization') {
244
+ const platform = await Platform.getShared();
245
+ const payingOrganizationId = payment.payingOrganizationId;
246
+ if (payingOrganizationId && organization.id === platform.membershipOrganizationId) {
247
+ await STPackageService.markFailedPayment(payingOrganizationId);
248
+ }
249
+ }
250
+ }
251
+
252
+ static async saveMandateIfNeeded({ payment, sellingOrganizationId }: { payment: Payment; sellingOrganizationId: string }) {
138
253
  // Save as default
139
254
  if (payment.createMandate && payment.createMandate.saveAsDefault) {
140
255
  if (payment.mandateId && payment.status === PaymentStatus.Succeeded) {
@@ -142,9 +257,9 @@ export class PaymentService {
142
257
  await PaymentMandateService.setDefaultMandate({
143
258
  mandateId: payment.mandateId,
144
259
  payingOrganizationId: payment.payingOrganizationId,
145
- sellingOrganization,
146
- payingUserId: payment.payingUserId
147
- })
260
+ sellingOrganizationId,
261
+ payingUserId: payment.payingUserId,
262
+ });
148
263
  } catch (e) {
149
264
  // Ignore as setting the payment status is more important
150
265
  console.error(e);
@@ -165,6 +280,11 @@ export class PaymentService {
165
280
  return;
166
281
  }
167
282
 
283
+ if (org && payment.organizationId !== org.id && org.id !== payment.payingOrganizationId) {
284
+ console.error('Non-matching organization found for payment', payment.id, 'org', org.id);
285
+ return;
286
+ }
287
+
168
288
  // Explicitly set userId to null, because all actions caused by a poll are not caused by the currently signed in user, but the paying user id
169
289
  return await AuditLogService.setContext({ userId: payment.payingUserId ?? null, source: AuditLogSource.Payment }, async () => {
170
290
  if (!payment.organizationId) {
@@ -194,29 +314,29 @@ export class PaymentService {
194
314
  status = PaymentStatus.Failed;
195
315
  }
196
316
 
317
+ console.log('Stripe setting status to', status);
197
318
  await this.handlePaymentStatusUpdate(payment, organization, status);
198
- }
199
- catch (e) {
319
+ } catch (e) {
200
320
  console.error('Payment check failed Stripe', payment.id, e);
201
321
  if (this.isManualExpired(payment.status, payment)) {
202
322
  console.error('Manually marking Stripe payment as expired', payment.id);
203
323
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
204
324
  }
205
325
  }
206
- }
207
- else if (payment.provider === PaymentProvider.Mollie) {
326
+ } else if (payment.provider === PaymentProvider.Mollie) {
208
327
  try {
209
328
  const mollieClient = await MollieService.create({
210
- sellingOrganization: organization
211
- });
329
+ sellingOrganization: organization,
330
+ });
212
331
  if (mollieClient) {
213
- let {status} = await mollieClient.getStatus(payment, cancel || this.shouldTryToCancel(payment.status, payment));
332
+ let { status } = await mollieClient.getStatus(payment, cancel || this.shouldTryToCancel(payment.status, payment));
214
333
 
215
334
  if (this.isManualExpired(status, payment)) {
216
335
  console.error('Manually marking Mollie payment as expired', payment.id);
217
336
  status = PaymentStatus.Failed;
218
337
  }
219
338
 
339
+ console.log('Mollie setting status to', status);
220
340
  await this.handlePaymentStatusUpdate(payment, organization, status);
221
341
  } else {
222
342
  console.error('Missing Mollie Credentials for payment', payment.id);
@@ -225,7 +345,6 @@ export class PaymentService {
225
345
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
226
346
  }
227
347
  }
228
-
229
348
  } catch (e) {
230
349
  console.error('Payment check failed Mollie', payment.id, e);
231
350
  if (this.isManualExpired(payment.status, payment)) {
@@ -233,9 +352,8 @@ export class PaymentService {
233
352
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
234
353
  }
235
354
  }
236
- }
237
- else if (payment.provider === PaymentProvider.Buckaroo) {
238
- const helper = new BuckarooHelper(organization.privateMeta.buckarooSettings?.key ?? '', organization.privateMeta.buckarooSettings?.secret ?? '', organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production');
355
+ } else if (payment.provider === PaymentProvider.Buckaroo) {
356
+ const helper = new BuckarooHelper(organization.privateMeta.buckarooSettings?.key ?? '', organization.privateMeta.buckarooSettings?.secret ?? '', testMode);
239
357
  try {
240
358
  let status = await helper.getStatus(payment);
241
359
 
@@ -244,17 +362,16 @@ export class PaymentService {
244
362
  status = PaymentStatus.Failed;
245
363
  }
246
364
 
365
+ console.log('Buckaroo setting status to', status);
247
366
  await this.handlePaymentStatusUpdate(payment, organization, status);
248
- }
249
- catch (e) {
367
+ } catch (e) {
250
368
  console.error('Payment check failed Buckaroo', payment.id, e);
251
369
  if (this.isManualExpired(payment.status, payment)) {
252
370
  console.error('Manually marking Buckaroo payment as expired', payment.id);
253
371
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
254
372
  }
255
373
  }
256
- }
257
- else if (payment.provider == PaymentProvider.Payconiq) {
374
+ } else if (payment.provider == PaymentProvider.Payconiq) {
258
375
  // Check status
259
376
 
260
377
  const payconiqPayments = await PayconiqPayment.where({ paymentId: payment.id }, { limit: 1 });
@@ -272,8 +389,7 @@ export class PaymentService {
272
389
  console.error('Manually cancelling Payconiq payment', payment.id);
273
390
  if (await payconiqPayment.cancel(organization)) {
274
391
  status = PaymentStatus.Failed;
275
- }
276
- else {
392
+ } else {
277
393
  console.error('Failed to manually cancel payment');
278
394
  }
279
395
  }
@@ -284,8 +400,7 @@ export class PaymentService {
284
400
  }
285
401
 
286
402
  await this.handlePaymentStatusUpdate(payment, organization, status);
287
- }
288
- else {
403
+ } else {
289
404
  console.warn('Payconiq payment is missing for organization ' + organization.id + ' while checking payment status...');
290
405
 
291
406
  if (this.isManualExpired(payment.status, payment)) {
@@ -293,19 +408,17 @@ export class PaymentService {
293
408
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
294
409
  }
295
410
  }
296
- }
297
- else {
411
+ } else {
298
412
  console.error('Invalid payment provider', payment.provider, 'for payment', payment.id);
299
413
  if (this.isManualExpired(payment.status, payment)) {
300
414
  console.error('Manually marking unknown payment as expired', payment.id);
301
415
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
302
416
  }
303
417
  }
304
- }
305
- else {
418
+ } else {
306
419
  // Do a manual update if needed
307
420
  if (payment.status === PaymentStatus.Succeeded) {
308
- if ( (payment.provider === PaymentProvider.Mollie && STAMHOOFD.environment === 'development')) {
421
+ if ((payment.provider === PaymentProvider.Mollie && STAMHOOFD.environment === 'development')) {
309
422
  // Update the status
310
423
  await StripeHelper.getStatus(payment, false, testMode);
311
424
  }
@@ -370,7 +483,7 @@ export class PaymentService {
370
483
  // should be subtracted, not added
371
484
  const balanceItemsTotalPrice = payment.price - payment.roundingAmount;
372
485
 
373
- const {roundingAmount, price} = this.round(balanceItemsTotalPrice);
486
+ const { roundingAmount, price } = this.round(balanceItemsTotalPrice);
374
487
  payment.roundingAmount = roundingAmount;
375
488
  payment.price = price;
376
489
  }
@@ -382,7 +495,7 @@ export class PaymentService {
382
495
  if (difference === 0) {
383
496
  return {
384
497
  price: amount,
385
- roundingAmount: 0
498
+ roundingAmount: 0,
386
499
  };
387
500
  }
388
501
 
@@ -392,11 +505,11 @@ export class PaymentService {
392
505
 
393
506
  return {
394
507
  price: amount + difference,
395
- roundingAmount: difference
396
- }
508
+ roundingAmount: difference,
509
+ };
397
510
  }
398
511
 
399
- static calculateTotalPrice({ balanceItems, organization, members}: {
512
+ static calculateTotalPrice({ balanceItems, organization, members }: {
400
513
  balanceItems: Map<BalanceItem, number>;
401
514
  organization: Organization;
402
515
  members?: Member[];
@@ -458,19 +571,19 @@ export class PaymentService {
458
571
  });
459
572
  }
460
573
 
461
- return { hasNegative, price, roundingAmount, names }
574
+ return { hasNegative, price, roundingAmount, names };
462
575
  }
463
576
 
464
- static validateTotalPrice({ price, roundingAmount, checkout}: {
465
- price: number, // already rounded
466
- roundingAmount: number,
577
+ static validateTotalPrice({ price, roundingAmount, checkout }: {
578
+ price: number; // already rounded
579
+ roundingAmount: number;
467
580
  checkout: Pick<Checkoutable<never>, 'totalPrice'>;
468
581
  }) {
469
582
  // total price without rounding
470
- const balanceItemsPrice = price - roundingAmount;
583
+ const balanceItemsPrice = price - roundingAmount;
471
584
 
472
585
  // also accept rounding that might have happend in the frontend and that was correct
473
- if (checkout.totalPrice !== null && checkout.totalPrice !== balanceItemsPrice && checkout.totalPrice !== price) {
586
+ if (checkout.totalPrice !== null && checkout.totalPrice !== balanceItemsPrice && checkout.totalPrice !== price) {
474
587
  throw new SimpleError({
475
588
  code: 'changed_price',
476
589
  message: $t(`%vk`, { total: Formatter.price(price) }),
@@ -478,9 +591,7 @@ export class PaymentService {
478
591
  }
479
592
  }
480
593
 
481
- static async validateCustomer({ price, hasNegative, user, checkout, payingOrganization}: {
482
- price: number,
483
- hasNegative: boolean,
594
+ static async validateCustomer({ user, checkout, payingOrganization }: {
484
595
  user: User | null;
485
596
  checkout: Pick<Checkoutable<never>, 'customer'>;
486
597
  payingOrganization?: Organization | null;
@@ -490,84 +601,82 @@ export class PaymentService {
490
601
  firstName: user?.firstName,
491
602
  lastName: user?.lastName,
492
603
  email: user?.email,
493
- phone: checkout.customer?.phone
604
+ phone: checkout.customer?.phone,
494
605
  });
495
606
 
496
607
  // Use structured transfer description prefix
497
608
  let prefix = '';
498
609
 
499
610
  if (payingOrganization) {
500
- if (price !== 0 || hasNegative || checkout.customer) {
501
- if (!checkout.customer) {
502
- throw new SimpleError({
503
- code: 'missing_fields',
504
- message: 'customer is required when paying as an organization',
505
- human: $t(`%vz`),
506
- });
507
- }
611
+ if (!checkout.customer) {
612
+ throw new SimpleError({
613
+ code: 'missing_fields',
614
+ message: 'customer is required when paying as an organization',
615
+ human: $t(`%vz`),
616
+ });
617
+ }
508
618
 
509
- if (!checkout.customer.company) {
510
- throw new SimpleError({
511
- code: 'missing_fields',
512
- message: 'customer.company is required when paying as an organization',
513
- human: $t(`%w0`),
514
- });
515
- }
619
+ if (!checkout.customer.company) {
620
+ throw new SimpleError({
621
+ code: 'missing_fields',
622
+ message: 'customer.company is required when paying as an organization',
623
+ human: $t(`%w0`),
624
+ });
625
+ }
516
626
 
517
- // Search company id
518
- // this avoids needing to check the VAT number every time
519
- const id = checkout.customer.company.id;
520
- const foundCompany = payingOrganization.meta.companies.find(c => c.id === id);
627
+ // Search company id
628
+ // this avoids needing to check the VAT number every time
629
+ const id = checkout.customer.company.id;
630
+ let foundCompany = payingOrganization.meta.companies.find(c => c.id === id);
521
631
 
522
- if (!foundCompany) {
632
+ if (!foundCompany) {
633
+ if (payingOrganization.meta.companies.length > 0) {
523
634
  throw new SimpleError({
524
635
  code: 'invalid_data',
525
- message: $t(`%w1`),
636
+ message: 'Cannot set company data. Please save the company data to the paying organization meta data before using it.',
637
+ human: $t(`%w1`),
526
638
  });
527
639
  }
528
640
 
529
- if (!checkout.customer.company.equals(foundCompany)) {
530
- throw new SimpleError({
531
- code: 'invalid_data',
532
- message: 'Cannot change company data. Please save the company data to the paying organization meta data before using it.'
533
- });
534
- }
641
+ // In case of no organizations, we ignore the id of the default, as this changes every time
642
+ // we'll only compare the contents
643
+ foundCompany = payingOrganization.defaultCompanies[0];
644
+ }
535
645
 
536
- customer.company = foundCompany;
646
+ if (!checkout.customer.company.equals(foundCompany)) {
647
+ throw new SimpleError({
648
+ code: 'invalid_data',
649
+ message: 'Cannot change company data. Please save the company data to the paying organization meta data before using it.',
650
+ human: $t(`%w1`),
651
+ });
652
+ }
537
653
 
538
- const orgNumber = parseInt(payingOrganization.uri);
654
+ customer.company = foundCompany;
539
655
 
540
- if (orgNumber !== 0 && !isNaN(orgNumber)) {
541
- prefix = orgNumber + '';
542
- }
543
- }
544
- else {
545
- // Zero amount payment (without refunds) without specifying a company will just use the default company to link to the payment
546
- // It doesn't really matter since the price is zero and we won't invoice it.
547
- const company = this.getDefaultCompanyForOrganization(payingOrganization);
548
- if (company) {
549
- customer.company = company;
550
- }
656
+ const orgNumber = parseInt(payingOrganization.uri);
657
+
658
+ if (orgNumber !== 0 && !isNaN(orgNumber)) {
659
+ prefix = orgNumber + '';
551
660
  }
552
661
  } else {
553
662
  if (checkout.customer && checkout.customer.company) {
554
- customer.company = checkout.customer.company.clone()
663
+ customer.company = checkout.customer.company.clone();
555
664
  await ViesHelper.checkCompany(checkout.customer.company, customer.company);
556
665
  }
557
666
  }
558
667
 
559
- if (price !== 0 && customer.company?.VATNumber !== checkout.customer?.company?.VATNumber) {
668
+ if (customer.company?.VATNumber !== checkout.customer?.company?.VATNumber) {
560
669
  // Security check: because previous validation and generation might have used the VATNumber from the checkout
561
670
  throw new SimpleError({
562
671
  code: 'changed_VAT_number',
563
- message: 'Unexpected VAT number change'
564
- })
672
+ message: 'Unexpected VAT number change',
673
+ });
565
674
  }
566
675
 
567
676
  return { customer, prefix };
568
677
  }
569
678
 
570
- static async createProForma({ balanceItems, organization, user, members, checkout, payingOrganization}: {
679
+ static async createProForma({ balanceItems, organization, user, members, checkout, payingOrganization }: {
571
680
  balanceItems: Map<BalanceItem, number>;
572
681
  organization: Organization;
573
682
  user: User;
@@ -576,12 +685,12 @@ export class PaymentService {
576
685
  payingOrganization?: Organization | null;
577
686
  }) {
578
687
  // Calculate total price to pay
579
- const { price, hasNegative, roundingAmount } = PaymentService.calculateTotalPrice({ balanceItems, organization, members })
580
- PaymentService.validateTotalPrice({ price, roundingAmount, checkout })
688
+ const { price, roundingAmount } = PaymentService.calculateTotalPrice({ balanceItems, organization, members });
689
+ PaymentService.validateTotalPrice({ price, roundingAmount, checkout });
690
+
691
+ const { customer } = await PaymentService.validateCustomer({ user, checkout, payingOrganization });
692
+ const { seller } = PaymentService.validateVATRates({ customer, sellingOrganization: organization, balanceItems });
581
693
 
582
- const { customer, } = await PaymentService.validateCustomer({ user, checkout, payingOrganization, price, hasNegative })
583
- const { seller } = PaymentService.validateVATRates({ customer, organization, balanceItems });
584
-
585
694
  // Create invoice instead from fictive PaymentGeneral
586
695
  const fakePaymentGeneral = PaymentGeneral.create({
587
696
  id: 'pro-forma',
@@ -592,15 +701,15 @@ export class PaymentService {
592
701
  return BalanceItemPaymentDetailed.create({
593
702
  id: 'pro-forma-' + balanceItem.id,
594
703
  balanceItem: balanceItem.getStructure(),
595
- price
596
- })
597
- })
598
- })
704
+ price,
705
+ });
706
+ }),
707
+ });
599
708
 
600
709
  let invoice: Invoice | null = null;
601
710
 
602
711
  try {
603
- invoice = Invoice.create({
712
+ invoice = Invoice.create({
604
713
  seller,
605
714
  customer,
606
715
  payments: [fakePaymentGeneral],
@@ -617,46 +726,46 @@ export class PaymentService {
617
726
 
618
727
  return {
619
728
  invoice,
620
- payment: fakePaymentGeneral
621
- }
729
+ payment: fakePaymentGeneral,
730
+ };
622
731
  }
623
732
 
624
- static async registerChargeback(payment: Payment, amount: number) {
733
+ static async registerChargeback(payment: Payment, amount: number, date: Date) {
625
734
  if (amount !== payment.price) {
626
735
  // Creates issues to know what balance item was paid and what was not.
627
- throw new Error('Cannot register chargeback with different amount than the payment for payment ' + payment.id)
736
+ throw new Error('Cannot register chargeback with different amount than the payment for payment ' + payment.id);
628
737
  }
629
- const balanceItemPayments = await BalanceItemPayment.select().where('paymentId', payment.id).fetch()
630
- const items = await BalanceItem.getByIDs(...Formatter.uniqueArray(balanceItemPayments.map(b => b.balanceItemId)))
738
+ const balanceItemPayments = await BalanceItemPayment.select().where('paymentId', payment.id).fetch();
739
+ const items = await BalanceItem.getByIDs(...Formatter.uniqueArray(balanceItemPayments.map(b => b.balanceItemId)));
631
740
 
632
- // Done validation
741
+ // Done validation
633
742
  const chargeback = new Payment();
634
743
 
635
744
  // Who will receive this money?
636
745
  chargeback.organizationId = payment.organizationId!;
637
746
 
638
747
  // Who paid
639
- chargeback.payingUserId = payment.payingUserId
640
- chargeback.payingOrganizationId = payment.payingOrganizationId
748
+ chargeback.payingUserId = payment.payingUserId;
749
+ chargeback.payingOrganizationId = payment.payingOrganizationId;
641
750
  chargeback.customer = payment.customer;
642
751
 
643
- chargeback.status = PaymentStatus.Succeeded;
644
- chargeback.paidAt = new Date();
752
+ chargeback.status = PaymentStatus.Created;
645
753
  chargeback.price = -payment.price;
646
754
  chargeback.roundingAmount = -payment.roundingAmount;
647
755
  chargeback.method = payment.method;
648
756
  chargeback.type = PaymentType.Chargeback;
757
+ chargeback.mandateId = payment.mandateId; ;
758
+ chargeback.adminUserId = payment.adminUserId;
649
759
 
650
760
  chargeback.provider = payment.provider;
651
- chargeback.stripeAccountId = payment.stripeAccountId
652
-
761
+ chargeback.stripeAccountId = payment.stripeAccountId;
762
+ chargeback.reversingPaymentId = payment.id;
653
763
  await chargeback.save();
654
764
 
655
-
656
765
  for (const original of balanceItemPayments) {
657
766
  // Create one balance item payment to pay it in one payment
658
767
  const balanceItemPayment = new BalanceItemPayment();
659
- balanceItemPayment.balanceItemId = original.balanceItemId
768
+ balanceItemPayment.balanceItemId = original.balanceItemId;
660
769
  balanceItemPayment.paymentId = chargeback.id;
661
770
  balanceItemPayment.organizationId = chargeback.organizationId;
662
771
  balanceItemPayment.price = -original.price;
@@ -666,10 +775,19 @@ export class PaymentService {
666
775
  // Update cached balance items pending amount (only created balance items, because those are involved in the payment)
667
776
  await BalanceItemService.updatePaidAndPending(items);
668
777
 
778
+ const organization = await Organization.getByID(chargeback.organizationId, true);
779
+
780
+ try {
781
+ await this.handlePaymentStatusUpdate(chargeback, organization, PaymentStatus.Succeeded, date);
782
+ } catch (e) {
783
+ console.error('Failed to mark chargeback as succeeded', e);
784
+ await chargeback.delete();
785
+ throw e;
786
+ }
669
787
  return chargeback;
670
788
  }
671
789
 
672
- static async createPayment({ balanceItems, organization, user, members, checkout, payingOrganization, serviceFeeType, createMandate, useMandate, paymentConfiguration, privatePaymentConfiguration}: {
790
+ static async createPayment({ balanceItems, organization, user, members, checkout, payingOrganization, serviceFeeType, createMandate, useMandate, paymentConfiguration, privatePaymentConfiguration, adminUserId }: {
673
791
  balanceItems: Map<BalanceItem, number>;
674
792
  organization: Organization;
675
793
  user: User | null;
@@ -677,34 +795,35 @@ export class PaymentService {
677
795
  checkout: Pick<Checkoutable<never>, 'paymentMethod' | 'totalPrice' | 'customer' | 'cancelUrl' | 'redirectUrl'>;
678
796
  payingOrganization?: Organization | null;
679
797
  serviceFeeType: 'webshop' | 'members' | 'tickets' | 'system';
680
- createMandate: CreateMandateSettings | null,
681
- useMandate: PaymentMandate | null,
682
- paymentConfiguration: PaymentConfiguration,
683
- privatePaymentConfiguration: PrivatePaymentConfiguration,
798
+ createMandate: CreateMandateSettings | null;
799
+ useMandate: PaymentMandate | null;
800
+ paymentConfiguration: PaymentConfiguration;
801
+ privatePaymentConfiguration: PrivatePaymentConfiguration;
802
+ adminUserId?: string | null;
684
803
  }) {
685
804
  if (balanceItems.size === 0) {
686
805
  return null;
687
806
  }
688
807
 
689
808
  // Calculate total price to pay
690
- const { price, roundingAmount, hasNegative, names } = this.calculateTotalPrice({ balanceItems, organization, members })
691
- PaymentService.validateTotalPrice({ price, roundingAmount, checkout })
809
+ const { price, roundingAmount, hasNegative, names } = this.calculateTotalPrice({ balanceItems, organization, members });
810
+ PaymentService.validateTotalPrice({ price, roundingAmount, checkout });
692
811
 
693
- const { customer, prefix } = await this.validateCustomer({ user, checkout, payingOrganization, price, hasNegative })
694
- this.validateVATRates({ customer, organization, balanceItems });
695
-
696
- const {method, type, mandate} = await this.validatePaymentMethod({
812
+ const { customer, prefix } = await this.validateCustomer({ user, checkout, payingOrganization });
813
+ this.validateVATRates({ customer, sellingOrganization: organization, balanceItems });
814
+
815
+ const { method, type, mandate } = await this.validatePaymentMethod({
697
816
  method: checkout.paymentMethod ?? PaymentMethod.Unknown,
698
817
  mandate: useMandate,
699
818
  createMandate: !!createMandate,
700
819
  customer,
701
820
  price,
702
821
  hasNegative,
703
- balanceItems,
822
+ balanceItems,
704
823
  paymentConfiguration,
705
824
  user,
706
825
  payingOrganization: payingOrganization ?? null,
707
- sellingOrganization: organization
826
+ sellingOrganization: organization,
708
827
  });
709
828
 
710
829
  // Check URL's set fro online payments
@@ -724,8 +843,8 @@ export class PaymentService {
724
843
  throw new SimpleError({
725
844
  code: 'cannot_create_mandate_for_provider',
726
845
  message: 'Saving a payment method is not yet supported for this payment method',
727
- human: $t('%1U0')
728
- })
846
+ human: $t('%1U0'),
847
+ });
729
848
  }
730
849
  }
731
850
 
@@ -746,7 +865,9 @@ export class PaymentService {
746
865
  payment.roundingAmount = roundingAmount;
747
866
  payment.method = method;
748
867
  payment.type = type;
749
- payment.createMandate = createMandate
868
+ payment.createMandate = createMandate;
869
+ payment.mandateId = mandate?.id ?? null;
870
+ payment.adminUserId = adminUserId ?? null;
750
871
 
751
872
  if (price === 0) {
752
873
  payment.status = PaymentStatus.Succeeded;
@@ -755,7 +876,8 @@ export class PaymentService {
755
876
 
756
877
  payment.provider = provider;
757
878
  payment.stripeAccountId = stripeAccount?.id ?? null;
758
- ServiceFeeHelper.setServiceFee(payment, organization, serviceFeeType, [...balanceItems.entries()].map(([_, p]) => p));
879
+ await ServiceFeeHelper.setServiceFee(payment, organization, serviceFeeType, [...balanceItems.entries()].map(([_, p]) => p));
880
+ await ServiceFeeHelper.setTransferFee({ payment, organization, stripeAccount });
759
881
 
760
882
  // Add transfer description
761
883
  if (payment.method === PaymentMethod.Transfer) {
@@ -810,13 +932,11 @@ export class PaymentService {
810
932
  // Send a small reminder email
811
933
  try {
812
934
  await this.sendTransferEmail(customer, user?.id ?? null, organization, payment);
813
- }
814
- catch (e) {
935
+ } catch (e) {
815
936
  console.error('Failed to send transfer email');
816
937
  console.error(e);
817
938
  }
818
- }
819
- else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
939
+ } else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
820
940
  if ((!checkout.redirectUrl || !checkout.cancelUrl) && !mandate) {
821
941
  throw new Error('Should have been caught earlier');
822
942
  }
@@ -825,7 +945,7 @@ export class PaymentService {
825
945
  _redirectUrl.searchParams.set('paymentId', payment.id);
826
946
  _redirectUrl.searchParams.set('organizationId', payment.payingOrganizationId ?? organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
827
947
 
828
- const _cancelUrl = new URL(checkout.cancelUrl?? ('https://' + STAMHOOFD.domains.dashboard));
948
+ const _cancelUrl = new URL(checkout.cancelUrl ?? ('https://' + STAMHOOFD.domains.dashboard));
829
949
  _cancelUrl.searchParams.set('paymentId', payment.id);
830
950
  _cancelUrl.searchParams.set('cancel', 'true');
831
951
  _cancelUrl.searchParams.set('organizationId', payment.payingOrganizationId ?? organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
@@ -834,18 +954,18 @@ export class PaymentService {
834
954
  const cancelUrl = _cancelUrl.href;
835
955
 
836
956
  const webhookUrl = 'https://' + organization.getApiHost() + '/v' + Version + '/payments/' + encodeURIComponent(payment.id) + '?exchange=true';
837
-
957
+
838
958
  if (payment.provider === PaymentProvider.Stripe) {
839
959
  if (createMandate || mandate) {
840
960
  // Already checked, but for security
841
- throw new Error('Unsupported')
961
+ throw new Error('Unsupported');
842
962
  }
843
963
  if (!customer.email) {
844
964
  throw new SimpleError({
845
965
  code: 'missing_email',
846
966
  message: 'Email address is required for online payments via Stripe',
847
- human: $t('%1SJ')
848
- })
967
+ human: $t('%1SJ'),
968
+ });
849
969
  }
850
970
  const stripeResult = await StripeHelper.createPayment({
851
971
  payment,
@@ -866,8 +986,7 @@ export class PaymentService {
866
986
  },
867
987
  });
868
988
  paymentUrl = stripeResult.paymentUrl;
869
- }
870
- else if (payment.provider === PaymentProvider.Mollie) {
989
+ } else if (payment.provider === PaymentProvider.Mollie) {
871
990
  const mollieResult = await MollieService.createPayment({
872
991
  payment,
873
992
  redirectUrl,
@@ -886,18 +1005,16 @@ export class PaymentService {
886
1005
  mandate,
887
1006
  });
888
1007
  paymentUrl = mollieResult.paymentUrl;
889
- }
890
- else if (payment.provider === PaymentProvider.Payconiq) {
1008
+ } else if (payment.provider === PaymentProvider.Payconiq) {
891
1009
  if (createMandate || mandate) {
892
1010
  // Already checked, but for security
893
- throw new Error('Unsupported')
1011
+ throw new Error('Unsupported');
894
1012
  }
895
1013
  ({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl));
896
- }
897
- else if (payment.provider === PaymentProvider.Buckaroo) {
1014
+ } else if (payment.provider === PaymentProvider.Buckaroo) {
898
1015
  if (createMandate || mandate) {
899
1016
  // Already checked, but for security
900
- throw new Error('Unsupported')
1017
+ throw new Error('Unsupported');
901
1018
  }
902
1019
 
903
1020
  // Increase request timeout because buckaroo is super slow (in development)
@@ -916,20 +1033,18 @@ export class PaymentService {
916
1033
  }
917
1034
  }
918
1035
  }
919
- }
920
- catch (e) {
1036
+ } catch (e) {
921
1037
  await PaymentService.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
922
1038
  throw e;
923
1039
  }
924
1040
 
925
1041
  // TypeScript thinks status cannot change to Failed, but it can.
926
- if (payment.status === PaymentStatus.Succeeded || (payment.status as PaymentStatus) === PaymentStatus.Failed) {
1042
+ if (payment.status === PaymentStatus.Succeeded || (payment.status as PaymentStatus) === PaymentStatus.Failed) {
927
1043
  // force update
928
- const updateTo = payment.status
929
- payment.status = PaymentStatus.Created
1044
+ const updateTo = payment.status;
1045
+ payment.status = PaymentStatus.Created;
930
1046
  await PaymentService.handlePaymentStatusUpdate(payment, organization, updateTo);
931
- }
932
- else if (payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Unknown || (payment.method === PaymentMethod.DirectDebit && mandate)) {
1047
+ } else if (payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Unknown || (payment.method === PaymentMethod.DirectDebit && mandate)) {
933
1048
  // Mark valid (not same as paid) if needed
934
1049
  let hasBundleDiscount = false;
935
1050
  for (const [balanceItem] of balanceItems) {
@@ -959,7 +1074,7 @@ export class PaymentService {
959
1074
  static async sendTransferEmail(customer: PaymentCustomer, userId: string | null, organization: Organization, payment: Payment) {
960
1075
  const email = customer.dynamicEmail;
961
1076
  if (!email) {
962
- console.warn('Skipped sending transfer email because of missing email address', payment.id)
1077
+ console.warn('Skipped sending transfer email because of missing email address', payment.id);
963
1078
  return;
964
1079
  }
965
1080
  const paymentGeneral = await payment.getGeneralStructure();
@@ -994,37 +1109,110 @@ export class PaymentService {
994
1109
  });
995
1110
  }
996
1111
 
997
- static async validatePaymentMethod({
1112
+ static async validateMandate({
1113
+ method,
1114
+ paymentConfiguration,
1115
+ mandate,
1116
+ payingOrganization,
1117
+ sellingOrganization,
1118
+ user,
1119
+ }: {
1120
+ method: PaymentMethod;
1121
+ paymentConfiguration: PaymentConfiguration;
1122
+ mandate: PaymentMandate;
1123
+ payingOrganization: Organization | null;
1124
+ sellingOrganization: Organization;
1125
+ user: User | null;
1126
+ }) {
1127
+ if (!paymentConfiguration.enableMandates) {
1128
+ throw new SimpleError({
1129
+ code: 'mandates_disabled',
1130
+ message: 'Cannot pay with mandate',
1131
+ human: $t('%1SK'),
1132
+ });
1133
+ }
1134
+
1135
+ // Validate mandate + update payment method based on the mandate
1136
+ if (method !== PaymentMethod.Unknown) {
1137
+ throw new SimpleError({
1138
+ code: 'invalid_data',
1139
+ message: 'Cannot combine setting mandate with method',
1140
+ });
1141
+ }
1142
+
1143
+ const allMandates = await PaymentMandateService.getMandates({
1144
+ payingOrganization,
1145
+ sellingOrganization,
1146
+ user,
1147
+ });
1148
+
1149
+ const existingMandate = allMandates.find(m => m.id === mandate.id && m.provider === mandate.provider);
1150
+ if (!existingMandate) {
1151
+ throw new SimpleError({
1152
+ code: 'not_found',
1153
+ message: 'Mandate not found',
1154
+ human: $t('%1TQ'),
1155
+ field: 'mandateId',
1156
+ });
1157
+ }
1158
+
1159
+ if (existingMandate.status !== PaymentMandateStatus.Valid) {
1160
+ throw new SimpleError({
1161
+ code: 'mandate_not_valid',
1162
+ message: 'Mandate not valid',
1163
+ human: $t('%1QA'),
1164
+ field: 'mandateId',
1165
+ });
1166
+ }
1167
+ return existingMandate;
1168
+ }
1169
+
1170
+ static async validatePaymentMethod({
998
1171
  method,
999
1172
  customer,
1000
1173
  price,
1001
1174
  hasNegative,
1002
- balanceItems,
1175
+ balanceItems,
1003
1176
  paymentConfiguration,
1004
1177
  mandate,
1005
1178
  payingOrganization,
1006
1179
  sellingOrganization,
1007
1180
  user,
1008
- createMandate
1009
- }: {
1010
- method: PaymentMethod,
1011
- customer: PaymentCustomer,
1012
- price: number,
1013
- hasNegative: boolean,
1014
- balanceItems: Map<BalanceItem, number>;
1015
- paymentConfiguration: PaymentConfiguration,
1016
- mandate: PaymentMandate | null,
1017
- payingOrganization: Organization | null,
1018
- sellingOrganization: Organization,
1019
- user: User | null,
1020
- createMandate: boolean
1181
+ createMandate,
1182
+ }: {
1183
+ method: PaymentMethod;
1184
+ customer: PaymentCustomer;
1185
+ price: number;
1186
+ hasNegative: boolean;
1187
+ balanceItems: Map<BalanceItem, number>;
1188
+ paymentConfiguration: PaymentConfiguration;
1189
+ mandate: PaymentMandate | null;
1190
+ payingOrganization: Organization | null;
1191
+ sellingOrganization: Organization;
1192
+ user: User | null;
1193
+ createMandate: boolean;
1021
1194
  }) {
1022
1195
  if (price === 0) {
1023
1196
  if (balanceItems.size === 0) {
1024
- throw new Error('Empty payment')
1197
+ throw new Error('Empty payment');
1025
1198
  }
1026
1199
 
1027
- if (createMandate) {
1200
+ // We still need to validate the mandate, because
1201
+ // we still allow setting a new default payment method
1202
+ // for zero payments
1203
+ // - e.g. activating a new package with an existing mandate
1204
+ const existingMandate = mandate
1205
+ ? await this.validateMandate({
1206
+ method,
1207
+ mandate,
1208
+ paymentConfiguration,
1209
+ payingOrganization,
1210
+ sellingOrganization,
1211
+ user,
1212
+ })
1213
+ : null;
1214
+
1215
+ if (createMandate && !existingMandate) {
1028
1216
  throw new SimpleError({
1029
1217
  code: 'cannot_create_mandate_without_payment',
1030
1218
  message: 'Cannot create saved payment method without payment of a small amount',
@@ -1037,88 +1225,56 @@ export class PaymentService {
1037
1225
  return {
1038
1226
  method: PaymentMethod.Unknown,
1039
1227
  type: PaymentType.Reallocation,
1040
- mandate: null
1041
- }
1228
+ mandate: existingMandate,
1229
+ };
1042
1230
  }
1043
1231
 
1044
1232
  return {
1045
1233
  // Free purchase
1046
1234
  method: PaymentMethod.Unknown,
1047
1235
  type: PaymentType.Payment,
1048
- mandate: null
1049
- }
1236
+ mandate: existingMandate,
1237
+ };
1050
1238
  }
1051
1239
 
1052
1240
  if (mandate) {
1053
- if (!paymentConfiguration.enableMandates) {
1054
- throw new SimpleError({
1055
- code: 'mandates_disabled',
1056
- message: 'Cannot pay with mandate',
1057
- human: $t('%1SK')
1058
- });
1059
- }
1060
-
1061
- // Validate mandate + update payment method based on the mandate
1062
- if (method !== PaymentMethod.Unknown) {
1063
- throw new SimpleError({
1064
- code: 'invalid_data',
1065
- message: 'Cannot combine setting mandate with method'
1066
- });
1067
- }
1068
-
1069
- const allMandates = await PaymentMandateService.getMandates({
1241
+ const existingMandate = await this.validateMandate({
1242
+ method,
1243
+ mandate,
1244
+ paymentConfiguration,
1070
1245
  payingOrganization,
1071
1246
  sellingOrganization,
1072
1247
  user,
1073
1248
  });
1074
1249
 
1075
- const existingMandate = allMandates.find(m => m.id === mandate.id && m.provider === mandate.provider);
1076
- if (!existingMandate) {
1077
- throw new SimpleError({
1078
- code: 'not_found',
1079
- message: 'Mandate not found',
1080
- human: $t('%1TQ'),
1081
- field: 'mandateId'
1082
- });
1083
- }
1084
-
1085
- if (existingMandate.status !== PaymentMandateStatus.Valid) {
1086
- throw new SimpleError({
1087
- code: 'mandate_not_valid',
1088
- message: 'Mandate not valid',
1089
- human: $t('%1QA'),
1090
- field: 'mandateId'
1091
- });
1092
- }
1093
-
1094
1250
  switch (existingMandate.type) {
1095
1251
  case PaymentMandateType.CreditCard:
1096
1252
  return {
1097
1253
  mandate: existingMandate,
1098
1254
  method: PaymentMethod.CreditCard,
1099
- type: PaymentType.Payment
1100
- }
1255
+ type: PaymentType.Payment,
1256
+ };
1101
1257
  case PaymentMandateType.DirectDebit:
1102
1258
  return {
1103
1259
  mandate: existingMandate,
1104
1260
  method: PaymentMethod.DirectDebit,
1105
- type: PaymentType.Payment
1106
- }
1261
+ type: PaymentType.Payment,
1262
+ };
1107
1263
  }
1108
1264
 
1109
- throw new Error('Unsupported mandate type')
1265
+ throw new Error('Unsupported mandate type');
1110
1266
  }
1111
1267
 
1112
1268
  if (createMandate) {
1113
1269
  if (!paymentConfiguration.enableMandates) {
1114
- throw new SimpleError({
1270
+ throw new SimpleError({
1115
1271
  code: 'mandates_disabled',
1116
1272
  message: 'Cannot pay with mandate',
1117
- human: $t('%1R1')
1273
+ human: $t('%1R1'),
1118
1274
  });
1119
1275
  }
1120
1276
  }
1121
-
1277
+
1122
1278
  if (method === PaymentMethod.Unknown) {
1123
1279
  throw new SimpleError({
1124
1280
  code: 'invalid_data',
@@ -1130,7 +1286,7 @@ export class PaymentService {
1130
1286
  const allowedPaymentMethods = paymentConfiguration.getAvailablePaymentMethods({
1131
1287
  amount: price,
1132
1288
  customer: customer,
1133
- forMandate: createMandate
1289
+ forMandate: createMandate,
1134
1290
  });
1135
1291
 
1136
1292
  if (!allowedPaymentMethods.includes(method)) {
@@ -1143,41 +1299,64 @@ export class PaymentService {
1143
1299
  return {
1144
1300
  method,
1145
1301
  type: PaymentType.Payment,
1146
- mandate: null
1147
- }
1148
- }
1149
-
1150
- static getDefaultCompanyForOrganization(sellingOrganization: Organization) {
1151
- return sellingOrganization.meta.companies[0];
1302
+ mandate: null,
1303
+ };
1152
1304
  }
1153
1305
 
1154
- static getVATExcempt({ customer, sellingOrganization }: { customer: PaymentCustomer | null; sellingOrganization: Organization; }) {
1306
+ static validateVATRates({ customer, sellingOrganization, balanceItems }: { customer: PaymentCustomer; sellingOrganization: Organization; balanceItems: Map<BalanceItem, number> }) {
1155
1307
  // Validate VAT rates for this customer
1156
- const seller = this.getDefaultCompanyForOrganization(sellingOrganization)
1157
- if (seller && seller.VATNumber && seller.address && customer && customer.company) {
1158
- // B2B validation
1159
- if (!customer.company.address) {
1160
- throw new SimpleError({
1161
- code: 'missing_field',
1162
- message: 'Company address missing',
1163
- human: $t('%1LH'),
1164
- field: 'customer.company.address',
1165
- });
1308
+ const seller = VATService.getDefaultCompanyForOrganization(sellingOrganization);
1309
+ const excempt = VATService.isVATExcempt({
1310
+ company: customer.company,
1311
+ sellingOrganization,
1312
+ });
1313
+
1314
+ // Reverse charged vat applicable?
1315
+ if (excempt) {
1316
+ // Check VAT Exempt is set on each an every balance item with a non-zero price
1317
+ for (const [item] of balanceItems) {
1318
+ if (item.VATExcempt !== VATExcemptReason.IntraCommunityServices && item.VATExcempt !== VATExcemptReason.IntraCommunityGoods) {
1319
+ throw new SimpleError({
1320
+ code: 'VAT_error',
1321
+ message: 'Intra community VAT reverse charge not supported for this purchase',
1322
+ human: $t('%1LI'),
1323
+ });
1324
+ }
1325
+
1326
+ // We also need to know the VAT rate exactly to be sure the VAT is removed from the purchase
1327
+ // If VAT is not included, we don't need to know the VAT percentage until the payment is invoiced
1328
+ if (item.VATPercentage === null && item.VATIncluded) {
1329
+ throw new SimpleError({
1330
+ code: 'VAT_error',
1331
+ message: 'Intra community VAT reverse charge is not supported for this purchase because of missing VAT rates',
1332
+ human: $t('%1LJ'),
1333
+ });
1334
+ }
1166
1335
  }
1336
+ } else {
1337
+ // Fine to just not have setup VAT rates yet if the price is guaranteed to include VAT
1338
+ for (const [item] of balanceItems) {
1339
+ if (item.VATExcempt === VATExcemptReason.IntraCommunityGoods || item.VATExcempt === VATExcemptReason.IntraCommunityServices) {
1340
+ throw new SimpleError({
1341
+ code: 'VAT_error',
1342
+ message: 'Unexpected reverse charge applied',
1343
+ human: $t('%1LK'),
1344
+ });
1345
+ }
1167
1346
 
1168
- // Reverse charged vat applicable?
1169
- if (customer.company.address.country !== seller.address.country && customer.company.VATNumber) {
1170
- return VATExcemptReason.IntraCommunity;
1347
+ if (seller && seller.VATNumber) {
1348
+ if (!item.VATIncluded && item.VATPercentage === null) {
1349
+ throw new SimpleError({
1350
+ code: 'VAT_error',
1351
+ message: 'Missing VAT percentage',
1352
+ human: $t('%1LL'),
1353
+ });
1354
+ }
1355
+ }
1171
1356
  }
1172
1357
  }
1173
1358
 
1174
- return null
1175
- }
1176
-
1177
- static validateVATRates({ customer, organization, balanceItems }: { customer: PaymentCustomer; organization: Organization; balanceItems: Map<BalanceItem, number> }) {
1178
- // Validate VAT rates for this customer
1179
- const seller = this.getDefaultCompanyForOrganization(organization)
1180
- if (seller && seller.VATNumber && seller.address && customer.company) {
1359
+ /* if (seller && seller.VATNumber && seller.address && customer.company) {
1181
1360
  // B2B validation
1182
1361
  if (!customer.company.address) {
1183
1362
  throw new SimpleError({
@@ -1188,56 +1367,13 @@ export class PaymentService {
1188
1367
  });
1189
1368
  }
1190
1369
 
1191
- // Reverse charged vat applicable?
1192
- if (customer.company.address.country !== seller.address.country && customer.company.VATNumber) {
1193
- // Check VAT Exempt is set on each an every balance item with a non-zero price
1194
- for (const [item] of balanceItems) {
1195
- if (item.VATExcempt !== VATExcemptReason.IntraCommunity) {
1196
- throw new SimpleError({
1197
- code: 'VAT_error',
1198
- message: 'Intra community VAT reverse charge not supported for this purchase',
1199
- human: $t('%1LI'),
1200
- });
1201
- }
1202
-
1203
- // We also need to know the VAT rate exactly to be sure the VAT is removed from the purchase
1204
- // If VAT is not included, we don't need to know the VAT percentage until the payment is invoiced
1205
- if (item.VATPercentage === null && item.VATIncluded) {
1206
- throw new SimpleError({
1207
- code: 'VAT_error',
1208
- message: 'Intra community VAT reverse charge is not supported for this purchase because of missing VAT rates',
1209
- human: $t('%1LJ'),
1210
- });
1211
- }
1212
- }
1213
- }
1214
- else {
1215
- // Fine to just not have setup VAT rates yet if the price is guaranteed to include VAT
1216
- for (const [item] of balanceItems) {
1217
- if (item.VATExcempt === VATExcemptReason.IntraCommunity) {
1218
- throw new SimpleError({
1219
- code: 'VAT_error',
1220
- message: 'Unexpected reverse charge applied',
1221
- human: $t('%1LK'),
1222
- });
1223
- }
1224
-
1225
- if (!item.VATIncluded && item.VATPercentage === null) {
1226
- throw new SimpleError({
1227
- code: 'VAT_error',
1228
- message: 'Missing VAT percentage',
1229
- human: $t('%1LL'),
1230
- });
1231
- }
1232
- }
1233
- }
1234
1370
  }
1235
1371
  else {
1236
1372
  // B2C / C2B / C2C
1237
1373
 
1238
1374
  // You cannot buy balance items with VAT if you didn't set up a VAT number.
1239
1375
  for (const [item] of balanceItems) {
1240
- if (item.VATExcempt === VATExcemptReason.IntraCommunity) {
1376
+ if (item.VATExcempt === VATExcemptReason.IntraCommunityGoods || item.VATExcempt === VATExcemptReason.IntraCommunityServices) {
1241
1377
  throw new SimpleError({
1242
1378
  code: 'VAT_error',
1243
1379
  message: 'Unexpected reverse charge applied',
@@ -1255,8 +1391,8 @@ export class PaymentService {
1255
1391
  }
1256
1392
  }
1257
1393
  }
1258
- }
1394
+ } */
1259
1395
 
1260
- return {seller}
1396
+ return { seller };
1261
1397
  }
1262
1398
  };