@stamhoofd/backend 2.137.4 → 2.138.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 (142) hide show
  1. package/package.json +26 -17
  2. package/src/boot.ts +5 -0
  3. package/src/crons/balance-emails.ts +2 -1
  4. package/src/crons/delete-expired-mfa-tokens.ts +35 -0
  5. package/src/crons/drip-emails.ts +2 -1
  6. package/src/crons/index.ts +1 -0
  7. package/src/crons/invoices.ts +5 -3
  8. package/src/crons.ts +4 -2
  9. package/src/email-recipient-loaders/orders.ts +2 -1
  10. package/src/email-recipient-loaders/organizations.ts +3 -2
  11. package/src/email-recipient-loaders/payments.ts +3 -2
  12. package/src/endpoints/auth/ConfirmTOTPEndpoint.ts +84 -0
  13. package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -2
  14. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +61 -1
  15. package/src/endpoints/auth/CreateTokenEndpoint.ts +135 -7
  16. package/src/endpoints/auth/DeletePasskeyEndpoint.ts +61 -0
  17. package/src/endpoints/auth/DeleteTOTPEndpoint.ts +62 -0
  18. package/src/endpoints/auth/DeleteUserEndpoint.ts +1 -1
  19. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +4 -2
  20. package/src/endpoints/auth/GetMFAChallengeEndpoint.ts +56 -0
  21. package/src/endpoints/auth/GetMFAStatusEndpoint.ts +31 -0
  22. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -1
  23. package/src/endpoints/auth/MFA.security.test.ts +688 -0
  24. package/src/endpoints/auth/MFA.test.ts +1398 -0
  25. package/src/endpoints/auth/OpenIDConnectAuthTokenEndpoint.ts +2 -2
  26. package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
  27. package/src/endpoints/auth/RegenerateRecoveryCodesEndpoint.ts +51 -0
  28. package/src/endpoints/auth/RegisterPasskeyEndpoint.ts +99 -0
  29. package/src/endpoints/auth/RegisterPasskeyOptionsEndpoint.ts +49 -0
  30. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
  31. package/src/endpoints/auth/SetupTOTPEndpoint.ts +54 -0
  32. package/src/endpoints/auth/SignupEndpoint.ts +17 -4
  33. package/src/endpoints/auth/VerifyEmailEndpoint.ts +16 -0
  34. package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -3
  35. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
  36. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
  37. package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
  38. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
  39. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
  40. package/src/endpoints/global/email/PatchEmailEndpoint.ts +5 -3
  41. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.ts +2 -1
  42. package/src/endpoints/global/email-recipients/RetryEmailRecipientEndpoint.ts +2 -1
  43. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +11 -6
  44. package/src/endpoints/global/files/UploadFile.test.ts +206 -0
  45. package/src/endpoints/global/files/UploadFile.ts +31 -6
  46. package/src/endpoints/global/files/UploadImage.test.ts +177 -0
  47. package/src/endpoints/global/files/UploadImage.ts +23 -4
  48. package/src/endpoints/global/files/upload-security.test.ts +837 -0
  49. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
  50. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.ts +2 -1
  51. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
  52. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +4 -1
  53. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +35 -1
  54. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -0
  55. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +92 -0
  56. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.ts +43 -0
  57. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
  58. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -0
  59. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
  60. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +3 -2
  61. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +32 -3
  62. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +4 -1
  63. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +100 -0
  64. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +4 -1
  65. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +5 -5
  66. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
  67. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +160 -0
  68. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.ts +44 -0
  69. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
  70. package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
  71. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
  72. package/src/excel-loaders/balance-items.ts +1 -1
  73. package/src/excel-loaders/event-notifications.ts +6 -7
  74. package/src/excel-loaders/members.test.ts +2 -2
  75. package/src/excel-loaders/members.ts +9 -10
  76. package/src/excel-loaders/organizations.ts +14 -20
  77. package/src/excel-loaders/payments.ts +1 -1
  78. package/src/excel-loaders/platform-memberships.ts +7 -7
  79. package/src/excel-loaders/platform-sheets.test.ts +113 -0
  80. package/src/excel-loaders/receivable-balances.ts +1 -1
  81. package/src/excel-loaders/registrations.ts +9 -9
  82. package/src/helpers/AdminPermissionChecker.ts +1 -1
  83. package/src/helpers/AuthenticatedStructures.ts +21 -10
  84. package/src/helpers/Context.ts +114 -4
  85. package/src/helpers/EmailBuilder.test.ts +287 -0
  86. package/src/helpers/EmailBuilder.ts +811 -0
  87. package/src/helpers/EmailResumer.ts +2 -1
  88. package/src/helpers/ForwardHandler.ts +2 -1
  89. package/src/helpers/MFAEncryption.ts +34 -0
  90. package/src/helpers/MembershipCharger.ts +3 -2
  91. package/src/helpers/RecoveryCodeHelper.ts +81 -0
  92. package/src/helpers/TOTPHelper.ts +79 -0
  93. package/src/helpers/TenantContext.test.ts +96 -0
  94. package/src/helpers/TenantContext.ts +67 -0
  95. package/src/helpers/TwoFactorHelper.ts +356 -0
  96. package/src/helpers/WebauthnHelper.ts +211 -0
  97. package/src/helpers/data/aaguids.json +1006 -0
  98. package/src/middleware/TenantScopeMiddleware.test.ts +41 -0
  99. package/src/middleware/TenantScopeMiddleware.ts +24 -0
  100. package/src/seeds/1785417302-fill-user-last-active-at.sql +6 -0
  101. package/src/services/AdminSessionService.ts +41 -0
  102. package/src/services/BalanceItemService.ts +2 -1
  103. package/src/services/DocumentRenderService.test.ts +229 -0
  104. package/src/services/DocumentRenderService.ts +180 -0
  105. package/src/services/EmailPreviewService.test.ts +300 -0
  106. package/src/services/EmailPreviewService.ts +239 -0
  107. package/src/services/EmailSendService.test.ts +1218 -0
  108. package/src/services/EmailSendService.ts +705 -0
  109. package/src/services/EventNotificationService.ts +2 -1
  110. package/src/services/FileSignService.test.ts +85 -0
  111. package/src/services/FileSignService.ts +23 -1
  112. package/src/services/InvoicePdfService.ts +3 -3
  113. package/src/services/InvoiceService.ts +4 -2
  114. package/src/services/InvoiceXMLService.ts +2 -1
  115. package/src/services/OrderService.test.ts +308 -0
  116. package/src/services/OrderService.ts +215 -0
  117. package/src/services/OrganizationAdminService.test.ts +47 -0
  118. package/src/services/OrganizationAdminService.ts +139 -0
  119. package/src/services/OrganizationDNSService.test.ts +177 -0
  120. package/src/services/OrganizationDNSService.ts +282 -0
  121. package/src/services/OrganizationEmailService.test.ts +57 -0
  122. package/src/services/OrganizationEmailService.ts +212 -0
  123. package/src/services/PasswordForgotService.test.ts +99 -0
  124. package/src/services/PasswordForgotService.ts +38 -0
  125. package/src/services/PaymentService.ts +5 -3
  126. package/src/services/PlatformMembershipService.test.ts +180 -0
  127. package/src/services/PlatformMembershipService.ts +281 -4
  128. package/src/services/ReferralService.ts +3 -2
  129. package/src/services/RegistrationService.ts +4 -2
  130. package/src/services/SSOService.ts +106 -14
  131. package/src/services/STPackageService.test.ts +191 -0
  132. package/src/services/STPackageService.ts +115 -1
  133. package/src/services/TwoFactorAuditLogService.ts +64 -0
  134. package/src/services/VerificationCodeService.test.ts +71 -0
  135. package/src/services/VerificationCodeService.ts +101 -0
  136. package/tests/e2e/api-rate-limits.test.ts +1 -1
  137. package/tests/e2e/documents.test.ts +2 -1
  138. package/tests/e2e/private-files.test.ts +77 -14
  139. package/tests/helpers/MFATestHelper.ts +42 -0
  140. package/tests/helpers/TestServer.ts +4 -0
  141. package/tests/helpers/index.ts +1 -0
  142. package/tsconfig.build.json +2 -1
@@ -0,0 +1,160 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import type { Organization, User } from '@stamhoofd/models';
3
+ import { OrganizationFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import { PermissionLevel, Permissions, UserPermissions } from '@stamhoofd/structures';
5
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
6
+
7
+ import { testServer } from '../../../../../tests/helpers/TestServer.js';
8
+ import { AdminSessionService } from '../../../../services/AdminSessionService.js';
9
+ import { SignOutOrganizationAdminsEndpoint } from './SignOutOrganizationAdminsEndpoint.js';
10
+
11
+ describe('Endpoint.SignOutOrganizationAdmins', () => {
12
+ const endpoint = new SignOutOrganizationAdminsEndpoint();
13
+
14
+ beforeEach(() => {
15
+ TestUtils.setEnvironment('userMode', 'platform');
16
+ });
17
+
18
+ const signOut = async (organization: Organization, token: Token) => {
19
+ const request = Request.buildJson('POST', '/organization/admins/sign-out', organization.getApiHost());
20
+ request.headers.authorization = 'Bearer ' + token.accessToken;
21
+ return await testServer.test(endpoint, request);
22
+ };
23
+
24
+ /**
25
+ * In platform mode users are not owned by an organization: they have permissions for it.
26
+ */
27
+ const createAdmin = async (organization: Organization, level = PermissionLevel.Full) => {
28
+ const user = await new UserFactory({
29
+ organization,
30
+ permissions: Permissions.create({ level }),
31
+ }).create();
32
+ user.organizationId = null;
33
+ await user.save();
34
+ return user;
35
+ };
36
+
37
+ const tokenExists = async (token: Token) => {
38
+ return !!(await Token.getByAccessToken(token.accessToken, true));
39
+ };
40
+
41
+ test('all sessions of all admins are signed out, except the one making the request', async () => {
42
+ const organization = await new OrganizationFactory({}).create();
43
+ const otherOrganization = await new OrganizationFactory({}).create();
44
+
45
+ const admin = await createAdmin(organization);
46
+ const otherAdmin = await createAdmin(organization);
47
+ const otherOrganizationAdmin = await createAdmin(otherOrganization);
48
+ const member = await new UserFactory({ organization }).create();
49
+
50
+ const currentToken = await Token.createToken(admin, new Date());
51
+ const secondDeviceOfAdmin = await Token.createToken(admin, new Date());
52
+ const otherAdminToken = await Token.createToken(otherAdmin, new Date());
53
+ const otherOrganizationAdminToken = await Token.createToken(otherOrganizationAdmin, new Date());
54
+ const memberToken = await Token.createToken(member, new Date());
55
+
56
+ const response = await signOut(organization, currentToken);
57
+
58
+ // The other device of the admin is signed out too: only this session is spared
59
+ expect(response.body.count).toBe(2);
60
+ expect(await tokenExists(currentToken)).toBe(true);
61
+ expect(await tokenExists(secondDeviceOfAdmin)).toBe(false);
62
+ expect(await tokenExists(otherAdminToken)).toBe(false);
63
+
64
+ // Users without permissions in this organization are not affected
65
+ expect(await tokenExists(otherOrganizationAdminToken)).toBe(true);
66
+ expect(await tokenExists(memberToken)).toBe(true);
67
+ });
68
+
69
+ test('an admin without full access cannot sign out the other admins', async () => {
70
+ const organization = await new OrganizationFactory({}).create();
71
+
72
+ const admin = await createAdmin(organization, PermissionLevel.Write);
73
+ const otherAdmin = await createAdmin(organization);
74
+ const otherAdminToken = await Token.createToken(otherAdmin, new Date());
75
+
76
+ await expect(signOut(organization, await Token.createToken(admin, new Date()))).rejects.toThrow(STExpect.simpleError({
77
+ code: 'permission_denied',
78
+ }));
79
+ expect(await tokenExists(otherAdminToken)).toBe(true);
80
+ });
81
+
82
+ test('an api user cannot sign out the admins', async () => {
83
+ const organization = await new OrganizationFactory({}).create();
84
+
85
+ const apiUser = await new UserFactory({ organization, apiUser: true }).create();
86
+ apiUser.permissions = UserPermissions.create({
87
+ organizationPermissions: new Map([[organization.id, Permissions.create({ level: PermissionLevel.Full })]]),
88
+ });
89
+ await apiUser.save();
90
+
91
+ const otherAdmin = await createAdmin(organization);
92
+ const otherAdminToken = await Token.createToken(otherAdmin, new Date());
93
+
94
+ await expect(signOut(organization, await Token.createApiToken(apiUser))).rejects.toThrow(STExpect.simpleError({
95
+ code: 'permission_denied',
96
+ }));
97
+ expect(await tokenExists(otherAdminToken)).toBe(true);
98
+ });
99
+
100
+ test('an organization without admins is a no-op', async () => {
101
+ const organization = await new OrganizationFactory({}).create();
102
+ const member = await new UserFactory({ organization }).create();
103
+ const memberToken = await Token.createToken(member, new Date());
104
+
105
+ // Not reachable through the endpoint (the caller is an admin themselves), but the
106
+ // empty list of users may not turn into a query that deletes everything.
107
+ expect(await AdminSessionService.signOutOrganizationAdmins(organization.id, null)).toBe(0);
108
+ expect(await tokenExists(memberToken)).toBe(true);
109
+ });
110
+
111
+ describe('userMode organization', () => {
112
+ beforeEach(() => {
113
+ TestUtils.setEnvironment('userMode', 'organization');
114
+ });
115
+
116
+ test('all sessions of all admins are signed out, except the one making the request', async () => {
117
+ const organization = await new OrganizationFactory({}).create();
118
+
119
+ const admin: User = await new UserFactory({
120
+ organization,
121
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
122
+ }).create();
123
+ const otherAdmin: User = await new UserFactory({
124
+ organization,
125
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
126
+ }).create();
127
+ const member = await new UserFactory({ organization }).create();
128
+
129
+ const otherAdminToken = await Token.createToken(otherAdmin, new Date());
130
+ const memberToken = await Token.createToken(member, new Date());
131
+
132
+ const response = await signOut(organization, await Token.createToken(admin, new Date()));
133
+
134
+ expect(response.body.count).toBe(1);
135
+ expect(await tokenExists(otherAdminToken)).toBe(false);
136
+ expect(await tokenExists(memberToken)).toBe(true);
137
+ });
138
+
139
+ test('api users keep their token, because they cannot complete a login', async () => {
140
+ const organization = await new OrganizationFactory({}).create();
141
+
142
+ const admin: User = await new UserFactory({
143
+ organization,
144
+ permissions: Permissions.create({ level: PermissionLevel.Full }),
145
+ }).create();
146
+
147
+ const apiUser = await new UserFactory({ organization, apiUser: true }).create();
148
+ apiUser.permissions = UserPermissions.create({
149
+ organizationPermissions: new Map([[organization.id, Permissions.create({ level: PermissionLevel.Full })]]),
150
+ });
151
+ await apiUser.save();
152
+ const apiToken = await Token.createApiToken(apiUser);
153
+
154
+ const response = await signOut(organization, await Token.createToken(admin, new Date()));
155
+
156
+ expect(response.body.count).toBe(0);
157
+ expect(await tokenExists(apiToken)).toBe(true);
158
+ });
159
+ });
160
+ });
@@ -0,0 +1,44 @@
1
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../../helpers/Context.js';
6
+ import { AdminSessionService } from '../../../../services/AdminSessionService.js';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = undefined;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ /**
14
+ * End the sessions of all administrators of this organization, so they have to sign in
15
+ * again. Used after making two-factor authentication required, which is only enforced on
16
+ * new logins.
17
+ */
18
+ export class SignOutOrganizationAdminsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ protected doesMatch(request: Request): [true, Params] | [false] {
20
+ if (request.method !== 'POST') {
21
+ return [false];
22
+ }
23
+
24
+ const params = Endpoint.parseParameters(request.url, '/organization/admins/sign-out', {});
25
+
26
+ if (params) {
27
+ return [true, params as Params];
28
+ }
29
+ return [false];
30
+ }
31
+
32
+ async handle(_: DecodedRequest<Params, Query, Body>) {
33
+ const organization = await Context.setOrganizationScope();
34
+ const { token } = await Context.authenticate();
35
+
36
+ if (!await Context.auth.canManageAdmins(organization.id)) {
37
+ throw Context.auth.error();
38
+ }
39
+
40
+ const count = await AdminSessionService.signOutOrganizationAdmins(organization.id, token.accessToken);
41
+
42
+ return new Response(CountResponse.create({ count }));
43
+ }
44
+ }
@@ -10,6 +10,7 @@ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceIt
10
10
  import { Context } from '../../../../helpers/Context.js';
11
11
  import { ServiceFeeHelper } from '../../../../helpers/ServiceFeeHelper.js';
12
12
  import { AuditLogService } from '../../../../services/AuditLogService.js';
13
+ import { OrderService } from '../../../../services/OrderService.js';
13
14
  import { PaymentService } from '../../../../services/PaymentService.js';
14
15
  import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService.js';
15
16
 
@@ -151,7 +152,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
151
152
  order.data.paymentMethod = PaymentMethod.Unknown;
152
153
 
153
154
  // Mark this order as paid
154
- await order.markPaid(null, organization, webshop);
155
+ await OrderService.markPaid(order, null, organization, webshop);
155
156
  await order.save();
156
157
  } else {
157
158
  const payment = new Payment();
@@ -217,12 +218,12 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
217
218
  await balanceItemPayment.save();
218
219
 
219
220
  if (payment.method === PaymentMethod.Transfer) {
220
- await order.markValid(payment, []);
221
+ await OrderService.markValid(order, payment, []);
221
222
  await payment.save();
222
223
  await order.save();
223
224
  } else if (payment.method === PaymentMethod.PointOfSale) {
224
225
  // Not really paid, but needed to create the tickets if needed
225
- await order.markPaid(payment, organization, webshop);
226
+ await OrderService.markPaid(order, payment, organization, webshop);
226
227
  await payment.save();
227
228
  await order.save();
228
229
  } else {
@@ -5,6 +5,7 @@ import { signInternal } from '@stamhoofd/backend-env';
5
5
  import { Document, Organization } from '@stamhoofd/models';
6
6
 
7
7
  import { Context } from '../../../helpers/Context.js';
8
+ import { DocumentRenderService } from '../../../services/DocumentRenderService.js';
8
9
  type Params = { id: string };
9
10
  type Query = undefined;
10
11
  type Body = undefined;
@@ -45,7 +46,7 @@ export class GetDocumentHtml extends Endpoint<Params, Query, Body, ResponseBody>
45
46
  });
46
47
  }
47
48
 
48
- const html = await document.getRenderedHtml(organization);
49
+ const html = await DocumentRenderService.getRenderedHtml(document, organization);
49
50
  if (!html) {
50
51
  throw new SimpleError({
51
52
  code: 'failed_generating',
@@ -16,6 +16,7 @@ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
16
16
  import { StripeHelper } from '../../../helpers/StripeHelper.js';
17
17
  import { AuditLogService } from '../../../services/AuditLogService.js';
18
18
  import { MollieService } from '../../../services/MollieService.js';
19
+ import { OrderService } from '../../../services/OrderService.js';
19
20
  import { PaymentService } from '../../../services/PaymentService.js';
20
21
  import { UitpasService } from '../../../services/uitpas/UitpasService.js';
21
22
  import { WebshopAuthHelper } from './WebshopAuthHelper.js';
@@ -171,7 +172,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
171
172
  order.data.paymentMethod = PaymentMethod.Unknown;
172
173
 
173
174
  // Mark this order as paid
174
- await order.markPaid(null, organization, webshop);
175
+ await OrderService.markPaid(order, null, organization, webshop);
175
176
  await order.save();
176
177
  } else {
177
178
  const payment = new Payment();
@@ -242,7 +243,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
242
243
  const description = webshop.meta.name + ' - ' + organization.name;
243
244
 
244
245
  if (payment.method === PaymentMethod.Transfer) {
245
- await order.markValid(payment, []);
246
+ await OrderService.markValid(order, payment, []);
246
247
 
247
248
  if (order.number) {
248
249
  balanceItem.description = order.generateBalanceDescription(webshop);
@@ -253,7 +254,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
253
254
  await payment.save();
254
255
  } else if (payment.method === PaymentMethod.PointOfSale) {
255
256
  // Not really paid, but needed to create the tickets if needed
256
- await order.markPaid(payment, organization, webshop);
257
+ await OrderService.markPaid(order, payment, organization, webshop);
257
258
 
258
259
  if (order.number) {
259
260
  balanceItem.description = order.generateBalanceDescription(webshop);
@@ -16,7 +16,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.BalanceItems, {
16
16
  results: await BalanceItem.getStructureWithPayments(data.results),
17
17
  });
18
18
  },
19
- sheets: [
19
+ getSheets: () => [
20
20
  {
21
21
  id: 'balanceItems',
22
22
  name: $t(`%1LA`),
@@ -1,14 +1,14 @@
1
1
  import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
3
- import type { EventNotification, LimitedFilteredRequest } from '@stamhoofd/structures';
4
- import { EventNotificationStatus, EventNotificationStatusHelper, ExcelExportType, Platform as PlatformStruct } from '@stamhoofd/structures';
3
+ import type { EventNotification, LimitedFilteredRequest, Platform as PlatformStruct } from '@stamhoofd/structures';
4
+ import { EventNotificationStatus, EventNotificationStatusHelper, ExcelExportType } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { GetEventNotificationsEndpoint } from '../endpoints/global/events/GetEventNotificationsEndpoint.js';
7
7
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
8
8
  import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
9
9
 
10
10
  // Assign to a typed variable to assure we have correct type checking in place
11
- const sheet: XlsxTransformerSheet<EventNotification, EventNotification> = {
11
+ const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<EventNotification, EventNotification> => ({
12
12
  id: 'event-notifications',
13
13
  name: $t(`%1FR`),
14
14
  columns: [
@@ -118,18 +118,17 @@ const sheet: XlsxTransformerSheet<EventNotification, EventNotification> = {
118
118
  matchId: 'recordAnswers',
119
119
  getRecordAnswers: (notification: EventNotification) => notification.recordAnswers,
120
120
  getRecordCategories: () => {
121
- const platform = PlatformStruct.shared;
122
121
  return platform.config.eventNotificationTypes.flatMap(r => r.recordCategories);
123
122
  },
124
123
  }),
125
124
  ],
126
- };
125
+ });
127
126
 
128
127
  ExportToExcelEndpoint.loaders.set(ExcelExportType.EventNotifications, {
129
128
  fetch: async (query: LimitedFilteredRequest) => {
130
129
  return await GetEventNotificationsEndpoint.buildData(query);
131
130
  },
132
- sheets: [
133
- sheet,
131
+ getSheets: platform => [
132
+ getSheet(platform),
134
133
  ],
135
134
  });
@@ -1,7 +1,7 @@
1
1
  import type { XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
2
2
  import type { PlatformMember } from '@stamhoofd/structures';
3
3
  import { EmergencyContact, MemberDetails, MembersBlob, MemberWithRegistrationsBlob, Organization, Platform, PlatformFamily } from '@stamhoofd/structures';
4
- import { baseMemberColumns } from './members.js';
4
+ import { getBaseMemberColumns } from './members.js';
5
5
 
6
6
  describe('Member excel export', () => {
7
7
  describe('emergencyContacts column', () => {
@@ -21,7 +21,7 @@ describe('Member excel export', () => {
21
21
  }
22
22
 
23
23
  function getColumn() {
24
- const column = baseMemberColumns.find(c => 'id' in c && c.id === 'emergencyContacts') as XlsxTransformerConcreteColumn<PlatformMember> | undefined;
24
+ const column = getBaseMemberColumns(Platform.create({})).find(c => 'id' in c && c.id === 'emergencyContacts') as XlsxTransformerConcreteColumn<PlatformMember> | undefined;
25
25
 
26
26
  if (!column) {
27
27
  throw new Error('Column emergencyContacts not found');
@@ -1,8 +1,8 @@
1
1
  import type { XlsxTransformerColumn, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
3
3
  import { Platform } from '@stamhoofd/models';
4
- import type { LimitedFilteredRequest, PlatformMember } from '@stamhoofd/structures';
5
- import { ExcelExportType, Gender, GroupType, MembershipStatus, PlatformFamily, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
4
+ import type { LimitedFilteredRequest, PlatformMember, Platform as PlatformStruct } from '@stamhoofd/structures';
5
+ import { ExcelExportType, Gender, GroupType, MembershipStatus, PlatformFamily, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
8
8
  import { GetMembersEndpoint } from '../endpoints/global/members/GetMembersEndpoint.js';
@@ -10,7 +10,7 @@ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
10
10
  import { Context } from '../helpers/Context.js';
11
11
  import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
12
12
 
13
- export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
13
+ export const getBaseMemberColumns = (platform: PlatformStruct): XlsxTransformerColumn<PlatformMember>[] => [
14
14
  {
15
15
  id: 'id',
16
16
  name: $t(`%d`),
@@ -244,7 +244,6 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
244
244
  matchId: 'recordAnswers',
245
245
  getRecordAnswers: ({ patchedMember: object }: PlatformMember) => object.details.recordAnswers,
246
246
  getRecordCategories: () => {
247
- const platform = PlatformStruct.shared;
248
247
  const organization = Context.organization;
249
248
 
250
249
  return [
@@ -256,11 +255,11 @@ export const baseMemberColumns: XlsxTransformerColumn<PlatformMember>[] = [
256
255
  ];
257
256
 
258
257
  // Assign to a typed variable to assure we have correct type checking in place
259
- const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
258
+ const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<PlatformMember, PlatformMember> => ({
260
259
  id: 'members',
261
260
  name: $t(`%1EH`),
262
261
  columns: [
263
- ...baseMemberColumns,
262
+ ...getBaseMemberColumns(platform),
264
263
  {
265
264
  id: 'group',
266
265
  name: $t(`%wH`),
@@ -281,7 +280,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
281
280
  getValue: (member: PlatformMember) => {
282
281
  const groups = member.filterRegistrations({ currentPeriod: true, types: [GroupType.Membership], organizationId: Context.organization?.id });
283
282
  const defaultAgeGroupIds = Formatter.uniqueArray(groups.filter(o => o.group.defaultAgeGroupId));
284
- const defaultAgeGroups = defaultAgeGroupIds.map(o => PlatformStruct.shared.config.defaultAgeGroups.find(g => g.id === o.group.defaultAgeGroupId)?.name ?? $t(`%wJ`));
283
+ const defaultAgeGroups = defaultAgeGroupIds.map(o => platform.config.defaultAgeGroups.find(g => g.id === o.group.defaultAgeGroupId)?.name ?? $t(`%wJ`));
285
284
  const str = Formatter.joinLast(Formatter.uniqueArray(defaultAgeGroups).sort(), ', ', ' ' + $t(`%M1`) + ' ') || Context.i18n.$t('%5D');
286
285
 
287
286
  return {
@@ -472,7 +471,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
472
471
  },
473
472
  },
474
473
  ],
475
- };
474
+ });
476
475
 
477
476
  ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
478
477
  fetch: async (query: LimitedFilteredRequest) => {
@@ -486,8 +485,8 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
486
485
  next: result.next,
487
486
  });
488
487
  },
489
- sheets: [
490
- sheet,
488
+ getSheets: platform => [
489
+ getSheet(platform),
491
490
  ],
492
491
  });
493
492
 
@@ -1,8 +1,8 @@
1
1
  import { ArrayDecoder, field } from '@simonbackx/simple-encoding';
2
2
  import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
3
- import { Group, Member, MemberResponsibilityRecord } from '@stamhoofd/models';
4
- import type { LimitedFilteredRequest, Premise } from '@stamhoofd/structures';
5
- import { ExcelExportType, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, Organization as OrganizationStruct, PaginatedResponse, Platform as PlatformStruct } from '@stamhoofd/structures';
3
+ import { Group, Member, MemberResponsibilityRecord, Platform } from '@stamhoofd/models';
4
+ import type { LimitedFilteredRequest, Platform as PlatformStruct, Premise } from '@stamhoofd/structures';
5
+ import { ExcelExportType, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, Organization as OrganizationStruct, PaginatedResponse } from '@stamhoofd/structures';
6
6
  import { Formatter, Sorter } from '@stamhoofd/utility';
7
7
  import { GetOrganizationsEndpoint } from '../endpoints/admin/organizations/GetOrganizationsEndpoint.js';
8
8
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
@@ -27,7 +27,7 @@ class MemberResponsibilityRecordWithMemberAndOrganization extends MemberResponsi
27
27
  type Object = OrganizationWithResponsibilities;
28
28
 
29
29
  // Assign to a typed variable to assure we have correct type checking in place
30
- const sheet: XlsxTransformerSheet<Object, Object> = {
30
+ const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<Object, Object> => ({
31
31
  id: 'organizations',
32
32
  name: $t(`%wP`),
33
33
  columns: [
@@ -60,8 +60,6 @@ const sheet: XlsxTransformerSheet<Object, Object> = {
60
60
  name: $t(`%13`),
61
61
  width: 50,
62
62
  getValue: (object: Object) => {
63
- const platform = PlatformStruct.shared;
64
-
65
63
  return {
66
64
  value: object.meta.tags.map(tag => platform.config.tags.find(t => t.id === tag)?.name ?? $t(`%Gr`)).join(', '),
67
65
  };
@@ -76,17 +74,15 @@ const sheet: XlsxTransformerSheet<Object, Object> = {
76
74
  matchId: 'recordAnswers',
77
75
  getRecordAnswers: (object: Object) => object.getRecordAnswers(),
78
76
  getRecordCategories: () => {
79
- const platform = PlatformStruct.shared;
80
-
81
77
  return [
82
78
  ...platform.config.organizationLevelRecordsConfiguration.recordCategories,
83
79
  ];
84
80
  },
85
81
  }),
86
82
  ],
87
- };
83
+ });
88
84
 
89
- const responsibilities: XlsxTransformerSheet<Object, MemberResponsibilityRecordWithMemberAndOrganization> = {
85
+ const getResponsibilitiesSheet = (platform: PlatformStruct): XlsxTransformerSheet<Object, MemberResponsibilityRecordWithMemberAndOrganization> => ({
90
86
  id: 'responsibilities',
91
87
  name: $t(`%7D`),
92
88
  transform(organization) {
@@ -125,7 +121,6 @@ const responsibilities: XlsxTransformerSheet<Object, MemberResponsibilityRecordW
125
121
  name: $t(`%H2`),
126
122
  width: 50,
127
123
  getValue: (object: MemberResponsibilityRecordWithMemberAndOrganization) => {
128
- const platform = PlatformStruct.shared;
129
124
  const responsibility = platform.config.responsibilities.find(r => r.id === object.responsibilityId) ?? object.organization.privateMeta?.responsibilities.find(r => r.id === object.responsibilityId);
130
125
 
131
126
  if (!responsibility) {
@@ -168,10 +163,10 @@ const responsibilities: XlsxTransformerSheet<Object, MemberResponsibilityRecordW
168
163
  getAddress: object => object.member.details.address ?? object.member.details.parents[0]?.address ?? object.member.details.parents[1]?.address ?? null,
169
164
  }),
170
165
  ],
171
- };
166
+ });
172
167
 
173
168
  type PremiseWithOrganization = { organization: Object; premise: Premise };
174
- const premises: XlsxTransformerSheet<Object, PremiseWithOrganization> = {
169
+ const getPremisesSheet = (platform: PlatformStruct): XlsxTransformerSheet<Object, PremiseWithOrganization> => ({
175
170
  id: 'premises',
176
171
  name: $t(`%6c`),
177
172
  transform(organization) {
@@ -219,7 +214,6 @@ const premises: XlsxTransformerSheet<Object, PremiseWithOrganization> = {
219
214
  width: 20,
220
215
  getValue: (object: PremiseWithOrganization) => {
221
216
  const ids = object.premise.premiseTypeIds;
222
- const platform = PlatformStruct.shared;
223
217
  return {
224
218
  value: ids.map(id => platform.config.premiseTypes.find(t => t.id === id)?.name ?? $t(`%Gr`)).join(', '),
225
219
  };
@@ -230,7 +224,7 @@ const premises: XlsxTransformerSheet<Object, PremiseWithOrganization> = {
230
224
  getAddress: object => object.premise.address,
231
225
  }),
232
226
  ],
233
- };
227
+ });
234
228
 
235
229
  ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
236
230
  fetch: async (query: LimitedFilteredRequest) => {
@@ -253,7 +247,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
253
247
  const groups = await Group.getByIDs(...groupIds);
254
248
  const memberStructs = await AuthenticatedStructures.members(members);
255
249
  const groupStructs = await AuthenticatedStructures.groups(groups);
256
- const platform = PlatformStruct.shared;
250
+ const platform = await Platform.getSharedStruct();
257
251
 
258
252
  const mappedOrganizations = organizations.results.map((o) => {
259
253
  const resp = responsibilities.filter(r => r.organizationId === o.id);
@@ -295,9 +289,9 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Organizations, {
295
289
  next: organizations.next,
296
290
  });
297
291
  },
298
- sheets: [
299
- sheet,
300
- responsibilities,
301
- premises,
292
+ getSheets: platform => [
293
+ getSheet(platform),
294
+ getResponsibilitiesSheet(platform),
295
+ getPremisesSheet(platform),
302
296
  ],
303
297
  });
@@ -62,7 +62,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Payments, {
62
62
  }),
63
63
  });
64
64
  },
65
- sheets: [
65
+ getSheets: () => [
66
66
  {
67
67
  id: 'payments',
68
68
  name: $t(`%1JH`),
@@ -1,12 +1,12 @@
1
1
  import type { XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { XlsxBuiltInNumberFormat } from '@stamhoofd/excel-writer';
3
- import type { LimitedFilteredRequest, PlatformMembership } from '@stamhoofd/structures';
4
- import { ExcelExportType, PaginatedResponse, Platform } from '@stamhoofd/structures';
3
+ import type { LimitedFilteredRequest, PlatformMembership, Platform as PlatformStruct } from '@stamhoofd/structures';
4
+ import { ExcelExportType, PaginatedResponse } from '@stamhoofd/structures';
5
5
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
6
6
  import { GetPlatformMembershipsEndpoint } from '../endpoints/global/platform-memberships/GetPlatformMembershipsEndpoint.js';
7
7
 
8
8
  // Assign to a typed variable to assure we have correct type checking in place
9
- const sheet: XlsxTransformerSheet<PlatformMembership> = {
9
+ const getSheet = (platform: PlatformStruct): XlsxTransformerSheet<PlatformMembership> => ({
10
10
  id: 'platform-memberships',
11
11
  name: $t('%1EI'),
12
12
  columns: [
@@ -23,7 +23,7 @@ const sheet: XlsxTransformerSheet<PlatformMembership> = {
23
23
  name: $t('%1LP'),
24
24
  width: 40,
25
25
  getValue: (membership: PlatformMembership) => {
26
- const membershipType = Platform.shared.config.membershipTypes.find(m => m.id === membership.membershipTypeId);
26
+ const membershipType = platform.config.membershipTypes.find(m => m.id === membership.membershipTypeId);
27
27
  const value = membershipType ? membershipType.name : '';
28
28
  return { value };
29
29
  },
@@ -244,7 +244,7 @@ const sheet: XlsxTransformerSheet<PlatformMembership> = {
244
244
  }),
245
245
  },
246
246
  ],
247
- };
247
+ });
248
248
 
249
249
  ExportToExcelEndpoint.loaders.set(ExcelExportType.PlatformMemberships, {
250
250
  fetch: async (query: LimitedFilteredRequest) => {
@@ -255,7 +255,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.PlatformMemberships, {
255
255
  next: data.next,
256
256
  });
257
257
  },
258
- sheets: [
259
- sheet,
258
+ getSheets: platform => [
259
+ getSheet(platform),
260
260
  ],
261
261
  });