@stamhoofd/backend 2.120.6 → 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 (434) hide show
  1. package/output-2.txt +1 -0
  2. package/output.txt +1 -0
  3. package/package.json +70 -53
  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 +46 -0
  8. package/src/audit-logs/init.ts +9 -1
  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 +7 -0
  19. package/src/crons/invoices.ts +160 -0
  20. package/src/crons/members-fees.ts +50 -0
  21. package/src/crons/mollie-chargebacks.ts +86 -0
  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 +54 -74
  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 +87 -46
  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 +51 -0
  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 +32 -13
  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 +38 -33
  122. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  123. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +494 -0
  124. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +213 -0
  125. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +404 -0
  126. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +167 -0
  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 +19 -6
  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/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +4 -9
  142. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  143. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  144. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +41 -18
  145. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  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 +12 -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 +311 -0
  152. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  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 +15 -4
  168. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +11 -2
  169. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +8 -8
  170. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +10 -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 +67 -20
  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 +24 -17
  185. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +134 -0
  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 +32 -22
  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 +29 -8
  215. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +21 -28
  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 +15 -13
  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 +28 -50
  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 +60 -65
  246. package/src/helpers/AuthenticatedStructures.ts +153 -51
  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 +21 -0
  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 +741 -0
  268. package/src/helpers/RecordAnswerHelper.ts +116 -0
  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 +15 -38
  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 +11 -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/migrations/1750090030-records-configuration.ts +92 -0
  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 +95 -27
  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/1779121239-default-invoice-email-template.sql +3 -0
  300. package/src/migrations/1779121240-invoice-email-template.sql +6 -0
  301. package/src/migrations/1779986893-default-payment-failed-email-templates.sql +4 -0
  302. package/src/migrations/1780578166-convert-legacy-billing.ts +13 -0
  303. package/src/{seeds/1755876819-remove-duplicate-members.ts → migrations/1780665426-remove-duplicate-members.ts} +23 -4
  304. package/src/migrations/1780933598-document-groups.ts +88 -0
  305. package/src/migrations/1781692342-balance-item-relations.ts +13 -0
  306. package/src/migrations/1781857866-platform-admins.ts +103 -0
  307. package/src/migrations/1781857867-rm-old-platform-admins.ts +25 -0
  308. package/src/seeds/0000000001-throw.ts +5 -0
  309. package/src/seeds/1760702452-update-balance-price-paid.ts +19 -0
  310. package/src/seeds/1760702453-update-balance-invoiced.ts +19 -0
  311. package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +51 -14
  312. package/src/seeds/1773754928-force-save-members.ts +41 -8
  313. package/src/seeds/1780665427-schedule-stock-updates.ts +45 -0
  314. package/src/seeds/1780665428-group-update-occupancy.ts +31 -0
  315. package/src/seeds/1780915001-fill-payment-customer.ts +70 -0
  316. package/src/seeds/1780915002-update-orders.ts +39 -0
  317. package/src/seeds/1782047708-organization-company-address.ts +44 -0
  318. package/src/seeds-temporary/1769088653-uitpas-status.ts +1 -2
  319. package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.test.ts +69 -0
  320. package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.ts +85 -0
  321. package/src/services/AuditLogService.test.ts +51 -0
  322. package/src/services/AuditLogService.ts +45 -0
  323. package/src/services/BalanceItemService.ts +148 -55
  324. package/src/services/CpuService.ts +3 -6
  325. package/src/services/DatabaseCollationService.test.ts +1 -2
  326. package/src/services/DocumentService.ts +1 -2
  327. package/src/services/EventNotificationService.ts +1 -1
  328. package/src/services/FileSignService.ts +2 -4
  329. package/src/services/InvoicePdfService.ts +286 -0
  330. package/src/services/InvoiceService.ts +330 -77
  331. package/src/services/InvoiceXMLService.ts +424 -0
  332. package/src/services/MemberNumberService.ts +6 -11
  333. package/src/services/MemberRecordStore.ts +8 -10
  334. package/src/services/MollieService.ts +530 -0
  335. package/src/services/PaymentMandateService.ts +214 -0
  336. package/src/services/PaymentReallocationService.test.ts +1 -1
  337. package/src/services/PaymentReallocationService.ts +7 -9
  338. package/src/services/PaymentService.ts +820 -328
  339. package/src/services/PlatformMembershipService.ts +5 -9
  340. package/src/services/ReferralService.ts +223 -0
  341. package/src/services/RegistrationService.ts +125 -54
  342. package/src/services/SSOService.test.ts +119 -0
  343. package/src/services/SSOService.ts +56 -49
  344. package/src/services/STPackageService.ts +97 -68
  345. package/src/services/UniqueMemberNumberService.ts +69 -0
  346. package/src/services/UniqueUserService.ts +3 -6
  347. package/src/services/VATService.ts +85 -0
  348. package/src/services/data/invoice.hbs.html +686 -0
  349. package/src/services/uitpas/PassholderEndpoints.ts +3 -6
  350. package/src/services/uitpas/UitpasService.ts +6 -10
  351. package/src/services/uitpas/checkUitpasNumbers.ts +2 -4
  352. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +3 -6
  353. package/src/services/uitpas/searchUitpasEvents.ts +1 -2
  354. package/src/sql-filters/audit-logs.ts +1 -1
  355. package/src/sql-filters/balance-item-payments.ts +1 -1
  356. package/src/sql-filters/balance-items.ts +1 -1
  357. package/src/sql-filters/base-registration-filter-compilers.ts +5 -33
  358. package/src/sql-filters/document-templates.ts +1 -1
  359. package/src/sql-filters/documents.ts +1 -1
  360. package/src/sql-filters/email-recipients.ts +1 -1
  361. package/src/sql-filters/emails.ts +1 -1
  362. package/src/sql-filters/event-notifications.ts +1 -1
  363. package/src/sql-filters/events.ts +6 -1
  364. package/src/sql-filters/groups.ts +16 -1
  365. package/src/sql-filters/invoiced-balance-items.ts +1 -1
  366. package/src/sql-filters/invoices.ts +1 -1
  367. package/src/sql-filters/member-responsibility-records.ts +1 -1
  368. package/src/sql-filters/members.ts +1 -1
  369. package/src/sql-filters/orders.ts +4 -4
  370. package/src/sql-filters/organization-registration-periods.ts +1 -1
  371. package/src/sql-filters/organizations.ts +1 -1
  372. package/src/sql-filters/payments.ts +5 -0
  373. package/src/sql-filters/platform-memberships.ts +3 -3
  374. package/src/sql-filters/receivable-balances.ts +1 -1
  375. package/src/sql-filters/registration-invitations.ts +90 -0
  376. package/src/sql-filters/registration-periods.ts +1 -1
  377. package/src/sql-filters/registrations.ts +1 -1
  378. package/src/sql-filters/tickets.ts +1 -1
  379. package/src/sql-filters/users.ts +1 -1
  380. package/src/sql-sorters/groups.ts +23 -0
  381. package/src/sql-sorters/platform-memberships.ts +16 -16
  382. package/src/sql-sorters/registration-invitations.ts +36 -0
  383. package/tests/actions/patchOrganizationMember.ts +1 -1
  384. package/tests/actions/patchUserMember.ts +1 -1
  385. package/tests/assertions/assertBalances.ts +6 -12
  386. package/tests/e2e/api-rate-limits.test.ts +12 -24
  387. package/tests/e2e/bundle-discounts.test.ts +4 -1
  388. package/tests/e2e/charge-members.test.ts +13 -11
  389. package/tests/e2e/documents.test.ts +4 -1
  390. package/tests/e2e/register.test.ts +7 -9
  391. package/tests/e2e/stock.test.ts +4 -3
  392. package/tests/e2e/tickets.test.ts +1 -1
  393. package/tests/helpers/MollieMocker.ts +462 -0
  394. package/tests/helpers/PayconiqMocker.ts +138 -45
  395. package/tests/helpers/StripeMocker.ts +9 -6
  396. package/tests/helpers/index.ts +4 -0
  397. package/tests/init/initAdmin.ts +2 -2
  398. package/tests/init/initBundleDiscount.ts +1 -1
  399. package/tests/init/initMembershipOrganization.ts +13 -0
  400. package/tests/init/initPermissionRole.ts +3 -4
  401. package/tests/vitest.global.setup.ts +1 -1
  402. package/vitest.config.js +7 -0
  403. package/.nvmrc +0 -1
  404. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
  405. package/src/helpers/GroupBuilder.ts +0 -418
  406. package/src/seeds/0000000000-example.ts +0 -11
  407. package/src/seeds/1722256498-group-update-occupancy.ts +0 -52
  408. package/src/seeds/1726572303-schedule-stock-updates.ts +0 -52
  409. package/src/seeds/1726847064-setup-steps.ts +0 -16
  410. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +0 -31
  411. package/src/seeds/1729253172-update-orders.ts +0 -21
  412. package/src/seeds/1733319079-fill-paying-organization-ids.ts +0 -68
  413. package/src/seeds/1733748412-update-deleted-order-numbers.ts +0 -109
  414. package/src/seeds/1733994455-balance-item-status-open.ts +0 -30
  415. package/src/seeds/1740046783-update-membership.ts +0 -17
  416. package/src/seeds/1741008870-fix-auditlog-description.ts +0 -50
  417. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +0 -88
  418. package/src/seeds/1741268179-fix-group-dates-of-events.ts +0 -32
  419. package/src/seeds/1750090030-records-configuration.ts +0 -26
  420. package/src/seeds/1751445358-upload-email-attachments.ts +0 -106
  421. package/src/seeds/1755532883-update-email-sender-ids.ts +0 -47
  422. package/src/seeds/1755790070-fill-email-recipient-errors.ts +0 -96
  423. package/src/seeds/1756115432-remove-old-drafts.ts +0 -16
  424. package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +0 -30
  425. package/src/seeds/1756303697-update-email-counts.ts +0 -76
  426. package/src/seeds/1761665607-sync-member-users.ts +0 -59
  427. package/src/seeds/1765896674-document-update-year.ts +0 -66
  428. /package/src/{seeds → migrations}/0000000001-development-user.ts +0 -0
  429. /package/src/{seeds → migrations}/0000000002-clear-stamhoofd-email-templates.ts +0 -0
  430. /package/src/{seeds → migrations}/0000000003-default-email-templates.ts +0 -0
  431. /package/src/{seeds → migrations}/1752848561-groups-registration-periods.test.ts +0 -0
  432. /package/src/{seeds → migrations}/1766150402-document-published-at.test.ts +0 -0
  433. /package/src/{seeds → migrations}/1766150402-document-published-at.ts +0 -0
  434. /package/src/{seeds → migrations}/data/default-email-templates.sql +0 -0
@@ -1,30 +1,61 @@
1
- import { createMollieClient, PaymentMethod as molliePaymentMethod, PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
2
- import { SimpleError } from '@simonbackx/simple-errors';
3
- import type { BalanceItem, Member, User } from '@stamhoofd/models';
4
- import { BalanceItemPayment, Group, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, sendEmailTemplate } from '@stamhoofd/models';
1
+ import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
2
+ import type { Member, User } from '@stamhoofd/models';
3
+ import { BalanceItem, BalanceItemPayment, Group, Organization, PayconiqPayment, Payment, Platform, sendEmailTemplate, UsedRegisterCode } from '@stamhoofd/models';
5
4
  import { QueueHandler } from '@stamhoofd/queues';
6
- import type { Checkoutable, PaymentConfiguration } from '@stamhoofd/structures';
7
- import { AuditLogSource, BalanceItemType, EmailTemplateType, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, PaymentType, Recipient, VATExcemptReason, Version } from '@stamhoofd/structures';
5
+ import type { Checkoutable, PaymentConfiguration, PrivatePaymentConfiguration } 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';
8
+ import type { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
9
+ import { PaymentMandateStatus, PaymentMandateType } from '@stamhoofd/structures/PaymentMandate.js';
8
10
  import { Formatter } from '@stamhoofd/utility';
9
11
  import { buildReplacementOptions, getEmailReplacementsForPayment } from '../email-replacements/getEmailReplacementsForPayment.js';
10
12
  import { BuckarooHelper } from '../helpers/BuckarooHelper.js';
11
13
  import { Context } from '../helpers/Context.js';
12
14
  import { ServiceFeeHelper } from '../helpers/ServiceFeeHelper.js';
13
15
  import { StripeHelper } from '../helpers/StripeHelper.js';
16
+ import { ViesHelper } from '../helpers/ViesHelper.js';
14
17
  import { AuditLogService } from './AuditLogService.js';
15
18
  import { BalanceItemPaymentService } from './BalanceItemPaymentService.js';
16
19
  import { BalanceItemService } from './BalanceItemService.js';
20
+ import { MollieService } from './MollieService.js';
21
+ import { PaymentMandateService } from './PaymentMandateService.js';
22
+ import { ReferralService } from './ReferralService.js';
23
+ import { VATService } from './VATService.js';
24
+ import { STPackageService } from './STPackageService.js';
17
25
 
18
26
  export class PaymentService {
19
- 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) {
20
38
  if (payment.status === status) {
21
39
  return;
22
40
  }
23
41
 
42
+ if (status === PaymentStatus.Failed && payment.invoiceId) {
43
+ throw new SimpleError({
44
+ code: 'cannot_fail',
45
+ message: 'A payment that has been invoiced cannot be marked as failed. Instead create a separate chargeback payment with a negative amount.',
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);
52
+ }
53
+
24
54
  await AuditLogService.setContext({ fallbackUserId: payment.payingUserId, source: AuditLogSource.Payment, fallbackOrganizationId: payment.organizationId }, async () => {
25
55
  if (status === PaymentStatus.Succeeded) {
56
+ console.log('Marking as succeeded', payment.id);
26
57
  payment.status = PaymentStatus.Succeeded;
27
- payment.paidAt = new Date();
58
+ payment.paidAt = paidAt ?? new Date();
28
59
  await payment.save();
29
60
 
30
61
  // Prevent concurrency issues
@@ -42,7 +73,31 @@ export class PaymentService {
42
73
 
43
74
  // Flush caches so data is up to date in response
44
75
  await BalanceItemService.flushCaches(organization.id);
76
+
77
+ await this.updateReversedPaymentsFor(payment);
45
78
  });
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
+
91
+ // It is possible the mandate succeeds immediately, in which case we might
92
+ // need to save it as the default payment method
93
+ await this.saveMandateIfNeeded({
94
+ payment,
95
+ sellingOrganizationId: organization.id,
96
+ });
97
+
98
+ if (payment.type === PaymentType.Chargeback) {
99
+ await this.handleChargebackCreated(payment, organization);
100
+ }
46
101
  return;
47
102
  }
48
103
 
@@ -69,11 +124,14 @@ export class PaymentService {
69
124
 
70
125
  // Flush caches so data is up to date in response
71
126
  await BalanceItemService.flushCaches(organization.id);
127
+
128
+ await this.updateReversedPaymentsFor(payment);
72
129
  });
73
130
  }
74
131
 
75
132
  // Moved to failed
76
133
  if (status === PaymentStatus.Failed) {
134
+ console.log('Marking as failed', payment.id);
77
135
  await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
78
136
  const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
79
137
  (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
@@ -88,6 +146,10 @@ export class PaymentService {
88
146
  // Flush caches so data is up to date in response
89
147
  await BalanceItemService.flushCaches(organization.id);
90
148
  });
149
+
150
+ if (payment.type === PaymentType.Payment) {
151
+ await this.handlePaymentFailed(payment, organization);
152
+ }
91
153
  }
92
154
 
93
155
  // If OLD status was FAILED, we need to revert the actions
@@ -110,6 +172,102 @@ export class PaymentService {
110
172
  });
111
173
  }
112
174
 
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 }) {
253
+ // Save as default
254
+ if (payment.createMandate && payment.createMandate.saveAsDefault) {
255
+ if (payment.mandateId && payment.status === PaymentStatus.Succeeded) {
256
+ try {
257
+ await PaymentMandateService.setDefaultMandate({
258
+ mandateId: payment.mandateId,
259
+ payingOrganizationId: payment.payingOrganizationId,
260
+ sellingOrganizationId,
261
+ payingUserId: payment.payingUserId,
262
+ });
263
+ } catch (e) {
264
+ // Ignore as setting the payment status is more important
265
+ console.error(e);
266
+ }
267
+ }
268
+ }
269
+ }
270
+
113
271
  /**
114
272
  * ID of payment is needed because of race conditions (need to fetch payment in a race condition save queue)
115
273
  */
@@ -122,6 +280,11 @@ export class PaymentService {
122
280
  return;
123
281
  }
124
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
+
125
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
126
289
  return await AuditLogService.setContext({ userId: payment.payingUserId ?? null, source: AuditLogSource.Payment }, async () => {
127
290
  if (!payment.organizationId) {
@@ -129,11 +292,15 @@ export class PaymentService {
129
292
  return;
130
293
  }
131
294
 
132
- const organization = org ?? await Organization.getByID(payment.organizationId);
295
+ const organization = org && org.id === payment.organizationId ? org : await Organization.getByID(payment.organizationId);
133
296
  if (!organization) {
134
297
  console.error('Organization not found for payment', payment.id);
135
298
  return;
136
299
  }
300
+ if (org && org.id !== payment.organizationId && org.id !== payment.payingOrganizationId) {
301
+ console.error('Non-matching organization found for payment', payment.id, 'org', org.id);
302
+ return;
303
+ }
137
304
 
138
305
  const testMode = organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production';
139
306
 
@@ -147,101 +314,46 @@ export class PaymentService {
147
314
  status = PaymentStatus.Failed;
148
315
  }
149
316
 
317
+ console.log('Stripe setting status to', status);
150
318
  await this.handlePaymentStatusUpdate(payment, organization, status);
151
- }
152
- catch (e) {
319
+ } catch (e) {
153
320
  console.error('Payment check failed Stripe', payment.id, e);
154
321
  if (this.isManualExpired(payment.status, payment)) {
155
322
  console.error('Manually marking Stripe payment as expired', payment.id);
156
323
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
157
324
  }
158
325
  }
159
- }
160
- else if (payment.provider === PaymentProvider.Mollie) {
161
- // check status via mollie
162
- const molliePayments = await MolliePayment.where({ paymentId: payment.id }, { limit: 1 });
163
- if (molliePayments.length === 1) {
164
- const molliePayment = molliePayments[0];
165
- // check status
166
- const token = await MollieToken.getTokenFor(organization.id);
167
-
168
- if (token) {
169
- try {
170
- const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
171
- let mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
172
- testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
173
- });
174
-
175
- console.log(mollieData); // log to log files to check issues
176
-
177
- const details = mollieData.details as any;
178
- if (details?.consumerName) {
179
- payment.ibanName = details.consumerName;
180
- }
181
- if (details?.consumerAccount) {
182
- payment.iban = details.consumerAccount;
183
- }
184
- if (details?.cardHolder) {
185
- payment.ibanName = details.cardHolder;
186
- }
187
- if (details?.cardNumber) {
188
- payment.iban = 'xxxx xxxx xxxx ' + details.cardNumber;
189
- }
190
-
191
- if (mollieData.status === MolliePaymentStatus.paid) {
192
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded);
193
- }
194
- else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
195
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
196
- }
197
- else if ((cancel || this.shouldTryToCancel(payment.status, payment)) && mollieData.isCancelable) {
198
- console.log('Cancelling Mollie payment on request', payment.id);
199
- mollieData = await mollieClient.payments.cancel(molliePayment.mollieId);
200
-
201
- if (mollieData.status === MolliePaymentStatus.paid) {
202
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded);
203
- }
204
- else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
205
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
206
- }
207
- else if (this.isManualExpired(payment.status, payment)) {
208
- // Mollie still returning pending after 1 day: mark as failed
209
- console.error('Manually marking Mollie payment as expired', payment.id);
210
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
211
- }
212
- }
213
- else if (this.isManualExpired(payment.status, payment)) {
214
- // Mollie still returning pending after 1 day: mark as failed
215
- console.error('Manually marking Mollie payment as expired', payment.id);
216
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
217
- }
218
- }
219
- catch (e) {
220
- console.error('Payment check failed Mollie', payment.id, e);
221
- if (this.isManualExpired(payment.status, payment)) {
222
- console.error('Manually marking Mollie payment as expired', payment.id);
223
- await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
224
- }
326
+ } else if (payment.provider === PaymentProvider.Mollie) {
327
+ try {
328
+ const mollieClient = await MollieService.create({
329
+ sellingOrganization: organization,
330
+ });
331
+ if (mollieClient) {
332
+ let { status } = await mollieClient.getStatus(payment, cancel || this.shouldTryToCancel(payment.status, payment));
333
+
334
+ if (this.isManualExpired(status, payment)) {
335
+ console.error('Manually marking Mollie payment as expired', payment.id);
336
+ status = PaymentStatus.Failed;
225
337
  }
226
- }
227
- else {
228
- console.warn('Mollie payment is missing for organization ' + organization.id + ' while checking payment status...');
229
338
 
339
+ console.log('Mollie setting status to', status);
340
+ await this.handlePaymentStatusUpdate(payment, organization, status);
341
+ } else {
342
+ console.error('Missing Mollie Credentials for payment', payment.id);
230
343
  if (this.isManualExpired(payment.status, payment)) {
231
- console.error('Manually marking payment without mollie token as expired', payment.id);
344
+ console.error('Manually marking Mollie payment as expired', payment.id);
232
345
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
233
346
  }
234
347
  }
235
- }
236
- else {
348
+ } catch (e) {
349
+ console.error('Payment check failed Mollie', payment.id, e);
237
350
  if (this.isManualExpired(payment.status, payment)) {
238
- console.error('Manually marking payment without mollie payments as expired', payment.id);
351
+ console.error('Manually marking Mollie payment as expired', payment.id);
239
352
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
240
353
  }
241
354
  }
242
- }
243
- else if (payment.provider == PaymentProvider.Buckaroo) {
244
- 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);
245
357
  try {
246
358
  let status = await helper.getStatus(payment);
247
359
 
@@ -250,17 +362,16 @@ export class PaymentService {
250
362
  status = PaymentStatus.Failed;
251
363
  }
252
364
 
365
+ console.log('Buckaroo setting status to', status);
253
366
  await this.handlePaymentStatusUpdate(payment, organization, status);
254
- }
255
- catch (e) {
367
+ } catch (e) {
256
368
  console.error('Payment check failed Buckaroo', payment.id, e);
257
369
  if (this.isManualExpired(payment.status, payment)) {
258
370
  console.error('Manually marking Buckaroo payment as expired', payment.id);
259
371
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
260
372
  }
261
373
  }
262
- }
263
- else if (payment.provider == PaymentProvider.Payconiq) {
374
+ } else if (payment.provider == PaymentProvider.Payconiq) {
264
375
  // Check status
265
376
 
266
377
  const payconiqPayments = await PayconiqPayment.where({ paymentId: payment.id }, { limit: 1 });
@@ -278,8 +389,7 @@ export class PaymentService {
278
389
  console.error('Manually cancelling Payconiq payment', payment.id);
279
390
  if (await payconiqPayment.cancel(organization)) {
280
391
  status = PaymentStatus.Failed;
281
- }
282
- else {
392
+ } else {
283
393
  console.error('Failed to manually cancel payment');
284
394
  }
285
395
  }
@@ -290,8 +400,7 @@ export class PaymentService {
290
400
  }
291
401
 
292
402
  await this.handlePaymentStatusUpdate(payment, organization, status);
293
- }
294
- else {
403
+ } else {
295
404
  console.warn('Payconiq payment is missing for organization ' + organization.id + ' while checking payment status...');
296
405
 
297
406
  if (this.isManualExpired(payment.status, payment)) {
@@ -299,19 +408,17 @@ export class PaymentService {
299
408
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
300
409
  }
301
410
  }
302
- }
303
- else {
411
+ } else {
304
412
  console.error('Invalid payment provider', payment.provider, 'for payment', payment.id);
305
413
  if (this.isManualExpired(payment.status, payment)) {
306
414
  console.error('Manually marking unknown payment as expired', payment.id);
307
415
  await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
308
416
  }
309
417
  }
310
- }
311
- else {
418
+ } else {
312
419
  // Do a manual update if needed
313
420
  if (payment.status === PaymentStatus.Succeeded) {
314
- if (payment.provider === PaymentProvider.Stripe) {
421
+ if ((payment.provider === PaymentProvider.Mollie && STAMHOOFD.environment === 'development')) {
315
422
  // Update the status
316
423
  await StripeHelper.getStatus(payment, false, testMode);
317
424
  }
@@ -329,6 +436,18 @@ export class PaymentService {
329
436
  return true;
330
437
  }
331
438
  }
439
+
440
+ if (STAMHOOFD.environment === 'development') {
441
+ // In development, we expire all direct debits and other paymetns after 1 hour, because they need manual changes
442
+ // otherwise they will remain stuck in the dev environment, poluting the UI
443
+ if ((status === PaymentStatus.Pending || status === PaymentStatus.Created)) {
444
+ // If payment is not succeeded after one day, mark as failed
445
+ if (payment.createdAt < new Date(new Date().getTime() - 60 * 1000 * 60)) {
446
+ return true;
447
+ }
448
+ }
449
+ }
450
+
332
451
  return false;
333
452
  }
334
453
 
@@ -336,7 +455,7 @@ export class PaymentService {
336
455
  * Try to cancel a payment that is still pending
337
456
  */
338
457
  static shouldTryToCancel(status: PaymentStatus, payment: Payment): boolean {
339
- if ((status === PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
458
+ if ((status === PaymentStatus.Pending || status === PaymentStatus.Created) && (payment.method !== PaymentMethod.DirectDebit || STAMHOOFD.environment === 'development')) {
340
459
  let timeout = STAMHOOFD.environment === 'development' ? 60 * 1000 * 2 : 60 * 1000 * 30;
341
460
 
342
461
  // If payconiq and not yet 'identified' (scanned), cancel after 5 minutes
@@ -356,39 +475,45 @@ export class PaymentService {
356
475
  * we'll need to round the payment to 1 cent. That can cause issues in the financial statements because
357
476
  * the total amount of balances does not match the total amount received/paid.
358
477
  *
359
- * To fix that, we create an extra balance item with the difference. So the rounding always matches.
360
- *
361
- * TODO: update this method to generate a virtual invoice and use the price of the invoice instead of the rounded payment price, so we don't get differences in calculation
478
+ * To fix that, we create an extra roundingAmount with the difference. So the rounding always matches.
362
479
  */
363
- static round(payment: Payment) {
364
- const amount = payment.price;
365
- const rounded = Payment.roundPrice(payment.price);
480
+ static roundPayment(payment: Payment) {
481
+ // Calculate total price of the balance items
482
+ // this fixes issus when the method is called multiple times
483
+ // should be subtracted, not added
484
+ const balanceItemsTotalPrice = payment.price - payment.roundingAmount;
485
+
486
+ const { roundingAmount, price } = this.round(balanceItemsTotalPrice);
487
+ payment.roundingAmount = roundingAmount;
488
+ payment.price = price;
489
+ }
490
+
491
+ static round(amount: number) {
492
+ const rounded = Payment.roundPrice(amount);
366
493
  const difference = rounded - amount;
367
494
 
368
495
  if (difference === 0) {
369
- return;
496
+ return {
497
+ price: amount,
498
+ roundingAmount: 0,
499
+ };
370
500
  }
371
501
 
372
502
  if (difference > 100 || difference < -100) {
373
- throw new Error('Unexpected rounding difference of ' + difference + ' for payment ' + payment.id);
503
+ throw new Error('Unexpected rounding difference of ' + difference + ' for price ' + amount.toString());
374
504
  }
375
505
 
376
- payment.roundingAmount = difference;
377
-
378
- // Change payment total price
379
- payment.price += difference;
506
+ return {
507
+ price: amount + difference,
508
+ roundingAmount: difference,
509
+ };
380
510
  }
381
511
 
382
- static async createPayment({ balanceItems, organization, user, members, checkout, payingOrganization, serviceFeeType }: {
512
+ static calculateTotalPrice({ balanceItems, organization, members }: {
383
513
  balanceItems: Map<BalanceItem, number>;
384
514
  organization: Organization;
385
- user: User;
386
515
  members?: Member[];
387
- checkout: Pick<Checkoutable<never>, 'paymentMethod' | 'totalPrice' | 'customer' | 'cancelUrl' | 'redirectUrl'>;
388
- payingOrganization?: Organization | null;
389
- serviceFeeType: 'webshop' | 'members' | 'tickets' | 'system';
390
516
  }) {
391
- // Calculate total price to pay
392
517
  let totalPrice = 0;
393
518
  const names: {
394
519
  firstName: string;
@@ -437,113 +562,272 @@ export class PaymentService {
437
562
  });
438
563
  }
439
564
  }
565
+ const { price, roundingAmount } = this.round(totalPrice);
440
566
 
441
- if (totalPrice < 0) {
442
- // todo: try to make it non-negative by reducing some balance items
567
+ if (price < 0) {
443
568
  throw new SimpleError({
444
569
  code: 'negative_price',
445
570
  message: $t(`%vl`),
446
571
  });
447
572
  }
448
573
 
449
- if (checkout.totalPrice !== null && totalPrice !== checkout.totalPrice) {
450
- // Changed!
574
+ return { hasNegative, price, roundingAmount, names };
575
+ }
576
+
577
+ static validateTotalPrice({ price, roundingAmount, checkout }: {
578
+ price: number; // already rounded
579
+ roundingAmount: number;
580
+ checkout: Pick<Checkoutable<never>, 'totalPrice'>;
581
+ }) {
582
+ // total price without rounding
583
+ const balanceItemsPrice = price - roundingAmount;
584
+
585
+ // also accept rounding that might have happend in the frontend and that was correct
586
+ if (checkout.totalPrice !== null && checkout.totalPrice !== balanceItemsPrice && checkout.totalPrice !== price) {
451
587
  throw new SimpleError({
452
588
  code: 'changed_price',
453
- message: $t(`%vk`, { total: Formatter.price(totalPrice) }),
589
+ message: $t(`%vk`, { total: Formatter.price(price) }),
454
590
  });
455
591
  }
592
+ }
456
593
 
457
- const payment = new Payment();
458
-
459
- // Who will receive this money?
460
- payment.organizationId = organization.id;
461
-
462
- // Who paid
463
- payment.payingUserId = user.id;
464
- payment.payingOrganizationId = payingOrganization?.id ?? null;
465
-
594
+ static async validateCustomer({ user, checkout, payingOrganization }: {
595
+ user: User | null;
596
+ checkout: Pick<Checkoutable<never>, 'customer'>;
597
+ payingOrganization?: Organization | null;
598
+ }) {
466
599
  // Fill in customer default value
467
- payment.customer = PaymentCustomer.create({
468
- firstName: user.firstName,
469
- lastName: user.lastName,
470
- email: user.email,
600
+ const customer = PaymentCustomer.create({
601
+ firstName: user?.firstName,
602
+ lastName: user?.lastName,
603
+ email: user?.email,
604
+ phone: checkout.customer?.phone,
471
605
  });
472
606
 
473
607
  // Use structured transfer description prefix
474
608
  let prefix = '';
475
609
 
476
610
  if (payingOrganization) {
477
- if (totalPrice !== 0 || hasNegative || checkout.customer) {
478
- if (!checkout.customer) {
479
- throw new SimpleError({
480
- code: 'missing_fields',
481
- message: 'customer is required when paying as an organization',
482
- human: $t(`%vz`),
483
- });
484
- }
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
+ }
485
618
 
486
- if (!checkout.customer.company) {
487
- throw new SimpleError({
488
- code: 'missing_fields',
489
- message: 'customer.company is required when paying as an organization',
490
- human: $t(`%w0`),
491
- });
492
- }
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
+ }
493
626
 
494
- // Search company id
495
- // this avoids needing to check the VAT number every time
496
- const id = checkout.customer.company.id;
497
- 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);
498
631
 
499
- if (!foundCompany) {
632
+ if (!foundCompany) {
633
+ if (payingOrganization.meta.companies.length > 0) {
500
634
  throw new SimpleError({
501
635
  code: 'invalid_data',
502
- 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`),
503
638
  });
504
639
  }
505
640
 
506
- payment.customer.company = foundCompany;
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
+ }
507
645
 
508
- const orgNumber = parseInt(payingOrganization.uri);
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
+ }
509
653
 
510
- if (orgNumber !== 0 && !isNaN(orgNumber)) {
511
- prefix = orgNumber + '';
512
- }
654
+ customer.company = foundCompany;
655
+
656
+ const orgNumber = parseInt(payingOrganization.uri);
657
+
658
+ if (orgNumber !== 0 && !isNaN(orgNumber)) {
659
+ prefix = orgNumber + '';
513
660
  }
514
- else {
515
- // Zero amount payment (without refunds) without specifying a company will just use the default company to link to the payment
516
- // It doesn't really matter since the price is zero and we won't invoice it.
517
- const company = payingOrganization.meta.companies[0];
518
- if (company) {
519
- payment.customer.company = company;
520
- }
661
+ } else {
662
+ if (checkout.customer && checkout.customer.company) {
663
+ customer.company = checkout.customer.company.clone();
664
+ await ViesHelper.checkCompany(checkout.customer.company, customer.company);
521
665
  }
522
666
  }
523
667
 
524
- // Validate VAT rates for this customer
525
- await this.validateVATRates({ customer: payment.customer, organization, balanceItems });
668
+ if (customer.company?.VATNumber !== checkout.customer?.company?.VATNumber) {
669
+ // Security check: because previous validation and generation might have used the VATNumber from the checkout
670
+ throw new SimpleError({
671
+ code: 'changed_VAT_number',
672
+ message: 'Unexpected VAT number change',
673
+ });
674
+ }
526
675
 
527
- payment.status = PaymentStatus.Created;
528
- payment.paidAt = null;
529
- payment.price = totalPrice;
530
- PaymentService.round(payment);
531
- totalPrice = payment.price;
676
+ return { customer, prefix };
677
+ }
532
678
 
533
- if (totalPrice === 0) {
534
- payment.status = PaymentStatus.Succeeded;
535
- payment.paidAt = new Date();
679
+ static async createProForma({ balanceItems, organization, user, members, checkout, payingOrganization }: {
680
+ balanceItems: Map<BalanceItem, number>;
681
+ organization: Organization;
682
+ user: User;
683
+ members?: Member[];
684
+ checkout: Pick<Checkoutable<never>, 'totalPrice' | 'customer' | 'cancelUrl' | 'redirectUrl'>;
685
+ payingOrganization?: Organization | null;
686
+ }) {
687
+ // Calculate total price to pay
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 });
693
+
694
+ // Create invoice instead from fictive PaymentGeneral
695
+ const fakePaymentGeneral = PaymentGeneral.create({
696
+ id: 'pro-forma',
697
+ price,
698
+ customer,
699
+ method: PaymentMethod.Unknown,
700
+ balanceItemPayments: [...balanceItems.entries()].map(([balanceItem, price]) => {
701
+ return BalanceItemPaymentDetailed.create({
702
+ id: 'pro-forma-' + balanceItem.id,
703
+ balanceItem: balanceItem.getStructure(),
704
+ price,
705
+ });
706
+ }),
707
+ });
708
+
709
+ let invoice: Invoice | null = null;
710
+
711
+ try {
712
+ invoice = Invoice.create({
713
+ seller,
714
+ customer,
715
+ payments: [fakePaymentGeneral],
716
+ });
717
+ invoice.buildFromPayments();
718
+ } catch (e) {
719
+ if ((isSimpleError(e) || isSimpleErrors(e)) && e.hasCode('balance_item_without_vat_percentage')) {
720
+ // ok
721
+ // Missing VAT that prevents invoice from being created
722
+ } else {
723
+ throw e;
724
+ }
725
+ }
726
+
727
+ return {
728
+ invoice,
729
+ payment: fakePaymentGeneral,
730
+ };
731
+ }
732
+
733
+ static async registerChargeback(payment: Payment, amount: number, date: Date) {
734
+ if (amount !== payment.price) {
735
+ // Creates issues to know what balance item was paid and what was not.
736
+ throw new Error('Cannot register chargeback with different amount than the payment for payment ' + payment.id);
737
+ }
738
+ const balanceItemPayments = await BalanceItemPayment.select().where('paymentId', payment.id).fetch();
739
+ const items = await BalanceItem.getByIDs(...Formatter.uniqueArray(balanceItemPayments.map(b => b.balanceItemId)));
740
+
741
+ // Done validation
742
+ const chargeback = new Payment();
743
+
744
+ // Who will receive this money?
745
+ chargeback.organizationId = payment.organizationId!;
746
+
747
+ // Who paid
748
+ chargeback.payingUserId = payment.payingUserId;
749
+ chargeback.payingOrganizationId = payment.payingOrganizationId;
750
+ chargeback.customer = payment.customer;
751
+
752
+ chargeback.status = PaymentStatus.Created;
753
+ chargeback.price = -payment.price;
754
+ chargeback.roundingAmount = -payment.roundingAmount;
755
+ chargeback.method = payment.method;
756
+ chargeback.type = PaymentType.Chargeback;
757
+ chargeback.mandateId = payment.mandateId; ;
758
+ chargeback.adminUserId = payment.adminUserId;
759
+
760
+ chargeback.provider = payment.provider;
761
+ chargeback.stripeAccountId = payment.stripeAccountId;
762
+ chargeback.reversingPaymentId = payment.id;
763
+ await chargeback.save();
764
+
765
+ for (const original of balanceItemPayments) {
766
+ // Create one balance item payment to pay it in one payment
767
+ const balanceItemPayment = new BalanceItemPayment();
768
+ balanceItemPayment.balanceItemId = original.balanceItemId;
769
+ balanceItemPayment.paymentId = chargeback.id;
770
+ balanceItemPayment.organizationId = chargeback.organizationId;
771
+ balanceItemPayment.price = -original.price;
772
+ await balanceItemPayment.save();
773
+ }
774
+
775
+ // Update cached balance items pending amount (only created balance items, because those are involved in the payment)
776
+ await BalanceItemService.updatePaidAndPending(items);
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;
536
786
  }
787
+ return chargeback;
788
+ }
537
789
 
538
- // Validate payment method after customer is defined
539
- const paymentConfiguration = organization.meta.registrationPaymentConfiguration;
540
- const privatePaymentConfiguration = organization.privateMeta.registrationPaymentConfiguration;
790
+ static async createPayment({ balanceItems, organization, user, members, checkout, payingOrganization, serviceFeeType, createMandate, useMandate, paymentConfiguration, privatePaymentConfiguration, adminUserId }: {
791
+ balanceItems: Map<BalanceItem, number>;
792
+ organization: Organization;
793
+ user: User | null;
794
+ members?: Member[];
795
+ checkout: Pick<Checkoutable<never>, 'paymentMethod' | 'totalPrice' | 'customer' | 'cancelUrl' | 'redirectUrl'>;
796
+ payingOrganization?: Organization | null;
797
+ serviceFeeType: 'webshop' | 'members' | 'tickets' | 'system';
798
+ createMandate: CreateMandateSettings | null;
799
+ useMandate: PaymentMandate | null;
800
+ paymentConfiguration: PaymentConfiguration;
801
+ privatePaymentConfiguration: PrivatePaymentConfiguration;
802
+ adminUserId?: string | null;
803
+ }) {
804
+ if (balanceItems.size === 0) {
805
+ return null;
806
+ }
541
807
 
542
- payment.method = checkout.paymentMethod ?? PaymentMethod.Unknown;
543
- await this.validatePaymentMethod({ payment, balanceItems, paymentConfiguration });
808
+ // Calculate total price to pay
809
+ const { price, roundingAmount, hasNegative, names } = this.calculateTotalPrice({ balanceItems, organization, members });
810
+ PaymentService.validateTotalPrice({ price, roundingAmount, checkout });
811
+
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({
816
+ method: checkout.paymentMethod ?? PaymentMethod.Unknown,
817
+ mandate: useMandate,
818
+ createMandate: !!createMandate,
819
+ customer,
820
+ price,
821
+ hasNegative,
822
+ balanceItems,
823
+ paymentConfiguration,
824
+ user,
825
+ payingOrganization: payingOrganization ?? null,
826
+ sellingOrganization: organization,
827
+ });
544
828
 
545
- // Validate URL's for online payments before saving the payment
546
- if ((payment.method !== PaymentMethod.Transfer && payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) && (!checkout.redirectUrl || !checkout.cancelUrl)) {
829
+ // Check URL's set fro online payments
830
+ if ((method !== PaymentMethod.Transfer && method !== PaymentMethod.PointOfSale && method !== PaymentMethod.Unknown) && (!checkout.redirectUrl || !checkout.cancelUrl) && !mandate) {
547
831
  throw new SimpleError({
548
832
  code: 'missing_fields',
549
833
  message: 'redirectUrl or cancelUrl is missing and is required for non-zero online payments',
@@ -551,6 +835,50 @@ export class PaymentService {
551
835
  });
552
836
  }
553
837
 
838
+ // Determine the payment provider (throws if invalid)
839
+ const { provider, stripeAccount } = await organization.getPaymentProviderFor(method, mandate, privatePaymentConfiguration);
840
+
841
+ if (createMandate && !mandate) {
842
+ if (provider !== PaymentProvider.Mollie) {
843
+ throw new SimpleError({
844
+ code: 'cannot_create_mandate_for_provider',
845
+ message: 'Saving a payment method is not yet supported for this payment method',
846
+ human: $t('%1U0'),
847
+ });
848
+ }
849
+ }
850
+
851
+ // Done validation
852
+ const payment = new Payment();
853
+
854
+ // Who will receive this money?
855
+ payment.organizationId = organization.id;
856
+
857
+ // Who paid
858
+ payment.payingUserId = !payingOrganization ? (user?.id ?? null) : null;
859
+ payment.payingOrganizationId = payingOrganization?.id ?? null;
860
+ payment.customer = customer;
861
+
862
+ payment.status = PaymentStatus.Created;
863
+ payment.paidAt = null;
864
+ payment.price = price;
865
+ payment.roundingAmount = roundingAmount;
866
+ payment.method = method;
867
+ payment.type = type;
868
+ payment.createMandate = createMandate;
869
+ payment.mandateId = mandate?.id ?? null;
870
+ payment.adminUserId = adminUserId ?? null;
871
+
872
+ if (price === 0) {
873
+ payment.status = PaymentStatus.Succeeded;
874
+ payment.paidAt = new Date();
875
+ }
876
+
877
+ payment.provider = provider;
878
+ payment.stripeAccountId = stripeAccount?.id ?? null;
879
+ await ServiceFeeHelper.setServiceFee(payment, organization, serviceFeeType, [...balanceItems.entries()].map(([_, p]) => p));
880
+ await ServiceFeeHelper.setTransferFee({ payment, organization, stripeAccount });
881
+
554
882
  // Add transfer description
555
883
  if (payment.method === PaymentMethod.Transfer) {
556
884
  // remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
@@ -571,27 +899,19 @@ export class PaymentService {
571
899
  {
572
900
  name: groupedNames,
573
901
  naam: groupedNames,
574
- email: user.email,
902
+ email: customer.email ?? user?.email ?? '',
575
903
  prefix,
576
904
  },
577
905
  );
578
906
  }
579
907
 
580
- // Determine the payment provider
581
- // Throws if invalid
582
- const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, privatePaymentConfiguration);
583
- payment.provider = provider;
584
- payment.stripeAccountId = stripeAccount?.id ?? null;
585
- ServiceFeeHelper.setServiceFee(payment, organization, serviceFeeType, [...balanceItems.entries()].map(([_, p]) => p));
586
-
587
908
  await payment.save();
909
+
910
+ // Create online payment and balance item payments
588
911
  let paymentUrl: string | null = null;
589
912
  let paymentQRCode: string | null = null;
590
913
  const description = organization.name + ' ' + payment.id;
591
914
 
592
- // Create balance item payments
593
- const balanceItemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = [];
594
-
595
915
  try {
596
916
  for (const [balanceItem, price] of balanceItems) {
597
917
  // Create one balance item payment to pay it in one payment
@@ -600,35 +920,35 @@ export class PaymentService {
600
920
  balanceItemPayment.paymentId = payment.id;
601
921
  balanceItemPayment.organizationId = organization.id;
602
922
  balanceItemPayment.price = price;
603
- await balanceItemPayment.save();
604
923
 
605
- balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem));
924
+ await balanceItemPayment.save();
606
925
  }
607
926
 
927
+ // Update cached balance items pending amount (only created balance items, because those are involved in the payment)
928
+ await BalanceItemService.updatePaidAndPending([...balanceItems.keys()]);
929
+
608
930
  // Update balance items
609
931
  if (payment.method === PaymentMethod.Transfer) {
610
932
  // Send a small reminder email
611
933
  try {
612
- await this.sendTransferEmail(user, organization, payment);
613
- }
614
- catch (e) {
934
+ await this.sendTransferEmail(customer, user?.id ?? null, organization, payment);
935
+ } catch (e) {
615
936
  console.error('Failed to send transfer email');
616
937
  console.error(e);
617
938
  }
618
- }
619
- else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
620
- if (!checkout.redirectUrl || !checkout.cancelUrl) {
939
+ } else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
940
+ if ((!checkout.redirectUrl || !checkout.cancelUrl) && !mandate) {
621
941
  throw new Error('Should have been caught earlier');
622
942
  }
623
943
 
624
- const _redirectUrl = new URL(checkout.redirectUrl);
944
+ const _redirectUrl = new URL(checkout.redirectUrl ?? ('https://' + STAMHOOFD.domains.dashboard));
625
945
  _redirectUrl.searchParams.set('paymentId', payment.id);
626
- _redirectUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
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
627
947
 
628
- const _cancelUrl = new URL(checkout.cancelUrl);
948
+ const _cancelUrl = new URL(checkout.cancelUrl ?? ('https://' + STAMHOOFD.domains.dashboard));
629
949
  _cancelUrl.searchParams.set('paymentId', payment.id);
630
950
  _cancelUrl.searchParams.set('cancel', 'true');
631
- _cancelUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
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
632
952
 
633
953
  const redirectUrl = _redirectUrl.href;
634
954
  const cancelUrl = _cancelUrl.href;
@@ -636,6 +956,17 @@ export class PaymentService {
636
956
  const webhookUrl = 'https://' + organization.getApiHost() + '/v' + Version + '/payments/' + encodeURIComponent(payment.id) + '?exchange=true';
637
957
 
638
958
  if (payment.provider === PaymentProvider.Stripe) {
959
+ if (createMandate || mandate) {
960
+ // Already checked, but for security
961
+ throw new Error('Unsupported');
962
+ }
963
+ if (!customer.email) {
964
+ throw new SimpleError({
965
+ code: 'missing_email',
966
+ message: 'Email address is required for online payments via Stripe',
967
+ human: $t('%1SJ'),
968
+ });
969
+ }
639
970
  const stripeResult = await StripeHelper.createPayment({
640
971
  payment,
641
972
  stripeAccount,
@@ -644,65 +975,48 @@ export class PaymentService {
644
975
  statementDescriptor: organization.name,
645
976
  metadata: {
646
977
  organization: organization.id,
647
- user: user.id,
978
+ user: user?.id,
648
979
  payment: payment.id,
649
980
  },
650
981
  i18n: Context.i18n,
651
- lineItems: balanceItemPayments,
652
982
  organization,
653
983
  customer: {
654
- name: user.name ?? names[0]?.name ?? $t(`%Gr`),
655
- email: user.email,
984
+ name: customer.name ?? names[0]?.name ?? $t(`%Gr`),
985
+ email: customer.email,
656
986
  },
657
987
  });
658
988
  paymentUrl = stripeResult.paymentUrl;
659
- }
660
- else if (payment.provider === PaymentProvider.Mollie) {
661
- // Mollie payment
662
- const token = await MollieToken.getTokenFor(organization.id);
663
- if (!token) {
664
- throw new SimpleError({
665
- code: '',
666
- message: $t(`%w3`, { method: PaymentMethodHelper.getName(payment.method) }),
667
- });
668
- }
669
- const profileId = organization.privateMeta.mollieProfile?.id ?? await token.getProfileId(organization.getHost());
670
- if (!profileId) {
671
- throw new SimpleError({
672
- code: '',
673
- message: $t(`%w4`, { method: PaymentMethodHelper.getName(payment.method) }),
674
- });
675
- }
676
- const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
677
- const locale = Context.i18n.locale.replace('-', '_');
678
- const molliePayment = await mollieClient.payments.create({
679
- amount: {
680
- currency: 'EUR',
681
- value: (totalPrice / 100).toFixed(2),
682
- },
683
- method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
684
- testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
685
- profileId,
686
- description,
989
+ } else if (payment.provider === PaymentProvider.Mollie) {
990
+ const mollieResult = await MollieService.createPayment({
991
+ payment,
687
992
  redirectUrl,
993
+ cancelUrl,
688
994
  webhookUrl,
995
+ description,
689
996
  metadata: {
690
- paymentId: payment.id,
997
+ organization: organization.id,
998
+ user: user?.id,
999
+ payment: payment.id,
691
1000
  },
692
- locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
1001
+ sellingOrganization: organization,
1002
+ payingOrganization: payingOrganization ?? null,
1003
+ user,
1004
+ customer,
1005
+ mandate,
693
1006
  });
694
- paymentUrl = molliePayment.getCheckoutUrl();
695
-
696
- // Save payment
697
- const dbPayment = new MolliePayment();
698
- dbPayment.paymentId = payment.id;
699
- dbPayment.mollieId = molliePayment.id;
700
- await dbPayment.save();
701
- }
702
- else if (payment.provider === PaymentProvider.Payconiq) {
1007
+ paymentUrl = mollieResult.paymentUrl;
1008
+ } else if (payment.provider === PaymentProvider.Payconiq) {
1009
+ if (createMandate || mandate) {
1010
+ // Already checked, but for security
1011
+ throw new Error('Unsupported');
1012
+ }
703
1013
  ({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl));
704
- }
705
- else if (payment.provider === PaymentProvider.Buckaroo) {
1014
+ } else if (payment.provider === PaymentProvider.Buckaroo) {
1015
+ if (createMandate || mandate) {
1016
+ // Already checked, but for security
1017
+ throw new Error('Unsupported');
1018
+ }
1019
+
706
1020
  // Increase request timeout because buckaroo is super slow (in development)
707
1021
  Context.request.request?.setTimeout(60 * 1000);
708
1022
  const buckaroo = new BuckarooHelper(organization.privateMeta?.buckarooSettings?.key ?? '', organization.privateMeta?.buckarooSettings?.secret ?? '', organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production');
@@ -719,14 +1033,19 @@ export class PaymentService {
719
1033
  }
720
1034
  }
721
1035
  }
722
- }
723
- catch (e) {
1036
+ } catch (e) {
724
1037
  await PaymentService.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
725
1038
  throw e;
726
1039
  }
727
1040
 
728
- // Mark valid if needed
729
- if (payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Unknown) {
1041
+ // TypeScript thinks status cannot change to Failed, but it can.
1042
+ if (payment.status === PaymentStatus.Succeeded || (payment.status as PaymentStatus) === PaymentStatus.Failed) {
1043
+ // force update
1044
+ const updateTo = payment.status;
1045
+ payment.status = PaymentStatus.Created;
1046
+ await PaymentService.handlePaymentStatusUpdate(payment, organization, updateTo);
1047
+ } else if (payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Unknown || (payment.method === PaymentMethod.DirectDebit && mandate)) {
1048
+ // Mark valid (not same as paid) if needed
730
1049
  let hasBundleDiscount = false;
731
1050
  for (const [balanceItem] of balanceItems) {
732
1051
  // Mark valid
@@ -745,7 +1064,6 @@ export class PaymentService {
745
1064
 
746
1065
  return {
747
1066
  payment,
748
- balanceItemPayments,
749
1067
  provider,
750
1068
  stripeAccount,
751
1069
  paymentUrl,
@@ -753,7 +1071,12 @@ export class PaymentService {
753
1071
  };
754
1072
  }
755
1073
 
756
- static async sendTransferEmail(user: User, organization: Organization, payment: Payment) {
1074
+ static async sendTransferEmail(customer: PaymentCustomer, userId: string | null, organization: Organization, payment: Payment) {
1075
+ const email = customer.dynamicEmail;
1076
+ if (!email) {
1077
+ console.warn('Skipped sending transfer email because of missing email address', payment.id);
1078
+ return;
1079
+ }
757
1080
  const paymentGeneral = await payment.getGeneralStructure();
758
1081
  const groupIds = paymentGeneral.groupIds;
759
1082
 
@@ -761,10 +1084,10 @@ export class PaymentService {
761
1084
 
762
1085
  const recipients = [
763
1086
  Recipient.create({
764
- firstName: user.firstName,
765
- lastName: user.lastName,
766
- email: user.email,
767
- userId: user.id,
1087
+ firstName: customer.firstName,
1088
+ lastName: customer.lastName,
1089
+ email: email,
1090
+ userId,
768
1091
  replacements,
769
1092
  }),
770
1093
  ];
@@ -786,88 +1109,242 @@ export class PaymentService {
786
1109
  });
787
1110
  }
788
1111
 
789
- static async validatePaymentMethod({ payment, balanceItems, paymentConfiguration }: { payment: Payment; balanceItems: Map<BalanceItem, number>; paymentConfiguration: PaymentConfiguration }) {
790
- if (payment.price === 0) {
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({
1171
+ method,
1172
+ customer,
1173
+ price,
1174
+ hasNegative,
1175
+ balanceItems,
1176
+ paymentConfiguration,
1177
+ mandate,
1178
+ payingOrganization,
1179
+ sellingOrganization,
1180
+ user,
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;
1194
+ }) {
1195
+ if (price === 0) {
791
1196
  if (balanceItems.size === 0) {
792
- return;
1197
+ throw new Error('Empty payment');
1198
+ }
1199
+
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) {
1216
+ throw new SimpleError({
1217
+ code: 'cannot_create_mandate_without_payment',
1218
+ message: 'Cannot create saved payment method without payment of a small amount',
1219
+ human: $t(`%1Ss`),
1220
+ });
793
1221
  }
1222
+
794
1223
  // Create an egalizing payment
795
- payment.method = PaymentMethod.Unknown;
1224
+ if (hasNegative) {
1225
+ return {
1226
+ method: PaymentMethod.Unknown,
1227
+ type: PaymentType.Reallocation,
1228
+ mandate: existingMandate,
1229
+ };
1230
+ }
1231
+
1232
+ return {
1233
+ // Free purchase
1234
+ method: PaymentMethod.Unknown,
1235
+ type: PaymentType.Payment,
1236
+ mandate: existingMandate,
1237
+ };
1238
+ }
1239
+
1240
+ if (mandate) {
1241
+ const existingMandate = await this.validateMandate({
1242
+ method,
1243
+ mandate,
1244
+ paymentConfiguration,
1245
+ payingOrganization,
1246
+ sellingOrganization,
1247
+ user,
1248
+ });
796
1249
 
797
- if ([...balanceItems.values()].find(b => b < 0)) {
798
- payment.type = PaymentType.Reallocation;
1250
+ switch (existingMandate.type) {
1251
+ case PaymentMandateType.CreditCard:
1252
+ return {
1253
+ mandate: existingMandate,
1254
+ method: PaymentMethod.CreditCard,
1255
+ type: PaymentType.Payment,
1256
+ };
1257
+ case PaymentMandateType.DirectDebit:
1258
+ return {
1259
+ mandate: existingMandate,
1260
+ method: PaymentMethod.DirectDebit,
1261
+ type: PaymentType.Payment,
1262
+ };
799
1263
  }
1264
+
1265
+ throw new Error('Unsupported mandate type');
800
1266
  }
801
- else if (payment.method === PaymentMethod.Unknown) {
1267
+
1268
+ if (createMandate) {
1269
+ if (!paymentConfiguration.enableMandates) {
1270
+ throw new SimpleError({
1271
+ code: 'mandates_disabled',
1272
+ message: 'Cannot pay with mandate',
1273
+ human: $t('%1R1'),
1274
+ });
1275
+ }
1276
+ }
1277
+
1278
+ if (method === PaymentMethod.Unknown) {
802
1279
  throw new SimpleError({
803
1280
  code: 'invalid_data',
804
1281
  message: $t(`%vy`),
805
1282
  });
806
1283
  }
807
- else {
808
- // Validate payment method
809
- const allowedPaymentMethods = paymentConfiguration.getAvailablePaymentMethods({
810
- amount: payment.price,
811
- customer: payment.customer,
812
- });
813
1284
 
814
- if (!allowedPaymentMethods.includes(payment.method)) {
815
- throw new SimpleError({
816
- code: 'invalid_payment_method',
817
- message: $t(`%vp`),
818
- });
819
- }
1285
+ // Validate payment method
1286
+ const allowedPaymentMethods = paymentConfiguration.getAvailablePaymentMethods({
1287
+ amount: price,
1288
+ customer: customer,
1289
+ forMandate: createMandate,
1290
+ });
1291
+
1292
+ if (!allowedPaymentMethods.includes(method)) {
1293
+ throw new SimpleError({
1294
+ code: 'invalid_payment_method',
1295
+ message: $t(`%vp`),
1296
+ });
820
1297
  }
1298
+
1299
+ return {
1300
+ method,
1301
+ type: PaymentType.Payment,
1302
+ mandate: null,
1303
+ };
821
1304
  }
822
1305
 
823
- static async validateVATRates({ customer, organization, balanceItems }: { customer: PaymentCustomer; organization: Organization; balanceItems: Map<BalanceItem, number> }) {
1306
+ static validateVATRates({ customer, sellingOrganization, balanceItems }: { customer: PaymentCustomer; sellingOrganization: Organization; balanceItems: Map<BalanceItem, number> }) {
824
1307
  // Validate VAT rates for this customer
825
- const seller = organization.meta.companies[0];
826
- if (seller && seller.VATNumber && seller.address && customer.company) {
827
- // B2B validation
828
- if (!customer.company.address) {
829
- throw new SimpleError({
830
- code: 'missing_field',
831
- message: 'Company address missing',
832
- human: $t('%1LH'),
833
- field: 'customer.company.address',
834
- });
835
- }
1308
+ const seller = VATService.getDefaultCompanyForOrganization(sellingOrganization);
1309
+ const excempt = VATService.isVATExcempt({
1310
+ company: customer.company,
1311
+ sellingOrganization,
1312
+ });
836
1313
 
837
- // Reverse charged vat applicable?
838
- if (customer.company.address.country !== seller.address.country) {
839
- // Check VAT Exempt is set on each an every balance item with a non-zero price
840
- for (const [item] of balanceItems) {
841
- if (item.VATExcempt !== VATExcemptReason.IntraCommunity) {
842
- throw new SimpleError({
843
- code: 'VAT_error',
844
- message: 'Intra community VAT reverse charge not supported for this purchase',
845
- human: $t('%1LI'),
846
- });
847
- }
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
+ }
848
1325
 
849
- // We also need to know the VAT rate exactly to be sure the VAT is removed from the purchase
850
- // If VAT is not included, we don't need to know the VAT percentage until the payment is invoiced
851
- if (item.VATPercentage === null && item.VATIncluded) {
852
- throw new SimpleError({
853
- code: 'VAT_error',
854
- message: 'Intra community VAT reverse charge is not supported for this purchase because of missing VAT rates',
855
- human: $t('%1LJ'),
856
- });
857
- }
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
+ });
858
1334
  }
859
1335
  }
860
- else {
861
- // Fine to just not have setup VAT rates yet if the price is guaranteed to include VAT
862
- for (const [item] of balanceItems) {
863
- if (item.VATExcempt === VATExcemptReason.IntraCommunity) {
864
- throw new SimpleError({
865
- code: 'VAT_error',
866
- message: 'Unexpected reverse charge applied',
867
- human: $t('%1LK'),
868
- });
869
- }
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
+ }
870
1346
 
1347
+ if (seller && seller.VATNumber) {
871
1348
  if (!item.VATIncluded && item.VATPercentage === null) {
872
1349
  throw new SimpleError({
873
1350
  code: 'VAT_error',
@@ -878,12 +1355,25 @@ export class PaymentService {
878
1355
  }
879
1356
  }
880
1357
  }
1358
+
1359
+ /* if (seller && seller.VATNumber && seller.address && customer.company) {
1360
+ // B2B validation
1361
+ if (!customer.company.address) {
1362
+ throw new SimpleError({
1363
+ code: 'missing_field',
1364
+ message: 'Company address missing',
1365
+ human: $t('%1LH'),
1366
+ field: 'customer.company.address',
1367
+ });
1368
+ }
1369
+
1370
+ }
881
1371
  else {
882
1372
  // B2C / C2B / C2C
883
1373
 
884
1374
  // You cannot buy balance items with VAT if you didn't set up a VAT number.
885
1375
  for (const [item] of balanceItems) {
886
- if (item.VATExcempt === VATExcemptReason.IntraCommunity) {
1376
+ if (item.VATExcempt === VATExcemptReason.IntraCommunityGoods || item.VATExcempt === VATExcemptReason.IntraCommunityServices) {
887
1377
  throw new SimpleError({
888
1378
  code: 'VAT_error',
889
1379
  message: 'Unexpected reverse charge applied',
@@ -901,6 +1391,8 @@ export class PaymentService {
901
1391
  }
902
1392
  }
903
1393
  }
904
- }
1394
+ } */
1395
+
1396
+ return { seller };
905
1397
  }
906
1398
  };