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