@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
@@ -0,0 +1,462 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import type { Organization } from '@stamhoofd/models';
3
+ import { MollieToken } from '@stamhoofd/models';
4
+ import { Version } from '@stamhoofd/structures';
5
+ import nock from 'nock';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+
8
+ import { ExchangePaymentEndpoint } from '../../src/endpoints/organization/shared/ExchangePaymentEndpoint.js';
9
+ import { testServer } from './TestServer.js';
10
+ import { resetNock } from './resetNock.js';
11
+
12
+ /**
13
+ * In-memory representation of a Mollie payment (transaction).
14
+ * Only the fields the backend (MollieService / PaymentService) actually reads are kept.
15
+ */
16
+ export type MollieMockPayment = {
17
+ id: string;
18
+ status: 'open' | 'paid' | 'failed' | 'canceled' | 'expired' | 'pending';
19
+ amount: { currency: string; value: string };
20
+ /** The internal Stamhoofd payment id (from metadata.paymentId), used to drive the exchange */
21
+ internalPaymentId: string | null;
22
+ redirectUrl: string | null;
23
+ sequenceType: 'oneoff' | 'first' | 'recurring';
24
+ customerId: string | null;
25
+ mandateId: string | null;
26
+ isCancelable: boolean;
27
+ details: Record<string, unknown> | null;
28
+ };
29
+
30
+ export type MollieMockMandate = {
31
+ id: string;
32
+ customerId: string;
33
+ status: 'valid' | 'pending' | 'invalid';
34
+ method: 'creditcard' | 'directdebit';
35
+ details: Record<string, unknown>;
36
+ createdAt: string;
37
+ };
38
+
39
+ export type MollieMockChargeback = {
40
+ id: string;
41
+ paymentId: string;
42
+ amount: { currency: string; value: string };
43
+ createdAt: string;
44
+ };
45
+
46
+ const MOLLIE_CHECKOUT_URL = 'https://molliecheckout/';
47
+
48
+ /**
49
+ * Mirrors the StripeMocker (tests/helpers/StripeMocker.ts) but for Mollie.
50
+ *
51
+ * The Mollie node client (@mollie/api-client v4) performs its HTTP requests through node-fetch v2,
52
+ * which uses Node's http/https module under the hood. nock can therefore intercept every call to
53
+ * https://api.mollie.com - exactly like the StripeMocker does for Stripe.
54
+ *
55
+ * Usage:
56
+ * const mollieMocker = new MollieMocker();
57
+ * mollieMocker.start();
58
+ * await mollieMocker.setupToken(membershipOrganization);
59
+ * ... drive a checkout in the UI ...
60
+ * await mollieMocker.succeedPayment(); // marks the last payment paid + creates a mandate
61
+ * mollieMocker.stop();
62
+ */
63
+ export class MollieMocker {
64
+ payments: MollieMockPayment[] = [];
65
+ customers: { id: string }[] = [];
66
+ mandates: MollieMockMandate[] = [];
67
+ chargebacks: MollieMockChargeback[] = [];
68
+
69
+ #forceFailure = false;
70
+
71
+ reset() {
72
+ this.payments = [];
73
+ this.customers = [];
74
+ this.mandates = [];
75
+ this.chargebacks = [];
76
+ this.#forceFailure = false;
77
+ }
78
+
79
+ /**
80
+ * Make every newly created payment fail immediately when it is exchanged.
81
+ */
82
+ forceFailure() {
83
+ this.#forceFailure = true;
84
+ }
85
+
86
+ createId(prefix: string) {
87
+ return prefix + '_' + uuidv4().replaceAll('-', '');
88
+ }
89
+
90
+ /**
91
+ * Create a Mollie OAuth token for the selling organization so MollieService.create() works.
92
+ * Without a token, MollieService.create() returns null and no Mollie request is ever made.
93
+ */
94
+ async setupToken(organization: Organization) {
95
+ // Drop any cached token for this organization (the cache survives between tests within a worker)
96
+ MollieToken.knownTokens.delete(organization.id);
97
+
98
+ const existing = await MollieToken.where({ organizationId: organization.id });
99
+ const token = existing[0] ?? new MollieToken();
100
+ token.organizationId = organization.id;
101
+ token.accessToken = 'access_test_' + uuidv4().replaceAll('-', '');
102
+ token.refreshToken = 'refresh_test_' + uuidv4().replaceAll('-', '');
103
+ // Far in the future so getAccessToken() never tries to refresh (which would hit the real Mollie OAuth API)
104
+ token.expiresOn = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365);
105
+ await token.save();
106
+
107
+ MollieToken.knownTokens.set(organization.id, token);
108
+ return token;
109
+ }
110
+
111
+ start() {
112
+ const matcher = /\/v2\/.*/;
113
+
114
+ const interceptor = nock('https://api.mollie.com').persist();
115
+
116
+ interceptor.get(matcher).reply((uri) => {
117
+ return this.#handle('GET', uri, undefined);
118
+ });
119
+
120
+ interceptor.post(matcher).reply((uri, body) => {
121
+ return this.#handle('POST', uri, body);
122
+ });
123
+
124
+ interceptor.delete(matcher).reply((uri, body) => {
125
+ return this.#handle('DELETE', uri, body);
126
+ });
127
+ }
128
+
129
+ stop() {
130
+ resetNock();
131
+ }
132
+
133
+ /**
134
+ * Route a request to the correct in-memory handler based on its path.
135
+ * Returns a nock-style [statusCode, body] tuple.
136
+ */
137
+ #handle(method: string, uri: string, body: unknown): [number, unknown?] {
138
+ const path = uri.split('?')[0].replace(/^\/v2\//, '');
139
+ const parts = path.split('/').filter(p => p.length > 0);
140
+ const decoded = this.#decodeBody(body);
141
+
142
+ // payments
143
+ if (parts[0] === 'payments') {
144
+ if (method === 'POST' && parts.length === 1) {
145
+ return this.#createPayment(decoded);
146
+ }
147
+ if (method === 'GET' && parts.length === 2) {
148
+ return this.#getPayment(parts[1]);
149
+ }
150
+ if (method === 'DELETE' && parts.length === 2) {
151
+ return this.#cancelPayment(parts[1]);
152
+ }
153
+ }
154
+
155
+ // customers + nested mandates
156
+ if (parts[0] === 'customers') {
157
+ if (method === 'POST' && parts.length === 1) {
158
+ return this.#createCustomer(decoded);
159
+ }
160
+ if (method === 'GET' && parts.length === 2) {
161
+ return this.#getCustomer(parts[1]);
162
+ }
163
+ if (parts[2] === 'mandates') {
164
+ if (method === 'GET' && parts.length === 3) {
165
+ return this.#listMandates(parts[1]);
166
+ }
167
+ if (method === 'DELETE' && parts.length === 4) {
168
+ return this.#revokeMandate(parts[3]);
169
+ }
170
+ }
171
+ }
172
+
173
+ if (parts[0] === 'profiles' && method === 'GET') {
174
+ return this.#listResource('profiles', [this.#profile()]);
175
+ }
176
+
177
+ if (parts[0] === 'onboarding' && method === 'GET') {
178
+ return [200, this.#onboarding()];
179
+ }
180
+
181
+ if (parts[0] === 'chargebacks' && method === 'GET') {
182
+ return this.#listResource('chargebacks', this.chargebacks.map(c => this.#chargebackResource(c)));
183
+ }
184
+
185
+ console.error('MollieMocker: unhandled request', method, uri);
186
+ return [500, { detail: 'MollieMocker: unhandled request ' + method + ' ' + uri }];
187
+ }
188
+
189
+ // ---- Payments ---------------------------------------------------------
190
+
191
+ #createPayment(body: Record<string, any>): [number, unknown] {
192
+ const id = this.createId('tr');
193
+ const payment: MollieMockPayment = {
194
+ id,
195
+ status: 'open',
196
+ amount: body.amount ?? { currency: 'EUR', value: '0.00' },
197
+ internalPaymentId: body.metadata?.paymentId ?? null,
198
+ redirectUrl: body.redirectUrl ?? null,
199
+ sequenceType: body.sequenceType ?? 'oneoff',
200
+ customerId: body.customerId ?? null,
201
+ mandateId: body.mandateId ?? null,
202
+ isCancelable: true,
203
+ details: null,
204
+ };
205
+ this.payments.push(payment);
206
+ return [201, this.#paymentResource(payment)];
207
+ }
208
+
209
+ #getPayment(id: string): [number, unknown] {
210
+ const payment = this.payments.find(p => p.id === id);
211
+ if (!payment) {
212
+ return [404, { detail: 'Payment not found' }];
213
+ }
214
+ return [200, this.#paymentResource(payment)];
215
+ }
216
+
217
+ #cancelPayment(id: string): [number, unknown] {
218
+ const payment = this.payments.find(p => p.id === id);
219
+ if (!payment) {
220
+ return [404, { detail: 'Payment not found' }];
221
+ }
222
+ payment.status = 'canceled';
223
+ return [200, this.#paymentResource(payment)];
224
+ }
225
+
226
+ #paymentResource(payment: MollieMockPayment) {
227
+ return {
228
+ resource: 'payment',
229
+ id: payment.id,
230
+ mode: 'test',
231
+ status: payment.status,
232
+ isCancelable: payment.isCancelable,
233
+ amount: payment.amount,
234
+ sequenceType: payment.sequenceType,
235
+ customerId: payment.customerId ?? undefined,
236
+ mandateId: payment.mandateId ?? undefined,
237
+ details: payment.details ?? undefined,
238
+ metadata: payment.internalPaymentId ? { paymentId: payment.internalPaymentId } : undefined,
239
+ _links: {
240
+ self: { href: 'https://api.mollie.com/v2/payments/' + payment.id, type: 'application/hal+json' },
241
+ checkout: { href: MOLLIE_CHECKOUT_URL, type: 'text/html' },
242
+ },
243
+ };
244
+ }
245
+
246
+ // ---- Customers --------------------------------------------------------
247
+
248
+ #createCustomer(body: Record<string, any>): [number, unknown] {
249
+ const id = this.createId('cst');
250
+ this.customers.push({ id });
251
+ return [201, this.#customerResource(id, body)];
252
+ }
253
+
254
+ #getCustomer(id: string): [number, unknown] {
255
+ // Always return a valid test-mode customer (mirrors a real, previously created customer)
256
+ if (!this.customers.find(c => c.id === id)) {
257
+ this.customers.push({ id });
258
+ }
259
+ return [200, this.#customerResource(id, {})];
260
+ }
261
+
262
+ #customerResource(id: string, body: Record<string, any>) {
263
+ return {
264
+ resource: 'customer',
265
+ id,
266
+ mode: 'test',
267
+ name: body.name ?? 'Test',
268
+ email: body.email ?? undefined,
269
+ metadata: body.metadata ?? undefined,
270
+ _links: { self: { href: 'https://api.mollie.com/v2/customers/' + id, type: 'application/hal+json' } },
271
+ };
272
+ }
273
+
274
+ // ---- Mandates ---------------------------------------------------------
275
+
276
+ /**
277
+ * Register a (valid) mandate for a customer, simulating a saved card.
278
+ */
279
+ addMandate(options: { customerId: string; cardNumber?: string; cardLabel?: string; cardHolder?: string }): MollieMockMandate {
280
+ const mandate: MollieMockMandate = {
281
+ id: this.createId('mdt'),
282
+ customerId: options.customerId,
283
+ status: 'valid',
284
+ method: 'creditcard',
285
+ details: {
286
+ cardHolder: options.cardHolder ?? 'John Doe',
287
+ cardNumber: options.cardNumber ?? '1234',
288
+ cardLabel: options.cardLabel ?? 'Visa',
289
+ cardExpiryDate: '2030-12-31',
290
+ },
291
+ createdAt: new Date().toISOString(),
292
+ };
293
+ this.mandates.push(mandate);
294
+ return mandate;
295
+ }
296
+
297
+ #listMandates(customerId: string): [number, unknown] {
298
+ const mandates = this.mandates.filter(m => m.customerId === customerId).map(m => this.#mandateResource(m));
299
+ return this.#listResource('mandates', mandates);
300
+ }
301
+
302
+ #revokeMandate(mandateId: string): [number, unknown] {
303
+ this.mandates = this.mandates.filter(m => m.id !== mandateId);
304
+ return [204, undefined];
305
+ }
306
+
307
+ #mandateResource(mandate: MollieMockMandate) {
308
+ return {
309
+ resource: 'mandate',
310
+ id: mandate.id,
311
+ mode: 'test',
312
+ status: mandate.status,
313
+ method: mandate.method,
314
+ details: mandate.details,
315
+ customerId: mandate.customerId,
316
+ createdAt: mandate.createdAt,
317
+ _links: { self: { href: 'https://api.mollie.com/v2/customers/' + mandate.customerId + '/mandates/' + mandate.id, type: 'application/hal+json' } },
318
+ };
319
+ }
320
+
321
+ // ---- Profiles / onboarding -------------------------------------------
322
+
323
+ #profile() {
324
+ const id = 'pfl_test';
325
+ return {
326
+ resource: 'profile',
327
+ id,
328
+ mode: 'test',
329
+ name: 'Stamhoofd',
330
+ website: 'https://www.stamhoofd.be',
331
+ status: 'verified',
332
+ _links: { self: { href: 'https://api.mollie.com/v2/profiles/' + id, type: 'application/hal+json' } },
333
+ };
334
+ }
335
+
336
+ #onboarding() {
337
+ return {
338
+ resource: 'onboarding',
339
+ name: 'Stamhoofd',
340
+ status: 'completed',
341
+ canReceivePayments: true,
342
+ canReceiveSettlements: true,
343
+ _links: { self: { href: 'https://api.mollie.com/v2/onboarding/me', type: 'application/hal+json' } },
344
+ };
345
+ }
346
+
347
+ // ---- Chargebacks ------------------------------------------------------
348
+
349
+ /**
350
+ * Register a chargeback for a (succeeded) Mollie payment. Used to drive the mollie-chargebacks cron.
351
+ * The amount must equal the original payment price (PaymentService.registerChargeback enforces this).
352
+ */
353
+ createChargeback(payment: MollieMockPayment): MollieMockChargeback {
354
+ const chargeback: MollieMockChargeback = {
355
+ id: this.createId('chb'),
356
+ paymentId: payment.id,
357
+ amount: payment.amount,
358
+ createdAt: new Date().toISOString(),
359
+ };
360
+ this.chargebacks.push(chargeback);
361
+ return chargeback;
362
+ }
363
+
364
+ #chargebackResource(chargeback: MollieMockChargeback) {
365
+ return {
366
+ resource: 'chargeback',
367
+ id: chargeback.id,
368
+ amount: chargeback.amount,
369
+ paymentId: chargeback.paymentId,
370
+ createdAt: chargeback.createdAt,
371
+ _links: { self: { href: 'https://api.mollie.com/v2/payments/' + chargeback.paymentId + '/chargebacks/' + chargeback.id, type: 'application/hal+json' } },
372
+ };
373
+ }
374
+
375
+ // ---- List helper ------------------------------------------------------
376
+
377
+ #listResource(binderName: string, items: unknown[]): [number, unknown] {
378
+ return [200, {
379
+ count: items.length,
380
+ _embedded: { [binderName]: items },
381
+ _links: {
382
+ self: { href: 'https://api.mollie.com/v2/' + binderName, type: 'application/hal+json' },
383
+ documentation: { href: 'https://docs.mollie.com', type: 'text/html' },
384
+ next: null,
385
+ previous: null,
386
+ },
387
+ }];
388
+ }
389
+
390
+ // ---- Driving payment status ------------------------------------------
391
+
392
+ getLastPayment(): MollieMockPayment {
393
+ return this.payments[this.payments.length - 1];
394
+ }
395
+
396
+ /**
397
+ * Mark a payment as paid. For first/recurring-setup payments this also creates a valid mandate
398
+ * for the payment's customer (simulating Mollie creating the mandate after the first payment),
399
+ * and links it on the payment so the backend stores it as the default mandate.
400
+ *
401
+ * Finally it triggers the payment exchange (like Mollie's webhook) so the backend processes the
402
+ * status change: marking balance items paid, activating packages and saving the default mandate.
403
+ */
404
+ async succeedPayment(payment: MollieMockPayment = this.getLastPayment(), options: { cardNumber?: string; cardLabel?: string } = {}) {
405
+ if (this.#forceFailure) {
406
+ return this.failPayment(payment);
407
+ }
408
+
409
+ payment.status = 'paid';
410
+ payment.isCancelable = false;
411
+
412
+ if ((payment.sequenceType === 'first') && payment.customerId && !payment.mandateId) {
413
+ const mandate = this.addMandate({
414
+ customerId: payment.customerId,
415
+ cardNumber: options.cardNumber,
416
+ cardLabel: options.cardLabel,
417
+ });
418
+ payment.mandateId = mandate.id;
419
+ }
420
+
421
+ payment.details = {
422
+ cardHolder: 'John Doe',
423
+ cardNumber: options.cardNumber ?? '1234',
424
+ cardLabel: options.cardLabel ?? 'Visa',
425
+ };
426
+
427
+ await this.#exchange(payment);
428
+ }
429
+
430
+ async failPayment(payment: MollieMockPayment = this.getLastPayment()) {
431
+ payment.status = 'failed';
432
+ payment.isCancelable = false;
433
+ await this.#exchange(payment);
434
+ }
435
+
436
+ /**
437
+ * Trigger the payment exchange endpoint (the equivalent of a Mollie webhook), so the backend
438
+ * polls the (now updated) Mollie status and processes it.
439
+ */
440
+ async #exchange(payment: MollieMockPayment) {
441
+ if (!payment.internalPaymentId) {
442
+ return;
443
+ }
444
+ const endpoint = new ExchangePaymentEndpoint();
445
+ const request = Request.buildJson('POST', `/v${Version}/payments/${encodeURIComponent(payment.internalPaymentId)}?exchange=true`, undefined, undefined);
446
+ await testServer.test(endpoint, request);
447
+ }
448
+
449
+ #decodeBody(body: unknown): Record<string, any> {
450
+ if (!body) {
451
+ return {};
452
+ }
453
+ if (typeof body === 'string') {
454
+ try {
455
+ return JSON.parse(body);
456
+ } catch {
457
+ return {};
458
+ }
459
+ }
460
+ return body as Record<string, any>;
461
+ }
462
+ }
@@ -1,9 +1,40 @@
1
- import { PayconiqAccount } from '@stamhoofd/structures';
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { PayconiqAccount, Version } from '@stamhoofd/structures';
2
3
  import { TestUtils } from '@stamhoofd/test-utils';
3
4
  import nock from 'nock';
4
5
  import { v4 as uuidv4 } from 'uuid';
5
6
 
7
+ /**
8
+ * In-memory representation of a Payconiq payment.
9
+ * Only the fields the backend (PayconiqPayment) actually reads are kept.
10
+ */
11
+ export type PayconiqMockPayment = {
12
+ payconiqId: string;
13
+ /** The internal Stamhoofd payment id (parsed from the callbackUrl), used to drive the exchange */
14
+ internalPaymentId: string | null;
15
+ /** The url Payconiq redirects back to after an app-to-app payment */
16
+ returnUrl: string | null;
17
+ /** Payconiq status string (see PayconiqPayment.getResponseStatus) */
18
+ status: 'PENDING' | 'IDENTIFIED' | 'AUTHORIZED' | 'SUCCEEDED' | 'FAILED' | 'EXPIRED' | 'CANCELLED';
19
+ /** Amount in euro (2 decimals), as sent to Payconiq */
20
+ amount: number;
21
+ };
22
+
23
+ /**
24
+ * Mocks the Payconiq API (https://api.ext.payconiq.com) for in-process Playwright/integration tests.
25
+ *
26
+ * Unlike StripeMocker/MollieMocker this stays static, because resetNock() re-installs
27
+ * PayconiqMocker.setup() after every nock.cleanAll(): Payconiq is mocked globally for tests.
28
+ *
29
+ * Usage:
30
+ * PayconiqMocker.setup(); // (re)install the nock interceptors
31
+ * PayconiqMocker.reset(); // clear tracked payments (between tests)
32
+ * ... drive a checkout in the UI (provider = Payconiq) ...
33
+ * await PayconiqMocker.succeedPayment(); // mark the last payment paid + run the exchange
34
+ */
6
35
  export class PayconiqMocker {
36
+ static payments: PayconiqMockPayment[] = [];
37
+
7
38
  static infect() {
8
39
  if (STAMHOOFD.environment !== 'test') {
9
40
  throw new Error('PayconiqMocker can only be used in test environment');
@@ -32,54 +63,116 @@ export class PayconiqMocker {
32
63
  }
33
64
 
34
65
  static setup() {
35
- nock('https://api.ext.payconiq.com')
36
- .persist()
37
- .post('/v3/payments')
38
- .reply((uri, body) => {
39
- // todo: do something smarter with the body
40
-
41
- return [200, {
42
- paymentId: uuidv4(),
43
- debtor: {
44
- name: 'Test User',
45
- iban: 'BE41878878996410', // random iban
46
- },
47
- _links: {
48
- checkout: {
49
- href: 'https://payconiq.com/pay/2/5bdb1685b93d1c000bde96f2?token=530ea8a4ec8ded7d87620c8637354022cd965b143f257f8f8cb118e7f4a22d8f&returnUrl=https%3A%2F%2Fummy.webshop%2Fcheckout%2Fsuccess',
50
- },
51
- qrcode: {
52
- href: 'https://payconiq.com/pay/2/5bdb1685b93d1c000bde96f2?token=530ea8a4ec8ded7d87620c8637354022cd965b143f257f8f8cb118e7f4a22d8f&returnUrl=https%3A%2F%2Fummy.webshop%2Fcheckout%2Fsuccess',
53
- },
54
- },
55
- }];
56
- });
66
+ const handlePost = (body: any) => {
67
+ const decoded = this.#decodeBody(body);
68
+ const payconiqId = uuidv4();
57
69
 
58
- nock('https://merchant.api.preprod.bancontact.net')
59
- .persist()
60
- .post('/v3/payments')
61
- .reply((uri, body) => {
62
- // todo: do something smarter with the body
63
-
64
- return [200, {
65
- paymentId: uuidv4(),
66
- debtor: {
67
- name: 'Test User',
68
- iban: 'BE41878878996410', // random iban
69
- },
70
- _links: {
71
- checkout: {
72
- href: 'https://payconiq.com/pay/2/5bdb1685b93d1c000bde96f2?token=530ea8a4ec8ded7d87620c8637354022cd965b143f257f8f8cb118e7f4a22d8f&returnUrl=https%3A%2F%2Fummy.webshop%2Fcheckout%2Fsuccess',
73
- },
74
- qrcode: {
75
- href: 'https://payconiq.com/pay/2/5bdb1685b93d1c000bde96f2?token=530ea8a4ec8ded7d87620c8637354022cd965b143f257f8f8cb118e7f4a22d8f&returnUrl=https%3A%2F%2Fummy.webshop%2Fcheckout%2Fsuccess',
76
- },
77
- },
78
- }];
70
+ // The internal payment id is part of the callbackUrl: /payments/<id>?exchange=true
71
+ let internalPaymentId: string | null = null;
72
+ if (typeof decoded.callbackUrl === 'string') {
73
+ const match = decoded.callbackUrl.match(/\/payments\/([^?/]+)/);
74
+ if (match) {
75
+ internalPaymentId = decodeURIComponent(match[1]);
76
+ }
77
+ }
78
+
79
+ this.payments.push({
80
+ payconiqId,
81
+ internalPaymentId,
82
+ returnUrl: typeof decoded.returnUrl === 'string' ? decoded.returnUrl : null,
83
+ status: 'PENDING',
84
+ amount: typeof decoded.amount === 'number' ? decoded.amount : 0,
79
85
  });
86
+
87
+ // The checkout href is opened by the frontend (app-to-app / QR). Tests intercept it.
88
+ const checkout = 'https://payconiq-checkout.test/' + payconiqId;
89
+ return [200, {
90
+ paymentId: payconiqId,
91
+ debtor: {
92
+ name: 'Test User',
93
+ iban: 'BE41878878996410', // random iban
94
+ },
95
+ _links: {
96
+ checkout: { href: checkout },
97
+ qrcode: { href: checkout },
98
+ },
99
+ }];
100
+ };
101
+
102
+ const handleGet = (uri: string) => {
103
+ const payconiqId = uri.split('/').pop()!.split('?')[0];
104
+ const payment = this.payments.find(p => p.payconiqId === payconiqId);
105
+ if (!payment) {
106
+ return [404, { message: 'Payment not found' }];
107
+ }
108
+ return [200, {
109
+ paymentId: payment.payconiqId,
110
+ status: payment.status,
111
+ amount: payment.amount,
112
+ debtor: {
113
+ name: 'Test User',
114
+ iban: 'BE41878878996410',
115
+ },
116
+ }];
117
+ };
118
+
119
+ // In test mode the backend talks to the Wero/Bancontact host (api.ext.payconiq.com is
120
+ // the legacy host); mock both so the status poll works regardless.
121
+ for (const host of ['https://api.ext.payconiq.com', 'https://merchant.api.preprod.bancontact.net']) {
122
+ nock(host).persist().post('/v3/payments').reply((_uri, body) => handlePost(body));
123
+ nock(host).persist().get(/\/v3\/payments\/.+/).reply(uri => handleGet(uri) as [number, unknown]);
124
+ nock(host).persist().delete(/\/v3\/payments\/.+/).reply(() => [204, undefined]);
125
+ }
80
126
  }
81
127
 
82
128
  static reset() {
83
- // todo: clean up any state if needed
129
+ this.payments = [];
130
+ }
131
+
132
+ static getLastPayment(): PayconiqMockPayment {
133
+ return this.payments[this.payments.length - 1];
134
+ }
135
+
136
+ /**
137
+ * Mark a payment as succeeded and trigger the payment exchange (the equivalent of the Payconiq
138
+ * callback), so the backend processes the status change (balance items, tickets, ...).
139
+ */
140
+ static async succeedPayment(payment: PayconiqMockPayment = this.getLastPayment()) {
141
+ payment.status = 'SUCCEEDED';
142
+ await this.#exchange(payment);
143
+ }
144
+
145
+ static async failPayment(payment: PayconiqMockPayment = this.getLastPayment()) {
146
+ payment.status = 'FAILED';
147
+ await this.#exchange(payment);
148
+ }
149
+
150
+ static async #exchange(payment: PayconiqMockPayment) {
151
+ if (!payment.internalPaymentId) {
152
+ return;
153
+ }
154
+
155
+ // For some reason the tests go crazy when we try to import this at the top even when we preload the environment it
156
+ // despearately imports the file before loading the STAMHOOFD environment...
157
+ const { ExchangePaymentEndpoint } = await import('../../src/endpoints/organization/shared/ExchangePaymentEndpoint.js');
158
+ const { testServer } = await import('./TestServer.js');
159
+
160
+ const endpoint = new ExchangePaymentEndpoint();
161
+ const request = Request.buildJson('POST', `/v${Version}/payments/${encodeURIComponent(payment.internalPaymentId)}?exchange=true`, undefined, undefined);
162
+ await testServer.test(endpoint, request);
163
+ }
164
+
165
+ static #decodeBody(body: unknown): Record<string, any> {
166
+ if (!body) {
167
+ return {};
168
+ }
169
+ if (typeof body === 'string') {
170
+ try {
171
+ return JSON.parse(body);
172
+ } catch {
173
+ return {};
174
+ }
175
+ }
176
+ return body as Record<string, any>;
84
177
  }
85
178
  }