@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.
- package/output-2.txt +1 -0
- package/output.txt +1 -0
- package/package.json +69 -52
- package/src/audit-logs/EventNotificationLogger.ts +40 -0
- package/src/audit-logs/ModelLogger.ts +19 -15
- package/src/audit-logs/OrganizationLogger.ts +1 -1
- package/src/audit-logs/RegistrationInvitationLogger.ts +24 -24
- package/src/audit-logs/init.ts +8 -2
- package/src/boot.ts +19 -18
- package/src/crons/amazon-ses.ts +13 -26
- package/src/crons/balance-emails.ts +3 -5
- package/src/crons/clearExcelCache.test.ts +1 -1
- package/src/crons/clearExcelCache.ts +1 -2
- package/src/crons/drip-emails.ts +42 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +4 -0
- package/src/crons/helpers/isOutside.ts +11 -0
- package/src/crons/helpers/useSavedIterator.ts +32 -0
- package/src/crons/index.ts +5 -0
- package/src/crons/invoices.ts +32 -38
- package/src/crons/members-fees.ts +50 -0
- package/src/crons/mollie-chargebacks.ts +14 -15
- package/src/crons/service-fees.ts +152 -0
- package/src/crons/stripe-invoices.ts +48 -0
- package/src/crons/transfer-fees.ts +176 -0
- package/src/crons/update-cached-balances.ts +4 -4
- package/src/crons.ts +7 -64
- package/src/debug.ts +4 -2
- package/src/email-recipient-loaders/documents.ts +1 -1
- package/src/email-recipient-loaders/members.ts +1 -1
- package/src/email-recipient-loaders/payments.ts +4 -6
- package/src/email-recipient-loaders/registrations.ts +1 -1
- package/src/email-replacements/getEmailReplacementsForPayment.ts +5 -9
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +1 -1
- package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +1 -1
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +3 -5
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +13 -7
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +4 -6
- package/src/endpoints/admin/organizations/PatchPackagesEndpoint.test.ts +10 -6
- package/src/endpoints/admin/organizations/PatchPackagesEndpoint.ts +39 -21
- package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +1 -1
- package/src/endpoints/auth/CreateAdminEndpoint.ts +23 -5
- package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/DeleteUserEndpoint.ts +3 -3
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +1 -1
- package/src/endpoints/auth/GetUserEndpoint.ts +1 -1
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +1 -1
- package/src/endpoints/auth/PatchUserEndpoint.ts +5 -8
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/SignupEndpoint.ts +2 -3
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +20 -3
- package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +2 -1
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +2 -4
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +1 -1
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +5 -7
- package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +2 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +3 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +1 -2
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +4 -6
- package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +1 -1
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +6 -10
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +4 -7
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +6 -9
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +1 -1
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +1 -1
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -2
- package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +1 -1
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -3
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +49 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +5 -8
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +12 -44
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +128 -127
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -9
- package/src/endpoints/global/files/GetFileCache.ts +3 -4
- package/src/endpoints/global/files/UploadFile.ts +3 -4
- package/src/endpoints/global/files/UploadImage.ts +3 -4
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +1 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +1 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +4 -2
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +2 -3
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +1 -1
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +2 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +10 -12
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +195 -12
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +16 -22
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +6 -30
- package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +1 -1
- package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +1 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +40 -8
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +2 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -2
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +4 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +4 -5
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +9 -16
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -1
- package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -11
- package/src/endpoints/global/platform-memberships/GetPlatformMembershipsCountEndpoint.ts +31 -31
- package/src/endpoints/global/platform-memberships/GetPlatformMembershipsEndpoint.ts +18 -20
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +1 -1
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1 -1
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +6 -10
- package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +35 -6
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +1 -1
- package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +1 -1
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +90 -6
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +35 -34
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +1 -1
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +62 -63
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +6 -9
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +51 -52
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +6 -7
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +13 -5
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +2 -3
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +1 -1
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +1 -2
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.ts +2 -4
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +7 -9
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +73 -0
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.ts +89 -0
- package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +1 -5
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +11 -11
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +9 -9
- package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableInvoicesEndpoint.ts +68 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/billing/GetRegisterCodeEndpoint.ts +66 -0
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.test.ts +949 -0
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +60 -57
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +20 -20
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +10 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +10 -8
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +5 -6
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +11 -2
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +4 -5
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +7 -7
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +50 -12
- package/src/endpoints/organization/dashboard/organization/SearchUitpasOrganizersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +5 -10
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +21 -14
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +35 -28
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +19 -18
- package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +4 -5
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +4 -6
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +10 -15
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +11 -2
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +4 -6
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +22 -7
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +20 -27
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +95 -0
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -2
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +1 -1
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +3 -5
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +4 -1
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +14 -3
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +14 -2
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +37 -16
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +34 -2
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +4 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +74 -3
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +16 -33
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -5
- package/src/endpoints/organization/webshops/WebshopAuthHelper.ts +36 -0
- package/src/endpoints/system/HealthEndpoint.ts +1 -1
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +1 -1
- package/src/excel-loaders/members.ts +1 -1
- package/src/excel-loaders/organizations.ts +3 -3
- package/src/excel-loaders/payments.ts +31 -1
- package/src/excel-loaders/platform-memberships.ts +13 -14
- package/src/excel-loaders/receivable-balances.ts +2 -2
- package/src/excel-loaders/registrations.ts +2 -3
- package/src/helpers/AddressValidator.ts +7 -14
- package/src/helpers/AdminPermissionChecker.ts +50 -63
- package/src/helpers/AuthenticatedStructures.ts +69 -55
- package/src/helpers/BalanceItemRelationsBackfiller.test.ts +174 -0
- package/src/helpers/BalanceItemRelationsBackfiller.ts +163 -0
- package/src/helpers/BuckarooHelper.ts +2 -4
- package/src/helpers/CheckSettlements.ts +12 -24
- package/src/helpers/Context.ts +15 -10
- package/src/helpers/CookieHelper.ts +1 -2
- package/src/helpers/EmailResumer.ts +1 -2
- package/src/helpers/FileCache.ts +2 -3
- package/src/helpers/FinancialSupportHelper.ts +2 -2
- package/src/helpers/FlagMomentCleanup.ts +1 -2
- package/src/helpers/ForwardHandler.ts +5 -10
- package/src/helpers/GlobalHelper.ts +3 -3
- package/src/helpers/LegacyBillingConverter.ts +787 -0
- package/src/helpers/MemberMerger.test.ts +782 -0
- package/src/helpers/MemberMerger.ts +600 -0
- package/src/helpers/MemberUserSyncer.ts +6 -12
- package/src/helpers/MembershipCharger.ts +2 -4
- package/src/helpers/PaymentCustomerResolver.test.ts +203 -0
- package/src/helpers/PaymentCustomerResolver.ts +144 -0
- package/src/helpers/PeriodHelper.ts +2 -3
- package/src/helpers/RecordAnswerHelper.test.ts +105 -110
- package/src/helpers/RecordAnswerHelper.ts +7 -7
- package/src/helpers/SeedTools.ts +106 -0
- package/src/helpers/ServiceFeeHelper.ts +125 -15
- package/src/helpers/SetupStepUpdater.ts +38 -32
- package/src/helpers/StamhoofdFilterAccessHelper.ts +52 -0
- package/src/helpers/StripeHelper.ts +14 -36
- package/src/helpers/StripeInvoicer.ts +419 -0
- package/src/helpers/StripePayoutChecker.ts +5 -10
- package/src/helpers/TagHelper.ts +7 -8
- package/src/helpers/TemporaryMemberAccess.ts +1 -2
- package/src/helpers/ThrottledQueue.ts +1 -2
- package/src/helpers/UitpasTokenRepository.ts +1 -2
- package/src/helpers/ViesHelper.ts +7 -11
- package/src/helpers/XlsxTransformerColumnHelper.ts +1 -1
- package/src/helpers/outstandingBalanceJoin.ts +2 -2
- package/src/helpers/updateMemberDetailsUitpasNumber.ts +3 -5
- package/src/migrate.ts +12 -5
- package/src/{seeds → migrations}/0000000004-single-organization.ts +10 -1
- package/src/migrations/0000000006-default-organization.ts +139 -0
- package/src/{seeds → migrations}/1715028563-user-permissions.ts +17 -27
- package/src/{seeds/1765896674-document-update-year.test.ts → migrations/1750090029-document-update-year.test.ts} +27 -26
- package/src/migrations/1750090029-document-update-year.ts +103 -0
- package/src/{seeds → migrations}/1750090030-records-configuration.ts +33 -32
- package/src/migrations/1750090031-records-configuration-webshops.ts +36 -0
- package/src/migrations/1752848540-national-numbers.ts +585 -0
- package/src/migrations/1752848550-records-configuration-group-filter.ts +1064 -0
- package/src/{seeds → migrations}/1752848561-groups-registration-periods.ts +70 -26
- package/src/{seeds → migrations}/1754560914-groups-prices.test.ts +1 -1
- package/src/{seeds → migrations}/1754560914-groups-prices.ts +9 -15
- package/src/{seeds → migrations}/1756293699-fill-previous-next-period-id.ts +1 -2
- package/src/migrations/1761665607-sync-member-users.ts +40 -0
- package/src/migrations/1779121240-invoice-email-template.sql +6 -0
- package/src/migrations/1779986893-default-payment-failed-email-templates.sql +4 -0
- package/src/migrations/1780578166-convert-legacy-billing.ts +13 -0
- package/src/{seeds/1755876819-remove-duplicate-members.ts → migrations/1780665426-remove-duplicate-members.ts} +23 -4
- package/src/migrations/1780933598-document-groups.ts +88 -0
- package/src/migrations/1781692342-balance-item-relations.ts +13 -0
- package/src/migrations/1781857866-platform-admins.ts +103 -0
- package/src/migrations/1781857867-rm-old-platform-admins.ts +25 -0
- package/src/seeds/0000000001-throw.ts +5 -0
- package/src/seeds/1760702452-update-balance-price-paid.ts +19 -0
- package/src/seeds/1760702453-update-balance-invoiced.ts +19 -0
- package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +51 -14
- package/src/seeds/1773754928-force-save-members.ts +41 -8
- package/src/seeds/1780665427-schedule-stock-updates.ts +45 -0
- package/src/seeds/1780665428-group-update-occupancy.ts +31 -0
- package/src/seeds/1780915001-fill-payment-customer.ts +70 -0
- package/src/seeds/1780915002-update-orders.ts +39 -0
- package/src/seeds/1782047708-organization-company-address.ts +44 -0
- package/src/seeds-temporary/1769088653-uitpas-status.ts +1 -2
- package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.test.ts +69 -0
- package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.ts +85 -0
- package/src/services/AuditLogService.test.ts +51 -0
- package/src/services/AuditLogService.ts +45 -0
- package/src/services/BalanceItemService.ts +141 -44
- package/src/services/CpuService.ts +3 -6
- package/src/services/DatabaseCollationService.test.ts +1 -2
- package/src/services/DocumentService.ts +1 -2
- package/src/services/EventNotificationService.ts +1 -1
- package/src/services/FileSignService.ts +2 -4
- package/src/services/InvoicePdfService.ts +286 -0
- package/src/services/InvoiceService.ts +204 -251
- package/src/services/InvoiceXMLService.ts +424 -0
- package/src/services/MemberNumberService.ts +6 -11
- package/src/services/MemberRecordStore.ts +8 -10
- package/src/services/MollieService.ts +114 -121
- package/src/services/PaymentMandateService.ts +45 -45
- package/src/services/PaymentReallocationService.test.ts +1 -1
- package/src/services/PaymentReallocationService.ts +7 -9
- package/src/services/PaymentService.ts +444 -308
- package/src/services/PlatformMembershipService.ts +4 -8
- package/src/services/ReferralService.ts +223 -0
- package/src/services/RegistrationService.ts +63 -53
- package/src/services/SSOService.test.ts +119 -0
- package/src/services/SSOService.ts +56 -49
- package/src/services/STPackageService.ts +97 -61
- package/src/services/UniqueMemberNumberService.ts +69 -0
- package/src/services/UniqueUserService.ts +3 -6
- package/src/services/VATService.ts +85 -0
- package/src/services/uitpas/PassholderEndpoints.ts +3 -6
- package/src/services/uitpas/UitpasService.ts +6 -10
- package/src/services/uitpas/checkUitpasNumbers.ts +2 -4
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +3 -6
- package/src/services/uitpas/searchUitpasEvents.ts +1 -2
- package/src/sql-filters/audit-logs.ts +1 -1
- package/src/sql-filters/balance-item-payments.ts +1 -1
- package/src/sql-filters/balance-items.ts +1 -1
- package/src/sql-filters/base-registration-filter-compilers.ts +5 -33
- package/src/sql-filters/document-templates.ts +1 -1
- package/src/sql-filters/documents.ts +1 -1
- package/src/sql-filters/email-recipients.ts +1 -1
- package/src/sql-filters/emails.ts +1 -1
- package/src/sql-filters/event-notifications.ts +1 -1
- package/src/sql-filters/events.ts +6 -1
- package/src/sql-filters/groups.ts +5 -0
- package/src/sql-filters/invoiced-balance-items.ts +1 -1
- package/src/sql-filters/invoices.ts +1 -1
- package/src/sql-filters/member-responsibility-records.ts +1 -1
- package/src/sql-filters/members.ts +1 -1
- package/src/sql-filters/orders.ts +4 -4
- package/src/sql-filters/organization-registration-periods.ts +1 -1
- package/src/sql-filters/organizations.ts +1 -1
- package/src/sql-filters/platform-memberships.ts +3 -3
- package/src/sql-filters/receivable-balances.ts +1 -1
- package/src/sql-filters/registration-periods.ts +1 -1
- package/src/sql-filters/registrations.ts +1 -1
- package/src/sql-filters/tickets.ts +1 -1
- package/src/sql-filters/users.ts +1 -1
- package/src/sql-sorters/groups.ts +23 -0
- package/src/sql-sorters/platform-memberships.ts +16 -16
- package/src/sql-sorters/registration-invitations.ts +2 -2
- package/tests/actions/patchOrganizationMember.ts +1 -1
- package/tests/actions/patchUserMember.ts +1 -1
- package/tests/assertions/assertBalances.ts +6 -12
- package/tests/e2e/api-rate-limits.test.ts +12 -24
- package/tests/e2e/bundle-discounts.test.ts +4 -1
- package/tests/e2e/charge-members.test.ts +13 -11
- package/tests/e2e/documents.test.ts +4 -1
- package/tests/e2e/register.test.ts +7 -9
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +1 -1
- package/tests/helpers/MollieMocker.ts +462 -0
- package/tests/helpers/PayconiqMocker.ts +138 -45
- package/tests/helpers/StripeMocker.ts +9 -6
- package/tests/helpers/index.ts +4 -0
- package/tests/init/initAdmin.ts +2 -2
- package/tests/init/initBundleDiscount.ts +1 -1
- package/tests/init/initMembershipOrganization.ts +13 -0
- package/tests/init/initPermissionRole.ts +3 -4
- package/tests/vitest.global.setup.ts +1 -1
- package/vitest.config.js +6 -0
- package/.nvmrc +0 -1
- package/src/helpers/GroupBuilder.ts +0 -418
- package/src/seeds/0000000000-example.ts +0 -11
- package/src/seeds/1722256498-group-update-occupancy.ts +0 -52
- package/src/seeds/1726572303-schedule-stock-updates.ts +0 -52
- package/src/seeds/1726847064-setup-steps.ts +0 -16
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +0 -31
- package/src/seeds/1729253172-update-orders.ts +0 -21
- package/src/seeds/1733319079-fill-paying-organization-ids.ts +0 -68
- package/src/seeds/1733748412-update-deleted-order-numbers.ts +0 -109
- package/src/seeds/1733994455-balance-item-status-open.ts +0 -30
- package/src/seeds/1740046783-update-membership.ts +0 -17
- package/src/seeds/1741008870-fix-auditlog-description.ts +0 -50
- package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +0 -88
- package/src/seeds/1741268179-fix-group-dates-of-events.ts +0 -32
- package/src/seeds/1751445358-upload-email-attachments.ts +0 -106
- package/src/seeds/1755532883-update-email-sender-ids.ts +0 -47
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +0 -96
- package/src/seeds/1756115432-remove-old-drafts.ts +0 -16
- package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +0 -30
- package/src/seeds/1756303697-update-email-counts.ts +0 -76
- package/src/seeds/1761665607-sync-member-users.ts +0 -59
- package/src/seeds/1765896674-document-update-year.ts +0 -66
- /package/src/{seeds → migrations}/0000000001-development-user.ts +0 -0
- /package/src/{seeds → migrations}/0000000002-clear-stamhoofd-email-templates.ts +0 -0
- /package/src/{seeds → migrations}/0000000003-default-email-templates.ts +0 -0
- /package/src/{seeds → migrations}/1752848561-groups-registration-periods.test.ts +0 -0
- /package/src/{seeds → migrations}/1766150402-document-published-at.test.ts +0 -0
- /package/src/{seeds → migrations}/1766150402-document-published-at.ts +0 -0
- /package/src/{seeds → migrations}/1779121239-default-invoice-email-template.sql +0 -0
- /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
|
+
});
|