@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
@@ -175,7 +175,7 @@ export class PlatformMembershipService {
175
175
  }
176
176
  const types = platform.config.membershipTypes.filter(m => m.behaviour === PlatformMembershipTypeBehaviour.Period).map(m => m.id);
177
177
  if (types.length === 0) {
178
- if (!silent) {
178
+ if (!silent && STAMHOOFD.environment !== 'test') {
179
179
  console.warn('No membership types found for memberships');
180
180
  }
181
181
  return;
@@ -353,8 +353,7 @@ export class PlatformMembershipService {
353
353
  // Update the price and dates of this active membership (could have changed)
354
354
  try {
355
355
  await m.calculatePrice(me, cheapestMembership.registration);
356
- }
357
- catch (e) {
356
+ } catch (e) {
358
357
  // Ignore error: membership might not be available anymore
359
358
  if (!silent) {
360
359
  console.error('Failed to calculate price for active membership', m.id, e);
@@ -377,14 +376,12 @@ export class PlatformMembershipService {
377
376
  console.log('Removing membership because cheaper membership found or duplicate, for: ' + me.id + ' - membership ' + m.id);
378
377
  }
379
378
  await m.doDelete();
380
- }
381
- else if (m.membershipTypeId === cheapestMembership.membership.id) {
379
+ } else if (m.membershipTypeId === cheapestMembership.membership.id) {
382
380
  // Update price
383
381
  if (!m.locked) {
384
382
  try {
385
383
  await m.calculatePrice(me);
386
- }
387
- catch (e) {
384
+ } catch (e) {
388
385
  // Ignore error: membership might not be available anymore
389
386
  if (!silent) {
390
387
  console.error('Failed to calculate price for undeletable membership', m.id, e);
@@ -409,8 +406,7 @@ export class PlatformMembershipService {
409
406
  if (me.details.memberNumber === null) {
410
407
  try {
411
408
  await MemberNumberService.assignMemberNumber(me, membership);
412
- }
413
- catch (error) {
409
+ } catch (error) {
414
410
  console.error(`Failed to assign member number for id ${me.id}: ${error.message}`);
415
411
  // If the assignment of the member number fails the membership is not created but the member is registered
416
412
  continue;
@@ -0,0 +1,223 @@
1
+ import type { Model } from '@simonbackx/simple-database';
2
+ import { SimpleError } from '@simonbackx/simple-errors';
3
+ import type { EmailInterfaceBase } from '@stamhoofd/email';
4
+ import { Email } from '@stamhoofd/email';
5
+ import { BalanceItem, Organization, Platform, RegisterCode, UsedRegisterCode } from '@stamhoofd/models';
6
+ import { BalanceItemStatus, BalanceItemType } from '@stamhoofd/structures';
7
+ import { Formatter } from '@stamhoofd/utility';
8
+ import { VATService } from './VATService.js';
9
+
10
+ export class ReferralService {
11
+ /**
12
+ * Prepares a register code for usage.
13
+ * Returns the models to save and the emails to send when succesful
14
+ */
15
+ static async markUsed(organization: Organization, codeString: string): Promise<{ models: Model[]; emails: EmailInterfaceBase[] }> {
16
+ const alreadyUsed = await UsedRegisterCode.getFor(organization.id);
17
+ if (alreadyUsed) {
18
+ throw new SimpleError({
19
+ code: 'invalid_field',
20
+ message: 'Invalid register code',
21
+ human: $t('%1Ws'),
22
+ field: 'registerCode',
23
+ });
24
+ }
25
+
26
+ const code = await RegisterCode.getByID(codeString);
27
+ if (!code) {
28
+ throw new SimpleError({
29
+ code: 'invalid_field',
30
+ message: 'Invalid register code',
31
+ human: $t('%1Ua'),
32
+ field: 'registerCode',
33
+ });
34
+ }
35
+
36
+ const otherOrganization = code.organizationId ? await Organization.getByID(code.organizationId) : undefined;
37
+
38
+ const membershipOrganizationId = (await Platform.getShared()).membershipOrganizationId;
39
+ if (!membershipOrganizationId) {
40
+ throw new SimpleError({
41
+ code: 'invalid_field',
42
+ message: 'Referral codes unavailable',
43
+ human: $t('%1YF'),
44
+ field: 'registerCode',
45
+ });
46
+ }
47
+ const membershipOrganization = await Organization.getByID(membershipOrganizationId, true);
48
+
49
+ const delayEmails: EmailInterfaceBase[] = [];
50
+ let credit: BalanceItem | undefined = undefined;
51
+
52
+ if (code.value > 0) {
53
+ credit = new BalanceItem();
54
+ credit.type = BalanceItemType.ReferralDiscount;
55
+ credit.description = otherOrganization ? ('Tegoed gekregen van ' + otherOrganization.name) : code.description;
56
+ credit.payingOrganizationId = organization.id;
57
+ credit.organizationId = membershipOrganization.id;
58
+ credit.VATPercentage = 21;
59
+ credit.VATExcempt = VATService.getVATExcempt({
60
+ company: organization.defaultCompanies[0] ?? null,
61
+ sellingOrganization: membershipOrganization,
62
+ type: 'services',
63
+ });
64
+ credit.VATIncluded = false;
65
+ credit.quantity = 1;
66
+ credit.unitPrice = -code.value;
67
+ credit.createdAt = new Date();
68
+ credit.status = BalanceItemStatus.Due;
69
+
70
+ // Expire in one year (will get extended for every purchase or activation)
71
+ // credit.expireAt = new Date()
72
+ // credit.expireAt.setFullYear(credit.expireAt.getFullYear() + 1)
73
+ // credit.expireAt.setMilliseconds(0)
74
+
75
+ // Save later
76
+ }
77
+
78
+ if (otherOrganization) {
79
+ const admins = await otherOrganization.getAdminToEmails();
80
+ if (admins) {
81
+ if (code.invoiceValue) {
82
+ // Delay email until everything is validated and saved
83
+ delayEmails.push({
84
+ to: admins,
85
+ subject: organization.name + ' heeft jullie doorverwijzingslink gebruikt 🥳',
86
+ type: 'transactional',
87
+ text: 'Dag ' + otherOrganization.name + ',\n\nGoed nieuws! ' + organization.name + ' heeft jullie doorverwijzingslink gebruikt om zich op Stamhoofd te registreren.\n\n— Stamhoofd',
88
+ });
89
+ } else {
90
+ // Delay email until everything is validated and saved
91
+ delayEmails.push({
92
+ to: admins,
93
+ subject: organization.name + ' heeft jullie doorverwijzingslink gebruikt 🥳',
94
+ type: 'transactional',
95
+ text: 'Dag ' + otherOrganization.name + ',\n\nGoed nieuws! ' + organization.name + ' heeft jullie doorverwijzingslink gebruikt om zich op Stamhoofd te registreren. Als zij minstens 1 euro op Stamhoofd uitgeven ontvangen jullie een tegoed dat kan oplopen tot 100 euro per vereniging (zie daarvoor Stamhoofd > Instellingen). Lees zeker onze tips na om nog een groter bedrag te verzamelen.\n\n— Stamhoofd',
96
+ });
97
+ }
98
+ }
99
+ } else {
100
+ delayEmails.push({
101
+ to: [
102
+ { email: 'hallo@stamhoofd.be' },
103
+ ],
104
+ subject: organization.name + ' heeft jullie doorverwijzingslink gebruikt 🥳',
105
+ type: 'transactional',
106
+ text: 'Dag Stamhoofd,\n\nGoed nieuws! ' + organization.name + ' heeft jullie doorverwijzingslink ' + code.code + ' gebruikt om zich op Stamhoofd te registreren. \n\n— Stamhoofd',
107
+ });
108
+ }
109
+
110
+ // Save that we used this code (so we can reward the other organization)
111
+ const usedCode = new UsedRegisterCode();
112
+ usedCode.organizationId = organization.id;
113
+ usedCode.code = code.code;
114
+
115
+ // Save later
116
+ return {
117
+ models: credit ? [credit, usedCode] : [usedCode],
118
+ emails: delayEmails,
119
+ };
120
+ }
121
+
122
+ static async reward(usedCode: UsedRegisterCode) {
123
+ if (usedCode.balanceItemId) {
124
+ // Already received
125
+ console.error('Already rewarded for used register code ' + usedCode.id);
126
+ return;
127
+ }
128
+
129
+ const code = await RegisterCode.getByID(usedCode.code);
130
+ if (!code || !code.organizationId) {
131
+ console.error("Couldn't find code " + usedCode.code + ' for used register code ' + usedCode.id);
132
+ return;
133
+ }
134
+
135
+ const organization = await Organization.getByID(usedCode.organizationId);
136
+ if (!organization) {
137
+ console.error("Couldn't find organization with id " + usedCode.organizationId + ' for used register code ' + usedCode.id);
138
+ return;
139
+ }
140
+
141
+ const receivingOrganization = await Organization.getByID(code.organizationId);
142
+ if (!receivingOrganization) {
143
+ console.error("Couldn't find receiving organization with id " + code.organizationId + ' for used register code ' + usedCode.id);
144
+ return;
145
+ }
146
+ const membershipOrganizationId = (await Platform.getShared()).membershipOrganizationId!;
147
+ const membershipOrganization = await Organization.getByID(membershipOrganizationId, true);
148
+
149
+ const usedCount = await UsedRegisterCode.getUsedCount(usedCode.code) + 1;
150
+
151
+ const credit = new BalanceItem();
152
+ credit.type = BalanceItemType.ReferralDiscount;
153
+ credit.description = $t('%1ZE', { 'organization-name': organization.name });
154
+ credit.payingOrganizationId = code.organizationId;
155
+ credit.organizationId = (await Platform.getShared()).membershipOrganizationId!; // where do we get this discount from
156
+ credit.VATPercentage = 21;
157
+ credit.VATExcempt = VATService.getVATExcempt({
158
+ company: organization.defaultCompanies[0] ?? null,
159
+ sellingOrganization: membershipOrganization,
160
+ type: 'services',
161
+ });
162
+ credit.VATIncluded = false;
163
+ credit.quantity = 1;
164
+ credit.unitPrice = -(code.invoiceValue ? 0 : Math.min(100 * 100_00, usedCount * 10 * 100_00));
165
+ credit.createdAt = new Date();
166
+ credit.status = BalanceItemStatus.Due;
167
+
168
+ // Expire in one year (will get extended for every purchase or activation)
169
+ // credit.expireAt = new Date()
170
+ // credit.expireAt.setFullYear(credit.expireAt.getFullYear() + 1)
171
+ // credit.expireAt.setMilliseconds(0)
172
+
173
+ await credit.save();
174
+
175
+ usedCode.balanceItemId = credit.id;
176
+ await usedCode.save();
177
+
178
+ if (code.invoiceValue) {
179
+ // The receiving organization is invoiced instead of the organization for the usage of Stamhoofd.
180
+ const item = new BalanceItem();
181
+ item.type = BalanceItemType.Other;
182
+ item.description = 'Aankoop Stamhoofd voor ' + organization.name;
183
+ item.payingOrganizationId = receivingOrganization.id;
184
+ item.organizationId = membershipOrganizationId;
185
+ item.VATPercentage = 21;
186
+ item.VATExcempt = VATService.getVATExcempt({
187
+ company: organization.defaultCompanies[0] ?? null,
188
+ sellingOrganization: membershipOrganization,
189
+ type: 'services',
190
+ });
191
+ item.VATIncluded = false;
192
+ item.quantity = 1;
193
+ item.unitPrice = code.invoiceValue;
194
+ item.createdAt = new Date();
195
+ item.status = BalanceItemStatus.Due;
196
+ await item.save();
197
+ }
198
+
199
+ const admins = await receivingOrganization.getAdminRecipients();
200
+ if (admins) {
201
+ if (code.invoiceValue) {
202
+ Email.send({
203
+ to: admins,
204
+ from: Email.getQuickFromEmail(receivingOrganization.address.country),
205
+ subject: `${organization.name} heeft jullie tegoed gebruikt`,
206
+ text: 'Dag ' + receivingOrganization.name + ',\n\n' + organization.name + ' had jullie doorverwijzingslink gebruikt om zich op Stamhoofd te registreren, en nu hebben ze dit ook gebruikt. Zoals afgesproken wordt hiervoor ' + Formatter.price(code.invoiceValue) + ' aangerekend via jullie openstaand saldo in jullie Stamhoofd account.'
207
+ + '\n\n— Stamhoofd',
208
+ });
209
+ } else {
210
+ // Delay email until everything is validated and saved
211
+ Email.send({
212
+ from: Email.getQuickFromEmail(receivingOrganization.address.country),
213
+ to: admins,
214
+ subject: 'Je hebt ' + Formatter.price(-credit.unitPrice) + ' tegoed ontvangen 💰',
215
+ text: 'Dag ' + receivingOrganization.name + ',\n\nGeweldig nieuws! ' + organization.name + ' had jullie doorverwijzingslink gebruikt om zich op Stamhoofd te registreren, en nu hebben ze ook voor het eerst minstens één euro uitgegeven. Daardoor ontvangen jullie ' + Formatter.price(-credit.unitPrice) + ' tegoed voor Stamhoofd (zie daarvoor Stamhoofd > Instellingen). '
216
+ + (-credit.unitPrice <= 90 * 100_00 ? ('Bij de volgende vereniging ontvangen jullie nog meer: ' + Formatter.price(-credit.unitPrice + 10 * 100_00) + '. ') : '')
217
+ + (-credit.unitPrice <= 80 * 100_00 ? ('En dat blijft oplopen tot € 100,00 per vereniging die je aanbrengt.') : '')
218
+ + 'Doe zo verder! Lees zeker onze tips na om nog een groter bedrag te verzamelen.\n\n— Stamhoofd',
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
@@ -1,12 +1,12 @@
1
1
  import { ManyToOneRelation } from '@simonbackx/simple-database';
2
- import { BalanceItem, Document, Group, Member, Organization, Registration, sendEmailTemplate } from '@stamhoofd/models';
3
- import { AppliedRegistrationDiscount, AuditLogSource, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, EmailTemplateType, Recipient, Replacement, StockReservation, TranslatedString, Version } from '@stamhoofd/structures';
2
+ import { encodeObject } from '@simonbackx/simple-encoding';
3
+ import { BalanceItem, Document, Group, Member, Organization, Registration, RegistrationInvitation, sendEmailTemplate } from '@stamhoofd/models';
4
+ import { QueueHandler } from '@stamhoofd/queues';
5
+ import { AppliedRegistrationDiscount, AuditLogSource, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, EmailTemplateType, getAppHost, GroupType, Recipient, Replacement, StockReservation, Version } from '@stamhoofd/structures';
6
+ import { Formatter } from '@stamhoofd/utility';
4
7
  import { AuditLogService } from './AuditLogService.js';
5
8
  import { GroupService } from './GroupService.js';
6
9
  import { PlatformMembershipService } from './PlatformMembershipService.js';
7
- import { QueueHandler } from '@stamhoofd/queues';
8
- import { Formatter } from '@stamhoofd/utility';
9
- import { encodeObject } from '@simonbackx/simple-encoding';
10
10
 
11
11
  export const RegistrationService = {
12
12
  /**
@@ -55,6 +55,8 @@ export const RegistrationService = {
55
55
  await registration.save();
56
56
  RegistrationService.scheduleStockUpdate(registration.id);
57
57
 
58
+ await this.handleWaitingListRegistrationAndInvitation(registration);
59
+
58
60
  await PlatformMembershipService.updateMembershipsForId(registration.memberId);
59
61
 
60
62
  if (registration.sendConfirmationEmail) {
@@ -76,6 +78,65 @@ export const RegistrationService = {
76
78
  return true;
77
79
  },
78
80
 
81
+ /**
82
+ * Delete all the invitations for the group and member linked to the registration.
83
+ * Will not throw if it fails.
84
+ * @param registration
85
+ */
86
+ async handleWaitingListRegistrationAndInvitation(registration: Registration): Promise<void> {
87
+ const group = await Group.getByID(registration.groupId);
88
+
89
+ if (!group || group.type === GroupType.WaitingList) {
90
+ // skip if waiting list (waiting lists cannot have invitations)
91
+ return;
92
+ }
93
+
94
+ // get invitation for this group and member (can only be 1)
95
+ const invitation = await RegistrationInvitation.select()
96
+ .where('groupId', registration.groupId)
97
+ .andWhere('memberId', registration.memberId)
98
+ .first(false);
99
+
100
+ const waitingListIds = new Set<string>();
101
+
102
+ if (group.waitingListId) {
103
+ // we should check if the member is on the waiting list of this group
104
+ waitingListIds.add(group.waitingListId);
105
+ }
106
+
107
+ if (invitation) {
108
+ // delete the invitation
109
+ try {
110
+ await invitation.delete();
111
+ } catch (e) {
112
+ // an error should not stop other logic (impact of an error is low)
113
+ console.error(e);
114
+ }
115
+ }
116
+
117
+ // unsubscribe from waiting list
118
+ if (group.waitingListId) {
119
+ try {
120
+ // get the registration for the waiting list (should only be 1)
121
+ const waitingListRegistration = await Registration.select()
122
+ .where('groupId', group.waitingListId)
123
+ .where('memberId', registration.memberId)
124
+ .where('deactivatedAt', null)
125
+ .whereNot('registeredAt', null)
126
+ .first(false);
127
+
128
+ if (waitingListRegistration) {
129
+ // unsubscribe for waiting list (waiting lists cannot have a price -> balance items should not be updated)
130
+ await RegistrationService.deactivate(waitingListRegistration);
131
+ await GroupService.updateOccupancy(group.waitingListId);
132
+ }
133
+ } catch (e) {
134
+ // an error should not stop other logic (impact of an error is low)
135
+ console.error(e);
136
+ }
137
+ }
138
+ },
139
+
79
140
  async getRecipients(registration: Registration, organization: Organization, group: Group) {
80
141
  const member = await Member.getByIdWithUsers(registration.memberId);
81
142
 
@@ -101,7 +162,7 @@ export const RegistrationService = {
101
162
  }),
102
163
  Replacement.create({
103
164
  token: 'registerUrl',
104
- value: 'https://' + organization.getHost(),
165
+ value: 'https://' + getAppHost('registration', organization, user.permissions?.forOrganization(organization)?.isEmpty === false),
105
166
  }),
106
167
  Replacement.create({
107
168
  token: 'groupName',
@@ -223,9 +284,65 @@ export const RegistrationService = {
223
284
  * + in combination with validation and reading the webshop
224
285
  */
225
286
  scheduleStockUpdate(id: string) {
287
+ this.scheduleStockUpdateAsync(id);
288
+ },
289
+
290
+ /**
291
+ * Avoid calling this method directly. This can cause race conditions.
292
+ */
293
+ async unsafeStockUpdate(updated: Registration) {
294
+ // Start with clearing all the stock reservations we've already made
295
+ if (updated.stockReservations) {
296
+ const groupIds = Formatter.uniqueArray(updated.stockReservations.flatMap(r => r.objectType === 'Group' ? [r.objectId] : []));
297
+ for (const groupId of groupIds) {
298
+ const stocks = StockReservation.filter('Group', groupId, updated.stockReservations);
299
+
300
+ // Technically we don't need to await this, but okay...
301
+ await Group.freeStockReservations(groupId, stocks);
302
+ }
303
+ }
304
+
305
+ if (updated.shouldIncludeStock()) {
306
+ const groupStockReservations: StockReservation[] = [
307
+ // Group level stock reservations (stored in the group)
308
+ StockReservation.create({
309
+ objectId: updated.groupPrice.id,
310
+ objectType: 'GroupPrice',
311
+ amount: 1,
312
+ }),
313
+ ...updated.options.map((o) => {
314
+ return StockReservation.create({
315
+ objectId: o.option.id,
316
+ objectType: 'GroupOption',
317
+ amount: o.amount,
318
+ });
319
+ }),
320
+ ];
321
+
322
+ await Group.applyStockReservations(updated.groupId, groupStockReservations);
323
+
324
+ updated.stockReservations = [
325
+ // Global level stock reservations (stored in each group)
326
+ StockReservation.create({
327
+ objectId: updated.groupId,
328
+ objectType: 'Group',
329
+ amount: 1,
330
+ children: groupStockReservations,
331
+ }),
332
+ ];
333
+ await updated.save();
334
+ } else {
335
+ if (updated.stockReservations.length) {
336
+ updated.stockReservations = [];
337
+ await updated.save();
338
+ }
339
+ }
340
+ },
341
+
342
+ async scheduleStockUpdateAsync(id: string) {
226
343
  QueueHandler.cancel('registration-stock-update-' + id);
227
344
 
228
- AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
345
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
229
346
  await QueueHandler.schedule('registration-stock-update-' + id, async function (this: undefined) {
230
347
  const updated = await Registration.getByID(id);
231
348
 
@@ -233,53 +350,7 @@ export const RegistrationService = {
233
350
  return;
234
351
  }
235
352
 
236
- // Start with clearing all the stock reservations we've already made
237
- if (updated.stockReservations) {
238
- const groupIds = Formatter.uniqueArray(updated.stockReservations.flatMap(r => r.objectType === 'Group' ? [r.objectId] : []));
239
- for (const groupId of groupIds) {
240
- const stocks = StockReservation.filter('Group', groupId, updated.stockReservations);
241
-
242
- // Technically we don't need to await this, but okay...
243
- await Group.freeStockReservations(groupId, stocks);
244
- }
245
- }
246
-
247
- if (updated.shouldIncludeStock()) {
248
- const groupStockReservations: StockReservation[] = [
249
- // Group level stock reservations (stored in the group)
250
- StockReservation.create({
251
- objectId: updated.groupPrice.id,
252
- objectType: 'GroupPrice',
253
- amount: 1,
254
- }),
255
- ...updated.options.map((o) => {
256
- return StockReservation.create({
257
- objectId: o.option.id,
258
- objectType: 'GroupOption',
259
- amount: o.amount,
260
- });
261
- }),
262
- ];
263
-
264
- await Group.applyStockReservations(updated.groupId, groupStockReservations);
265
-
266
- updated.stockReservations = [
267
- // Global level stock reservations (stored in each group)
268
- StockReservation.create({
269
- objectId: updated.groupId,
270
- objectType: 'Group',
271
- amount: 1,
272
- children: groupStockReservations,
273
- }),
274
- ];
275
- await updated.save();
276
- }
277
- else {
278
- if (updated.stockReservations.length) {
279
- updated.stockReservations = [];
280
- await updated.save();
281
- }
282
- }
353
+ await RegistrationService.unsafeStockUpdate(updated);
283
354
  });
284
355
  }).catch(console.error);
285
356
  },
@@ -0,0 +1,119 @@
1
+ import { SimpleError } from '@simonbackx/simple-errors';
2
+ import { Request } from '@simonbackx/simple-endpoints';
3
+ import { Platform } from '@stamhoofd/models';
4
+ import { LoginMethod, LoginMethodConfig, LoginProviderType, OpenIDClientConfiguration } from '@stamhoofd/structures';
5
+ import { afterEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { ContextInstance } from '../helpers/Context.js';
8
+ import { SSOService } from './SSOService.js';
9
+
10
+ describe('SSOService', () => {
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ describe('fromContext', () => {
16
+ it('does not validate by default', async () => {
17
+ mockPlatform({
18
+ ssoConfiguration: OpenIDClientConfiguration.create({}),
19
+ loginMethods: new Map(),
20
+ });
21
+
22
+ const service = await runWithContext(() => SSOService.fromContext(LoginProviderType.SSO));
23
+
24
+ expect(service).toBeInstanceOf(SSOService);
25
+ });
26
+
27
+ it('validates when requested explicitly', async () => {
28
+ mockPlatform({
29
+ ssoConfiguration: OpenIDClientConfiguration.create({}),
30
+ loginMethods: new Map(),
31
+ });
32
+
33
+ await expect(runWithContext(() => SSOService.fromContext(LoginProviderType.SSO, { validate: true }))).rejects.toThrow('SSO not configured (correctly)');
34
+ });
35
+ });
36
+
37
+ describe('validateConfiguration', () => {
38
+ it('validates the OpenID client configuration getter', () => {
39
+ const service = createService({
40
+ googleConfiguration: null,
41
+ loginMethods: new Map([[LoginMethod.Google, LoginMethodConfig.create({})]]),
42
+ provider: LoginProviderType.Google,
43
+ organization: {},
44
+ });
45
+
46
+ expect(() => service.validateConfiguration()).toThrow(SimpleError);
47
+ expect(() => service.validateConfiguration()).toThrow('SSO not configured');
48
+ });
49
+
50
+ it('validates the login method configuration getter', () => {
51
+ const service = createService({
52
+ ssoConfiguration: OpenIDClientConfiguration.create({}),
53
+ loginMethods: new Map(),
54
+ provider: LoginProviderType.SSO,
55
+ });
56
+
57
+ expect(() => service.validateConfiguration()).toThrow(SimpleError);
58
+ expect(() => service.validateConfiguration()).toThrow('SSO not configured (correctly)');
59
+ });
60
+
61
+ it('accepts complete SSO configuration', () => {
62
+ const service = createService({
63
+ ssoConfiguration: OpenIDClientConfiguration.create({}),
64
+ loginMethods: new Map([[LoginMethod.SSO, LoginMethodConfig.create({})]]),
65
+ provider: LoginProviderType.SSO,
66
+ });
67
+
68
+ expect(() => service.validateConfiguration()).not.toThrow();
69
+ });
70
+ });
71
+ });
72
+
73
+ async function runWithContext<T>(handler: () => Promise<T>) {
74
+ const context = new ContextInstance(new Request({
75
+ method: 'GET',
76
+ url: '/',
77
+ host: '',
78
+ }));
79
+
80
+ return await ContextInstance.asyncLocalStorage.run(context, handler);
81
+ }
82
+
83
+ function mockPlatform(options: {
84
+ loginMethods: Map<LoginMethod, LoginMethodConfig>;
85
+ ssoConfiguration?: OpenIDClientConfiguration | null;
86
+ googleConfiguration?: OpenIDClientConfiguration | null;
87
+ }) {
88
+ vi.spyOn(Platform, 'getForEditing').mockResolvedValue(createPlatform(options));
89
+ }
90
+
91
+ function createService(options: {
92
+ provider: LoginProviderType;
93
+ loginMethods: Map<LoginMethod, LoginMethodConfig>;
94
+ ssoConfiguration?: OpenIDClientConfiguration | null;
95
+ googleConfiguration?: OpenIDClientConfiguration | null;
96
+ organization?: any;
97
+ }) {
98
+ return new SSOService({
99
+ provider: options.provider,
100
+ platform: createPlatform(options),
101
+ organization: options.organization,
102
+ });
103
+ }
104
+
105
+ function createPlatform(options: {
106
+ loginMethods: Map<LoginMethod, LoginMethodConfig>;
107
+ ssoConfiguration?: OpenIDClientConfiguration | null;
108
+ googleConfiguration?: OpenIDClientConfiguration | null;
109
+ }) {
110
+ return {
111
+ config: {
112
+ loginMethods: options.loginMethods,
113
+ },
114
+ serverConfig: {
115
+ ssoConfiguration: options.ssoConfiguration,
116
+ googleConfiguration: options.googleConfiguration,
117
+ },
118
+ } as any;
119
+ }