@stamhoofd/backend 2.121.0 → 2.122.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (429) hide show
  1. package/output-2.txt +1 -0
  2. package/output.txt +1 -0
  3. package/package.json +69 -52
  4. package/src/audit-logs/EventNotificationLogger.ts +40 -0
  5. package/src/audit-logs/ModelLogger.ts +19 -15
  6. package/src/audit-logs/OrganizationLogger.ts +1 -1
  7. package/src/audit-logs/RegistrationInvitationLogger.ts +24 -24
  8. package/src/audit-logs/init.ts +8 -2
  9. package/src/boot.ts +19 -18
  10. package/src/crons/amazon-ses.ts +13 -26
  11. package/src/crons/balance-emails.ts +3 -5
  12. package/src/crons/clearExcelCache.test.ts +1 -1
  13. package/src/crons/clearExcelCache.ts +1 -2
  14. package/src/crons/drip-emails.ts +42 -0
  15. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +4 -0
  16. package/src/crons/helpers/isOutside.ts +11 -0
  17. package/src/crons/helpers/useSavedIterator.ts +32 -0
  18. package/src/crons/index.ts +5 -0
  19. package/src/crons/invoices.ts +32 -38
  20. package/src/crons/members-fees.ts +50 -0
  21. package/src/crons/mollie-chargebacks.ts +14 -15
  22. package/src/crons/service-fees.ts +152 -0
  23. package/src/crons/stripe-invoices.ts +48 -0
  24. package/src/crons/transfer-fees.ts +176 -0
  25. package/src/crons/update-cached-balances.ts +4 -4
  26. package/src/crons.ts +7 -64
  27. package/src/debug.ts +4 -2
  28. package/src/email-recipient-loaders/documents.ts +1 -1
  29. package/src/email-recipient-loaders/members.ts +1 -1
  30. package/src/email-recipient-loaders/payments.ts +4 -6
  31. package/src/email-recipient-loaders/registrations.ts +1 -1
  32. package/src/email-replacements/getEmailReplacementsForPayment.ts +5 -9
  33. package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +1 -1
  34. package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +1 -1
  35. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +3 -5
  36. package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +1 -1
  37. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
  38. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +13 -7
  39. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +4 -6
  40. package/src/endpoints/admin/organizations/PatchPackagesEndpoint.test.ts +10 -6
  41. package/src/endpoints/admin/organizations/PatchPackagesEndpoint.ts +39 -21
  42. package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +1 -1
  43. package/src/endpoints/auth/CreateAdminEndpoint.ts +23 -5
  44. package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -2
  45. package/src/endpoints/auth/DeleteTokenEndpoint.ts +1 -1
  46. package/src/endpoints/auth/DeleteUserEndpoint.ts +3 -3
  47. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  48. package/src/endpoints/auth/GetOtherUserEndpoint.ts +1 -1
  49. package/src/endpoints/auth/GetUserEndpoint.ts +1 -1
  50. package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +1 -1
  51. package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +1 -1
  52. package/src/endpoints/auth/PatchUserEndpoint.ts +5 -8
  53. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
  54. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
  55. package/src/endpoints/auth/SignupEndpoint.ts +2 -3
  56. package/src/endpoints/auth/VerifyEmailEndpoint.ts +20 -3
  57. package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +2 -1
  58. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +2 -4
  59. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +1 -1
  60. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +5 -7
  61. package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +2 -2
  62. package/src/endpoints/global/email/CreateEmailEndpoint.ts +3 -4
  63. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
  64. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +1 -2
  65. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +4 -6
  66. package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
  67. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
  68. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +1 -1
  69. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +6 -10
  70. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +4 -7
  71. package/src/endpoints/global/email/PatchEmailEndpoint.ts +6 -9
  72. package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +1 -1
  73. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +1 -1
  74. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +1 -1
  75. package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -2
  76. package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +1 -1
  77. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
  78. package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -3
  79. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +49 -4
  80. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +5 -8
  81. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +12 -44
  82. package/src/endpoints/global/events/PatchEventsEndpoint.ts +128 -127
  83. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -9
  84. package/src/endpoints/global/files/GetFileCache.ts +3 -4
  85. package/src/endpoints/global/files/UploadFile.ts +3 -4
  86. package/src/endpoints/global/files/UploadImage.ts +3 -4
  87. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +1 -1
  88. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +1 -1
  89. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +4 -2
  90. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +2 -3
  91. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +1 -1
  92. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +2 -2
  93. package/src/endpoints/global/members/GetMembersEndpoint.ts +10 -12
  94. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +195 -12
  95. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +16 -22
  96. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +6 -30
  97. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +1 -1
  98. package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +1 -1
  99. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +40 -8
  100. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +2 -2
  101. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -2
  102. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +4 -1
  103. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +4 -5
  104. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +9 -16
  105. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -1
  106. package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -1
  107. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +1 -1
  108. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -11
  109. package/src/endpoints/global/platform-memberships/GetPlatformMembershipsCountEndpoint.ts +31 -31
  110. package/src/endpoints/global/platform-memberships/GetPlatformMembershipsEndpoint.ts +18 -20
  111. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +1 -1
  112. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1 -1
  113. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +6 -10
  114. package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +35 -6
  115. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
  116. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +1 -1
  117. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +1 -1
  118. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
  119. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -3
  120. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +90 -6
  121. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +35 -34
  122. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +1 -1
  123. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +62 -63
  124. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +6 -9
  125. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +51 -52
  126. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +6 -7
  127. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsCountEndpoint.ts +43 -0
  128. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +1 -1
  129. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +13 -5
  130. package/src/endpoints/global/sso/GetSSOEndpoint.ts +2 -3
  131. package/src/endpoints/global/sso/SetSSOEndpoint.ts +1 -1
  132. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
  133. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +1 -2
  134. package/src/endpoints/global/webshops/GetWebshopsEndpoint.ts +2 -4
  135. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +2 -2
  136. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsCountEndpoint.ts +1 -1
  137. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.ts +3 -4
  138. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +7 -9
  139. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +73 -0
  140. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.ts +89 -0
  141. package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +1 -5
  142. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +11 -11
  143. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +2 -2
  144. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +8 -8
  145. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +9 -9
  146. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
  147. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableInvoicesEndpoint.ts +68 -0
  148. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +4 -4
  149. package/src/endpoints/organization/dashboard/billing/GetRegisterCodeEndpoint.ts +66 -0
  150. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.test.ts +949 -0
  151. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +60 -57
  152. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +20 -20
  153. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
  154. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +1 -1
  155. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +1 -1
  156. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
  157. package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +1 -1
  158. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +10 -2
  159. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +2 -2
  160. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.ts +2 -2
  161. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +1 -1
  162. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +1 -1
  163. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +10 -8
  164. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +1 -1
  165. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +5 -6
  166. package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +1 -1
  167. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +10 -6
  168. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +11 -2
  169. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +4 -5
  170. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +5 -5
  171. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +2 -2
  172. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +7 -7
  173. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +1 -1
  174. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +2 -2
  175. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +1 -1
  176. package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +1 -1
  177. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -2
  178. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +50 -12
  179. package/src/endpoints/organization/dashboard/organization/SearchUitpasOrganizersEndpoint.ts +2 -2
  180. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +5 -10
  181. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +1 -1
  182. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +1 -1
  183. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -4
  184. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +21 -14
  185. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +35 -28
  186. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +2 -3
  187. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesCountEndpoint.ts +1 -1
  188. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -1
  189. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
  190. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +2 -3
  191. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +1 -1
  192. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
  193. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +19 -18
  194. package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +4 -5
  195. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
  196. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -3
  197. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +2 -2
  198. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +1 -1
  199. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +2 -2
  200. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +1 -1
  201. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +1 -1
  202. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +1 -1
  203. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +1 -1
  204. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +1 -1
  205. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +4 -6
  206. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +10 -15
  207. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +1 -1
  208. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +2 -2
  209. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
  210. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +1 -1
  211. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
  212. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +11 -2
  213. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +4 -6
  214. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +22 -7
  215. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +20 -27
  216. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +1 -1
  217. package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +2 -2
  218. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +2 -3
  219. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +95 -0
  220. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -2
  221. package/src/endpoints/organization/shared/GetDocumentHtml.ts +1 -1
  222. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +1 -1
  223. package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +3 -5
  224. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +1 -1
  225. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +4 -1
  226. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +14 -3
  227. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +14 -2
  228. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +37 -16
  229. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +34 -2
  230. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +4 -2
  231. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +74 -3
  232. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +16 -33
  233. package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -5
  234. package/src/endpoints/organization/webshops/WebshopAuthHelper.ts +36 -0
  235. package/src/endpoints/system/HealthEndpoint.ts +1 -1
  236. package/src/excel-loaders/balance-items.ts +1 -1
  237. package/src/excel-loaders/event-notifications.ts +1 -1
  238. package/src/excel-loaders/members.ts +1 -1
  239. package/src/excel-loaders/organizations.ts +3 -3
  240. package/src/excel-loaders/payments.ts +31 -1
  241. package/src/excel-loaders/platform-memberships.ts +13 -14
  242. package/src/excel-loaders/receivable-balances.ts +2 -2
  243. package/src/excel-loaders/registrations.ts +2 -3
  244. package/src/helpers/AddressValidator.ts +7 -14
  245. package/src/helpers/AdminPermissionChecker.ts +50 -63
  246. package/src/helpers/AuthenticatedStructures.ts +69 -55
  247. package/src/helpers/BalanceItemRelationsBackfiller.test.ts +174 -0
  248. package/src/helpers/BalanceItemRelationsBackfiller.ts +163 -0
  249. package/src/helpers/BuckarooHelper.ts +2 -4
  250. package/src/helpers/CheckSettlements.ts +12 -24
  251. package/src/helpers/Context.ts +15 -10
  252. package/src/helpers/CookieHelper.ts +1 -2
  253. package/src/helpers/EmailResumer.ts +1 -2
  254. package/src/helpers/FileCache.ts +2 -3
  255. package/src/helpers/FinancialSupportHelper.ts +2 -2
  256. package/src/helpers/FlagMomentCleanup.ts +1 -2
  257. package/src/helpers/ForwardHandler.ts +5 -10
  258. package/src/helpers/GlobalHelper.ts +3 -3
  259. package/src/helpers/LegacyBillingConverter.ts +787 -0
  260. package/src/helpers/MemberMerger.test.ts +782 -0
  261. package/src/helpers/MemberMerger.ts +600 -0
  262. package/src/helpers/MemberUserSyncer.ts +6 -12
  263. package/src/helpers/MembershipCharger.ts +2 -4
  264. package/src/helpers/PaymentCustomerResolver.test.ts +203 -0
  265. package/src/helpers/PaymentCustomerResolver.ts +144 -0
  266. package/src/helpers/PeriodHelper.ts +2 -3
  267. package/src/helpers/RecordAnswerHelper.test.ts +105 -110
  268. package/src/helpers/RecordAnswerHelper.ts +7 -7
  269. package/src/helpers/SeedTools.ts +106 -0
  270. package/src/helpers/ServiceFeeHelper.ts +125 -15
  271. package/src/helpers/SetupStepUpdater.ts +38 -32
  272. package/src/helpers/StamhoofdFilterAccessHelper.ts +52 -0
  273. package/src/helpers/StripeHelper.ts +14 -36
  274. package/src/helpers/StripeInvoicer.ts +419 -0
  275. package/src/helpers/StripePayoutChecker.ts +5 -10
  276. package/src/helpers/TagHelper.ts +7 -8
  277. package/src/helpers/TemporaryMemberAccess.ts +1 -2
  278. package/src/helpers/ThrottledQueue.ts +1 -2
  279. package/src/helpers/UitpasTokenRepository.ts +1 -2
  280. package/src/helpers/ViesHelper.ts +7 -11
  281. package/src/helpers/XlsxTransformerColumnHelper.ts +1 -1
  282. package/src/helpers/outstandingBalanceJoin.ts +2 -2
  283. package/src/helpers/updateMemberDetailsUitpasNumber.ts +3 -5
  284. package/src/migrate.ts +12 -5
  285. package/src/{seeds → migrations}/0000000004-single-organization.ts +10 -1
  286. package/src/migrations/0000000006-default-organization.ts +139 -0
  287. package/src/{seeds → migrations}/1715028563-user-permissions.ts +17 -27
  288. package/src/{seeds/1765896674-document-update-year.test.ts → migrations/1750090029-document-update-year.test.ts} +27 -26
  289. package/src/migrations/1750090029-document-update-year.ts +103 -0
  290. package/src/{seeds → migrations}/1750090030-records-configuration.ts +33 -32
  291. package/src/migrations/1750090031-records-configuration-webshops.ts +36 -0
  292. package/src/migrations/1752848540-national-numbers.ts +585 -0
  293. package/src/migrations/1752848550-records-configuration-group-filter.ts +1064 -0
  294. package/src/{seeds → migrations}/1752848561-groups-registration-periods.ts +70 -26
  295. package/src/{seeds → migrations}/1754560914-groups-prices.test.ts +1 -1
  296. package/src/{seeds → migrations}/1754560914-groups-prices.ts +9 -15
  297. package/src/{seeds → migrations}/1756293699-fill-previous-next-period-id.ts +1 -2
  298. package/src/migrations/1761665607-sync-member-users.ts +40 -0
  299. package/src/migrations/1779121240-invoice-email-template.sql +6 -0
  300. package/src/migrations/1779986893-default-payment-failed-email-templates.sql +4 -0
  301. package/src/migrations/1780578166-convert-legacy-billing.ts +13 -0
  302. package/src/{seeds/1755876819-remove-duplicate-members.ts → migrations/1780665426-remove-duplicate-members.ts} +23 -4
  303. package/src/migrations/1780933598-document-groups.ts +88 -0
  304. package/src/migrations/1781692342-balance-item-relations.ts +13 -0
  305. package/src/migrations/1781857866-platform-admins.ts +103 -0
  306. package/src/migrations/1781857867-rm-old-platform-admins.ts +25 -0
  307. package/src/seeds/0000000001-throw.ts +5 -0
  308. package/src/seeds/1760702452-update-balance-price-paid.ts +19 -0
  309. package/src/seeds/1760702453-update-balance-invoiced.ts +19 -0
  310. package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +51 -14
  311. package/src/seeds/1773754928-force-save-members.ts +41 -8
  312. package/src/seeds/1780665427-schedule-stock-updates.ts +45 -0
  313. package/src/seeds/1780665428-group-update-occupancy.ts +31 -0
  314. package/src/seeds/1780915001-fill-payment-customer.ts +70 -0
  315. package/src/seeds/1780915002-update-orders.ts +39 -0
  316. package/src/seeds/1782047708-organization-company-address.ts +44 -0
  317. package/src/seeds-temporary/1769088653-uitpas-status.ts +1 -2
  318. package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.test.ts +69 -0
  319. package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.ts +85 -0
  320. package/src/services/AuditLogService.test.ts +51 -0
  321. package/src/services/AuditLogService.ts +45 -0
  322. package/src/services/BalanceItemService.ts +141 -44
  323. package/src/services/CpuService.ts +3 -6
  324. package/src/services/DatabaseCollationService.test.ts +1 -2
  325. package/src/services/DocumentService.ts +1 -2
  326. package/src/services/EventNotificationService.ts +1 -1
  327. package/src/services/FileSignService.ts +2 -4
  328. package/src/services/InvoicePdfService.ts +286 -0
  329. package/src/services/InvoiceService.ts +204 -251
  330. package/src/services/InvoiceXMLService.ts +424 -0
  331. package/src/services/MemberNumberService.ts +6 -11
  332. package/src/services/MemberRecordStore.ts +8 -10
  333. package/src/services/MollieService.ts +114 -121
  334. package/src/services/PaymentMandateService.ts +45 -45
  335. package/src/services/PaymentReallocationService.test.ts +1 -1
  336. package/src/services/PaymentReallocationService.ts +7 -9
  337. package/src/services/PaymentService.ts +444 -308
  338. package/src/services/PlatformMembershipService.ts +4 -8
  339. package/src/services/ReferralService.ts +223 -0
  340. package/src/services/RegistrationService.ts +63 -53
  341. package/src/services/SSOService.test.ts +119 -0
  342. package/src/services/SSOService.ts +56 -49
  343. package/src/services/STPackageService.ts +97 -61
  344. package/src/services/UniqueMemberNumberService.ts +69 -0
  345. package/src/services/UniqueUserService.ts +3 -6
  346. package/src/services/VATService.ts +85 -0
  347. package/src/services/uitpas/PassholderEndpoints.ts +3 -6
  348. package/src/services/uitpas/UitpasService.ts +6 -10
  349. package/src/services/uitpas/checkUitpasNumbers.ts +2 -4
  350. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +3 -6
  351. package/src/services/uitpas/searchUitpasEvents.ts +1 -2
  352. package/src/sql-filters/audit-logs.ts +1 -1
  353. package/src/sql-filters/balance-item-payments.ts +1 -1
  354. package/src/sql-filters/balance-items.ts +1 -1
  355. package/src/sql-filters/base-registration-filter-compilers.ts +5 -33
  356. package/src/sql-filters/document-templates.ts +1 -1
  357. package/src/sql-filters/documents.ts +1 -1
  358. package/src/sql-filters/email-recipients.ts +1 -1
  359. package/src/sql-filters/emails.ts +1 -1
  360. package/src/sql-filters/event-notifications.ts +1 -1
  361. package/src/sql-filters/events.ts +6 -1
  362. package/src/sql-filters/groups.ts +5 -0
  363. package/src/sql-filters/invoiced-balance-items.ts +1 -1
  364. package/src/sql-filters/invoices.ts +1 -1
  365. package/src/sql-filters/member-responsibility-records.ts +1 -1
  366. package/src/sql-filters/members.ts +1 -1
  367. package/src/sql-filters/orders.ts +4 -4
  368. package/src/sql-filters/organization-registration-periods.ts +1 -1
  369. package/src/sql-filters/organizations.ts +1 -1
  370. package/src/sql-filters/platform-memberships.ts +3 -3
  371. package/src/sql-filters/receivable-balances.ts +1 -1
  372. package/src/sql-filters/registration-periods.ts +1 -1
  373. package/src/sql-filters/registrations.ts +1 -1
  374. package/src/sql-filters/tickets.ts +1 -1
  375. package/src/sql-filters/users.ts +1 -1
  376. package/src/sql-sorters/groups.ts +23 -0
  377. package/src/sql-sorters/platform-memberships.ts +16 -16
  378. package/src/sql-sorters/registration-invitations.ts +2 -2
  379. package/tests/actions/patchOrganizationMember.ts +1 -1
  380. package/tests/actions/patchUserMember.ts +1 -1
  381. package/tests/assertions/assertBalances.ts +6 -12
  382. package/tests/e2e/api-rate-limits.test.ts +12 -24
  383. package/tests/e2e/bundle-discounts.test.ts +4 -1
  384. package/tests/e2e/charge-members.test.ts +13 -11
  385. package/tests/e2e/documents.test.ts +4 -1
  386. package/tests/e2e/register.test.ts +7 -9
  387. package/tests/e2e/stock.test.ts +4 -3
  388. package/tests/e2e/tickets.test.ts +1 -1
  389. package/tests/helpers/MollieMocker.ts +462 -0
  390. package/tests/helpers/PayconiqMocker.ts +138 -45
  391. package/tests/helpers/StripeMocker.ts +9 -6
  392. package/tests/helpers/index.ts +4 -0
  393. package/tests/init/initAdmin.ts +2 -2
  394. package/tests/init/initBundleDiscount.ts +1 -1
  395. package/tests/init/initMembershipOrganization.ts +13 -0
  396. package/tests/init/initPermissionRole.ts +3 -4
  397. package/tests/vitest.global.setup.ts +1 -1
  398. package/vitest.config.js +6 -0
  399. package/.nvmrc +0 -1
  400. package/src/helpers/GroupBuilder.ts +0 -418
  401. package/src/seeds/0000000000-example.ts +0 -11
  402. package/src/seeds/1722256498-group-update-occupancy.ts +0 -52
  403. package/src/seeds/1726572303-schedule-stock-updates.ts +0 -52
  404. package/src/seeds/1726847064-setup-steps.ts +0 -16
  405. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +0 -31
  406. package/src/seeds/1729253172-update-orders.ts +0 -21
  407. package/src/seeds/1733319079-fill-paying-organization-ids.ts +0 -68
  408. package/src/seeds/1733748412-update-deleted-order-numbers.ts +0 -109
  409. package/src/seeds/1733994455-balance-item-status-open.ts +0 -30
  410. package/src/seeds/1740046783-update-membership.ts +0 -17
  411. package/src/seeds/1741008870-fix-auditlog-description.ts +0 -50
  412. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +0 -88
  413. package/src/seeds/1741268179-fix-group-dates-of-events.ts +0 -32
  414. package/src/seeds/1751445358-upload-email-attachments.ts +0 -106
  415. package/src/seeds/1755532883-update-email-sender-ids.ts +0 -47
  416. package/src/seeds/1755790070-fill-email-recipient-errors.ts +0 -96
  417. package/src/seeds/1756115432-remove-old-drafts.ts +0 -16
  418. package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +0 -30
  419. package/src/seeds/1756303697-update-email-counts.ts +0 -76
  420. package/src/seeds/1761665607-sync-member-users.ts +0 -59
  421. package/src/seeds/1765896674-document-update-year.ts +0 -66
  422. /package/src/{seeds → migrations}/0000000001-development-user.ts +0 -0
  423. /package/src/{seeds → migrations}/0000000002-clear-stamhoofd-email-templates.ts +0 -0
  424. /package/src/{seeds → migrations}/0000000003-default-email-templates.ts +0 -0
  425. /package/src/{seeds → migrations}/1752848561-groups-registration-periods.test.ts +0 -0
  426. /package/src/{seeds → migrations}/1766150402-document-published-at.test.ts +0 -0
  427. /package/src/{seeds → migrations}/1766150402-document-published-at.ts +0 -0
  428. /package/src/{seeds → migrations}/1779121239-default-invoice-email-template.sql +0 -0
  429. /package/src/{seeds → migrations}/data/default-email-templates.sql +0 -0
@@ -0,0 +1,949 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import type { Token } from '@stamhoofd/models';
3
+ import { BalanceItem, GroupFactory, MemberFactory, Organization, OrganizationFactory, RegistrationFactory, STPackage, STPackageFactory } from '@stamhoofd/models';
4
+ import {
5
+ Address,
6
+ BalanceItemStatus,
7
+ BalanceItemType,
8
+ Company,
9
+ MollieOnboarding,
10
+ MollieStatus,
11
+ OrganizationCheckout,
12
+ PackagePurchases,
13
+ PaymentCustomer,
14
+ PaymentMethod,
15
+ STPackageBundle,
16
+ STPackageBundleHelper,
17
+ STPackageType,
18
+ VATExcemptReason,
19
+ } from '@stamhoofd/structures';
20
+ import type { PaymentMandate } from '@stamhoofd/structures/PaymentMandate.js';
21
+ import { CreateMandateSettings } from '@stamhoofd/structures/checkout/CreateMandateSettings.js';
22
+ import { TestUtils } from '@stamhoofd/test-utils';
23
+ import { Country } from '@stamhoofd/types/Country';
24
+ import { chargeMemberFeesForOrganization } from '../../../../crons/members-fees.js';
25
+ import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
26
+ import { STPackageService } from '../../../../services/STPackageService.js';
27
+ import { MollieMocker } from '../../../../../tests/helpers/MollieMocker.js';
28
+ import { testServer } from '../../../../../tests/helpers/TestServer.js';
29
+ import { initAdmin } from '../../../../../tests/init/index.js';
30
+ import { initMembershipOrganization } from '../../../../../tests/init/initMembershipOrganization.js';
31
+ import { OrganizationCheckoutEndpoint } from './OrganizationCheckoutEndpoint.js';
32
+
33
+ describe('Endpoint.OrganizationCheckoutEndpoint', () => {
34
+ const endpoint = new OrganizationCheckoutEndpoint();
35
+
36
+ // The membership organization is the seller. It is configured to receive Mollie payments and is
37
+ // located in Belgium with a valid VAT number (so reverse charged VAT can apply for foreign buyers).
38
+ let membershipOrganization: Organization;
39
+ let mollieMocker: MollieMocker;
40
+
41
+ const belgianAddress = () => Address.create({
42
+ street: 'Demostraat',
43
+ number: '12',
44
+ city: 'Gent',
45
+ postalCode: '9000',
46
+ country: Country.Belgium,
47
+ });
48
+
49
+ const dutchAddress = () => Address.create({
50
+ street: 'Demostraat',
51
+ number: '12',
52
+ city: 'Amsterdam',
53
+ postalCode: '1011 AA',
54
+ country: Country.Netherlands,
55
+ });
56
+
57
+ const post = async (sellingOrganizationId: string, body: OrganizationCheckout, organization: Organization, token: Token) => {
58
+ const request = Request.buildJson('POST', `/billing/${sellingOrganizationId}/checkout`, organization.getApiHost(), body);
59
+ request.headers.authorization = 'Bearer ' + token.accessToken;
60
+ return await testServer.test(endpoint, request);
61
+ };
62
+
63
+ /**
64
+ * Create a paying (buyer) organization + an admin with finance permissions.
65
+ * Optionally configures one billing company (otherwise the default company is used).
66
+ */
67
+ const createBuyer = async (company?: Company) => {
68
+ const organization = await new OrganizationFactory({}).create();
69
+
70
+ if (company) {
71
+ organization.meta.companies.push(company);
72
+ // Keep the organization address in sync with the company country (nicer/realistic)
73
+ organization.address = company.address ?? organization.address;
74
+ await organization.save();
75
+ }
76
+
77
+ const { adminToken } = await initAdmin({ organization });
78
+
79
+ // The customer.company that the checkout sends needs to equal a saved company (or the default one).
80
+ const customerCompany = company ?? organization.defaultCompanies[0];
81
+
82
+ return { organization, token: adminToken, customerCompany };
83
+ };
84
+
85
+ const buildCheckout = (options: {
86
+ bundles: STPackageBundle[];
87
+ customerCompany: Company;
88
+ createMandate?: boolean;
89
+ }) => {
90
+ return OrganizationCheckout.create({
91
+ purchases: PackagePurchases.create({
92
+ packageBundles: options.bundles,
93
+ }),
94
+ customer: PaymentCustomer.create({
95
+ firstName: 'Test',
96
+ lastName: 'Buyer',
97
+ email: 'buyer@example.com',
98
+ company: options.customerCompany,
99
+ }),
100
+ paymentMethod: options.createMandate ? PaymentMethod.CreditCard : null,
101
+ createMandate: options.createMandate ? CreateMandateSettings.create({ saveAsDefault: true }) : null,
102
+ redirectUrl: new URL('https://www.example.com/success'),
103
+ cancelUrl: new URL('https://www.example.com/cancel'),
104
+ });
105
+ };
106
+
107
+ const getPackage = async (organization: Organization, type: STPackageType) => {
108
+ const packages = await STPackage.select().where('organizationId', organization.id).fetch();
109
+ return packages.find(p => p.meta.type === type);
110
+ };
111
+
112
+ const getPackageBalanceItem = async (pack: STPackage) => {
113
+ const items = await BalanceItem.select().where('packageId', pack.id).fetch();
114
+ return items[0];
115
+ };
116
+
117
+ const getAdministrationFee = async (organization: Organization) => {
118
+ const items = await BalanceItem.select()
119
+ .where('payingOrganizationId', organization.id)
120
+ .where('type', BalanceItemType.AdministrationFee)
121
+ .fetch();
122
+ return items[0];
123
+ };
124
+
125
+ /**
126
+ * Re-fetch the organization so we read the freshly saved meta.packages
127
+ * (STPackageService.updateOrganizationPackages loads + saves a fresh model).
128
+ */
129
+ const getUpdatedPackages = async (organization: Organization) => {
130
+ const updated = await Organization.getByID(organization.id);
131
+ return updated!.meta.packages;
132
+ };
133
+
134
+ /**
135
+ * Register `count` active members (each in a non-waiting-list group, registered in the current
136
+ * period) so Organization.getActiveMembers() reports them when charging a per-member package.
137
+ */
138
+ const addActiveMembers = async (organization: Organization, count: number) => {
139
+ const group = await new GroupFactory({ organization }).create();
140
+ for (let i = 0; i < count; i++) {
141
+ const member = await new MemberFactory({ organization }).create();
142
+ await new RegistrationFactory({ member, group }).create();
143
+ }
144
+ };
145
+
146
+ beforeAll(async () => {
147
+ TestUtils.setEnvironment('userMode', 'organization');
148
+
149
+ mollieMocker = new MollieMocker();
150
+ mollieMocker.start();
151
+
152
+ membershipOrganization = await initMembershipOrganization();
153
+
154
+ // Make the seller a Belgian company with a valid VAT number
155
+ membershipOrganization.meta.companies = [
156
+ Company.create({
157
+ name: 'Stamhoofd',
158
+ companyNumber: '0700000000',
159
+ VATNumber: 'BE0700000000',
160
+ address: belgianAddress(),
161
+ }),
162
+ ];
163
+
164
+ // Configure Mollie so the seller can receive (mandate) payments
165
+ membershipOrganization.privateMeta.mollieOnboarding = MollieOnboarding.create({
166
+ canReceivePayments: true,
167
+ canReceiveSettlements: true,
168
+ status: MollieStatus.Completed,
169
+ });
170
+
171
+ const config = membershipOrganization.meta.registrationPaymentConfiguration;
172
+ for (const method of [PaymentMethod.CreditCard, PaymentMethod.Bancontact]) {
173
+ if (!config.paymentMethods.includes(method)) {
174
+ config.paymentMethods.push(method);
175
+ }
176
+ }
177
+ config.enableMandates = true;
178
+
179
+ await membershipOrganization.save();
180
+ await mollieMocker.setupToken(membershipOrganization);
181
+ });
182
+
183
+ afterAll(() => {
184
+ mollieMocker.stop();
185
+ });
186
+
187
+ beforeEach(() => {
188
+ TestUtils.setEnvironment('userMode', 'organization');
189
+ mollieMocker.reset();
190
+ });
191
+
192
+ afterEach(() => {
193
+ // Restore real timers in case a test froze the date (no-op otherwise)
194
+ vitest.useRealTimers();
195
+ });
196
+
197
+ /**
198
+ * The per-member price is prorated against the package's validity window, which depends on the
199
+ * current date (and whether the year-long window crosses a leap day). Freeze the date so the
200
+ * Members package always charges its full unit price (€1 / member) and the totals are deterministic.
201
+ */
202
+ const freezeDate = () => {
203
+ vitest.useFakeTimers({ shouldAdvanceTime: true, toFake: ['Date'] }).setSystemTime(new Date('2026-06-15T10:00:00.000Z'));
204
+ };
205
+ const MEMBER_UNIT_PRICE = 1_0000; // €1 per member (stored with 4 decimals)
206
+ const DAY = 1000 * 60 * 60 * 24;
207
+
208
+ /**
209
+ * Create an already-active package for the organization (validated in the past) with a custom
210
+ * validity window. Used to set up a "current" package that can be renewed.
211
+ */
212
+ const createActivePackage = async (organization: Organization, options: {
213
+ bundle: STPackageBundle;
214
+ validUntil: Date | null;
215
+ createdAt?: Date;
216
+ }) => {
217
+ const template = STPackageBundleHelper.getCurrentPackage(options.bundle, new Date(Date.now() - 300 * DAY));
218
+ const pack = new STPackage();
219
+ pack.organizationId = organization.id;
220
+ pack.meta = template.meta;
221
+ pack.validAt = new Date(Date.now() - 300 * DAY);
222
+ pack.validUntil = options.validUntil;
223
+ pack.removeAt = options.validUntil ? new Date(options.validUntil.getTime() + 31 * DAY) : null;
224
+ pack.createdAt = options.createdAt ?? new Date(Date.now() - 300 * DAY);
225
+ await pack.save();
226
+ await STPackageService.markValid(pack.id);
227
+ return (await STPackage.getByID(pack.id))!;
228
+ };
229
+
230
+ /**
231
+ * Simulate a previously saved (default) Mollie mandate for the buyer, and return it as a
232
+ * PaymentMandate the checkout can reference.
233
+ */
234
+ const setupExistingMandate = async (organization: Organization): Promise<PaymentMandate> => {
235
+ const customerId = mollieMocker.createId('cst');
236
+ mollieMocker.customers.push({ id: customerId });
237
+ const mandate = mollieMocker.addMandate({ customerId });
238
+
239
+ const fresh = (await Organization.getByID(organization.id))!;
240
+ fresh.serverMeta.mollieCustomerId = customerId;
241
+ fresh.serverMeta.mollieMandateId = mandate.id;
242
+ await fresh.save();
243
+
244
+ const mandates = await PaymentMandateService.getMandates({
245
+ payingOrganization: fresh,
246
+ sellingOrganization: (await Organization.getByID(membershipOrganization.id))!,
247
+ user: null,
248
+ });
249
+ const found = mandates.find(m => m.id === mandate.id);
250
+ if (!found) {
251
+ throw new Error('Expected mandate to be returned by getMandates');
252
+ }
253
+ return found;
254
+ };
255
+
256
+ /**
257
+ * Build a renewal checkout, either via explicit renew ids or via package bundles, and either
258
+ * paying with a new payment method (creating a mandate) or reusing an existing mandate.
259
+ */
260
+ const buildRenewalCheckout = (options: {
261
+ customerCompany: Company;
262
+ renewPackageIds?: string[];
263
+ bundles?: STPackageBundle[];
264
+ mandate?: PaymentMandate;
265
+ newPaymentMethod?: PaymentMethod;
266
+ }) => {
267
+ return OrganizationCheckout.create({
268
+ purchases: PackagePurchases.create({
269
+ renewPackageIds: options.renewPackageIds ?? [],
270
+ packageBundles: options.bundles ?? [],
271
+ }),
272
+ customer: PaymentCustomer.create({
273
+ firstName: 'Test',
274
+ lastName: 'Buyer',
275
+ email: 'buyer@example.com',
276
+ company: options.customerCompany,
277
+ }),
278
+ mandate: options.mandate ?? null,
279
+ paymentMethod: options.mandate ? null : (options.newPaymentMethod ?? null),
280
+ // Packages that require a mandate always need createMandate.saveAsDefault. When reusing an
281
+ // existing mandate it is passed alongside `mandate` and simply marks that mandate as default.
282
+ createMandate: CreateMandateSettings.create({ saveAsDefault: true }),
283
+ redirectUrl: new URL('https://www.example.com/success'),
284
+ cancelUrl: new URL('https://www.example.com/cancel'),
285
+ });
286
+ };
287
+
288
+ const getPackages = async (organization: Organization) => {
289
+ return await STPackage.select().where('organizationId', organization.id).fetch();
290
+ };
291
+
292
+ describe('Trials', () => {
293
+ test('Activating a trial works for an organization without any companies set (using the default company)', async () => {
294
+ const { organization, token, customerCompany } = await createBuyer();
295
+
296
+ const checkout = buildCheckout({
297
+ bundles: [STPackageBundle.TrialMembers],
298
+ customerCompany,
299
+ });
300
+
301
+ const response = await post(membershipOrganization.id, checkout, organization, token);
302
+ expect(response.status).toBe(200);
303
+
304
+ // A trial is free, so it is activated immediately (no online payment needed)
305
+ const pack = await getPackage(organization, STPackageType.TrialMembers);
306
+ expect(pack).toBeDefined();
307
+ expect(pack!.validAt).not.toBeNull();
308
+
309
+ // The organization now uses members, but only as a trial
310
+ const packages = await getUpdatedPackages(organization);
311
+ expect(packages.useMembers).toBe(true);
312
+ expect(packages.isMembersTrial).toBe(true);
313
+ expect(packages.useWebshops).toBe(false);
314
+ expect(packages.isWebshopsTrial).toBe(false);
315
+
316
+ // A trial is free
317
+ const balanceItem = await getPackageBalanceItem(pack!);
318
+ expect(balanceItem.unitPrice).toBe(0);
319
+ expect(balanceItem.priceWithVAT).toBe(0);
320
+
321
+ // No VAT reverse charge for a Belgian buyer
322
+ expect(balanceItem.VATExcempt).toBeNull();
323
+ });
324
+
325
+ test('Activating a trial works for a Dutch organization buying from the Belgian seller (both with a valid VAT number)', async () => {
326
+ const dutchCompany = Company.create({
327
+ name: 'Nederlandse vereniging',
328
+ companyNumber: '12345678',
329
+ VATNumber: 'NL123456789B01',
330
+ address: dutchAddress(),
331
+ });
332
+ const { organization, token, customerCompany } = await createBuyer(dutchCompany);
333
+
334
+ const checkout = buildCheckout({
335
+ bundles: [STPackageBundle.TrialMembers],
336
+ customerCompany,
337
+ });
338
+
339
+ const response = await post(membershipOrganization.id, checkout, organization, token);
340
+ expect(response.status).toBe(200);
341
+
342
+ const pack = await getPackage(organization, STPackageType.TrialMembers);
343
+ expect(pack).toBeDefined();
344
+ expect(pack!.validAt).not.toBeNull();
345
+
346
+ const packages = await getUpdatedPackages(organization);
347
+ expect(packages.useMembers).toBe(true);
348
+ expect(packages.isMembersTrial).toBe(true);
349
+ expect(packages.useWebshops).toBe(false);
350
+ expect(packages.isWebshopsTrial).toBe(false);
351
+
352
+ // A trial is free
353
+ const balanceItem = await getPackageBalanceItem(pack!);
354
+ expect(balanceItem.unitPrice).toBe(0);
355
+ expect(balanceItem.priceWithVAT).toBe(0);
356
+
357
+ // Intra community reverse charge applies (Dutch buyer, Belgian seller, both with VAT)
358
+ expect(balanceItem.VATExcempt).toBe(VATExcemptReason.IntraCommunityServices);
359
+ });
360
+ });
361
+
362
+ describe('Members package', () => {
363
+ test('Activating the members package charges all members of the organization (default company)', async () => {
364
+ freezeDate();
365
+ const { organization, token, customerCompany } = await createBuyer();
366
+ await addActiveMembers(organization, 10);
367
+
368
+ const checkout = buildCheckout({
369
+ bundles: [STPackageBundle.Members],
370
+ customerCompany,
371
+ createMandate: true,
372
+ });
373
+
374
+ const response = await post(membershipOrganization.id, checkout, organization, token);
375
+ expect(response.status).toBe(200);
376
+ expect(response.body.paymentUrl).not.toBeNull();
377
+
378
+ // Not valid yet, only after the (Mollie) payment succeeds
379
+ const pack = await getPackage(organization, STPackageType.Members);
380
+ expect(pack).toBeDefined();
381
+ expect(pack!.validAt).toBeNull();
382
+
383
+ await mollieMocker.succeedPayment();
384
+
385
+ await pack!.refresh();
386
+ expect(pack!.validAt).not.toBeNull();
387
+
388
+ // The organization now uses members as a full package (not a trial)
389
+ const packages = await getUpdatedPackages(organization);
390
+ expect(packages.useMembers).toBe(true);
391
+ expect(packages.isMembersTrial).toBe(false);
392
+ expect(packages.useWebshops).toBe(false);
393
+
394
+ // All 10 members are charged at €1 each
395
+ const balanceItem = await getPackageBalanceItem(pack!);
396
+ expect(balanceItem.quantity).toBe(10);
397
+ expect(balanceItem.unitPrice).toBe(MEMBER_UNIT_PRICE);
398
+ expect(balanceItem.priceWithoutVAT).toBe(10 * MEMBER_UNIT_PRICE); // €10
399
+
400
+ // Belgian buyer with no VAT number: 21% VAT is charged on top (no reverse charge)
401
+ expect(balanceItem.VATExcempt).toBeNull();
402
+ expect(balanceItem.VATPercentage).toBe(21);
403
+ expect(balanceItem.VATIncluded).toBe(false);
404
+ expect(balanceItem.VAT).toBe(2_1000); // 21% of €10 = €2.10
405
+ expect(balanceItem.priceWithVAT).toBe(12_1000); // €12.10
406
+ });
407
+
408
+ test('Activating the members package charges all members of the organization (Belgian VAT company)', async () => {
409
+ const belgianCompany = Company.create({
410
+ name: 'Belgische vereniging',
411
+ companyNumber: '0123456789',
412
+ VATNumber: 'BE0123456789',
413
+ address: belgianAddress(),
414
+ });
415
+ freezeDate();
416
+ const { organization, token, customerCompany } = await createBuyer(belgianCompany);
417
+ await addActiveMembers(organization, 10);
418
+
419
+ const checkout = buildCheckout({
420
+ bundles: [STPackageBundle.Members],
421
+ customerCompany,
422
+ createMandate: true,
423
+ });
424
+
425
+ const response = await post(membershipOrganization.id, checkout, organization, token);
426
+ expect(response.status).toBe(200);
427
+
428
+ await mollieMocker.succeedPayment();
429
+
430
+ const pack = await getPackage(organization, STPackageType.Members);
431
+ expect(pack).toBeDefined();
432
+ await pack!.refresh();
433
+ expect(pack!.validAt).not.toBeNull();
434
+
435
+ const packages = await getUpdatedPackages(organization);
436
+ expect(packages.useMembers).toBe(true);
437
+ expect(packages.isMembersTrial).toBe(false);
438
+ expect(packages.useWebshops).toBe(false);
439
+
440
+ // All 10 members are charged at €1 each
441
+ const balanceItem = await getPackageBalanceItem(pack!);
442
+ expect(balanceItem.quantity).toBe(10);
443
+ expect(balanceItem.unitPrice).toBe(MEMBER_UNIT_PRICE);
444
+ expect(balanceItem.priceWithoutVAT).toBe(10 * MEMBER_UNIT_PRICE); // €10
445
+
446
+ // Same country as the seller: 21% VAT is charged, no reverse charge
447
+ expect(balanceItem.VATExcempt).toBeNull();
448
+ expect(balanceItem.VATPercentage).toBe(21);
449
+ expect(balanceItem.VATIncluded).toBe(false);
450
+ expect(balanceItem.VAT).toBe(2_1000); // €2.10
451
+ expect(balanceItem.priceWithVAT).toBe(12_1000); // €12.10
452
+ });
453
+
454
+ test('Activating the members package charges all members of the organization (Dutch VAT company - no VAT)', async () => {
455
+ const dutchCompany = Company.create({
456
+ name: 'Nederlandse vereniging',
457
+ companyNumber: '12345678',
458
+ VATNumber: 'NL123456789B01',
459
+ address: dutchAddress(),
460
+ });
461
+ freezeDate();
462
+ const { organization, token, customerCompany } = await createBuyer(dutchCompany);
463
+ await addActiveMembers(organization, 10);
464
+
465
+ const checkout = buildCheckout({
466
+ bundles: [STPackageBundle.Members],
467
+ customerCompany,
468
+ createMandate: true,
469
+ });
470
+
471
+ const response = await post(membershipOrganization.id, checkout, organization, token);
472
+ expect(response.status).toBe(200);
473
+
474
+ await mollieMocker.succeedPayment();
475
+
476
+ const pack = await getPackage(organization, STPackageType.Members);
477
+ expect(pack).toBeDefined();
478
+ await pack!.refresh();
479
+ expect(pack!.validAt).not.toBeNull();
480
+
481
+ const packages = await getUpdatedPackages(organization);
482
+ expect(packages.useMembers).toBe(true);
483
+ expect(packages.isMembersTrial).toBe(false);
484
+ expect(packages.useWebshops).toBe(false);
485
+
486
+ // All 10 members are charged at €1 each
487
+ const balanceItem = await getPackageBalanceItem(pack!);
488
+ expect(balanceItem.quantity).toBe(10);
489
+ expect(balanceItem.unitPrice).toBe(MEMBER_UNIT_PRICE);
490
+ expect(balanceItem.priceWithoutVAT).toBe(10 * MEMBER_UNIT_PRICE); // €10
491
+
492
+ // Dutch buyer, Belgian seller, both with VAT: intra community reverse charge, no VAT charged
493
+ expect(balanceItem.VATExcempt).toBe(VATExcemptReason.IntraCommunityServices);
494
+ expect(balanceItem.VAT).toBe(0);
495
+ expect(balanceItem.priceWithVAT).toBe(10 * MEMBER_UNIT_PRICE); // €10, no VAT added
496
+ });
497
+ });
498
+
499
+ describe('Members package (edge cases)', () => {
500
+ test('Activating the members package with zero members charges the minimum of 1 member', async () => {
501
+ freezeDate();
502
+ const { organization, token, customerCompany } = await createBuyer();
503
+
504
+ // No members registered: the organization has zero active members
505
+ const checkout = buildCheckout({
506
+ bundles: [STPackageBundle.Members],
507
+ customerCompany,
508
+ createMandate: true,
509
+ });
510
+
511
+ const response = await post(membershipOrganization.id, checkout, organization, token);
512
+ expect(response.status).toBe(200);
513
+
514
+ await mollieMocker.succeedPayment();
515
+
516
+ const pack = await getPackage(organization, STPackageType.Members);
517
+ expect(pack).toBeDefined();
518
+ await pack!.refresh();
519
+ expect(pack!.validAt).not.toBeNull();
520
+
521
+ // We always charge at least 1 member (the minimum), even with zero active members
522
+ const balanceItem = await getPackageBalanceItem(pack!);
523
+ expect(balanceItem.quantity).toBe(1);
524
+ expect(balanceItem.unitPrice).toBe(MEMBER_UNIT_PRICE);
525
+ expect(balanceItem.priceWithoutVAT).toBe(MEMBER_UNIT_PRICE); // €1
526
+ // Belgian default company: 21% VAT on top
527
+ expect(balanceItem.VAT).toBe(2100); // 21% of €1 = €0.21
528
+ expect(balanceItem.priceWithVAT).toBe(1_2100); // €1.21
529
+ });
530
+ });
531
+
532
+ describe('Webshops package', () => {
533
+ test('Activating the webshops package works with the default company', async () => {
534
+ const { organization, token, customerCompany } = await createBuyer();
535
+
536
+ const checkout = buildCheckout({
537
+ bundles: [STPackageBundle.Webshops],
538
+ customerCompany,
539
+ createMandate: true,
540
+ });
541
+
542
+ const response = await post(membershipOrganization.id, checkout, organization, token);
543
+ expect(response.status).toBe(200);
544
+ expect(response.body.paymentUrl).not.toBeNull();
545
+
546
+ const pack = await getPackage(organization, STPackageType.Webshops);
547
+ expect(pack).toBeDefined();
548
+ expect(pack!.validAt).toBeNull();
549
+
550
+ await mollieMocker.succeedPayment();
551
+
552
+ await pack!.refresh();
553
+ expect(pack!.validAt).not.toBeNull();
554
+
555
+ // The organization now uses webshops as a full package (not a trial)
556
+ const packages = await getUpdatedPackages(organization);
557
+ expect(packages.useWebshops).toBe(true);
558
+ expect(packages.isWebshopsTrial).toBe(false);
559
+ expect(packages.useMembers).toBe(false);
560
+
561
+ // The webshops package itself is free (revenue comes from service fees per order)
562
+ const balanceItem = await getPackageBalanceItem(pack!);
563
+ expect(balanceItem.VATExcempt).toBeNull();
564
+ expect(balanceItem.unitPrice).toBe(0);
565
+ expect(balanceItem.priceWithVAT).toBe(0);
566
+
567
+ // But a €2 minimum is charged to set up the payment mandate
568
+ const adminFee = await getAdministrationFee(organization);
569
+ expect(adminFee).toBeDefined();
570
+ expect(adminFee.priceWithVAT).toBe(2_00);
571
+ });
572
+
573
+ test('Activating the webshops package works with a Belgian VAT company', async () => {
574
+ const belgianCompany = Company.create({
575
+ name: 'Belgische vereniging',
576
+ companyNumber: '0123456789',
577
+ VATNumber: 'BE0123456789',
578
+ address: belgianAddress(),
579
+ });
580
+ const { organization, token, customerCompany } = await createBuyer(belgianCompany);
581
+
582
+ const checkout = buildCheckout({
583
+ bundles: [STPackageBundle.Webshops],
584
+ customerCompany,
585
+ createMandate: true,
586
+ });
587
+
588
+ const response = await post(membershipOrganization.id, checkout, organization, token);
589
+ expect(response.status).toBe(200);
590
+
591
+ await mollieMocker.succeedPayment();
592
+
593
+ const pack = await getPackage(organization, STPackageType.Webshops);
594
+ expect(pack).toBeDefined();
595
+ await pack!.refresh();
596
+ expect(pack!.validAt).not.toBeNull();
597
+
598
+ const packages = await getUpdatedPackages(organization);
599
+ expect(packages.useWebshops).toBe(true);
600
+ expect(packages.isWebshopsTrial).toBe(false);
601
+ expect(packages.useMembers).toBe(false);
602
+
603
+ // The webshops package itself is free
604
+ const balanceItem = await getPackageBalanceItem(pack!);
605
+ expect(balanceItem.VATExcempt).toBeNull();
606
+ expect(balanceItem.unitPrice).toBe(0);
607
+ expect(balanceItem.priceWithVAT).toBe(0);
608
+ });
609
+
610
+ test('Activating the webshops package works with a Dutch VAT company - no VAT is charged', async () => {
611
+ const dutchCompany = Company.create({
612
+ name: 'Nederlandse vereniging',
613
+ companyNumber: '12345678',
614
+ VATNumber: 'NL123456789B01',
615
+ address: dutchAddress(),
616
+ });
617
+ const { organization, token, customerCompany } = await createBuyer(dutchCompany);
618
+
619
+ const checkout = buildCheckout({
620
+ bundles: [STPackageBundle.Webshops],
621
+ customerCompany,
622
+ createMandate: true,
623
+ });
624
+
625
+ const response = await post(membershipOrganization.id, checkout, organization, token);
626
+ expect(response.status).toBe(200);
627
+
628
+ await mollieMocker.succeedPayment();
629
+
630
+ const pack = await getPackage(organization, STPackageType.Webshops);
631
+ expect(pack).toBeDefined();
632
+ await pack!.refresh();
633
+ expect(pack!.validAt).not.toBeNull();
634
+
635
+ const packages = await getUpdatedPackages(organization);
636
+ expect(packages.useWebshops).toBe(true);
637
+ expect(packages.isWebshopsTrial).toBe(false);
638
+ expect(packages.useMembers).toBe(false);
639
+
640
+ // Reverse charge: no VAT is charged on the package balance item (which is free anyway)
641
+ const balanceItem = await getPackageBalanceItem(pack!);
642
+ expect(balanceItem.VATExcempt).toBe(VATExcemptReason.IntraCommunityServices);
643
+ expect(balanceItem.unitPrice).toBe(0);
644
+ expect(balanceItem.priceWithVAT).toBe(0);
645
+ });
646
+
647
+ test('Activating the webshops package ends an existing webshops trial', async () => {
648
+ const { organization, token, customerCompany } = await createBuyer();
649
+
650
+ // The organization already runs an active webshops trial
651
+ const trial = await new STPackageFactory({
652
+ organization,
653
+ bundle: STPackageBundle.TrialWebshops,
654
+ }).create();
655
+
656
+ // Sanity check: the trial is active before checkout
657
+ const activeBefore = await STPackageService.getActivePackages(organization.id);
658
+ expect(activeBefore.map(p => p.meta.type)).toContain(STPackageType.TrialWebshops);
659
+
660
+ const checkout = buildCheckout({
661
+ bundles: [STPackageBundle.Webshops],
662
+ customerCompany,
663
+ createMandate: true,
664
+ });
665
+
666
+ const response = await post(membershipOrganization.id, checkout, organization, token);
667
+ expect(response.status).toBe(200);
668
+
669
+ await mollieMocker.succeedPayment();
670
+
671
+ const pack = await getPackage(organization, STPackageType.Webshops);
672
+ expect(pack).toBeDefined();
673
+ await pack!.refresh();
674
+ expect(pack!.validAt).not.toBeNull();
675
+
676
+ // The trial has been ended (removeAt set in the past)
677
+ await trial.refresh();
678
+ expect(trial.removeAt).not.toBeNull();
679
+ expect(trial.removeAt!.getTime()).toBeLessThanOrEqual(Date.now());
680
+
681
+ // The trial is no longer active, but the full webshops package is.
682
+ // (meta.packages cannot distinguish this on its own: isWebshopsTrial is false because a
683
+ // full package is active, so we verify the trial end via the active packages directly.)
684
+ const activeAfter = await STPackageService.getActivePackages(organization.id);
685
+ const activeTypes = activeAfter.map(p => p.meta.type);
686
+ expect(activeTypes).toContain(STPackageType.Webshops);
687
+ expect(activeTypes).not.toContain(STPackageType.TrialWebshops);
688
+
689
+ const packages = await getUpdatedPackages(organization);
690
+ expect(packages.useWebshops).toBe(true);
691
+ expect(packages.isWebshopsTrial).toBe(false);
692
+ });
693
+ });
694
+
695
+ describe('Renewing packages', () => {
696
+ // Assert the renewed package starts when the current one ends (not now).
697
+ const expectStartsAtEndOfCurrent = (renewed: STPackage, current: STPackage) => {
698
+ expect(renewed.meta.startDate.getTime()).toBeGreaterThan(Date.now() + 20 * DAY);
699
+ expect(Math.abs(renewed.meta.startDate.getTime() - current.validUntil!.getTime())).toBeLessThan(DAY);
700
+ };
701
+
702
+ const findRenewed = (packages: STPackage[], type: STPackageType, excludeId: string) => {
703
+ return packages.find(p => p.meta.type === type && p.id !== excludeId);
704
+ };
705
+
706
+ test('Members renewal via renew ids, paying with a new payment method (Bancontact)', async () => {
707
+ freezeDate();
708
+ const { organization, token, customerCompany } = await createBuyer();
709
+
710
+ // A members package that is about to expire (so a renewal stays within the 2 year limit)
711
+ const current = await createActivePackage(organization, {
712
+ bundle: STPackageBundle.Members,
713
+ validUntil: new Date(Date.now() + 30 * DAY),
714
+ });
715
+
716
+ const checkout = buildRenewalCheckout({
717
+ customerCompany,
718
+ renewPackageIds: [current.id],
719
+ newPaymentMethod: PaymentMethod.Bancontact,
720
+ });
721
+
722
+ const response = await post(membershipOrganization.id, checkout, organization, token);
723
+ expect(response.status).toBe(200);
724
+ expect(response.body.paymentUrl).not.toBeNull();
725
+
726
+ await mollieMocker.succeedPayment();
727
+
728
+ const renewed = findRenewed(await getPackages(organization), STPackageType.Members, current.id)!;
729
+ expect(renewed).toBeDefined();
730
+ await renewed.refresh();
731
+ expect(renewed.validAt).not.toBeNull();
732
+ expectStartsAtEndOfCurrent(renewed, current);
733
+ });
734
+
735
+ test('Members renewal via renew ids, paying with an existing mandate', async () => {
736
+ freezeDate();
737
+ const { organization, token, customerCompany } = await createBuyer();
738
+ const mandate = await setupExistingMandate(organization);
739
+
740
+ const current = await createActivePackage(organization, {
741
+ bundle: STPackageBundle.Members,
742
+ validUntil: new Date(Date.now() + 30 * DAY),
743
+ });
744
+
745
+ const checkout = buildRenewalCheckout({
746
+ customerCompany,
747
+ renewPackageIds: [current.id],
748
+ mandate,
749
+ });
750
+
751
+ const response = await post(membershipOrganization.id, checkout, organization, token);
752
+ expect(response.status).toBe(200);
753
+
754
+ // A recurring Mollie payment is created against the existing mandate
755
+ await mollieMocker.succeedPayment();
756
+
757
+ const renewed = findRenewed(await getPackages(organization), STPackageType.Members, current.id)!;
758
+ expect(renewed).toBeDefined();
759
+ await renewed.refresh();
760
+ expect(renewed.validAt).not.toBeNull();
761
+ expectStartsAtEndOfCurrent(renewed, current);
762
+ });
763
+
764
+ test('Members renewal via package bundles starts at the end of the current package (existing mandate)', async () => {
765
+ freezeDate();
766
+ const { organization, token, customerCompany } = await createBuyer();
767
+ const mandate = await setupExistingMandate(organization);
768
+
769
+ const current = await createActivePackage(organization, {
770
+ bundle: STPackageBundle.Members,
771
+ validUntil: new Date(Date.now() + 30 * DAY),
772
+ });
773
+
774
+ const checkout = buildRenewalCheckout({
775
+ customerCompany,
776
+ bundles: [STPackageBundle.Members],
777
+ mandate,
778
+ });
779
+
780
+ const response = await post(membershipOrganization.id, checkout, organization, token);
781
+ expect(response.status).toBe(200);
782
+
783
+ await mollieMocker.succeedPayment();
784
+
785
+ const renewed = findRenewed(await getPackages(organization), STPackageType.Members, current.id)!;
786
+ expect(renewed).toBeDefined();
787
+ await renewed.refresh();
788
+ expect(renewed.validAt).not.toBeNull();
789
+ // Dates start from the end of the current package, not from now
790
+ expectStartsAtEndOfCurrent(renewed, current);
791
+ });
792
+
793
+ test('Members renewal via package bundles starts at the end of the current package (new Bancontact)', async () => {
794
+ freezeDate();
795
+ const { organization, token, customerCompany } = await createBuyer();
796
+
797
+ const current = await createActivePackage(organization, {
798
+ bundle: STPackageBundle.Members,
799
+ validUntil: new Date(Date.now() + 30 * DAY),
800
+ });
801
+
802
+ const checkout = buildRenewalCheckout({
803
+ customerCompany,
804
+ bundles: [STPackageBundle.Members],
805
+ newPaymentMethod: PaymentMethod.Bancontact,
806
+ });
807
+
808
+ const response = await post(membershipOrganization.id, checkout, organization, token);
809
+ expect(response.status).toBe(200);
810
+
811
+ await mollieMocker.succeedPayment();
812
+
813
+ const renewed = findRenewed(await getPackages(organization), STPackageType.Members, current.id)!;
814
+ expect(renewed).toBeDefined();
815
+ await renewed.refresh();
816
+ expect(renewed.validAt).not.toBeNull();
817
+ expectStartsAtEndOfCurrent(renewed, current);
818
+ });
819
+
820
+ test('Webshops renewal via package bundles starts at the end of the current package (existing mandate)', async () => {
821
+ freezeDate();
822
+ const { organization, token, customerCompany } = await createBuyer();
823
+ const mandate = await setupExistingMandate(organization);
824
+
825
+ // An (exceptionally) expiring webshops package, so a new one can stack after it
826
+ const current = await createActivePackage(organization, {
827
+ bundle: STPackageBundle.Webshops,
828
+ validUntil: new Date(Date.now() + 30 * DAY),
829
+ });
830
+
831
+ const checkout = buildRenewalCheckout({
832
+ customerCompany,
833
+ bundles: [STPackageBundle.Webshops],
834
+ mandate,
835
+ });
836
+
837
+ const response = await post(membershipOrganization.id, checkout, organization, token);
838
+ expect(response.status).toBe(200);
839
+
840
+ // The webshops package itself is free; paying with an existing mandate settles immediately
841
+ const renewed = findRenewed(await getPackages(organization), STPackageType.Webshops, current.id)!;
842
+ expect(renewed).toBeDefined();
843
+ await renewed.refresh();
844
+ expect(renewed.validAt).not.toBeNull();
845
+ expect(renewed.validUntil).toBeNull(); // a fresh webshops package is non-expiring
846
+ expectStartsAtEndOfCurrent(renewed, current);
847
+ });
848
+
849
+ test('Webshops renewal via package bundles starts at the end of the current package (new Bancontact)', async () => {
850
+ freezeDate();
851
+ const { organization, token, customerCompany } = await createBuyer();
852
+
853
+ const current = await createActivePackage(organization, {
854
+ bundle: STPackageBundle.Webshops,
855
+ validUntil: new Date(Date.now() + 30 * DAY),
856
+ });
857
+
858
+ const checkout = buildRenewalCheckout({
859
+ customerCompany,
860
+ bundles: [STPackageBundle.Webshops],
861
+ newPaymentMethod: PaymentMethod.Bancontact,
862
+ });
863
+
864
+ const response = await post(membershipOrganization.id, checkout, organization, token);
865
+ expect(response.status).toBe(200);
866
+
867
+ // A €2 minimum is charged to set up the new mandate
868
+ await mollieMocker.succeedPayment();
869
+
870
+ const renewed = findRenewed(await getPackages(organization), STPackageType.Webshops, current.id)!;
871
+ expect(renewed).toBeDefined();
872
+ await renewed.refresh();
873
+ expect(renewed.validAt).not.toBeNull();
874
+ expectStartsAtEndOfCurrent(renewed, current);
875
+
876
+ const adminFee = await getAdministrationFee(organization);
877
+ expect(adminFee).toBeDefined();
878
+ expect(adminFee.priceWithVAT).toBe(2_00);
879
+ });
880
+ });
881
+
882
+ describe('Members fees cron (charging extra members)', () => {
883
+ test('Members who register after activation are charged automatically', async () => {
884
+ freezeDate();
885
+ const { organization, token, customerCompany } = await createBuyer();
886
+ await addActiveMembers(organization, 10);
887
+
888
+ // Activate the members package: the initial 10 members are charged
889
+ const checkout = buildCheckout({
890
+ bundles: [STPackageBundle.Members],
891
+ customerCompany,
892
+ createMandate: true,
893
+ });
894
+ const response = await post(membershipOrganization.id, checkout, organization, token);
895
+ expect(response.status).toBe(200);
896
+ await mollieMocker.succeedPayment();
897
+
898
+ const pack = (await getPackage(organization, STPackageType.Members))!;
899
+ await pack.refresh();
900
+ expect(pack.validAt).not.toBeNull();
901
+
902
+ // The first charge covered exactly 10 members and is now paid (Due)
903
+ const initialItem = await getPackageBalanceItem(pack);
904
+ expect(initialItem.quantity).toBe(10);
905
+ expect(initialItem.status).toBe(BalanceItemStatus.Due);
906
+
907
+ // Pretend the package was bought a while ago (otherwise the cron treats the charge as
908
+ // not-yet-settled and would recharge everything).
909
+ pack.createdAt = new Date(Date.now() - 2 * DAY);
910
+ await pack.save();
911
+
912
+ // 5 more members register
913
+ await addActiveMembers(organization, 5);
914
+
915
+ // Run the nightly cron logic for this organization
916
+ await chargeMemberFeesForOrganization(
917
+ (await Organization.getByID(organization.id))!,
918
+ (await Organization.getByID(membershipOrganization.id))!,
919
+ );
920
+
921
+ // A new balance item charges only the 5 extra members
922
+ const items = await BalanceItem.select()
923
+ .where('packageId', pack.id)
924
+ .where('status', BalanceItemStatus.Due)
925
+ .fetch();
926
+ const extraItem = items.find(i => i.id !== initialItem.id);
927
+ expect(extraItem).toBeDefined();
928
+ expect(extraItem!.quantity).toBe(5);
929
+ expect(extraItem!.unitPrice).toBe(MEMBER_UNIT_PRICE);
930
+ expect(extraItem!.priceWithoutVAT).toBe(5 * MEMBER_UNIT_PRICE); // €5
931
+ expect(extraItem!.VATPercentage).toBe(21);
932
+
933
+ // The total charged quantity now matches the 15 active members
934
+ const totalCharged = items.reduce((sum, i) => sum + i.quantity, 0);
935
+ expect(totalCharged).toBe(15);
936
+
937
+ // Running the cron again does not double-charge (everyone is already paid/pending)
938
+ await chargeMemberFeesForOrganization(
939
+ (await Organization.getByID(organization.id))!,
940
+ (await Organization.getByID(membershipOrganization.id))!,
941
+ );
942
+ const itemsAfter = await BalanceItem.select()
943
+ .where('packageId', pack.id)
944
+ .where('status', BalanceItemStatus.Due)
945
+ .fetch();
946
+ expect(itemsAfter.reduce((sum, i) => sum + i.quantity, 0)).toBe(15);
947
+ });
948
+ });
949
+ });