@stamhoofd/backend 2.121.0 → 2.122.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/output-2.txt +1 -0
- package/output.txt +1 -0
- package/package.json +69 -52
- package/src/audit-logs/EventNotificationLogger.ts +40 -0
- package/src/audit-logs/ModelLogger.ts +19 -15
- package/src/audit-logs/OrganizationLogger.ts +1 -1
- package/src/audit-logs/RegistrationInvitationLogger.ts +24 -24
- package/src/audit-logs/init.ts +8 -2
- package/src/boot.ts +19 -18
- package/src/crons/amazon-ses.ts +13 -26
- package/src/crons/balance-emails.ts +3 -5
- package/src/crons/clearExcelCache.test.ts +1 -1
- package/src/crons/clearExcelCache.ts +1 -2
- package/src/crons/drip-emails.ts +42 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +4 -0
- package/src/crons/helpers/isOutside.ts +11 -0
- package/src/crons/helpers/useSavedIterator.ts +32 -0
- package/src/crons/index.ts +5 -0
- package/src/crons/invoices.ts +32 -38
- package/src/crons/members-fees.ts +50 -0
- package/src/crons/mollie-chargebacks.ts +14 -15
- package/src/crons/service-fees.ts +152 -0
- package/src/crons/stripe-invoices.ts +48 -0
- package/src/crons/transfer-fees.ts +176 -0
- package/src/crons/update-cached-balances.ts +4 -4
- package/src/crons.ts +7 -64
- package/src/debug.ts +4 -2
- package/src/email-recipient-loaders/documents.ts +1 -1
- package/src/email-recipient-loaders/members.ts +1 -1
- package/src/email-recipient-loaders/payments.ts +4 -6
- package/src/email-recipient-loaders/registrations.ts +1 -1
- package/src/email-replacements/getEmailReplacementsForPayment.ts +5 -9
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +1 -1
- package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +1 -1
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +3 -5
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +13 -7
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +4 -6
- package/src/endpoints/admin/organizations/PatchPackagesEndpoint.test.ts +10 -6
- package/src/endpoints/admin/organizations/PatchPackagesEndpoint.ts +39 -21
- package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +1 -1
- package/src/endpoints/auth/CreateAdminEndpoint.ts +23 -5
- package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/DeleteUserEndpoint.ts +3 -3
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +1 -1
- package/src/endpoints/auth/GetUserEndpoint.ts +1 -1
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +1 -1
- package/src/endpoints/auth/PatchUserEndpoint.ts +5 -8
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/SignupEndpoint.ts +2 -3
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +20 -3
- package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +2 -1
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +2 -4
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +1 -1
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +5 -7
- package/src/endpoints/global/caddy/CheckDomainCertEndpoint.ts +2 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +3 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +1 -2
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +4 -6
- package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +1 -1
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +6 -10
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +4 -7
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +6 -9
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +1 -1
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +1 -1
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +1 -1
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -2
- package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +1 -1
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +3 -3
- package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -3
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +49 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +5 -8
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +12 -44
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +128 -127
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -9
- package/src/endpoints/global/files/GetFileCache.ts +3 -4
- package/src/endpoints/global/files/UploadFile.ts +3 -4
- package/src/endpoints/global/files/UploadImage.ts +3 -4
- package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +1 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +1 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +4 -2
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +2 -3
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +1 -1
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +2 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +10 -12
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +195 -12
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +16 -22
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +6 -30
- package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +1 -1
- package/src/endpoints/global/organizations/CheckRegisterCodeEndpoint.ts +1 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +40 -8
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +2 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -2
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +4 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +4 -5
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +9 -16
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -1
- package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -11
- package/src/endpoints/global/platform-memberships/GetPlatformMembershipsCountEndpoint.ts +31 -31
- package/src/endpoints/global/platform-memberships/GetPlatformMembershipsEndpoint.ts +18 -20
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +1 -1
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1 -1
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +6 -10
- package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +35 -6
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +1 -1
- package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +1 -1
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -3
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +90 -6
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +35 -34
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +1 -1
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +62 -63
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +6 -9
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +51 -52
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +6 -7
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +1 -1
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +13 -5
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +2 -3
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +1 -1
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +2 -2
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +1 -2
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.ts +2 -4
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +7 -9
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +73 -0
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.ts +89 -0
- package/src/endpoints/organization/dashboard/billing/DeactivatePackageEndpoint.ts +1 -5
- package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +11 -11
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +9 -9
- package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableInvoicesEndpoint.ts +68 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/billing/GetRegisterCodeEndpoint.ts +66 -0
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.test.ts +949 -0
- package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +60 -57
- package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +20 -20
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +10 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +10 -8
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +5 -6
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +11 -2
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +4 -5
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +7 -7
- package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +50 -12
- package/src/endpoints/organization/dashboard/organization/SearchUitpasOrganizersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +5 -10
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +21 -14
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +35 -28
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +19 -18
- package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +4 -5
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +4 -6
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +10 -15
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +11 -2
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +4 -6
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +22 -7
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +20 -27
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +95 -0
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -2
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +1 -1
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +1 -1
- package/src/endpoints/organization/shared/GetUitpasNumberDetailsEndpoint.ts +3 -5
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +4 -1
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +14 -3
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +14 -2
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +37 -16
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +34 -2
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +4 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +74 -3
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +16 -33
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +3 -5
- package/src/endpoints/organization/webshops/WebshopAuthHelper.ts +36 -0
- package/src/endpoints/system/HealthEndpoint.ts +1 -1
- package/src/excel-loaders/balance-items.ts +1 -1
- package/src/excel-loaders/event-notifications.ts +1 -1
- package/src/excel-loaders/members.ts +1 -1
- package/src/excel-loaders/organizations.ts +3 -3
- package/src/excel-loaders/payments.ts +31 -1
- package/src/excel-loaders/platform-memberships.ts +13 -14
- package/src/excel-loaders/receivable-balances.ts +2 -2
- package/src/excel-loaders/registrations.ts +2 -3
- package/src/helpers/AddressValidator.ts +7 -14
- package/src/helpers/AdminPermissionChecker.ts +50 -63
- package/src/helpers/AuthenticatedStructures.ts +69 -55
- package/src/helpers/BalanceItemRelationsBackfiller.test.ts +174 -0
- package/src/helpers/BalanceItemRelationsBackfiller.ts +163 -0
- package/src/helpers/BuckarooHelper.ts +2 -4
- package/src/helpers/CheckSettlements.ts +12 -24
- package/src/helpers/Context.ts +15 -10
- package/src/helpers/CookieHelper.ts +1 -2
- package/src/helpers/EmailResumer.ts +1 -2
- package/src/helpers/FileCache.ts +2 -3
- package/src/helpers/FinancialSupportHelper.ts +2 -2
- package/src/helpers/FlagMomentCleanup.ts +1 -2
- package/src/helpers/ForwardHandler.ts +5 -10
- package/src/helpers/GlobalHelper.ts +3 -3
- package/src/helpers/LegacyBillingConverter.ts +787 -0
- package/src/helpers/MemberMerger.test.ts +782 -0
- package/src/helpers/MemberMerger.ts +600 -0
- package/src/helpers/MemberUserSyncer.ts +6 -12
- package/src/helpers/MembershipCharger.ts +2 -4
- package/src/helpers/PaymentCustomerResolver.test.ts +203 -0
- package/src/helpers/PaymentCustomerResolver.ts +144 -0
- package/src/helpers/PeriodHelper.ts +2 -3
- package/src/helpers/RecordAnswerHelper.test.ts +105 -110
- package/src/helpers/RecordAnswerHelper.ts +7 -7
- package/src/helpers/SeedTools.ts +106 -0
- package/src/helpers/ServiceFeeHelper.ts +125 -15
- package/src/helpers/SetupStepUpdater.ts +38 -32
- package/src/helpers/StamhoofdFilterAccessHelper.ts +52 -0
- package/src/helpers/StripeHelper.ts +14 -36
- package/src/helpers/StripeInvoicer.ts +419 -0
- package/src/helpers/StripePayoutChecker.ts +5 -10
- package/src/helpers/TagHelper.ts +7 -8
- package/src/helpers/TemporaryMemberAccess.ts +1 -2
- package/src/helpers/ThrottledQueue.ts +1 -2
- package/src/helpers/UitpasTokenRepository.ts +1 -2
- package/src/helpers/ViesHelper.ts +7 -11
- package/src/helpers/XlsxTransformerColumnHelper.ts +1 -1
- package/src/helpers/outstandingBalanceJoin.ts +2 -2
- package/src/helpers/updateMemberDetailsUitpasNumber.ts +3 -5
- package/src/migrate.ts +12 -5
- package/src/{seeds → migrations}/0000000004-single-organization.ts +10 -1
- package/src/migrations/0000000006-default-organization.ts +139 -0
- package/src/{seeds → migrations}/1715028563-user-permissions.ts +17 -27
- package/src/{seeds/1765896674-document-update-year.test.ts → migrations/1750090029-document-update-year.test.ts} +27 -26
- package/src/migrations/1750090029-document-update-year.ts +103 -0
- package/src/{seeds → migrations}/1750090030-records-configuration.ts +33 -32
- package/src/migrations/1750090031-records-configuration-webshops.ts +36 -0
- package/src/migrations/1752848540-national-numbers.ts +585 -0
- package/src/migrations/1752848550-records-configuration-group-filter.ts +1064 -0
- package/src/{seeds → migrations}/1752848561-groups-registration-periods.ts +70 -26
- package/src/{seeds → migrations}/1754560914-groups-prices.test.ts +1 -1
- package/src/{seeds → migrations}/1754560914-groups-prices.ts +9 -15
- package/src/{seeds → migrations}/1756293699-fill-previous-next-period-id.ts +1 -2
- package/src/migrations/1761665607-sync-member-users.ts +40 -0
- package/src/migrations/1779121240-invoice-email-template.sql +6 -0
- package/src/migrations/1779986893-default-payment-failed-email-templates.sql +4 -0
- package/src/migrations/1780578166-convert-legacy-billing.ts +13 -0
- package/src/{seeds/1755876819-remove-duplicate-members.ts → migrations/1780665426-remove-duplicate-members.ts} +23 -4
- package/src/migrations/1780933598-document-groups.ts +88 -0
- package/src/migrations/1781692342-balance-item-relations.ts +13 -0
- package/src/migrations/1781857866-platform-admins.ts +103 -0
- package/src/migrations/1781857867-rm-old-platform-admins.ts +25 -0
- package/src/seeds/0000000001-throw.ts +5 -0
- package/src/seeds/1760702452-update-balance-price-paid.ts +19 -0
- package/src/seeds/1760702453-update-balance-invoiced.ts +19 -0
- package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +51 -14
- package/src/seeds/1773754928-force-save-members.ts +41 -8
- package/src/seeds/1780665427-schedule-stock-updates.ts +45 -0
- package/src/seeds/1780665428-group-update-occupancy.ts +31 -0
- package/src/seeds/1780915001-fill-payment-customer.ts +70 -0
- package/src/seeds/1780915002-update-orders.ts +39 -0
- package/src/seeds/1782047708-organization-company-address.ts +44 -0
- package/src/seeds-temporary/1769088653-uitpas-status.ts +1 -2
- package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.test.ts +69 -0
- package/src/seeds-temporary/1780416000-migrate-audit-log-legacy-enums.ts +85 -0
- package/src/services/AuditLogService.test.ts +51 -0
- package/src/services/AuditLogService.ts +45 -0
- package/src/services/BalanceItemService.ts +141 -44
- package/src/services/CpuService.ts +3 -6
- package/src/services/DatabaseCollationService.test.ts +1 -2
- package/src/services/DocumentService.ts +1 -2
- package/src/services/EventNotificationService.ts +1 -1
- package/src/services/FileSignService.ts +2 -4
- package/src/services/InvoicePdfService.ts +286 -0
- package/src/services/InvoiceService.ts +204 -251
- package/src/services/InvoiceXMLService.ts +424 -0
- package/src/services/MemberNumberService.ts +6 -11
- package/src/services/MemberRecordStore.ts +8 -10
- package/src/services/MollieService.ts +114 -121
- package/src/services/PaymentMandateService.ts +45 -45
- package/src/services/PaymentReallocationService.test.ts +1 -1
- package/src/services/PaymentReallocationService.ts +7 -9
- package/src/services/PaymentService.ts +444 -308
- package/src/services/PlatformMembershipService.ts +4 -8
- package/src/services/ReferralService.ts +223 -0
- package/src/services/RegistrationService.ts +63 -53
- package/src/services/SSOService.test.ts +119 -0
- package/src/services/SSOService.ts +56 -49
- package/src/services/STPackageService.ts +97 -61
- package/src/services/UniqueMemberNumberService.ts +69 -0
- package/src/services/UniqueUserService.ts +3 -6
- package/src/services/VATService.ts +85 -0
- package/src/services/uitpas/PassholderEndpoints.ts +3 -6
- package/src/services/uitpas/UitpasService.ts +6 -10
- package/src/services/uitpas/checkUitpasNumbers.ts +2 -4
- package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +3 -6
- package/src/services/uitpas/searchUitpasEvents.ts +1 -2
- package/src/sql-filters/audit-logs.ts +1 -1
- package/src/sql-filters/balance-item-payments.ts +1 -1
- package/src/sql-filters/balance-items.ts +1 -1
- package/src/sql-filters/base-registration-filter-compilers.ts +5 -33
- package/src/sql-filters/document-templates.ts +1 -1
- package/src/sql-filters/documents.ts +1 -1
- package/src/sql-filters/email-recipients.ts +1 -1
- package/src/sql-filters/emails.ts +1 -1
- package/src/sql-filters/event-notifications.ts +1 -1
- package/src/sql-filters/events.ts +6 -1
- package/src/sql-filters/groups.ts +5 -0
- package/src/sql-filters/invoiced-balance-items.ts +1 -1
- package/src/sql-filters/invoices.ts +1 -1
- package/src/sql-filters/member-responsibility-records.ts +1 -1
- package/src/sql-filters/members.ts +1 -1
- package/src/sql-filters/orders.ts +4 -4
- package/src/sql-filters/organization-registration-periods.ts +1 -1
- package/src/sql-filters/organizations.ts +1 -1
- package/src/sql-filters/platform-memberships.ts +3 -3
- package/src/sql-filters/receivable-balances.ts +1 -1
- package/src/sql-filters/registration-periods.ts +1 -1
- package/src/sql-filters/registrations.ts +1 -1
- package/src/sql-filters/tickets.ts +1 -1
- package/src/sql-filters/users.ts +1 -1
- package/src/sql-sorters/groups.ts +23 -0
- package/src/sql-sorters/platform-memberships.ts +16 -16
- package/src/sql-sorters/registration-invitations.ts +2 -2
- package/tests/actions/patchOrganizationMember.ts +1 -1
- package/tests/actions/patchUserMember.ts +1 -1
- package/tests/assertions/assertBalances.ts +6 -12
- package/tests/e2e/api-rate-limits.test.ts +12 -24
- package/tests/e2e/bundle-discounts.test.ts +4 -1
- package/tests/e2e/charge-members.test.ts +13 -11
- package/tests/e2e/documents.test.ts +4 -1
- package/tests/e2e/register.test.ts +7 -9
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +1 -1
- package/tests/helpers/MollieMocker.ts +462 -0
- package/tests/helpers/PayconiqMocker.ts +138 -45
- package/tests/helpers/StripeMocker.ts +9 -6
- package/tests/helpers/index.ts +4 -0
- package/tests/init/initAdmin.ts +2 -2
- package/tests/init/initBundleDiscount.ts +1 -1
- package/tests/init/initMembershipOrganization.ts +13 -0
- package/tests/init/initPermissionRole.ts +3 -4
- package/tests/vitest.global.setup.ts +1 -1
- package/vitest.config.js +6 -0
- package/.nvmrc +0 -1
- package/src/helpers/GroupBuilder.ts +0 -418
- package/src/seeds/0000000000-example.ts +0 -11
- package/src/seeds/1722256498-group-update-occupancy.ts +0 -52
- package/src/seeds/1726572303-schedule-stock-updates.ts +0 -52
- package/src/seeds/1726847064-setup-steps.ts +0 -16
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +0 -31
- package/src/seeds/1729253172-update-orders.ts +0 -21
- package/src/seeds/1733319079-fill-paying-organization-ids.ts +0 -68
- package/src/seeds/1733748412-update-deleted-order-numbers.ts +0 -109
- package/src/seeds/1733994455-balance-item-status-open.ts +0 -30
- package/src/seeds/1740046783-update-membership.ts +0 -17
- package/src/seeds/1741008870-fix-auditlog-description.ts +0 -50
- package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +0 -88
- package/src/seeds/1741268179-fix-group-dates-of-events.ts +0 -32
- package/src/seeds/1751445358-upload-email-attachments.ts +0 -106
- package/src/seeds/1755532883-update-email-sender-ids.ts +0 -47
- package/src/seeds/1755790070-fill-email-recipient-errors.ts +0 -96
- package/src/seeds/1756115432-remove-old-drafts.ts +0 -16
- package/src/seeds/1756115433-fill-email-recipient-organization-id.ts +0 -30
- package/src/seeds/1756303697-update-email-counts.ts +0 -76
- package/src/seeds/1761665607-sync-member-users.ts +0 -59
- package/src/seeds/1765896674-document-update-year.ts +0 -66
- /package/src/{seeds → migrations}/0000000001-development-user.ts +0 -0
- /package/src/{seeds → migrations}/0000000002-clear-stamhoofd-email-templates.ts +0 -0
- /package/src/{seeds → migrations}/0000000003-default-email-templates.ts +0 -0
- /package/src/{seeds → migrations}/1752848561-groups-registration-periods.test.ts +0 -0
- /package/src/{seeds → migrations}/1766150402-document-published-at.test.ts +0 -0
- /package/src/{seeds → migrations}/1766150402-document-published-at.ts +0 -0
- /package/src/{seeds → migrations}/1779121239-default-invoice-email-template.sql +0 -0
- /package/src/{seeds → migrations}/data/default-email-templates.sql +0 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import type { Model } from '@simonbackx/simple-database';
|
|
2
|
+
import { BalanceItem, BalanceItemPayment, CachedBalance, Invoice, InvoicedBalanceItem, Organization, Payment, Platform, STCredit, STInvoice, STPendingInvoice, StripeAccount, UsedRegisterCode, User } from '@stamhoofd/models';
|
|
3
|
+
import { InvoiceCounter } from '@stamhoofd/models/helpers/InvoiceCounter.js';
|
|
4
|
+
import { QueryableModel } from '@stamhoofd/sql';
|
|
5
|
+
import type { STInvoiceItem, STInvoiceMeta } from '@stamhoofd/structures';
|
|
6
|
+
import {
|
|
7
|
+
BalanceItemRelation,
|
|
8
|
+
BalanceItemRelationType,
|
|
9
|
+
BalanceItemStatus,
|
|
10
|
+
BalanceItemType,
|
|
11
|
+
Company,
|
|
12
|
+
getPricingTypeName,
|
|
13
|
+
InvoicedBalanceItem as InvoicedBalanceItemStruct,
|
|
14
|
+
PaymentCustomer,
|
|
15
|
+
PaymentMethod,
|
|
16
|
+
PaymentProvider,
|
|
17
|
+
PaymentStatus,
|
|
18
|
+
PaymentType,
|
|
19
|
+
TranslatedString,
|
|
20
|
+
VATExcemptReason,
|
|
21
|
+
VATSubtotal,
|
|
22
|
+
} from '@stamhoofd/structures';
|
|
23
|
+
import { InvoiceStruct } from '@stamhoofd/structures/billing/Invoice.js';
|
|
24
|
+
import { STMath } from '@stamhoofd/utility';
|
|
25
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
26
|
+
import { SeedTools } from './SeedTools.js';
|
|
27
|
+
import { VATService } from '../services/VATService.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The seller (Stamhoofd) is a Belgian company, so the standard rate that applies to taxable sales is
|
|
31
|
+
* 21%. Used to restore the rate on intra-community reverse-charged invoices (legacy stored 0%).
|
|
32
|
+
*/
|
|
33
|
+
const STANDARD_VAT_PERCENTAGE = 21;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Converts the legacy SAAS billing models (STInvoice / STPendingInvoice / STCredit) into the new
|
|
37
|
+
* billing model (Invoice / InvoicedBalanceItem / BalanceItem / BalanceItemPayment).
|
|
38
|
+
*
|
|
39
|
+
* The conversion is idempotent: every new row reuses the legacy primary key as its own id, so a
|
|
40
|
+
* re-run skips anything that was already created.
|
|
41
|
+
*
|
|
42
|
+
* The critical correctness goal is that every legacy line tied to an STPackage becomes a BalanceItem
|
|
43
|
+
* with the correct packageId + amount, so STPackageService.getPaidOrPendingQuantity stays accurate and
|
|
44
|
+
* customers are never re-charged for something they already paid.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
interface ConversionContext {
|
|
48
|
+
membershipOrganization: Organization;
|
|
49
|
+
/** Point-in-time snapshot of the seller (Stamhoofd) used on every generated invoice. */
|
|
50
|
+
seller: Company;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Round a 4-decimal price down to whole cents. BalanceItem.save() rejects sub-cent unit prices. */
|
|
54
|
+
function roundToCents(value: number): number {
|
|
55
|
+
return STMath.round(value / 100) * 100;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Maps the legacy single VAT rate to the new model's (VATPercentage, VATExcempt) pair.
|
|
60
|
+
*
|
|
61
|
+
* Legacy stored a 0% rate for intra-community reverse charge (a foreign EU company with a non-Belgian
|
|
62
|
+
* VAT number); the new model marks this explicitly with VATExcempt = IntraCommunityServices while
|
|
63
|
+
* keeping the seller's standard rate. The VAT amount stays 0 either way, so totals are unchanged.
|
|
64
|
+
*/
|
|
65
|
+
function getVatInfo(meta: STInvoiceMeta): { VATPercentage: number; VATExcempt: VATExcemptReason | null } {
|
|
66
|
+
if (meta.VATPercentage === 0 && !!meta.companyVATNumber) {
|
|
67
|
+
return { VATPercentage: STANDARD_VAT_PERCENTAGE, VATExcempt: VATExcemptReason.IntraCommunityServices };
|
|
68
|
+
}
|
|
69
|
+
return { VATPercentage: meta.VATPercentage, VATExcempt: null };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertEqual(actual: number, expected: number, label: string, invoiceId: string) {
|
|
73
|
+
if (actual !== expected) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Legacy billing migration: ${label} mismatch for invoice ${invoiceId}: got ${actual}, expected ${expected}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds (but does not save) the BalanceItem for one legacy invoice/pending-invoice line.
|
|
82
|
+
* Mirrors STPackageService.chargePackage for the package relations/VAT mapping.
|
|
83
|
+
*/
|
|
84
|
+
async function buildBalanceItem(item: STInvoiceItem, options: {
|
|
85
|
+
legacyInvoice: STInvoice | STPendingInvoice;
|
|
86
|
+
legacyMeta: STInvoiceMeta;
|
|
87
|
+
payingOrganizationId: string | null;
|
|
88
|
+
ctx: ConversionContext;
|
|
89
|
+
status: BalanceItemStatus;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
paidAt: Date | null;
|
|
92
|
+
}): Promise<BalanceItem> {
|
|
93
|
+
const { legacyMeta: legacyMeta, payingOrganizationId, ctx, status, createdAt, paidAt } = options;
|
|
94
|
+
|
|
95
|
+
const balanceItem = new BalanceItem();
|
|
96
|
+
balanceItem.id = uuidv4(); // we cannot reuse old item ids as multiple invoices could reference the same id
|
|
97
|
+
balanceItem.organizationId = ctx.membershipOrganization.id;
|
|
98
|
+
balanceItem.payingOrganizationId = payingOrganizationId;
|
|
99
|
+
|
|
100
|
+
if (item.package) {
|
|
101
|
+
balanceItem.packageId = item.package.id;
|
|
102
|
+
balanceItem.type = BalanceItemType.STPackage;
|
|
103
|
+
balanceItem.relations.set(BalanceItemRelationType.STPackage, BalanceItemRelation.create({
|
|
104
|
+
id: item.package.id,
|
|
105
|
+
name: TranslatedString.create(item.package.meta.name),
|
|
106
|
+
}));
|
|
107
|
+
balanceItem.relations.set(BalanceItemRelationType.STPricingType, BalanceItemRelation.create({
|
|
108
|
+
id: item.package.meta.pricingType,
|
|
109
|
+
name: TranslatedString.create(getPricingTypeName(item.package.meta.pricingType)),
|
|
110
|
+
}));
|
|
111
|
+
balanceItem.startDate = item.package.meta.startDate ?? null;
|
|
112
|
+
balanceItem.endDate = item.package.endDate ?? null;
|
|
113
|
+
} else {
|
|
114
|
+
balanceItem.type = BalanceItemType.Other;
|
|
115
|
+
|
|
116
|
+
// If it is credit, try to link to the credit ID instead.
|
|
117
|
+
if (item.name === 'Gebruikt tegoed' && options.legacyInvoice instanceof STInvoice && payingOrganizationId) {
|
|
118
|
+
balanceItem.type = BalanceItemType.ReferralDiscount;
|
|
119
|
+
// Stop here, we need to find the existing one.
|
|
120
|
+
const creditId = options.legacyInvoice.creditId;
|
|
121
|
+
if (!creditId) {
|
|
122
|
+
// This happens sadly because of incorrect data in the v1 database
|
|
123
|
+
console.error('Missing creditId on invoice ' + options.legacyInvoice.id + ' / ' + options.legacyInvoice.number);
|
|
124
|
+
} else {
|
|
125
|
+
const original = await BalanceItem.getByID(creditId);
|
|
126
|
+
if (!original) {
|
|
127
|
+
console.error('Could not find BalanceItem for creditId ' + creditId + ' at invoice ' + options.legacyInvoice.id);
|
|
128
|
+
} else {
|
|
129
|
+
if (original.status !== BalanceItemStatus.Canceled) {
|
|
130
|
+
throw new Error('Mismatch in creditId ' + creditId + ' at invoice ' + options.legacyInvoice.id + ' already Due');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Set unitPrice = 0 of this item
|
|
134
|
+
if (original.unitPrice !== -item.unitPrice * 100) {
|
|
135
|
+
throw new Error('Mismatch in creditId ' + creditId + ' at invoice ' + options.legacyInvoice.id + ' expected ' + (-item.unitPrice * 100) + ' received ' + original.unitPrice);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Normally this is svaed as a balance item with a positive value
|
|
139
|
+
// to reduce a negative balance item that was added earlier as a discount.
|
|
140
|
+
// but if we would link a balance item payment, that would remove that offset completely.
|
|
141
|
+
// So we need to set that the 'usage' of this balance item was not payable all along, and link the
|
|
142
|
+
// negative payment item with it, so we get a positive amount open, leading to the correct behaviour
|
|
143
|
+
original.status = status; // Also for pending payments, as we'll need to readd the missed discount when the payment would fail
|
|
144
|
+
original.unitPrice = item.unitPrice * 100;
|
|
145
|
+
original.type = BalanceItemType.ReferralDiscount;
|
|
146
|
+
|
|
147
|
+
const vatInfo = getVatInfo(legacyMeta);
|
|
148
|
+
original.VATIncluded = legacyMeta.areItemsIncludingVAT;
|
|
149
|
+
original.VATPercentage = vatInfo.VATPercentage;
|
|
150
|
+
original.VATExcempt = vatInfo.VATExcempt;
|
|
151
|
+
|
|
152
|
+
// Don't copy the name, as in the past we would have put a reason why the credit was used
|
|
153
|
+
// but that should not be reused
|
|
154
|
+
original.description = item.name || item.description || '';
|
|
155
|
+
|
|
156
|
+
return original;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const vatInfo = getVatInfo(legacyMeta);
|
|
163
|
+
|
|
164
|
+
balanceItem.description = item.name || item.description || '';
|
|
165
|
+
balanceItem.amount = item.amount;
|
|
166
|
+
balanceItem.unitPrice = item.unitPrice * 100;
|
|
167
|
+
balanceItem.VATPercentage = vatInfo.VATPercentage;
|
|
168
|
+
balanceItem.VATIncluded = legacyMeta.areItemsIncludingVAT;
|
|
169
|
+
balanceItem.VATExcempt = vatInfo.VATExcempt;
|
|
170
|
+
balanceItem.status = status;
|
|
171
|
+
balanceItem.dueAt = null;
|
|
172
|
+
balanceItem.paidAt = paidAt;
|
|
173
|
+
balanceItem.createdAt = createdAt;
|
|
174
|
+
|
|
175
|
+
return balanceItem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildCustomer(meta: STInvoiceMeta): PaymentCustomer {
|
|
179
|
+
return PaymentCustomer.create({
|
|
180
|
+
email: meta.companyEmail,
|
|
181
|
+
company: Company.create({
|
|
182
|
+
name: meta.companyName,
|
|
183
|
+
VATNumber: meta.companyVATNumber,
|
|
184
|
+
companyNumber: meta.companyNumber,
|
|
185
|
+
address: meta.companyAddress,
|
|
186
|
+
administrationEmail: meta.companyEmail,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolves the legacy Payment linked to an invoice. Legacy payments for Stamhoofd often have a null
|
|
193
|
+
* organizationId; we set it to the membership organization (the seller) so the new payment flow and
|
|
194
|
+
* cached balances work.
|
|
195
|
+
*/
|
|
196
|
+
async function resolvePayment(stInvoice: STInvoice, payment: Payment, ctx: ConversionContext): Promise<Payment | null> {
|
|
197
|
+
let changed = false;
|
|
198
|
+
if (payment.organizationId === null) {
|
|
199
|
+
payment.organizationId = ctx.membershipOrganization.id;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
if (payment.payingOrganizationId === null && stInvoice.payingOrganizationId) {
|
|
203
|
+
payment.payingOrganizationId = stInvoice.payingOrganizationId;
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!payment.customer) {
|
|
208
|
+
payment.customer = buildCustomer(stInvoice.meta);
|
|
209
|
+
changed = true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (payment.price === 0 && payment.status !== PaymentStatus.Succeeded && payment.status !== PaymentStatus.Failed && payment.method === PaymentMethod.Unknown) {
|
|
213
|
+
payment.status = PaymentStatus.Succeeded;
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (changed) {
|
|
218
|
+
await payment.save();
|
|
219
|
+
}
|
|
220
|
+
return payment;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface ConversionResult {
|
|
224
|
+
balanceItemIds: string[];
|
|
225
|
+
payingOrganizationIds: string[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Full reconstruction of a paid/official invoice: Invoice + per-line BalanceItem (marked paid via a
|
|
230
|
+
* BalanceItemPayment linking the existing legacy Payment) + InvoicedBalanceItem.
|
|
231
|
+
*/
|
|
232
|
+
export async function convertPaidInvoice(stInvoice: STInvoice, payment: Payment, ctx: ConversionContext): Promise<ConversionResult> {
|
|
233
|
+
const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
|
|
234
|
+
const toSaveModels: Model[] = [];
|
|
235
|
+
|
|
236
|
+
if (await Invoice.getByID(stInvoice.id)) {
|
|
237
|
+
// Already migrated
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const legacyMeta = stInvoice.meta;
|
|
242
|
+
const payingOrganizationId = stInvoice.payingOrganizationId;
|
|
243
|
+
const invoicedAt = legacyMeta.date ?? stInvoice.paidAt ?? stInvoice.createdAt;
|
|
244
|
+
|
|
245
|
+
await resolvePayment(stInvoice, payment, ctx);
|
|
246
|
+
|
|
247
|
+
// Verify the legacy invoice is internally consistent before we reproduce it.
|
|
248
|
+
assertEqual(legacyMeta.priceWithoutVAT + legacyMeta.VAT, legacyMeta.priceWithVAT, 'priceWithVAT composition', stInvoice.id);
|
|
249
|
+
assertEqual(legacyMeta.priceWithVAT + legacyMeta.payableRoundingAmount, legacyMeta.totalPrice, 'totalPrice composition', stInvoice.id);
|
|
250
|
+
|
|
251
|
+
// The actual money received must match the invoice total.
|
|
252
|
+
if (payment && payment.status === PaymentStatus.Succeeded) {
|
|
253
|
+
assertEqual(payment.price, legacyMeta.totalPrice * 100, 'payment price vs invoice total', stInvoice.id);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// First create the balance items + their invoiced-line data (in memory). The InvoicedBalanceItem
|
|
257
|
+
// rows are only saved after the Invoice exists (foreign key).
|
|
258
|
+
const lines: { item: STInvoiceItem; balanceItem: BalanceItem; invoicedStruct: InvoicedBalanceItemStruct }[] = [];
|
|
259
|
+
|
|
260
|
+
const invoiceStruct = InvoiceStruct.create({
|
|
261
|
+
seller: ctx.seller,
|
|
262
|
+
customer: buildCustomer(legacyMeta),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
for (const item of legacyMeta.items) {
|
|
266
|
+
if (item.amount === 0) {
|
|
267
|
+
// Not possible to be on invoice, and also cannot be represented correctly since this isn't part of the invoice (items with amount 0, means not included)
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const balanceItem = await buildBalanceItem(item, {
|
|
272
|
+
legacyInvoice: stInvoice,
|
|
273
|
+
legacyMeta: legacyMeta,
|
|
274
|
+
payingOrganizationId,
|
|
275
|
+
ctx,
|
|
276
|
+
status: BalanceItemStatus.Due,
|
|
277
|
+
createdAt: invoicedAt,
|
|
278
|
+
paidAt: invoicedAt,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
result.balanceItemIds.push(balanceItem.id);
|
|
282
|
+
toSaveModels.push(balanceItem);
|
|
283
|
+
|
|
284
|
+
// Build the invoiced line (computes unit price excl. VAT, quantity, PEPPOL rounding, ...)
|
|
285
|
+
const invoicedStruct = InvoicedBalanceItemStruct.createFor(balanceItem.getStructure(), balanceItem.priceWithVAT);
|
|
286
|
+
lines.push({ item, balanceItem, invoicedStruct });
|
|
287
|
+
|
|
288
|
+
if (legacyMeta.areItemsIncludingVAT) {
|
|
289
|
+
if (item.amount === 1) {
|
|
290
|
+
// There could be very rare differences.
|
|
291
|
+
// E.g. 10,40495 rounded to 10,4050 in the balance item, and then to 10,41 in the invoiced struct
|
|
292
|
+
invoicedStruct.unitPrice = stInvoice.meta.includingVATToExcludingVAT(item.price) * 100;
|
|
293
|
+
invoicedStruct.quantity = 1_00_00;
|
|
294
|
+
|
|
295
|
+
if (invoicedStruct.unitPrice < 0) {
|
|
296
|
+
// unitPrice should always be positive in peppol
|
|
297
|
+
invoicedStruct.unitPrice = -invoicedStruct.unitPrice;
|
|
298
|
+
invoicedStruct.quantity = -invoicedStruct.quantity;
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
assertEqual(invoicedStruct.totalWithoutVAT, stInvoice.meta.includingVATToExcludingVAT(item.price) * 100, 'item ' + item.id + ' price without vat should match new', stInvoice.id);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
assertEqual(invoicedStruct.totalWithoutVAT, item.price * 100, 'item ' + item.id + ' price without vat should match new', stInvoice.id);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Mark the balance item paid by linking the existing legacy payment. We pay each item in full
|
|
308
|
+
// (priceWithVAT) so priceOpen becomes 0 regardless of invoice-level rounding.
|
|
309
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
310
|
+
balanceItemPayment.organizationId = ctx.membershipOrganization.id;
|
|
311
|
+
balanceItemPayment.paymentId = payment.id;
|
|
312
|
+
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
313
|
+
balanceItemPayment.price = balanceItem.priceWithVAT;
|
|
314
|
+
toSaveModels.push(balanceItemPayment);
|
|
315
|
+
|
|
316
|
+
invoiceStruct.addItem(invoicedStruct);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
invoiceStruct.calculateVAT();
|
|
320
|
+
invoiceStruct.payableRoundingAmount = legacyMeta.payableRoundingAmount * 100;
|
|
321
|
+
|
|
322
|
+
if (legacyMeta.useLegacyRounding) {
|
|
323
|
+
// we can ignore the subresult because we cannot get to the same result in any way using the new invoice calculation methods
|
|
324
|
+
// since these are old invoices, we should accept them as the calculation was not incorrect at the time of generating the invoice
|
|
325
|
+
const diff = invoiceStruct.totalWithVAT - legacyMeta.totalPrice * 100;
|
|
326
|
+
if (Math.abs(diff) > 3_00) {
|
|
327
|
+
throw new Error('Unexpected large difference between legacy calculation and modern calculation at ' + stInvoice.id);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
assertEqual(invoiceStruct.totalWithVAT, legacyMeta.totalPrice * 100, 'invoice totalWithVAT', stInvoice.id);
|
|
331
|
+
assertEqual(invoiceStruct.VATTotalAmount, legacyMeta.VAT * 100, 'invoice VAT', stInvoice.id);
|
|
332
|
+
assertEqual(invoiceStruct.totalWithoutVAT, legacyMeta.priceWithoutVAT * 100, 'invoice totalWithoutVAT', stInvoice.id);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const invoice = new Invoice();
|
|
336
|
+
invoice.id = stInvoice.id;
|
|
337
|
+
invoice.organizationId = ctx.membershipOrganization.id;
|
|
338
|
+
invoice.payingOrganizationId = payingOrganizationId;
|
|
339
|
+
invoice.seller = ctx.seller;
|
|
340
|
+
invoice.customer = buildCustomer(legacyMeta);
|
|
341
|
+
invoice.negativeInvoiceId = stInvoice.negativeInvoiceId;
|
|
342
|
+
|
|
343
|
+
// Carry the legacy totals over verbatim so the converted invoice is identical to the original.
|
|
344
|
+
|
|
345
|
+
invoice.totalWithoutVAT = legacyMeta.priceWithoutVAT * 100;
|
|
346
|
+
invoice.VATTotalAmount = legacyMeta.VAT * 100;
|
|
347
|
+
invoice.totalWithVAT = legacyMeta.totalPrice * 100;
|
|
348
|
+
invoice.payableRoundingAmount = legacyMeta.payableRoundingAmount * 100;
|
|
349
|
+
invoice.totalBalanceInvoicedAmount = invoiceStruct.totalBalanceInvoicedAmount;
|
|
350
|
+
|
|
351
|
+
const vatInfo = getVatInfo(legacyMeta);
|
|
352
|
+
invoice.VATTotal = [
|
|
353
|
+
VATSubtotal.create({
|
|
354
|
+
VATPercentage: vatInfo.VATPercentage,
|
|
355
|
+
VATExcempt: vatInfo.VATExcempt,
|
|
356
|
+
taxablePrice: legacyMeta.priceWithoutVAT * 100,
|
|
357
|
+
VAT: legacyMeta.VAT * 100,
|
|
358
|
+
}),
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
invoice.ipAddress = legacyMeta.ipAddress;
|
|
362
|
+
invoice.userAgent = legacyMeta.userAgent;
|
|
363
|
+
invoice.stripeAccountId = legacyMeta.stripeAccountId;
|
|
364
|
+
invoice.reference = stInvoice.reference;
|
|
365
|
+
invoice.didSendPeppol = stInvoice.didSendPeppol;
|
|
366
|
+
|
|
367
|
+
// Reuse the already generated PDF/XML files instead of regenerating them.
|
|
368
|
+
invoice.pdf = legacyMeta.pdf ?? null;
|
|
369
|
+
invoice.xml = legacyMeta.xml ?? null;
|
|
370
|
+
|
|
371
|
+
invoice.invoicedAt = invoicedAt;
|
|
372
|
+
invoice.createdAt = stInvoice.createdAt;
|
|
373
|
+
|
|
374
|
+
// Format the legacy integer number using the seller's invoice settings so that future numbering
|
|
375
|
+
// (InvoiceCounter.assignNextNumber) continues the same series.
|
|
376
|
+
if (stInvoice.number) {
|
|
377
|
+
invoice.number = stInvoice.number.toString();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// The converted invoice must match the original exactly.
|
|
381
|
+
assertEqual(invoice.totalWithVAT, legacyMeta.totalPrice * 100, 'invoice totalWithVAT', stInvoice.id);
|
|
382
|
+
assertEqual(invoice.VATTotalAmount, legacyMeta.VAT * 100, 'invoice VAT', stInvoice.id);
|
|
383
|
+
assertEqual(invoice.payableRoundingAmount, legacyMeta.payableRoundingAmount * 100, 'invoice rounding', stInvoice.id);
|
|
384
|
+
|
|
385
|
+
// Only save after the last assertions
|
|
386
|
+
await invoice.save();
|
|
387
|
+
await InvoiceCounter.resetNumbers(invoice.organizationId);
|
|
388
|
+
|
|
389
|
+
for (const model of toSaveModels) {
|
|
390
|
+
await model.save();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Now that the invoice exists, persist the invoiced balance items (FK -> invoices).
|
|
394
|
+
for (const { item, balanceItem, invoicedStruct } of lines) {
|
|
395
|
+
const invoiced = new InvoicedBalanceItem();
|
|
396
|
+
invoiced.organizationId = ctx.membershipOrganization.id;
|
|
397
|
+
invoiced.invoiceId = stInvoice.id;
|
|
398
|
+
invoiced.balanceItemId = balanceItem.id;
|
|
399
|
+
invoiced.name = item.name || invoicedStruct.name;
|
|
400
|
+
invoiced.description = item.description;
|
|
401
|
+
invoiced.balanceInvoicedAmount = invoicedStruct.balanceInvoicedAmount;
|
|
402
|
+
invoiced.quantity = invoicedStruct.quantity;
|
|
403
|
+
invoiced.unitPrice = invoicedStruct.unitPrice;
|
|
404
|
+
invoiced.VATPercentage = invoicedStruct.VATPercentage;
|
|
405
|
+
invoiced.VATIncluded = invoicedStruct.VATIncluded;
|
|
406
|
+
invoiced.VATExcempt = invoicedStruct.VATExcempt;
|
|
407
|
+
invoiced.totalWithoutVAT = invoicedStruct.totalWithoutVAT;
|
|
408
|
+
await invoiced.save();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (payingOrganizationId) {
|
|
412
|
+
result.payingOrganizationIds.push(payingOrganizationId);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (payment) {
|
|
416
|
+
payment.customer = invoice.customer;
|
|
417
|
+
payment.invoiceId = invoice.id;
|
|
418
|
+
await payment.save();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* A legacy invoice whose payment is still in flight (Created/Pending). We create Hidden balance items
|
|
426
|
+
* linked to the still-pending payment, so when it eventually settles the existing payment-success flow
|
|
427
|
+
* activates the package. No Invoice is created (it is not official yet).
|
|
428
|
+
*/
|
|
429
|
+
export async function convertInProgressInvoice(stInvoice: STInvoice, payment: Payment, ctx: ConversionContext): Promise<ConversionResult> {
|
|
430
|
+
const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
|
|
431
|
+
|
|
432
|
+
if (stInvoice.meta.items.length === 0) {
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
await resolvePayment(stInvoice, payment, ctx);
|
|
437
|
+
|
|
438
|
+
const meta = stInvoice.meta;
|
|
439
|
+
const payingOrganizationId = stInvoice.payingOrganizationId;
|
|
440
|
+
const createdAt = meta.date ?? stInvoice.createdAt;
|
|
441
|
+
|
|
442
|
+
for (const item of meta.items) {
|
|
443
|
+
const balanceItem = await buildBalanceItem(item, {
|
|
444
|
+
legacyInvoice: stInvoice,
|
|
445
|
+
legacyMeta: meta,
|
|
446
|
+
payingOrganizationId,
|
|
447
|
+
ctx,
|
|
448
|
+
status: payment.status === PaymentStatus.Succeeded ? BalanceItemStatus.Due : BalanceItemStatus.Hidden,
|
|
449
|
+
createdAt,
|
|
450
|
+
paidAt: null,
|
|
451
|
+
});
|
|
452
|
+
await balanceItem.save();
|
|
453
|
+
result.balanceItemIds.push(balanceItem.id);
|
|
454
|
+
|
|
455
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
456
|
+
balanceItemPayment.organizationId = ctx.membershipOrganization.id;
|
|
457
|
+
balanceItemPayment.paymentId = payment.id;
|
|
458
|
+
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
459
|
+
balanceItemPayment.price = balanceItem.priceWithVAT;
|
|
460
|
+
await balanceItemPayment.save();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (payingOrganizationId) {
|
|
464
|
+
result.payingOrganizationIds.push(payingOrganizationId);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The pending invoice holds amounts that were charged but not yet invoiced/paid. Each line becomes an
|
|
472
|
+
* unpaid Due balance item: counted by getPaidOrPendingQuantity (so it is not re-charged) and shown as
|
|
473
|
+
* an open balance the organization still owes.
|
|
474
|
+
*/
|
|
475
|
+
export async function convertPendingInvoice(pendingInvoice: STPendingInvoice, ctx: ConversionContext): Promise<ConversionResult> {
|
|
476
|
+
const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
|
|
477
|
+
|
|
478
|
+
if (!pendingInvoice.organizationId) {
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const meta = pendingInvoice.meta;
|
|
483
|
+
const payingOrganizationId = pendingInvoice.organizationId;
|
|
484
|
+
|
|
485
|
+
for (const item of meta.items) {
|
|
486
|
+
const balanceItem = await buildBalanceItem(item, {
|
|
487
|
+
legacyInvoice: pendingInvoice,
|
|
488
|
+
legacyMeta: meta,
|
|
489
|
+
payingOrganizationId,
|
|
490
|
+
ctx,
|
|
491
|
+
status: BalanceItemStatus.Due,
|
|
492
|
+
createdAt: item.date ?? pendingInvoice.createdAt,
|
|
493
|
+
paidAt: null,
|
|
494
|
+
});
|
|
495
|
+
await balanceItem.save();
|
|
496
|
+
result.balanceItemIds.push(balanceItem.id);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (result.balanceItemIds.length > 0) {
|
|
500
|
+
result.payingOrganizationIds.push(payingOrganizationId);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Each legacy credit row becomes a balance item (preserving the ledger). A positive legacy change is
|
|
508
|
+
* credit we owe the organization, so it becomes a negative balance (priceOpen < 0). Expired credits are
|
|
509
|
+
* kept as Canceled so they preserve the history without affecting the open balance.
|
|
510
|
+
*/
|
|
511
|
+
export async function convertCredit(credit: STCredit, ctx: ConversionContext): Promise<ConversionResult> {
|
|
512
|
+
const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
|
|
513
|
+
|
|
514
|
+
if (await BalanceItem.getByID(credit.id)) {
|
|
515
|
+
// Already migrated
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const org = await Organization.getByID(credit.organizationId);
|
|
520
|
+
if (!org) {
|
|
521
|
+
// No longer existing organization (STCredit has no foreign key)
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const balanceItem = new BalanceItem();
|
|
526
|
+
balanceItem.id = credit.id;
|
|
527
|
+
balanceItem.organizationId = ctx.membershipOrganization.id;
|
|
528
|
+
balanceItem.payingOrganizationId = credit.organizationId;
|
|
529
|
+
balanceItem.type = BalanceItemType.ReferralDiscount;
|
|
530
|
+
balanceItem.description = credit.description;
|
|
531
|
+
balanceItem.amount = 1;
|
|
532
|
+
balanceItem.unitPrice = -credit.change;
|
|
533
|
+
balanceItem.VATPercentage = 21;
|
|
534
|
+
balanceItem.VATIncluded = false;
|
|
535
|
+
|
|
536
|
+
// ALWAYS CANCELED.
|
|
537
|
+
// We'll add an extra item for the remaining to fix
|
|
538
|
+
// that we don't support linking these balance items with payments correctly
|
|
539
|
+
// and also supportint expiry dates in balance items is not possible
|
|
540
|
+
// since how they were built in the past
|
|
541
|
+
balanceItem.status = BalanceItemStatus.Canceled; // We'll set this to due when we link a payment with it later
|
|
542
|
+
balanceItem.dueAt = null;
|
|
543
|
+
balanceItem.createdAt = credit.createdAt;
|
|
544
|
+
await balanceItem.save();
|
|
545
|
+
|
|
546
|
+
// Relink any used register code that was rewarded with this credit to the new balance item.
|
|
547
|
+
const usedRegisterCodes = await UsedRegisterCode.select().where('creditId', credit.id).fetch();
|
|
548
|
+
for (const usedRegisterCode of usedRegisterCodes) {
|
|
549
|
+
usedRegisterCode.balanceItemId = balanceItem.id;
|
|
550
|
+
await usedRegisterCode.save();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
result.balanceItemIds.push(balanceItem.id);
|
|
554
|
+
result.payingOrganizationIds.push(credit.organizationId);
|
|
555
|
+
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Each legacy credit row becomes a balance item (preserving the ledger). A positive legacy change is
|
|
561
|
+
* credit we owe the organization, so it becomes a negative balance (priceOpen < 0). Expired credits are
|
|
562
|
+
* kept as Canceled so they preserve the history without affecting the open balance.
|
|
563
|
+
*/
|
|
564
|
+
export async function convertRemainingCredit(organization: Organization, balance: number, ctx: ConversionContext): Promise<ConversionResult> {
|
|
565
|
+
const result: ConversionResult = { balanceItemIds: [], payingOrganizationIds: [] };
|
|
566
|
+
|
|
567
|
+
const balanceItem = new BalanceItem();
|
|
568
|
+
balanceItem.organizationId = ctx.membershipOrganization.id;
|
|
569
|
+
balanceItem.payingOrganizationId = organization.id;
|
|
570
|
+
balanceItem.type = BalanceItemType.ReferralDiscount;
|
|
571
|
+
balanceItem.description = 'Tegoed';
|
|
572
|
+
balanceItem.amount = 1;
|
|
573
|
+
balanceItem.unitPrice = -balance;
|
|
574
|
+
balanceItem.VATPercentage = 21;
|
|
575
|
+
balanceItem.VATIncluded = false;
|
|
576
|
+
balanceItem.status = BalanceItemStatus.Due;
|
|
577
|
+
balanceItem.dueAt = null;
|
|
578
|
+
balanceItem.createdAt = new Date();
|
|
579
|
+
balanceItem.VATExcempt = VATService.getVATExcempt({
|
|
580
|
+
company: organization.defaultCompanies[0] ?? null,
|
|
581
|
+
sellingOrganization: ctx.membershipOrganization,
|
|
582
|
+
type: 'services',
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
await balanceItem.save();
|
|
586
|
+
|
|
587
|
+
result.balanceItemIds.push(balanceItem.id);
|
|
588
|
+
result.payingOrganizationIds.push(organization.id);
|
|
589
|
+
|
|
590
|
+
return result;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function refreshCaches(membershipOrganizationId: string, results: ConversionResult[]) {
|
|
594
|
+
const balanceItemIds = [...new Set(results.flatMap(r => r.balanceItemIds))];
|
|
595
|
+
const payingOrganizationIds = [...new Set(results.flatMap(r => r.payingOrganizationIds))];
|
|
596
|
+
|
|
597
|
+
if (balanceItemIds.length === 0) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Recompute the cached pricePaid/pricePending/priceOpen and priceInvoiced values.
|
|
602
|
+
await BalanceItem.updatePricePaid(balanceItemIds);
|
|
603
|
+
await BalanceItem.updateInvoiced(balanceItemIds);
|
|
604
|
+
|
|
605
|
+
// Recompute the receivable balances per (paying) organization.
|
|
606
|
+
if (payingOrganizationIds.length > 0) {
|
|
607
|
+
await CachedBalance.updateForOrganizations(membershipOrganizationId, payingOrganizationIds);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Orchestrates the full conversion. Throws when no membership organization is configured, and is a
|
|
613
|
+
* no-op in platform mode (platform deployments never had legacy SAAS billing).
|
|
614
|
+
*/
|
|
615
|
+
export async function runConversion(): Promise<void> {
|
|
616
|
+
if (STAMHOOFD.userMode === 'platform') {
|
|
617
|
+
console.log('[LegacyBillingConverter] Skipping: running in platform mode');
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const platform = await Platform.getShared();
|
|
622
|
+
if (!platform.membershipOrganizationId) {
|
|
623
|
+
throw new Error('[LegacyBillingConverter] No membership organization configured: cannot migrate legacy billing');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const membershipOrganization = await Organization.getByID(platform.membershipOrganizationId, true);
|
|
627
|
+
|
|
628
|
+
const ctx: ConversionContext = {
|
|
629
|
+
membershipOrganization,
|
|
630
|
+
seller: membershipOrganization.defaultCompanies[0],
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// First credits
|
|
634
|
+
let results: ConversionResult[] = [];
|
|
635
|
+
|
|
636
|
+
// All canceled credits (so we can link them with used referral codes)
|
|
637
|
+
await SeedTools.loop({
|
|
638
|
+
query: STCredit.select(),
|
|
639
|
+
batchSize: 100,
|
|
640
|
+
useTransactionPerBatch: true,
|
|
641
|
+
action: async (credit: STCredit) => {
|
|
642
|
+
results.push(await convertCredit(credit, ctx));
|
|
643
|
+
|
|
644
|
+
if (QueryableModel.shutdownMigrations) {
|
|
645
|
+
throw new Error('Gracefully stopping - migration supports retry');
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Remaining (because we don't support expiry dates)
|
|
651
|
+
await SeedTools.loop({
|
|
652
|
+
query: Organization.select(),
|
|
653
|
+
batchSize: 100,
|
|
654
|
+
useTransactionPerBatch: true,
|
|
655
|
+
action: async (organization: Organization) => {
|
|
656
|
+
const { balance } = await STCredit.getBalance(organization.id);
|
|
657
|
+
if (balance > 0) {
|
|
658
|
+
results.push(await convertRemainingCredit(organization, balance, ctx));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (QueryableModel.shutdownMigrations) {
|
|
662
|
+
throw new Error('Gracefully stopping - migration supports retry');
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
await refreshCaches(membershipOrganization.id, results);
|
|
668
|
+
const systemUser = await User.getSystem();
|
|
669
|
+
|
|
670
|
+
// 1. Invoices: paid (full reconstruction) or in-progress (Hidden + linked pending payment).
|
|
671
|
+
async function processInvoice(stInvoice: STInvoice, results: ConversionResult[]) {
|
|
672
|
+
let payment = stInvoice.paymentId ? (await Payment.getByID(stInvoice.paymentId) ?? null) : null;
|
|
673
|
+
try {
|
|
674
|
+
if (stInvoice.number !== null) {
|
|
675
|
+
if (!payment) {
|
|
676
|
+
if (stInvoice.meta.stripeAccountId) {
|
|
677
|
+
// Create a new unpaid payment
|
|
678
|
+
payment = new Payment();
|
|
679
|
+
payment.adminUserId = systemUser.id;
|
|
680
|
+
|
|
681
|
+
// Who will receive this money?
|
|
682
|
+
payment.organizationId = membershipOrganization.id;
|
|
683
|
+
|
|
684
|
+
// Who paid
|
|
685
|
+
payment.payingOrganizationId = stInvoice.payingOrganizationId;
|
|
686
|
+
payment.customer = null; // will get filled in later
|
|
687
|
+
|
|
688
|
+
payment.status = PaymentStatus.Succeeded;
|
|
689
|
+
payment.price = stInvoice.meta.totalPrice * 100;
|
|
690
|
+
payment.roundingAmount = 0;
|
|
691
|
+
payment.method = PaymentMethod.AccountDeductions;
|
|
692
|
+
payment.type = PaymentType.Payment;
|
|
693
|
+
payment.createMandate = null;
|
|
694
|
+
|
|
695
|
+
payment.provider = PaymentProvider.Stripe;
|
|
696
|
+
payment.createdAt = stInvoice.createdAt;
|
|
697
|
+
payment.paidAt = stInvoice.createdAt;
|
|
698
|
+
await payment.save();
|
|
699
|
+
} else {
|
|
700
|
+
if (stInvoice.meta.totalPrice < 0) {
|
|
701
|
+
// Refund
|
|
702
|
+
payment = new Payment();
|
|
703
|
+
payment.adminUserId = systemUser.id;
|
|
704
|
+
|
|
705
|
+
// Who will receive this money?
|
|
706
|
+
payment.organizationId = membershipOrganization.id;
|
|
707
|
+
|
|
708
|
+
// Who paid
|
|
709
|
+
payment.payingOrganizationId = stInvoice.payingOrganizationId;
|
|
710
|
+
payment.customer = null; // will get filled in later
|
|
711
|
+
|
|
712
|
+
payment.status = PaymentStatus.Succeeded;
|
|
713
|
+
payment.price = stInvoice.meta.totalPrice * 100;
|
|
714
|
+
payment.roundingAmount = 0;
|
|
715
|
+
payment.method = PaymentMethod.Unknown;
|
|
716
|
+
payment.type = PaymentType.Refund;
|
|
717
|
+
payment.createMandate = null;
|
|
718
|
+
payment.createdAt = stInvoice.createdAt;
|
|
719
|
+
payment.paidAt = stInvoice.createdAt;
|
|
720
|
+
|
|
721
|
+
// payment.provider = PaymentProvider.Mollie;
|
|
722
|
+
// We currently don't have an ID stored for these refunds: to complete in the future after migration
|
|
723
|
+
await payment.save();
|
|
724
|
+
} else {
|
|
725
|
+
throw new Error('Unexpected missing payment for invoice that was not for stripe invoice nor refund ' + stInvoice.id);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
results.push(await convertPaidInvoice(stInvoice, payment, ctx));
|
|
730
|
+
} else if (payment) {
|
|
731
|
+
results.push(await convertInProgressInvoice(stInvoice, payment, ctx));
|
|
732
|
+
}
|
|
733
|
+
} catch (e) {
|
|
734
|
+
console.error('Failure at ' + stInvoice.id, e);
|
|
735
|
+
throw e;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
results = [];
|
|
740
|
+
|
|
741
|
+
await SeedTools.loop({
|
|
742
|
+
query: STInvoice.select(),
|
|
743
|
+
batchSize: 100,
|
|
744
|
+
useTransactionPerBatch: true,
|
|
745
|
+
action: async (invoice: STInvoice) => {
|
|
746
|
+
// Make sure any negative invoices exist, because otherwise the foreign key will fail
|
|
747
|
+
if (invoice.negativeInvoiceId) {
|
|
748
|
+
const negativeInvoice = await STInvoice.getByID(invoice.negativeInvoiceId);
|
|
749
|
+
if (negativeInvoice) {
|
|
750
|
+
await processInvoice(negativeInvoice, results);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
await processInvoice(invoice, results);
|
|
754
|
+
|
|
755
|
+
if (QueryableModel.shutdownMigrations) {
|
|
756
|
+
throw new Error('Gracefully stopping - migration supports retry');
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
await refreshCaches(membershipOrganization.id, results);
|
|
762
|
+
|
|
763
|
+
// 2. Pending invoices (one per organization).
|
|
764
|
+
results = [];
|
|
765
|
+
await SeedTools.loop({
|
|
766
|
+
query: STPendingInvoice.select(),
|
|
767
|
+
batchSize: 100,
|
|
768
|
+
useTransactionPerBatch: true,
|
|
769
|
+
action: async (pendingInvoice: STPendingInvoice) => {
|
|
770
|
+
results.push(await convertPendingInvoice(pendingInvoice, ctx));
|
|
771
|
+
|
|
772
|
+
if (QueryableModel.shutdownMigrations) {
|
|
773
|
+
throw new Error('Gracefully stopping - migration supports retry');
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
await refreshCaches(membershipOrganization.id, results);
|
|
778
|
+
|
|
779
|
+
// Fix bbbaebe0-e749-4d9d-b3d8-327f43741f95 being succeeded but was refunded
|
|
780
|
+
const payment = await Payment.getByID('bbbaebe0-e749-4d9d-b3d8-327f43741f95');
|
|
781
|
+
if (payment) {
|
|
782
|
+
payment.status = PaymentStatus.Failed;
|
|
783
|
+
await payment.save();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
console.log('[LegacyBillingConverter] Conversion finished');
|
|
787
|
+
}
|