@stamhoofd/backend 2.115.1 → 2.117.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/migrations.ts +1 -1
- package/package.json +10 -10
- package/src/audit-logs/DocumentTemplateLogger.ts +1 -1
- package/src/audit-logs/EmailAddressLogger.ts +1 -1
- package/src/audit-logs/EmailLogger.ts +1 -1
- package/src/audit-logs/EmailTemplateLogger.ts +1 -1
- package/src/audit-logs/EventLogger.ts +1 -1
- package/src/audit-logs/GroupLogger.ts +1 -1
- package/src/audit-logs/MemberLogger.ts +1 -1
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +1 -1
- package/src/audit-logs/MemberResponsibilityRecordLogger.ts +1 -1
- package/src/audit-logs/ModelLogger.ts +27 -19
- package/src/audit-logs/OrderLogger.ts +1 -1
- package/src/audit-logs/OrganizationLogger.ts +23 -3
- package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +1 -1
- package/src/audit-logs/PaymentLogger.ts +1 -1
- package/src/audit-logs/PlatformLogger.ts +1 -1
- package/src/audit-logs/RegistrationLogger.ts +1 -1
- package/src/audit-logs/RegistrationPeriodLogger.ts +1 -1
- package/src/audit-logs/StripeAccountLogger.ts +1 -1
- package/src/audit-logs/UserLogger.ts +1 -1
- package/src/audit-logs/WebshopLogger.ts +1 -1
- package/src/audit-logs/init.ts +40 -0
- package/src/boot.ts +6 -4
- package/src/crons/amazon-ses.ts +1 -1
- package/src/crons/balance-emails.ts +1 -1
- package/src/crons/clearExcelCache.test.ts +1 -1
- package/src/crons/delete-archived-data.ts +47 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +1 -1
- package/src/crons/index.ts +1 -0
- package/src/crons.ts +3 -3
- package/src/debug.ts +230 -0
- package/src/email-recipient-loaders/documents.ts +1 -1
- package/src/email-recipient-loaders/members.ts +1 -1
- package/src/email-recipient-loaders/orders.ts +3 -3
- package/src/email-recipient-loaders/payments.ts +118 -353
- package/src/email-replacements/getEmailReplacementsForPayment.ts +321 -0
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +9 -7
- package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +2 -2
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +1 -1
- package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +16 -50
- package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +2 -2
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +4 -4
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +21 -6
- package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +9 -7
- package/src/endpoints/auth/CreateAdminEndpoint.ts +2 -2
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +2 -2
- package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +2 -2
- package/src/endpoints/auth/GetUserEndpoint.test.ts +2 -2
- package/src/endpoints/auth/GetUserEndpoint.ts +2 -2
- package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
- package/src/endpoints/auth/OpenIDConnectCallbackEndpoint.ts +2 -2
- package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +2 -2
- package/src/endpoints/auth/PatchUserEndpoint.ts +4 -4
- package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
- package/src/endpoints/auth/SignupEndpoint.ts +1 -1
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +1 -1
- package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +1 -1
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
- package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +191 -7
- package/src/endpoints/global/billing/DeactivatePackageEndpoint.ts +64 -0
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +2 -2
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +3 -3
- package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +1 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +2 -2
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +3 -3
- package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +62 -16
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +6 -6
- package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +2 -2
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +2 -2
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +4 -4
- package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +1 -1
- package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +1 -1
- package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +2 -2
- package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +4 -4
- package/src/endpoints/global/events/GetEventsEndpoint.ts +4 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -2
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +3 -3
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +5 -5
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +4 -4
- package/src/endpoints/global/files/GetFileCache.ts +2 -2
- package/src/endpoints/global/files/UploadFile.ts +2 -2
- package/src/endpoints/global/files/UploadImage.ts +1 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +3 -3
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +4 -4
- package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +4 -4
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +3 -3
- package/src/endpoints/global/members/GetMembersEndpoint.ts +4 -16
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +8 -7
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +1 -1
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +2 -2
- package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +23 -12
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +2 -2
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +1 -1
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +1 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +2 -2
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +3 -3
- package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -1
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +2 -2
- package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +2 -2
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -7
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +2 -2
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +1 -2
- package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +2 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +24 -389
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +3 -3
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
- package/src/endpoints/global/sso/GetSSOEndpoint.ts +2 -2
- package/src/endpoints/global/sso/SetSSOEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +55 -0
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsCountEndpoint.ts +43 -0
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.ts +160 -0
- package/src/endpoints/organization/dashboard/{payments → balance-items}/PatchBalanceItemsEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.test.ts +3 -3
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
- package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +37 -6
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +53 -0
- package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +1 -1
- 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 +2 -2
- package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +12 -1
- package/src/endpoints/organization/dashboard/organization/SearchUitpasOrganizersEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +12 -12
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesCountEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +3 -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 +3 -3
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +3 -3
- 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.test.ts +3 -3
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
- 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 +1 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +3 -3
- package/src/endpoints/organization/shared/GetDocumentHtml.ts +1 -1
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +2 -2
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +2 -2
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +1 -1
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -2
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +4 -4
- package/src/excel-loaders/balance-items.ts +268 -0
- package/src/excel-loaders/event-notifications.ts +3 -3
- package/src/excel-loaders/index.ts +6 -5
- package/src/excel-loaders/organizations.ts +4 -4
- package/src/excel-loaders/payments.ts +11 -3
- package/src/excel-loaders/receivable-balances.ts +2 -2
- package/src/helpers/AddressValidator.test.ts +1 -1
- package/src/helpers/AddressValidator.ts +1 -1
- package/src/helpers/AdminPermissionChecker.ts +20 -17
- package/src/helpers/AuthenticatedStructures.ts +194 -109
- package/src/helpers/Context.ts +2 -0
- package/src/helpers/EmailResumer.ts +1 -1
- package/src/helpers/FlagMomentCleanup.ts +1 -1
- package/src/helpers/ForwardHandler.test.ts +1 -1
- package/src/helpers/ForwardHandler.ts +1 -1
- package/src/helpers/GlobalHelper.ts +4 -4
- package/src/helpers/GroupBuilder.ts +417 -0
- package/src/helpers/GroupedThrottledQueue.test.ts +1 -1
- package/src/helpers/GroupedThrottledQueue.ts +1 -1
- package/src/helpers/MemberUserSyncer.test.ts +1 -1
- package/src/helpers/MemberUserSyncer.ts +1 -1
- package/src/helpers/PeriodHelper.ts +5 -45
- package/src/helpers/ServiceFeeHelper.ts +5 -1
- package/src/helpers/SetupStepUpdater.ts +5 -4
- package/src/helpers/TagHelper.ts +1 -1
- package/src/helpers/ThrottledQueue.test.ts +1 -1
- package/src/helpers/ViesHelper.ts +9 -0
- package/src/helpers/email-html-helpers.ts +0 -41
- package/src/helpers/outstandingBalanceJoin.ts +3 -1
- package/src/middleware/ContextMiddleware.ts +1 -1
- package/src/seeds/1726572303-schedule-stock-updates.ts +1 -1
- package/src/seeds/1726847064-setup-steps.ts +1 -1
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +1 -1
- package/src/seeds/1740046783-update-membership.ts +1 -1
- package/src/seeds/1754560914-groups-prices.test.ts +1 -1
- package/src/seeds/1755876819-remove-duplicate-members.ts +1 -1
- package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +1 -1
- package/src/seeds/1761665607-sync-member-users.ts +1 -1
- package/src/seeds/data/default-email-templates.sql +1 -1
- package/src/seeds-temporary/1732117645-move-rrn.ts +1 -1
- package/src/services/AuditLogService.ts +1 -41
- package/src/services/BalanceItemPaymentService.ts +1 -1
- package/src/services/BalanceItemService.ts +12 -5
- package/src/services/EventNotificationService.ts +3 -3
- package/src/services/InvoiceService.ts +131 -17
- package/src/services/MemberRecordStore.ts +1 -1
- package/src/services/PaymentReallocationService.test.ts +9 -10
- package/src/services/PaymentReallocationService.ts +1 -1
- package/src/services/PaymentService.ts +560 -18
- package/src/services/PlatformMembershipService.ts +3 -3
- package/src/services/RegistrationService.ts +3 -3
- package/src/services/SSOService.ts +2 -2
- package/src/services/STPackageService.ts +241 -0
- package/src/services/uitpas/UitpasService.test.ts +1 -1
- package/src/sql-filters/balance-items.ts +56 -0
- package/src/sql-filters/emails.ts +1 -1
- package/src/sql-filters/event-notifications.ts +2 -2
- package/src/sql-filters/events.ts +51 -0
- package/src/sql-filters/members.ts +38 -2
- package/src/sql-filters/receivable-balances.ts +3 -3
- package/src/sql-filters/users.ts +10 -0
- package/src/sql-sorters/balance-items.ts +36 -0
- package/src/sql-sorters/document-templates.ts +2 -2
- package/src/sql-sorters/documents.ts +2 -2
- package/src/sql-sorters/event-notifications.ts +1 -1
- package/src/sql-sorters/members.ts +2 -2
- package/src/sql-sorters/orders.ts +2 -2
- package/src/sql-sorters/tickets.ts +2 -2
- package/tests/actions/patchOrganizationMember.ts +3 -3
- package/tests/actions/patchPaymentStatus.ts +3 -3
- package/tests/actions/patchUserMember.ts +2 -2
- package/tests/assertions/assertBalances.ts +1 -1
- package/tests/e2e/api-rate-limits.test.ts +3 -3
- package/tests/e2e/charge-members.test.ts +14 -14
- package/tests/e2e/documents.test.ts +8 -8
- package/tests/e2e/private-files.test.ts +4 -4
- package/tests/e2e/register.test.ts +10 -10
- package/tests/e2e/stock.test.ts +4 -4
- package/tests/e2e/tickets.test.ts +4 -4
- package/tests/helpers/TestServer.ts +2 -2
- package/tests/init/index.ts +7 -7
- package/tests/init/initAdmin.ts +1 -1
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +0 -67
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import createMollieClient, { PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
|
|
2
|
-
import {
|
|
1
|
+
import createMollieClient, { PaymentMethod as molliePaymentMethod, PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
|
+
import { BalanceItem, BalanceItemPayment, Group, Member, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, sendEmailTemplate, User } from '@stamhoofd/models';
|
|
3
4
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
|
-
import { AuditLogSource,
|
|
5
|
+
import { AuditLogSource, BalanceItemType, Checkoutable, Country, EmailTemplateType, PaymentConfiguration, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, PaymentType, Recipient, VATExcemptReason, Version } from '@stamhoofd/structures';
|
|
6
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
+
import { buildReplacementOptions, getEmailReplacementsForPayment } from '../email-replacements/getEmailReplacementsForPayment.js';
|
|
5
8
|
import { BuckarooHelper } from '../helpers/BuckarooHelper.js';
|
|
9
|
+
import { Context } from '../helpers/Context.js';
|
|
10
|
+
import { ServiceFeeHelper } from '../helpers/ServiceFeeHelper.js';
|
|
6
11
|
import { StripeHelper } from '../helpers/StripeHelper.js';
|
|
7
12
|
import { AuditLogService } from './AuditLogService.js';
|
|
8
13
|
import { BalanceItemPaymentService } from './BalanceItemPaymentService.js';
|
|
9
14
|
import { BalanceItemService } from './BalanceItemService.js';
|
|
10
15
|
|
|
11
|
-
export
|
|
12
|
-
async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
16
|
+
export class PaymentService {
|
|
17
|
+
static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
13
18
|
if (payment.status === status) {
|
|
14
19
|
return;
|
|
15
20
|
}
|
|
@@ -32,6 +37,9 @@ export const PaymentService = {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
|
|
40
|
+
|
|
41
|
+
// Flush caches so data is up to date in response
|
|
42
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
35
43
|
});
|
|
36
44
|
return;
|
|
37
45
|
}
|
|
@@ -56,11 +64,14 @@ export const PaymentService = {
|
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
|
|
67
|
+
|
|
68
|
+
// Flush caches so data is up to date in response
|
|
69
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
59
70
|
});
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
// Moved to failed
|
|
63
|
-
if (status
|
|
74
|
+
if (status === PaymentStatus.Failed) {
|
|
64
75
|
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
65
76
|
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
66
77
|
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
@@ -71,6 +82,9 @@ export const PaymentService = {
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
|
|
85
|
+
|
|
86
|
+
// Flush caches so data is up to date in response
|
|
87
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
74
88
|
});
|
|
75
89
|
}
|
|
76
90
|
|
|
@@ -86,15 +100,18 @@ export const PaymentService = {
|
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
await BalanceItemService.updatePaidAndPending(balanceItemPayments.map(p => p.balanceItem));
|
|
103
|
+
|
|
104
|
+
// Flush caches so data is up to date in response
|
|
105
|
+
await BalanceItemService.flushCaches(organization.id);
|
|
89
106
|
});
|
|
90
107
|
}
|
|
91
108
|
});
|
|
92
|
-
}
|
|
109
|
+
}
|
|
93
110
|
|
|
94
111
|
/**
|
|
95
112
|
* ID of payment is needed because of race conditions (need to fetch payment in a race condition save queue)
|
|
96
113
|
*/
|
|
97
|
-
async pollStatus(paymentId: string, org: Organization | null, cancel = false): Promise<Payment | undefined> {
|
|
114
|
+
static async pollStatus(paymentId: string, org: Organization | null, cancel = false): Promise<Payment | undefined> {
|
|
98
115
|
// Prevent polling the same payment multiple times at the same time: create a queue to prevent races
|
|
99
116
|
return await QueueHandler.schedule('payments/' + paymentId, async () => {
|
|
100
117
|
// Get a new copy of the payment (is required to prevent concurreny bugs)
|
|
@@ -141,7 +158,7 @@ export const PaymentService = {
|
|
|
141
158
|
else if (payment.provider === PaymentProvider.Mollie) {
|
|
142
159
|
// check status via mollie
|
|
143
160
|
const molliePayments = await MolliePayment.where({ paymentId: payment.id }, { limit: 1 });
|
|
144
|
-
if (molliePayments.length
|
|
161
|
+
if (molliePayments.length === 1) {
|
|
145
162
|
const molliePayment = molliePayments[0];
|
|
146
163
|
// check status
|
|
147
164
|
const token = await MollieToken.getTokenFor(organization.id);
|
|
@@ -301,23 +318,23 @@ export const PaymentService = {
|
|
|
301
318
|
return payment;
|
|
302
319
|
});
|
|
303
320
|
});
|
|
304
|
-
}
|
|
321
|
+
}
|
|
305
322
|
|
|
306
|
-
isManualExpired(status: PaymentStatus, payment: Payment) {
|
|
307
|
-
if ((status
|
|
323
|
+
static isManualExpired(status: PaymentStatus, payment: Payment) {
|
|
324
|
+
if ((status === PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
308
325
|
// If payment is not succeeded after one day, mark as failed
|
|
309
326
|
if (payment.createdAt < new Date(new Date().getTime() - 60 * 1000 * 60 * 24)) {
|
|
310
327
|
return true;
|
|
311
328
|
}
|
|
312
329
|
}
|
|
313
330
|
return false;
|
|
314
|
-
}
|
|
331
|
+
}
|
|
315
332
|
|
|
316
333
|
/**
|
|
317
334
|
* Try to cancel a payment that is still pending
|
|
318
335
|
*/
|
|
319
|
-
shouldTryToCancel(status: PaymentStatus, payment: Payment): boolean {
|
|
320
|
-
if ((status
|
|
336
|
+
static shouldTryToCancel(status: PaymentStatus, payment: Payment): boolean {
|
|
337
|
+
if ((status === PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
321
338
|
let timeout = STAMHOOFD.environment === 'development' ? 60 * 1000 * 2 : 60 * 1000 * 30;
|
|
322
339
|
|
|
323
340
|
// If payconiq and not yet 'identified' (scanned), cancel after 5 minutes
|
|
@@ -330,7 +347,7 @@ export const PaymentService = {
|
|
|
330
347
|
}
|
|
331
348
|
}
|
|
332
349
|
return false;
|
|
333
|
-
}
|
|
350
|
+
}
|
|
334
351
|
|
|
335
352
|
/**
|
|
336
353
|
* Say the total amount to pay is 15,238 because (e.g. because of VAT). In that case,
|
|
@@ -341,7 +358,7 @@ export const PaymentService = {
|
|
|
341
358
|
*
|
|
342
359
|
* 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
|
|
343
360
|
*/
|
|
344
|
-
round(payment: Payment) {
|
|
361
|
+
static round(payment: Payment) {
|
|
345
362
|
const amount = payment.price;
|
|
346
363
|
const rounded = Payment.roundPrice(payment.price);
|
|
347
364
|
const difference = rounded - amount;
|
|
@@ -358,5 +375,530 @@ export const PaymentService = {
|
|
|
358
375
|
|
|
359
376
|
// Change payment total price
|
|
360
377
|
payment.price += difference;
|
|
361
|
-
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
static async createPayment({ balanceItems, organization, user, members, checkout, payingOrganization, serviceFeeType }: {
|
|
381
|
+
balanceItems: Map<BalanceItem, number>;
|
|
382
|
+
organization: Organization;
|
|
383
|
+
user: User;
|
|
384
|
+
members?: Member[];
|
|
385
|
+
checkout: Pick<Checkoutable<never>, 'paymentMethod' | 'totalPrice' | 'customer' | 'cancelUrl' | 'redirectUrl'>;
|
|
386
|
+
payingOrganization?: Organization | null;
|
|
387
|
+
serviceFeeType: 'webshop' | 'members' | 'tickets' | 'system';
|
|
388
|
+
}) {
|
|
389
|
+
// Calculate total price to pay
|
|
390
|
+
let totalPrice = 0;
|
|
391
|
+
const names: {
|
|
392
|
+
firstName: string;
|
|
393
|
+
lastName: string;
|
|
394
|
+
name: string;
|
|
395
|
+
}[] = [];
|
|
396
|
+
let hasNegative = false;
|
|
397
|
+
|
|
398
|
+
for (const [balanceItem, price] of balanceItems) {
|
|
399
|
+
if (organization.id !== balanceItem.organizationId) {
|
|
400
|
+
throw new Error('Unexpected balance item from other organization');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (price > 0 && price > Math.max(balanceItem.priceOpen, balanceItem.priceDue - balanceItem.pricePaid)) {
|
|
404
|
+
throw new SimpleError({
|
|
405
|
+
code: 'invalid_data',
|
|
406
|
+
message: $t(`38ddccb2-7cf6-4b47-aa71-d11ad73386d8`),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (price < 0 && price < Math.min(balanceItem.priceOpen, balanceItem.priceDue - balanceItem.pricePaid)) {
|
|
411
|
+
throw new SimpleError({
|
|
412
|
+
code: 'invalid_data',
|
|
413
|
+
message: $t(`dd14a1d9-c569-4d5e-bb26-569ecede4c52`),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (price < 0) {
|
|
418
|
+
hasNegative = true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
totalPrice += price;
|
|
422
|
+
|
|
423
|
+
if (price > 0 && balanceItem.memberId && balanceItem.type === BalanceItemType.Registration) {
|
|
424
|
+
const member = members?.find(m => m.id === balanceItem.memberId);
|
|
425
|
+
if (!member) {
|
|
426
|
+
throw new SimpleError({
|
|
427
|
+
code: 'invalid_data',
|
|
428
|
+
message: $t(`e64b8269-1cda-434d-8d6f-35be23a9d6e9`),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
names.push({
|
|
432
|
+
firstName: member.firstName,
|
|
433
|
+
lastName: member.lastName,
|
|
434
|
+
name: member.details.name,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (totalPrice < 0) {
|
|
440
|
+
// todo: try to make it non-negative by reducing some balance items
|
|
441
|
+
throw new SimpleError({
|
|
442
|
+
code: 'negative_price',
|
|
443
|
+
message: $t(`725715e5-b0ac-43c1-adef-dd42b8907327`),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (checkout.totalPrice !== null && totalPrice !== checkout.totalPrice) {
|
|
448
|
+
// Changed!
|
|
449
|
+
throw new SimpleError({
|
|
450
|
+
code: 'changed_price',
|
|
451
|
+
message: $t(`e424d549-2bb8-4103-9a14-ac4063d7d454`, { total: Formatter.price(totalPrice) }),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const payment = new Payment();
|
|
456
|
+
|
|
457
|
+
// Who will receive this money?
|
|
458
|
+
payment.organizationId = organization.id;
|
|
459
|
+
|
|
460
|
+
// Who paid
|
|
461
|
+
payment.payingUserId = user.id;
|
|
462
|
+
payment.payingOrganizationId = payingOrganization?.id ?? null;
|
|
463
|
+
|
|
464
|
+
// Fill in customer default value
|
|
465
|
+
payment.customer = PaymentCustomer.create({
|
|
466
|
+
firstName: user.firstName,
|
|
467
|
+
lastName: user.lastName,
|
|
468
|
+
email: user.email,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Use structured transfer description prefix
|
|
472
|
+
let prefix = '';
|
|
473
|
+
|
|
474
|
+
if (payingOrganization) {
|
|
475
|
+
if (totalPrice !== 0 || hasNegative || checkout.customer) {
|
|
476
|
+
if (!checkout.customer) {
|
|
477
|
+
throw new SimpleError({
|
|
478
|
+
code: 'missing_fields',
|
|
479
|
+
message: 'customer is required when paying as an organization',
|
|
480
|
+
human: $t(`d483aa9a-289c-4c59-955f-d2f99ec533ab`),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!checkout.customer.company) {
|
|
485
|
+
throw new SimpleError({
|
|
486
|
+
code: 'missing_fields',
|
|
487
|
+
message: 'customer.company is required when paying as an organization',
|
|
488
|
+
human: $t(`bc89861d-a799-4100-b06c-29d6808ba8d2`),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Search company id
|
|
493
|
+
// this avoids needing to check the VAT number every time
|
|
494
|
+
const id = checkout.customer.company.id;
|
|
495
|
+
const foundCompany = payingOrganization.meta.companies.find(c => c.id === id);
|
|
496
|
+
|
|
497
|
+
if (!foundCompany) {
|
|
498
|
+
throw new SimpleError({
|
|
499
|
+
code: 'invalid_data',
|
|
500
|
+
message: $t(`0ab71307-8f4f-4701-b120-b552a1b6bdd0`),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
payment.customer.company = foundCompany;
|
|
505
|
+
|
|
506
|
+
const orgNumber = parseInt(payingOrganization.uri);
|
|
507
|
+
|
|
508
|
+
if (orgNumber !== 0 && !isNaN(orgNumber)) {
|
|
509
|
+
prefix = orgNumber + '';
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
// Zero amount payment (without refunds) without specifying a company will just use the default company to link to the payment
|
|
514
|
+
// It doesn't really matter since the price is zero and we won't invoice it.
|
|
515
|
+
const company = payingOrganization.meta.companies[0];
|
|
516
|
+
if (company) {
|
|
517
|
+
payment.customer.company = company;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Validate VAT rates for this customer
|
|
523
|
+
await this.validateVATRates({ customer: payment.customer, organization, balanceItems });
|
|
524
|
+
|
|
525
|
+
payment.status = PaymentStatus.Created;
|
|
526
|
+
payment.paidAt = null;
|
|
527
|
+
payment.price = totalPrice;
|
|
528
|
+
PaymentService.round(payment);
|
|
529
|
+
totalPrice = payment.price;
|
|
530
|
+
|
|
531
|
+
if (totalPrice === 0) {
|
|
532
|
+
payment.status = PaymentStatus.Succeeded;
|
|
533
|
+
payment.paidAt = new Date();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Validate payment method after customer is defined
|
|
537
|
+
const paymentConfiguration = organization.meta.registrationPaymentConfiguration;
|
|
538
|
+
const privatePaymentConfiguration = organization.privateMeta.registrationPaymentConfiguration;
|
|
539
|
+
|
|
540
|
+
payment.method = checkout.paymentMethod ?? PaymentMethod.Unknown;
|
|
541
|
+
await this.validatePaymentMethod({ payment, balanceItems, paymentConfiguration });
|
|
542
|
+
|
|
543
|
+
// Validate URL's for online payments before saving the payment
|
|
544
|
+
if ((payment.method !== PaymentMethod.Transfer && payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) && (!checkout.redirectUrl || !checkout.cancelUrl)) {
|
|
545
|
+
throw new SimpleError({
|
|
546
|
+
code: 'missing_fields',
|
|
547
|
+
message: 'redirectUrl or cancelUrl is missing and is required for non-zero online payments',
|
|
548
|
+
human: $t(`ebe54b63-2de6-4f22-a5ed-d3fe65194562`),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Add transfer description
|
|
553
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
554
|
+
// remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
|
|
555
|
+
payment.transferSettings = organization.mappedTransferSettings;
|
|
556
|
+
|
|
557
|
+
if (!payment.transferSettings.iban) {
|
|
558
|
+
throw new SimpleError({
|
|
559
|
+
code: 'no_iban',
|
|
560
|
+
message: 'No IBAN',
|
|
561
|
+
human: $t(`cc8b5066-a7e4-4eae-b556-f56de5d3502c`),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const groupedNames = Formatter.groupNamesByFamily(names);
|
|
566
|
+
payment.generateDescription(
|
|
567
|
+
organization,
|
|
568
|
+
groupedNames,
|
|
569
|
+
{
|
|
570
|
+
name: groupedNames,
|
|
571
|
+
naam: groupedNames,
|
|
572
|
+
email: user.email,
|
|
573
|
+
prefix,
|
|
574
|
+
},
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Determine the payment provider
|
|
579
|
+
// Throws if invalid
|
|
580
|
+
const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, privatePaymentConfiguration);
|
|
581
|
+
payment.provider = provider;
|
|
582
|
+
payment.stripeAccountId = stripeAccount?.id ?? null;
|
|
583
|
+
ServiceFeeHelper.setServiceFee(payment, organization, serviceFeeType, [...balanceItems.entries()].map(([_, p]) => p));
|
|
584
|
+
|
|
585
|
+
await payment.save();
|
|
586
|
+
let paymentUrl: string | null = null;
|
|
587
|
+
let paymentQRCode: string | null = null;
|
|
588
|
+
const description = organization.name + ' ' + payment.id;
|
|
589
|
+
|
|
590
|
+
// Create balance item payments
|
|
591
|
+
const balanceItemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = [];
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
for (const [balanceItem, price] of balanceItems) {
|
|
595
|
+
// Create one balance item payment to pay it in one payment
|
|
596
|
+
const balanceItemPayment = new BalanceItemPayment();
|
|
597
|
+
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
598
|
+
balanceItemPayment.paymentId = payment.id;
|
|
599
|
+
balanceItemPayment.organizationId = organization.id;
|
|
600
|
+
balanceItemPayment.price = price;
|
|
601
|
+
await balanceItemPayment.save();
|
|
602
|
+
|
|
603
|
+
balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Update balance items
|
|
607
|
+
if (payment.method === PaymentMethod.Transfer) {
|
|
608
|
+
// Send a small reminder email
|
|
609
|
+
try {
|
|
610
|
+
await this.sendTransferEmail(user, organization, payment);
|
|
611
|
+
}
|
|
612
|
+
catch (e) {
|
|
613
|
+
console.error('Failed to send transfer email');
|
|
614
|
+
console.error(e);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (payment.method !== PaymentMethod.PointOfSale && payment.method !== PaymentMethod.Unknown) {
|
|
618
|
+
if (!checkout.redirectUrl || !checkout.cancelUrl) {
|
|
619
|
+
throw new Error('Should have been caught earlier');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const _redirectUrl = new URL(checkout.redirectUrl);
|
|
623
|
+
_redirectUrl.searchParams.set('paymentId', payment.id);
|
|
624
|
+
_redirectUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
|
|
625
|
+
|
|
626
|
+
const _cancelUrl = new URL(checkout.cancelUrl);
|
|
627
|
+
_cancelUrl.searchParams.set('paymentId', payment.id);
|
|
628
|
+
_cancelUrl.searchParams.set('cancel', 'true');
|
|
629
|
+
_cancelUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
|
|
630
|
+
|
|
631
|
+
const redirectUrl = _redirectUrl.href;
|
|
632
|
+
const cancelUrl = _cancelUrl.href;
|
|
633
|
+
|
|
634
|
+
const webhookUrl = 'https://' + organization.getApiHost() + '/v' + Version + '/payments/' + encodeURIComponent(payment.id) + '?exchange=true';
|
|
635
|
+
|
|
636
|
+
if (payment.provider === PaymentProvider.Stripe) {
|
|
637
|
+
const stripeResult = await StripeHelper.createPayment({
|
|
638
|
+
payment,
|
|
639
|
+
stripeAccount,
|
|
640
|
+
redirectUrl,
|
|
641
|
+
cancelUrl,
|
|
642
|
+
statementDescriptor: organization.name,
|
|
643
|
+
metadata: {
|
|
644
|
+
organization: organization.id,
|
|
645
|
+
user: user.id,
|
|
646
|
+
payment: payment.id,
|
|
647
|
+
},
|
|
648
|
+
i18n: Context.i18n,
|
|
649
|
+
lineItems: balanceItemPayments,
|
|
650
|
+
organization,
|
|
651
|
+
customer: {
|
|
652
|
+
name: user.name ?? names[0]?.name ?? $t(`bd1e59c8-3d4c-4097-ab35-0ce7b20d0e50`),
|
|
653
|
+
email: user.email,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
paymentUrl = stripeResult.paymentUrl;
|
|
657
|
+
}
|
|
658
|
+
else if (payment.provider === PaymentProvider.Mollie) {
|
|
659
|
+
// Mollie payment
|
|
660
|
+
const token = await MollieToken.getTokenFor(organization.id);
|
|
661
|
+
if (!token) {
|
|
662
|
+
throw new SimpleError({
|
|
663
|
+
code: '',
|
|
664
|
+
message: $t(`b77e1f68-8928-42a2-802b-059fa73bedc3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
const profileId = organization.privateMeta.mollieProfile?.id ?? await token.getProfileId(organization.getHost());
|
|
668
|
+
if (!profileId) {
|
|
669
|
+
throw new SimpleError({
|
|
670
|
+
code: '',
|
|
671
|
+
message: $t(`5574469f-8eee-47fe-9fb6-1b097142ac75`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
|
|
675
|
+
const locale = Context.i18n.locale.replace('-', '_');
|
|
676
|
+
const molliePayment = await mollieClient.payments.create({
|
|
677
|
+
amount: {
|
|
678
|
+
currency: 'EUR',
|
|
679
|
+
value: (totalPrice / 100).toFixed(2),
|
|
680
|
+
},
|
|
681
|
+
method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
|
|
682
|
+
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
|
|
683
|
+
profileId,
|
|
684
|
+
description,
|
|
685
|
+
redirectUrl,
|
|
686
|
+
webhookUrl,
|
|
687
|
+
metadata: {
|
|
688
|
+
paymentId: payment.id,
|
|
689
|
+
},
|
|
690
|
+
locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
|
|
691
|
+
});
|
|
692
|
+
paymentUrl = molliePayment.getCheckoutUrl();
|
|
693
|
+
|
|
694
|
+
// Save payment
|
|
695
|
+
const dbPayment = new MolliePayment();
|
|
696
|
+
dbPayment.paymentId = payment.id;
|
|
697
|
+
dbPayment.mollieId = molliePayment.id;
|
|
698
|
+
await dbPayment.save();
|
|
699
|
+
}
|
|
700
|
+
else if (payment.provider === PaymentProvider.Payconiq) {
|
|
701
|
+
({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl));
|
|
702
|
+
}
|
|
703
|
+
else if (payment.provider === PaymentProvider.Buckaroo) {
|
|
704
|
+
// Increase request timeout because buckaroo is super slow (in development)
|
|
705
|
+
Context.request.request?.setTimeout(60 * 1000);
|
|
706
|
+
const buckaroo = new BuckarooHelper(organization.privateMeta?.buckarooSettings?.key ?? '', organization.privateMeta?.buckarooSettings?.secret ?? '', organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production');
|
|
707
|
+
const ip = Context.request.getIP();
|
|
708
|
+
paymentUrl = await buckaroo.createPayment(payment, ip, description, redirectUrl, webhookUrl);
|
|
709
|
+
await payment.save();
|
|
710
|
+
|
|
711
|
+
// TypeScript doesn't understand that the status can change and isn't a const....
|
|
712
|
+
if ((payment.status as any) === PaymentStatus.Failed) {
|
|
713
|
+
throw new SimpleError({
|
|
714
|
+
code: 'payment_failed',
|
|
715
|
+
message: $t(`b77e1f68-8928-42a2-802b-059fa73bedc3`, { method: PaymentMethodHelper.getName(payment.method) }),
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
await PaymentService.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
723
|
+
throw e;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Mark valid if needed
|
|
727
|
+
if (payment.method === PaymentMethod.Transfer || payment.method === PaymentMethod.PointOfSale || payment.method === PaymentMethod.Unknown) {
|
|
728
|
+
let hasBundleDiscount = false;
|
|
729
|
+
for (const [balanceItem] of balanceItems) {
|
|
730
|
+
// Mark valid
|
|
731
|
+
await BalanceItemService.markPaid(balanceItem, payment, organization);
|
|
732
|
+
|
|
733
|
+
if (balanceItem.type === BalanceItemType.RegistrationBundleDiscount) {
|
|
734
|
+
hasBundleDiscount = true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Flush balance caches so we return an up-to-date balance
|
|
739
|
+
if (hasBundleDiscount) {
|
|
740
|
+
await BalanceItemService.flushRegistrationDiscountsCache();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return {
|
|
745
|
+
payment,
|
|
746
|
+
balanceItemPayments,
|
|
747
|
+
provider,
|
|
748
|
+
stripeAccount,
|
|
749
|
+
paymentUrl,
|
|
750
|
+
paymentQRCode,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
static async sendTransferEmail(user: User, organization: Organization, payment: Payment) {
|
|
755
|
+
const paymentGeneral = await payment.getGeneralStructure();
|
|
756
|
+
const groupIds = paymentGeneral.groupIds;
|
|
757
|
+
|
|
758
|
+
const replacements = getEmailReplacementsForPayment(paymentGeneral, await buildReplacementOptions([paymentGeneral]));
|
|
759
|
+
|
|
760
|
+
const recipients = [
|
|
761
|
+
Recipient.create({
|
|
762
|
+
firstName: user.firstName,
|
|
763
|
+
lastName: user.lastName,
|
|
764
|
+
email: user.email,
|
|
765
|
+
userId: user.id,
|
|
766
|
+
replacements,
|
|
767
|
+
}),
|
|
768
|
+
];
|
|
769
|
+
|
|
770
|
+
let group: Group | undefined | null = null;
|
|
771
|
+
|
|
772
|
+
if (groupIds.length === 1) {
|
|
773
|
+
group = await Group.getByID(groupIds[0]);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Create e-mail builder
|
|
777
|
+
await sendEmailTemplate(organization, {
|
|
778
|
+
template: {
|
|
779
|
+
type: groupIds.length ? EmailTemplateType.RegistrationTransferDetails : EmailTemplateType.RegistrationTransferDetails,
|
|
780
|
+
group,
|
|
781
|
+
},
|
|
782
|
+
type: 'transactional',
|
|
783
|
+
recipients,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
static async validatePaymentMethod({ payment, balanceItems, paymentConfiguration }: { payment: Payment; balanceItems: Map<BalanceItem, number>; paymentConfiguration: PaymentConfiguration }) {
|
|
788
|
+
if (payment.price === 0) {
|
|
789
|
+
if (balanceItems.size === 0) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
// Create an egalizing payment
|
|
793
|
+
payment.method = PaymentMethod.Unknown;
|
|
794
|
+
|
|
795
|
+
if ([...balanceItems.values()].find(b => b < 0)) {
|
|
796
|
+
payment.type = PaymentType.Reallocation;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
else if (payment.method === PaymentMethod.Unknown) {
|
|
800
|
+
throw new SimpleError({
|
|
801
|
+
code: 'invalid_data',
|
|
802
|
+
message: $t(`86c7b6f7-3ec9-4af3-a5e6-b5de6de80d73`),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// Validate payment method
|
|
807
|
+
const allowedPaymentMethods = paymentConfiguration.getAvailablePaymentMethods({
|
|
808
|
+
amount: payment.price,
|
|
809
|
+
customer: payment.customer,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
if (!allowedPaymentMethods.includes(payment.method)) {
|
|
813
|
+
throw new SimpleError({
|
|
814
|
+
code: 'invalid_payment_method',
|
|
815
|
+
message: $t(`2b1ca6a0-662e-4326-ada1-10239b6ddc6f`),
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
static async validateVATRates({ customer, organization, balanceItems }: { customer: PaymentCustomer; organization: Organization; balanceItems: Map<BalanceItem, number> }) {
|
|
822
|
+
// Validate VAT rates for this customer
|
|
823
|
+
const seller = organization.meta.companies[0];
|
|
824
|
+
if (seller && seller.VATNumber && seller.address && customer.company) {
|
|
825
|
+
// B2B validation
|
|
826
|
+
if (!customer.company.address) {
|
|
827
|
+
throw new SimpleError({
|
|
828
|
+
code: 'missing_field',
|
|
829
|
+
message: 'Company address missing',
|
|
830
|
+
human: $t('a7fbbe18-7c46-45df-b041-3c47ddd0794d'),
|
|
831
|
+
field: 'customer.company.address',
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Reverse charged vat applicable?
|
|
836
|
+
if (customer.company.address.country !== seller.address.country) {
|
|
837
|
+
// Check VAT Exempt is set on each an every balance item with a non-zero price
|
|
838
|
+
for (const [item] of balanceItems) {
|
|
839
|
+
if (item.VATExcempt !== VATExcemptReason.IntraCommunity) {
|
|
840
|
+
throw new SimpleError({
|
|
841
|
+
code: 'VAT_error',
|
|
842
|
+
message: 'Intra community VAT reverse charge not supported for this purchase',
|
|
843
|
+
human: $t('abba9d96-6089-4c49-b895-5c01cadd305a'),
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// We also need to know the VAT rate exactly to be sure the VAT is removed from the purchase
|
|
848
|
+
// If VAT is not included, we don't need to know the VAT percentage until the payment is invoiced
|
|
849
|
+
if (item.VATPercentage === null && item.VATIncluded) {
|
|
850
|
+
throw new SimpleError({
|
|
851
|
+
code: 'VAT_error',
|
|
852
|
+
message: 'Intra community VAT reverse charge is not supported for this purchase because of missing VAT rates',
|
|
853
|
+
human: $t('37947cd9-4661-4332-ada9-8ffde5db811d'),
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else {
|
|
859
|
+
// Fine to just not have setup VAT rates yet if the price is guaranteed to include VAT
|
|
860
|
+
for (const [item] of balanceItems) {
|
|
861
|
+
if (item.VATExcempt === VATExcemptReason.IntraCommunity) {
|
|
862
|
+
throw new SimpleError({
|
|
863
|
+
code: 'VAT_error',
|
|
864
|
+
message: 'Unexpected reverse charge applied',
|
|
865
|
+
human: $t('57ac8775-7a32-4fdc-a84b-628a27f8d43d'),
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (!item.VATIncluded && item.VATPercentage === null) {
|
|
870
|
+
throw new SimpleError({
|
|
871
|
+
code: 'VAT_error',
|
|
872
|
+
message: 'Missing VAT percentage',
|
|
873
|
+
human: $t('495255ae-3ec5-42ec-9887-f3fc4f016d96'),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
// B2C / C2B / C2C
|
|
881
|
+
|
|
882
|
+
// You cannot buy balance items with VAT if you didn't set up a VAT number.
|
|
883
|
+
for (const [item] of balanceItems) {
|
|
884
|
+
if (item.VATExcempt === VATExcemptReason.IntraCommunity) {
|
|
885
|
+
throw new SimpleError({
|
|
886
|
+
code: 'VAT_error',
|
|
887
|
+
message: 'Unexpected reverse charge applied',
|
|
888
|
+
human: $t('57ac8775-7a32-4fdc-a84b-628a27f8d43d'),
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (seller && seller.VATNumber) {
|
|
893
|
+
if (!item.VATIncluded && item.VATPercentage === null) {
|
|
894
|
+
throw new SimpleError({
|
|
895
|
+
code: 'VAT_error',
|
|
896
|
+
message: 'Missing VAT percentage',
|
|
897
|
+
human: $t('495255ae-3ec5-42ec-9887-f3fc4f016d96'),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
362
904
|
};
|