@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.
Files changed (284) hide show
  1. package/migrations.ts +1 -1
  2. package/package.json +10 -10
  3. package/src/audit-logs/DocumentTemplateLogger.ts +1 -1
  4. package/src/audit-logs/EmailAddressLogger.ts +1 -1
  5. package/src/audit-logs/EmailLogger.ts +1 -1
  6. package/src/audit-logs/EmailTemplateLogger.ts +1 -1
  7. package/src/audit-logs/EventLogger.ts +1 -1
  8. package/src/audit-logs/GroupLogger.ts +1 -1
  9. package/src/audit-logs/MemberLogger.ts +1 -1
  10. package/src/audit-logs/MemberPlatformMembershipLogger.ts +1 -1
  11. package/src/audit-logs/MemberResponsibilityRecordLogger.ts +1 -1
  12. package/src/audit-logs/ModelLogger.ts +27 -19
  13. package/src/audit-logs/OrderLogger.ts +1 -1
  14. package/src/audit-logs/OrganizationLogger.ts +23 -3
  15. package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +1 -1
  16. package/src/audit-logs/PaymentLogger.ts +1 -1
  17. package/src/audit-logs/PlatformLogger.ts +1 -1
  18. package/src/audit-logs/RegistrationLogger.ts +1 -1
  19. package/src/audit-logs/RegistrationPeriodLogger.ts +1 -1
  20. package/src/audit-logs/StripeAccountLogger.ts +1 -1
  21. package/src/audit-logs/UserLogger.ts +1 -1
  22. package/src/audit-logs/WebshopLogger.ts +1 -1
  23. package/src/audit-logs/init.ts +40 -0
  24. package/src/boot.ts +6 -4
  25. package/src/crons/amazon-ses.ts +1 -1
  26. package/src/crons/balance-emails.ts +1 -1
  27. package/src/crons/clearExcelCache.test.ts +1 -1
  28. package/src/crons/delete-archived-data.ts +47 -0
  29. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +1 -1
  30. package/src/crons/index.ts +1 -0
  31. package/src/crons.ts +3 -3
  32. package/src/debug.ts +230 -0
  33. package/src/email-recipient-loaders/documents.ts +1 -1
  34. package/src/email-recipient-loaders/members.ts +1 -1
  35. package/src/email-recipient-loaders/orders.ts +3 -3
  36. package/src/email-recipient-loaders/payments.ts +118 -353
  37. package/src/email-replacements/getEmailReplacementsForPayment.ts +321 -0
  38. package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +9 -7
  39. package/src/endpoints/admin/memberships/ChargeMembershipsEndpoint.ts +2 -2
  40. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +1 -1
  41. package/src/endpoints/admin/organizations/ChargeOrganizationsEndpoint.ts +16 -50
  42. package/src/endpoints/admin/organizations/GetOrganizationsCountEndpoint.ts +2 -2
  43. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +4 -4
  44. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +21 -6
  45. package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +9 -7
  46. package/src/endpoints/auth/CreateAdminEndpoint.ts +2 -2
  47. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +2 -2
  48. package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
  49. package/src/endpoints/auth/DeleteTokenEndpoint.ts +1 -1
  50. package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
  51. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  52. package/src/endpoints/auth/GetOtherUserEndpoint.ts +2 -2
  53. package/src/endpoints/auth/GetUserEndpoint.test.ts +2 -2
  54. package/src/endpoints/auth/GetUserEndpoint.ts +2 -2
  55. package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
  56. package/src/endpoints/auth/OpenIDConnectCallbackEndpoint.ts +2 -2
  57. package/src/endpoints/auth/OpenIDConnectStartEndpoint.ts +2 -2
  58. package/src/endpoints/auth/PatchUserEndpoint.ts +4 -4
  59. package/src/endpoints/auth/PollEmailVerificationEndpoint.ts +1 -1
  60. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +1 -1
  61. package/src/endpoints/auth/SignupEndpoint.ts +1 -1
  62. package/src/endpoints/auth/VerifyEmailEndpoint.ts +1 -1
  63. package/src/endpoints/global/addresses/ValidateAddressEndpoint.ts +1 -1
  64. package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +4 -4
  65. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +191 -7
  66. package/src/endpoints/global/billing/DeactivatePackageEndpoint.ts +64 -0
  67. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +2 -2
  68. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +3 -3
  69. package/src/endpoints/global/email/GetEmailAddressEndpoint.ts +1 -1
  70. package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
  71. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +2 -2
  72. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +3 -3
  73. package/src/endpoints/global/email/ManageEmailAddressEndpoint.ts +62 -16
  74. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +6 -6
  75. package/src/endpoints/global/email-recipients/GetEmailRecipientsCountEndpoint.ts +2 -2
  76. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +2 -2
  77. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +4 -4
  78. package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +1 -1
  79. package/src/endpoints/global/email-recipients/helpers/validateEmailRecipientFilter.ts +1 -1
  80. package/src/endpoints/global/events/GetEventNotificationsCountEndpoint.ts +2 -2
  81. package/src/endpoints/global/events/GetEventNotificationsEndpoint.ts +4 -4
  82. package/src/endpoints/global/events/GetEventsEndpoint.ts +4 -4
  83. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -2
  84. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.ts +3 -3
  85. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -2
  86. package/src/endpoints/global/events/PatchEventsEndpoint.ts +5 -5
  87. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +4 -4
  88. package/src/endpoints/global/files/GetFileCache.ts +2 -2
  89. package/src/endpoints/global/files/UploadFile.ts +2 -2
  90. package/src/endpoints/global/files/UploadImage.ts +1 -1
  91. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +3 -3
  92. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +4 -4
  93. package/src/endpoints/global/members/GetMemberFamilyEndpoint.ts +4 -4
  94. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +3 -3
  95. package/src/endpoints/global/members/GetMembersEndpoint.ts +4 -16
  96. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +8 -7
  97. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +1 -1
  98. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.test.ts +2 -2
  99. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +23 -12
  100. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.test.ts +2 -2
  101. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +1 -1
  102. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +1 -1
  103. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.test.ts +2 -2
  104. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +1 -1
  105. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +3 -3
  106. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +1 -1
  107. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +2 -2
  108. package/src/endpoints/global/platform/GetPlatformEndpoint.ts +1 -1
  109. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +2 -2
  110. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +7 -7
  111. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +2 -2
  112. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +1 -2
  113. package/src/endpoints/global/registration/GetUserDetailedPayableBalanceEndpoint.ts +2 -2
  114. package/src/endpoints/global/registration/GetUserDocumentsEndpoint.ts +2 -2
  115. package/src/endpoints/global/registration/GetUserMembersEndpoint.ts +2 -2
  116. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +2 -2
  117. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +2 -2
  118. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +24 -389
  119. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +3 -3
  120. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -2
  121. package/src/endpoints/global/sso/GetSSOEndpoint.ts +2 -2
  122. package/src/endpoints/global/sso/SetSSOEndpoint.ts +3 -3
  123. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemEndpoint.ts +55 -0
  124. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsCountEndpoint.ts +43 -0
  125. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.ts +160 -0
  126. package/src/endpoints/organization/dashboard/{payments → balance-items}/PatchBalanceItemsEndpoint.ts +1 -1
  127. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +2 -2
  128. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
  129. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.test.ts +3 -3
  130. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +3 -3
  131. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +1 -1
  132. package/src/endpoints/organization/dashboard/documents/GetDocumentsCountEndpoint.ts +2 -2
  133. package/src/endpoints/organization/dashboard/documents/PatchDocumentEndpoint.ts +2 -2
  134. package/src/endpoints/organization/dashboard/email/CheckEmailBouncesEndpoint.ts +37 -6
  135. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +2 -2
  136. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +3 -3
  137. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +2 -2
  138. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +1 -1
  139. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +53 -0
  140. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +2 -2
  141. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +3 -3
  142. package/src/endpoints/organization/dashboard/mollie/DisconnectMollieEndpoint.ts +2 -2
  143. package/src/endpoints/organization/dashboard/mollie/GetMollieDashboardEndpoint.ts +1 -1
  144. package/src/endpoints/organization/dashboard/nolt/CreateNoltTokenEndpoint.ts +1 -1
  145. package/src/endpoints/organization/dashboard/organization/GetOrganizationArchivedGroups.ts +2 -2
  146. package/src/endpoints/organization/dashboard/organization/GetOrganizationDeletedGroups.ts +2 -2
  147. package/src/endpoints/organization/dashboard/organization/GetUitpasClientIdEndpoint.ts +2 -2
  148. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +2 -2
  149. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +12 -1
  150. package/src/endpoints/organization/dashboard/organization/SearchUitpasOrganizersEndpoint.ts +2 -2
  151. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +2 -2
  152. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +2 -2
  153. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +2 -2
  154. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +4 -4
  155. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +12 -12
  156. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesCountEndpoint.ts +2 -2
  157. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +4 -4
  158. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +2 -2
  159. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +4 -4
  160. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +2 -2
  161. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +2 -2
  162. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +3 -3
  163. package/src/endpoints/organization/dashboard/registration-periods/SetupStepReviewEndpoint.ts +2 -2
  164. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +3 -3
  165. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +3 -3
  166. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +2 -2
  167. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountsEndpoint.ts +1 -1
  168. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +2 -2
  169. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +3 -3
  170. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +3 -3
  171. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +1 -1
  172. package/src/endpoints/organization/dashboard/users/DeleteUserEndpoint.ts +1 -1
  173. package/src/endpoints/organization/dashboard/users/GetApiUsersEndpoint.ts +1 -1
  174. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +1 -1
  175. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +3 -3
  176. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +1 -1
  177. package/src/endpoints/organization/dashboard/webshops/CreateWebshopEndpoint.ts +1 -1
  178. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +2 -2
  179. package/src/endpoints/organization/dashboard/webshops/GetDiscountCodesEndpoint.ts +1 -1
  180. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersCountEndpoint.ts +2 -2
  181. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.ts +5 -5
  182. package/src/endpoints/organization/dashboard/webshops/GetWebshopTicketsEndpoint.ts +5 -5
  183. package/src/endpoints/organization/dashboard/webshops/GetWebshopUriAvailabilityEndpoint.ts +1 -1
  184. package/src/endpoints/organization/dashboard/webshops/PatchDiscountCodesEndpoint.ts +1 -1
  185. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +1 -1
  186. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  187. package/src/endpoints/organization/dashboard/webshops/PatchWebshopTicketsEndpoint.ts +1 -1
  188. package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +2 -2
  189. package/src/endpoints/organization/dashboard/webshops/VerifyWebshopDomainEndpoint.ts +1 -1
  190. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +3 -3
  191. package/src/endpoints/organization/shared/GetDocumentHtml.ts +1 -1
  192. package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +2 -2
  193. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +2 -2
  194. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.ts +2 -2
  195. package/src/endpoints/organization/webshops/CheckWebshopDiscountCodesEndpoint.ts +1 -1
  196. package/src/endpoints/organization/webshops/GetOrderByPaymentEndpoint.ts +1 -1
  197. package/src/endpoints/organization/webshops/GetOrderEndpoint.ts +1 -1
  198. package/src/endpoints/organization/webshops/GetTicketsEndpoint.ts +1 -1
  199. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +2 -2
  200. package/src/endpoints/organization/webshops/GetWebshopEndpoint.ts +2 -2
  201. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +4 -4
  202. package/src/excel-loaders/balance-items.ts +268 -0
  203. package/src/excel-loaders/event-notifications.ts +3 -3
  204. package/src/excel-loaders/index.ts +6 -5
  205. package/src/excel-loaders/organizations.ts +4 -4
  206. package/src/excel-loaders/payments.ts +11 -3
  207. package/src/excel-loaders/receivable-balances.ts +2 -2
  208. package/src/helpers/AddressValidator.test.ts +1 -1
  209. package/src/helpers/AddressValidator.ts +1 -1
  210. package/src/helpers/AdminPermissionChecker.ts +20 -17
  211. package/src/helpers/AuthenticatedStructures.ts +194 -109
  212. package/src/helpers/Context.ts +2 -0
  213. package/src/helpers/EmailResumer.ts +1 -1
  214. package/src/helpers/FlagMomentCleanup.ts +1 -1
  215. package/src/helpers/ForwardHandler.test.ts +1 -1
  216. package/src/helpers/ForwardHandler.ts +1 -1
  217. package/src/helpers/GlobalHelper.ts +4 -4
  218. package/src/helpers/GroupBuilder.ts +417 -0
  219. package/src/helpers/GroupedThrottledQueue.test.ts +1 -1
  220. package/src/helpers/GroupedThrottledQueue.ts +1 -1
  221. package/src/helpers/MemberUserSyncer.test.ts +1 -1
  222. package/src/helpers/MemberUserSyncer.ts +1 -1
  223. package/src/helpers/PeriodHelper.ts +5 -45
  224. package/src/helpers/ServiceFeeHelper.ts +5 -1
  225. package/src/helpers/SetupStepUpdater.ts +5 -4
  226. package/src/helpers/TagHelper.ts +1 -1
  227. package/src/helpers/ThrottledQueue.test.ts +1 -1
  228. package/src/helpers/ViesHelper.ts +9 -0
  229. package/src/helpers/email-html-helpers.ts +0 -41
  230. package/src/helpers/outstandingBalanceJoin.ts +3 -1
  231. package/src/middleware/ContextMiddleware.ts +1 -1
  232. package/src/seeds/1726572303-schedule-stock-updates.ts +1 -1
  233. package/src/seeds/1726847064-setup-steps.ts +1 -1
  234. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +1 -1
  235. package/src/seeds/1740046783-update-membership.ts +1 -1
  236. package/src/seeds/1754560914-groups-prices.test.ts +1 -1
  237. package/src/seeds/1755876819-remove-duplicate-members.ts +1 -1
  238. package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +1 -1
  239. package/src/seeds/1761665607-sync-member-users.ts +1 -1
  240. package/src/seeds/data/default-email-templates.sql +1 -1
  241. package/src/seeds-temporary/1732117645-move-rrn.ts +1 -1
  242. package/src/services/AuditLogService.ts +1 -41
  243. package/src/services/BalanceItemPaymentService.ts +1 -1
  244. package/src/services/BalanceItemService.ts +12 -5
  245. package/src/services/EventNotificationService.ts +3 -3
  246. package/src/services/InvoiceService.ts +131 -17
  247. package/src/services/MemberRecordStore.ts +1 -1
  248. package/src/services/PaymentReallocationService.test.ts +9 -10
  249. package/src/services/PaymentReallocationService.ts +1 -1
  250. package/src/services/PaymentService.ts +560 -18
  251. package/src/services/PlatformMembershipService.ts +3 -3
  252. package/src/services/RegistrationService.ts +3 -3
  253. package/src/services/SSOService.ts +2 -2
  254. package/src/services/STPackageService.ts +241 -0
  255. package/src/services/uitpas/UitpasService.test.ts +1 -1
  256. package/src/sql-filters/balance-items.ts +56 -0
  257. package/src/sql-filters/emails.ts +1 -1
  258. package/src/sql-filters/event-notifications.ts +2 -2
  259. package/src/sql-filters/events.ts +51 -0
  260. package/src/sql-filters/members.ts +38 -2
  261. package/src/sql-filters/receivable-balances.ts +3 -3
  262. package/src/sql-filters/users.ts +10 -0
  263. package/src/sql-sorters/balance-items.ts +36 -0
  264. package/src/sql-sorters/document-templates.ts +2 -2
  265. package/src/sql-sorters/documents.ts +2 -2
  266. package/src/sql-sorters/event-notifications.ts +1 -1
  267. package/src/sql-sorters/members.ts +2 -2
  268. package/src/sql-sorters/orders.ts +2 -2
  269. package/src/sql-sorters/tickets.ts +2 -2
  270. package/tests/actions/patchOrganizationMember.ts +3 -3
  271. package/tests/actions/patchPaymentStatus.ts +3 -3
  272. package/tests/actions/patchUserMember.ts +2 -2
  273. package/tests/assertions/assertBalances.ts +1 -1
  274. package/tests/e2e/api-rate-limits.test.ts +3 -3
  275. package/tests/e2e/charge-members.test.ts +14 -14
  276. package/tests/e2e/documents.test.ts +8 -8
  277. package/tests/e2e/private-files.test.ts +4 -4
  278. package/tests/e2e/register.test.ts +10 -10
  279. package/tests/e2e/stock.test.ts +4 -4
  280. package/tests/e2e/tickets.test.ts +4 -4
  281. package/tests/helpers/TestServer.ts +2 -2
  282. package/tests/init/index.ts +7 -7
  283. package/tests/init/initAdmin.ts +1 -1
  284. 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 { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment } from '@stamhoofd/models';
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, BalanceItemRelation, BalanceItemStatus, BalanceItemType, PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
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 const PaymentService = {
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 == PaymentStatus.Failed) {
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 == 1) {
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 == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
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 == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
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
  };