@stamhoofd/backend 2.137.3 → 2.137.5

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 (68) hide show
  1. package/package.json +23 -17
  2. package/src/crons/drip-emails.ts +2 -1
  3. package/src/crons.ts +4 -2
  4. package/src/email-recipient-loaders/payments.ts +3 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +3 -2
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +2 -1
  7. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +3 -2
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +6 -3
  9. package/src/endpoints/auth/RetryEmailVerificationEndpoint.ts +2 -1
  10. package/src/endpoints/auth/SignupEndpoint.ts +4 -2
  11. package/src/endpoints/global/email/CreateEmailEndpoint.ts +2 -1
  12. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +1 -1
  13. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +2 -1
  14. package/src/endpoints/global/email/GetEmailEndpoint.ts +2 -1
  15. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +1 -1
  16. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +2 -1
  17. package/src/endpoints/global/email/PatchEmailEndpoint.ts +2 -1
  18. package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -5
  19. package/src/endpoints/global/files/UploadFile.test.ts +206 -0
  20. package/src/endpoints/global/files/UploadFile.ts +31 -6
  21. package/src/endpoints/global/files/UploadImage.test.ts +177 -0
  22. package/src/endpoints/global/files/UploadImage.ts +23 -4
  23. package/src/endpoints/global/files/upload-security.test.ts +837 -0
  24. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +1 -1
  25. package/src/endpoints/global/organizations/CreateOrganizationEndpoint.ts +2 -1
  26. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplateXML.ts +2 -1
  27. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -2
  28. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.ts +5 -3
  29. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +4 -3
  30. package/src/endpoints/organization/shared/GetDocumentHtml.ts +2 -1
  31. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -3
  32. package/src/excel-loaders/balance-items.ts +1 -1
  33. package/src/excel-loaders/event-notifications.ts +6 -7
  34. package/src/excel-loaders/members.test.ts +2 -2
  35. package/src/excel-loaders/members.ts +9 -10
  36. package/src/excel-loaders/organizations.ts +14 -20
  37. package/src/excel-loaders/payments.ts +1 -1
  38. package/src/excel-loaders/platform-memberships.ts +7 -7
  39. package/src/excel-loaders/platform-sheets.test.ts +113 -0
  40. package/src/excel-loaders/receivable-balances.ts +1 -1
  41. package/src/excel-loaders/registrations.ts +9 -9
  42. package/src/helpers/AdminPermissionChecker.ts +1 -1
  43. package/src/helpers/AuthenticatedStructures.ts +12 -9
  44. package/src/helpers/MembershipCharger.ts +3 -2
  45. package/src/services/BalanceItemService.ts +2 -1
  46. package/src/services/DocumentRenderService.test.ts +229 -0
  47. package/src/services/DocumentRenderService.ts +180 -0
  48. package/src/services/EmailPreviewService.test.ts +300 -0
  49. package/src/services/EmailPreviewService.ts +239 -0
  50. package/src/services/FileSignService.test.ts +85 -0
  51. package/src/services/FileSignService.ts +23 -1
  52. package/src/services/OrderService.test.ts +308 -0
  53. package/src/services/OrderService.ts +214 -0
  54. package/src/services/OrganizationDNSService.test.ts +177 -0
  55. package/src/services/OrganizationDNSService.ts +282 -0
  56. package/src/services/OrganizationEmailService.test.ts +57 -0
  57. package/src/services/OrganizationEmailService.ts +211 -0
  58. package/src/services/PasswordForgotService.test.ts +99 -0
  59. package/src/services/PasswordForgotService.ts +38 -0
  60. package/src/services/PlatformMembershipService.test.ts +180 -0
  61. package/src/services/PlatformMembershipService.ts +281 -4
  62. package/src/services/RegistrationService.ts +3 -2
  63. package/src/services/STPackageService.test.ts +191 -0
  64. package/src/services/STPackageService.ts +114 -2
  65. package/src/services/VerificationCodeService.test.ts +71 -0
  66. package/src/services/VerificationCodeService.ts +100 -0
  67. package/tests/e2e/documents.test.ts +2 -1
  68. package/tests/e2e/private-files.test.ts +77 -14
@@ -585,7 +585,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
585
585
  membership.locked = false;
586
586
 
587
587
  // Correct price and dates
588
- await membership.calculatePrice(member);
588
+ await PlatformMembershipService.calculatePrice(membership, member);
589
589
 
590
590
  const incompatible = await MemberPlatformMembership.select()
591
591
  .where('memberId', member.id)
@@ -12,6 +12,7 @@ import { Formatter } from '@stamhoofd/utility';
12
12
  import { v4 as uuidv4 } from 'uuid';
13
13
  import { AuditLogService } from '../../../services/AuditLogService.js';
14
14
  import { ReferralService } from '../../../services/ReferralService.js';
15
+ import { VerificationCodeService } from '../../../services/VerificationCodeService.js';
15
16
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
16
17
  import { Context } from '../../../helpers/Context.js';
17
18
 
@@ -160,7 +161,7 @@ export class CreateOrganizationEndpoint extends Endpoint<Params, Query, Body, Re
160
161
  await AuditLog.update().where('type', AuditLogType.OrganizationAdded).where('objectId', organization.id).set('userId', user.id).set('source', AuditLogSource.User).update();
161
162
 
162
163
  const code = await EmailVerificationCode.createFor(user, user.email);
163
- code.send(user, organization, request.i18n).catch(console.error);
164
+ VerificationCodeService.send(code, user, organization, request.i18n).catch(console.error);
164
165
 
165
166
  for (const email of delayEmails) {
166
167
  Email.send({
@@ -4,6 +4,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { DocumentTemplate } from '@stamhoofd/models';
5
5
 
6
6
  import { Context } from '../../../../helpers/Context.js';
7
+ import { DocumentRenderService } from '../../../../services/DocumentRenderService.js';
7
8
 
8
9
  type Params = { id: string };
9
10
  type Query = undefined;
@@ -42,7 +43,7 @@ export class GetDocumentTemplateXMLEndpoint extends Endpoint<Params, Query, Body
42
43
  }
43
44
 
44
45
  // Update documents
45
- const xml = await template.getRenderedXml(organization);
46
+ const xml = await DocumentRenderService.getRenderedXml(template, organization);
46
47
  if (!xml) {
47
48
  throw new SimpleError({
48
49
  code: 'failed_generating',
@@ -9,6 +9,7 @@ import NodeRSA from 'node-rsa';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
11
11
  import { Context } from '../../../../helpers/Context.js';
12
+ import { OrganizationDNSService } from '../../../../services/OrganizationDNSService.js';
12
13
 
13
14
  type Params = Record<string, never>;
14
15
  type Query = undefined;
@@ -169,7 +170,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
169
170
  }));
170
171
  } else {
171
172
  if (oldMailDomain) {
172
- organization.deleteAWSMailIdenitity(oldMailDomain).catch(console.error);
173
+ OrganizationDNSService.deleteAWSMailIdenitity(organization, oldMailDomain).catch(console.error);
173
174
  }
174
175
 
175
176
  if (organization.serverMeta.privateDKIMKey && organization.serverMeta.publicDKIMKey) {
@@ -188,7 +189,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
188
189
  } else {
189
190
  // Validate DNS-records if not empty
190
191
  console.log('Validating domains');
191
- await organization.updateDNSRecords();
192
+ await OrganizationDNSService.updateDNSRecords(organization);
192
193
  }
193
194
 
194
195
  console.log('Done.');
@@ -3,7 +3,7 @@ import { isPatch } from '@simonbackx/simple-encoding';
3
3
  import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
4
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
- import { Token, User } from '@stamhoofd/models';
6
+ import { Platform, Token, User } from '@stamhoofd/models';
7
7
  import { ApiUser, PermissionLevel, UserMeta, UserPermissions } from '@stamhoofd/structures';
8
8
  import { Context } from '../../../../helpers/Context.js';
9
9
 
@@ -58,10 +58,12 @@ export class PatchApiUserEndpoint extends Endpoint<Params, Query, Body, Response
58
58
  }
59
59
 
60
60
  if (request.body.permissions) {
61
+ const platform = await Platform.getSharedStruct();
62
+
61
63
  if (organization) {
62
64
  editUser.permissions = UserPermissions.limitedPatch(editUser.permissions, request.body.permissions, organization.id);
63
65
 
64
- if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess())) {
66
+ if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization, platform, { inheritFromPlatform: false })?.hasFullAccess())) {
65
67
  throw new SimpleError({
66
68
  code: 'permission_denied',
67
69
  message: 'Je kan jezelf niet verwijderen als hoofdbeheerder',
@@ -74,7 +76,7 @@ export class PatchApiUserEndpoint extends Endpoint<Params, Query, Body, Response
74
76
  editUser.permissions = request.body.permissions.isPut() ? request.body.permissions : null;
75
77
  }
76
78
 
77
- if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess()) {
79
+ if (editUser.id === user.id && !editUser.permissions?.forPlatform(platform)?.hasFullAccess()) {
78
80
  throw new SimpleError({
79
81
  code: 'permission_denied',
80
82
  message: 'Je kan jezelf niet verwijderen als hoofdbeheerder',
@@ -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
  });
@@ -0,0 +1,113 @@
1
+ import type { XlsxTransformerColumn, XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
2
+ import { isXlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
3
+ import { EventNotificationType, ExcelExportType, Organization, OrganizationMetaData, OrganizationTag, Platform, PlatformConfig, PlatformMembership, PlatformMembershipMemberDetails, PlatformMembershipOrganizationDetails, PlatformMembershipType, RecordCategory, RecordSettings, RecordType, TranslatedString } from '@stamhoofd/structures';
4
+ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
5
+ import './index.js';
6
+
7
+ /**
8
+ * The sheets are built per export request, so every callback should read the platform it was built
9
+ * with instead of a process wide singleton.
10
+ */
11
+ describe('Excel loaders platform argument', () => {
12
+ function getColumns(type: ExcelExportType, platform: Platform, sheetId: string): XlsxTransformerColumn<unknown>[] {
13
+ const loader = ExportToExcelEndpoint.loaders.get(type);
14
+
15
+ if (!loader) {
16
+ throw new Error('Loader ' + type + ' not registered');
17
+ }
18
+
19
+ const sheet = loader.getSheets(platform).find(s => s.id === sheetId);
20
+
21
+ if (!sheet) {
22
+ throw new Error('Sheet ' + sheetId + ' not found');
23
+ }
24
+
25
+ return sheet.columns;
26
+ }
27
+
28
+ function getColumn(type: ExcelExportType, platform: Platform, sheetId: string, columnId: string): XlsxTransformerConcreteColumn<unknown> {
29
+ const column = getColumns(type, platform, sheetId).find(c => isXlsxTransformerConcreteColumn(c) && c.id === columnId);
30
+
31
+ if (!column || !isXlsxTransformerConcreteColumn(column)) {
32
+ throw new Error('Column ' + columnId + ' not found');
33
+ }
34
+
35
+ return column;
36
+ }
37
+
38
+ function createPlatformWithTag(tagName: string) {
39
+ return Platform.create({
40
+ config: PlatformConfig.create({
41
+ tags: [
42
+ OrganizationTag.create({ id: 'tag-1', name: tagName }),
43
+ ],
44
+ }),
45
+ });
46
+ }
47
+
48
+ it('reads the organization tags from the platform the sheets were built with', () => {
49
+ const organization = Organization.create({
50
+ meta: OrganizationMetaData.create({ tags: ['tag-1'] }),
51
+ });
52
+
53
+ const first = getColumn(ExcelExportType.Organizations, createPlatformWithTag('Gewest Noord'), 'organizations', 'tags');
54
+ expect(first.getValue(organization).value).toBe('Gewest Noord');
55
+
56
+ // Building the sheets again with another platform should not reuse the first platform
57
+ const second = getColumn(ExcelExportType.Organizations, createPlatformWithTag('Gewest Zuid'), 'organizations', 'tags');
58
+ expect(second.getValue(organization).value).toBe('Gewest Zuid');
59
+ });
60
+
61
+ it('reads the membership type from the platform the sheets were built with', () => {
62
+ const platform = Platform.create({
63
+ config: PlatformConfig.create({
64
+ membershipTypes: [
65
+ PlatformMembershipType.create({ id: 'type-1', name: 'Aansluiting' }),
66
+ ],
67
+ }),
68
+ });
69
+
70
+ const membership = PlatformMembership.create({
71
+ memberId: 'member-1',
72
+ membershipTypeId: 'type-1',
73
+ organizationId: 'organization-1',
74
+ periodId: 'period-1',
75
+ member: PlatformMembershipMemberDetails.create({ firstName: 'John', lastName: 'Doe', memberNumber: null }),
76
+ organization: PlatformMembershipOrganizationDetails.create({}),
77
+ balanceItem: null,
78
+ });
79
+
80
+ const column = getColumn(ExcelExportType.PlatformMemberships, platform, 'platform-memberships', 'type');
81
+ expect(column.getValue(membership).value).toBe('Aansluiting');
82
+ });
83
+
84
+ it('reads the record categories of an event notification from the platform the sheets were built with', () => {
85
+ const platform = Platform.create({
86
+ config: PlatformConfig.create({
87
+ eventNotificationTypes: [
88
+ EventNotificationType.create({
89
+ recordCategories: [
90
+ RecordCategory.create({
91
+ name: new TranslatedString('Kamp'),
92
+ records: [
93
+ RecordSettings.create({
94
+ id: 'record-1',
95
+ name: new TranslatedString('Aantal deelnemers'),
96
+ type: RecordType.Text,
97
+ }),
98
+ ],
99
+ }),
100
+ ],
101
+ }),
102
+ ],
103
+ }),
104
+ });
105
+
106
+ const columns = getColumns(ExcelExportType.EventNotifications, platform, 'event-notifications');
107
+ const matched = columns.flatMap(c => isXlsxTransformerConcreteColumn(c) ? [] : (c.match('recordAnswers.record-1') ?? []));
108
+
109
+ expect(matched).toHaveLength(1);
110
+ expect(matched[0].name).toBe('Aantal deelnemers');
111
+ expect(matched[0].category).toBe('Kamp');
112
+ });
113
+ });
@@ -19,7 +19,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.ReceivableBalances, {
19
19
  ...data,
20
20
  });
21
21
  },
22
- sheets: [
22
+ getSheets: () => [
23
23
  {
24
24
  id: 'receivableBalances',
25
25
  name: $t(`%99`),