@stamhoofd/backend 2.90.3 → 2.92.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 (47) hide show
  1. package/package.json +10 -10
  2. package/src/audit-logs/EmailLogger.ts +4 -4
  3. package/src/audit-logs/ModelLogger.ts +0 -1
  4. package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +20 -0
  5. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +2 -0
  6. package/src/endpoints/global/email/CreateEmailEndpoint.ts +30 -7
  7. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +207 -0
  8. package/src/endpoints/global/email/GetEmailEndpoint.ts +5 -1
  9. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +404 -8
  10. package/src/endpoints/global/email/PatchEmailEndpoint.ts +67 -22
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +6 -4
  12. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -7
  13. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +112 -105
  14. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  15. package/src/endpoints/organization/dashboard/organization/SetUitpasClientCredentialsEndpoint.ts +5 -5
  16. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -1
  17. package/src/endpoints/organization/dashboard/webshops/DeleteWebshopEndpoint.ts +10 -1
  18. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
  19. package/src/endpoints/organization/dashboard/webshops/SearchUitpasEventsEndpoint.ts +1 -1
  20. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -67
  21. package/src/helpers/AdminPermissionChecker.ts +81 -10
  22. package/src/helpers/FlagMomentCleanup.ts +13 -1
  23. package/src/helpers/GroupedThrottledQueue.ts +5 -3
  24. package/src/helpers/PeriodHelper.ts +10 -137
  25. package/src/helpers/SetupStepUpdater.ts +54 -7
  26. package/src/helpers/UitpasTokenRepository.ts +3 -3
  27. package/src/seeds/1750090030-records-configuration.ts +5 -1
  28. package/src/seeds/1752848560-groups-registration-periods.ts +768 -0
  29. package/src/seeds/1755181288-remove-duplicate-members.ts +145 -0
  30. package/src/seeds/1755532883-update-email-sender-ids.ts +47 -0
  31. package/src/services/BalanceItemService.ts +12 -7
  32. package/src/services/DocumentService.ts +0 -1
  33. package/src/services/RegistrationService.ts +30 -1
  34. package/src/services/uitpas/UitpasService.ts +72 -3
  35. package/src/services/uitpas/cancelTicketSales.ts +1 -1
  36. package/src/services/uitpas/checkPermissionsFor.ts +9 -9
  37. package/src/services/uitpas/checkUitpasNumbers.ts +3 -2
  38. package/src/services/uitpas/getSocialTariffForEvent.ts +4 -4
  39. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +5 -5
  40. package/src/services/uitpas/registerTicketSales.ts +4 -4
  41. package/src/services/uitpas/searchUitpasEvents.ts +3 -3
  42. package/src/services/uitpas/searchUitpasOrganizers.ts +3 -3
  43. package/src/sql-filters/emails.ts +65 -0
  44. package/src/sql-filters/members.ts +1 -1
  45. package/src/sql-filters/organizations.ts +52 -0
  46. package/src/sql-sorters/emails.ts +47 -0
  47. package/tests/e2e/register.test.ts +1 -1
@@ -15,13 +15,15 @@ export class GroupedThrottledQueue<T> {
15
15
  * In milliseconds.
16
16
  */
17
17
  maxDelay: number | null = 10_000;
18
+ itemDelay: number | null = null;
18
19
 
19
20
  constructor(
20
21
  handler: (group: string, items: T[]) => Promise<void> | void,
21
- options: { maxDelay?: number | null } = {},
22
+ options: { maxDelay?: number | null; itemDelay?: number | null } = {},
22
23
  ) {
23
24
  this.handler = handler;
24
- this.maxDelay = options.maxDelay ?? 10_000;
25
+ this.maxDelay = options.maxDelay !== null ? (options.maxDelay ?? 10_000) : null;
26
+ this.itemDelay = options.itemDelay ?? null;
25
27
  }
26
28
 
27
29
  addItems(group: string, items: T[]): void {
@@ -32,7 +34,7 @@ export class GroupedThrottledQueue<T> {
32
34
  return;
33
35
  }
34
36
  const newQueue = new ThrottledQueue<T>(items => this.handler(group, items), {
35
- maxDelay: null,
37
+ maxDelay: this.itemDelay,
36
38
  emptyHandler: () => {
37
39
  this.queues.delete(group);
38
40
  },
@@ -1,11 +1,10 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
2
+ import { Group, Organization, OrganizationRegistrationPeriod, RegistrationPeriod } from '@stamhoofd/models';
3
3
  import { QueueHandler } from '@stamhoofd/queues';
4
- import { AuditLogSource, Group as GroupStruct, PermissionLevel } from '@stamhoofd/structures';
4
+ import { AuditLogSource, Group as GroupStruct } from '@stamhoofd/structures';
5
5
  import { PatchOrganizationRegistrationPeriodsEndpoint } from '../endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
6
- import { AuthenticatedStructures } from './AuthenticatedStructures';
7
- import { MemberUserSyncer } from './MemberUserSyncer';
8
6
  import { AuditLogService } from '../services/AuditLogService';
7
+ import { AuthenticatedStructures } from './AuthenticatedStructures';
9
8
  import { SetupStepUpdater } from './SetupStepUpdater';
10
9
 
11
10
  export class PeriodHelper {
@@ -18,88 +17,6 @@ export class PeriodHelper {
18
17
  });
19
18
  }
20
19
 
21
- static async stopAllResponsibilities() {
22
- console.log('Stopping all responsibilities');
23
- const platform = await Platform.getSharedPrivateStruct();
24
- const keepPlatformResponsibilityIds = platform.config.responsibilities.filter(r => !r.organizationBased).map(r => r.id);
25
- const keepResponsibilityIds = platform.config.responsibilities.filter(r => !r.organizationBased || r.permissions?.level === PermissionLevel.Full).map(r => r.id);
26
- const batchSize = 100;
27
-
28
- let lastId = '';
29
- let c = 0;
30
-
31
- while (true) {
32
- const records = await MemberResponsibilityRecord.where(
33
- {
34
- id: { sign: '>', value: lastId },
35
- endDate: null,
36
- },
37
- {
38
- limit: batchSize,
39
- sort: ['id'],
40
- },
41
- );
42
-
43
- for (const record of records) {
44
- lastId = record.id;
45
-
46
- const invalid = keepPlatformResponsibilityIds.includes(record.responsibilityId) && record.organizationId;
47
-
48
- if (!keepResponsibilityIds.includes(record.responsibilityId) || invalid) {
49
- record.endDate = new Date();
50
- await record.save();
51
- c++;
52
- }
53
- }
54
-
55
- if (records.length < batchSize) {
56
- break;
57
- }
58
- }
59
-
60
- console.log('Done: stopped all responsibilities: ' + c);
61
- }
62
-
63
- static async syncAllMemberUsers() {
64
- console.log('Syncing all members');
65
-
66
- let c = 0;
67
- let lastId: string = '';
68
-
69
- while (true) {
70
- const rawMembers = await Member.where({
71
- id: {
72
- value: lastId,
73
- sign: '>',
74
- },
75
- }, { limit: 500, sort: ['id'] });
76
-
77
- if (rawMembers.length === 0) {
78
- break;
79
- }
80
-
81
- const membersWithRegistrations = await Member.getBlobByIds(...rawMembers.map(m => m.id));
82
-
83
- const promises: Promise<any>[] = [];
84
-
85
- for (const memberWithRegistrations of membersWithRegistrations) {
86
- promises.push((async () => {
87
- await MemberUserSyncer.onChangeMember(memberWithRegistrations);
88
- c++;
89
-
90
- if (c % 10000 === 0) {
91
- console.log('Synced ' + c + ' members');
92
- }
93
- })());
94
- }
95
-
96
- await Promise.all(promises);
97
- lastId = rawMembers[rawMembers.length - 1].id;
98
- }
99
-
100
- console.log('Done: synced all members: ' + c);
101
- }
102
-
103
20
  static async createOrganizationPeriodForPeriod(organization: Organization, period: RegistrationPeriod) {
104
21
  const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: period.id, organizationId: organization.id }, { limit: 1 });
105
22
 
@@ -128,38 +45,15 @@ export class PeriodHelper {
128
45
  });
129
46
  }
130
47
 
131
- const batchSize = 10;
132
48
  await QueueHandler.schedule(tag, async () => {
133
- let lastId = '';
134
-
135
- while (true) {
136
- const organizations = await Organization.where(
137
- {
138
- id: { sign: '>', value: lastId },
139
- },
140
- {
141
- limit: batchSize,
142
- sort: ['id'],
143
- },
144
- );
145
-
146
- for (const organization of organizations) {
147
- try {
148
- await this.moveOrganizationToPeriod(organization, period);
149
- }
150
- catch (error) {
151
- console.error('Error moving organization to period', organization.id, error);
152
- }
153
- lastId = organization.id;
49
+ for await (const organization of Organization.select().all()) {
50
+ try {
51
+ await this.moveOrganizationToPeriod(organization, period);
154
52
  }
155
-
156
- if (organizations.length < batchSize) {
157
- break;
53
+ catch (error) {
54
+ console.error('Error moving organization to period', organization.id, error);
158
55
  }
159
56
  }
160
-
161
- await this.stopAllResponsibilities();
162
- await this.syncAllMemberUsers();
163
57
  });
164
58
 
165
59
  // When done: update setup steps
@@ -175,31 +69,10 @@ export class PeriodHelper {
175
69
 
176
70
  console.log(tag);
177
71
 
178
- const batchSize = 100;
179
72
  await QueueHandler.schedule(tag, async () => {
180
73
  await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
181
- let lastId = '';
182
-
183
- while (true) {
184
- const groups = await Group.where(
185
- {
186
- id: { sign: '>', value: lastId },
187
- periodId: period.id,
188
- },
189
- {
190
- limit: batchSize,
191
- sort: ['id'],
192
- },
193
- );
194
-
195
- for (const group of groups) {
196
- await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: group.id }), period);
197
- lastId = group.id;
198
- }
199
-
200
- if (groups.length < batchSize) {
201
- break;
202
- }
74
+ for await (const group of Group.select().where('periodId', period.id).all()) {
75
+ await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(GroupStruct.patch({ id: group.id }), period);
203
76
  }
204
77
  });
205
78
  });
@@ -7,6 +7,7 @@ import {
7
7
  Organization,
8
8
  OrganizationRegistrationPeriod,
9
9
  Platform,
10
+ Registration,
10
11
  } from '@stamhoofd/models';
11
12
  import { QueueHandler } from '@stamhoofd/queues';
12
13
  import { SQL, SQLWhereSign } from '@stamhoofd/sql';
@@ -15,11 +16,14 @@ import {
15
16
  GroupType,
16
17
  MemberResponsibility,
17
18
  Platform as PlatformStruct,
19
+ RecordCategory,
18
20
  SetupStepType,
19
21
  SetupSteps,
20
22
  } from '@stamhoofd/structures';
21
23
  import { Formatter } from '@stamhoofd/utility';
22
24
  import { AuditLogService } from '../services/AuditLogService';
25
+ import { GroupedThrottledQueue } from './GroupedThrottledQueue';
26
+ import { AuthenticatedStructures } from './AuthenticatedStructures';
23
27
 
24
28
  type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void | Promise<void>;
25
29
 
@@ -81,10 +85,25 @@ export class SetupStepUpdater {
81
85
  if (before !== after) {
82
86
  console.log('Updating setups steps for organization', event.model.organizationId, 'because of change in members in group', event.model.id, 'from', before, 'to', after, '(limited)');
83
87
  // We need to do a recalculation
84
- SetupStepUpdater.updateForOrganizationId(event.model.organizationId)
88
+ SetupStepUpdater.updateForOrganizationId(event.model.organizationId, { types: [SetupStepType.Registrations] })
85
89
  .catch(console.error);
86
90
  }
87
91
  });
92
+
93
+ const updateResponsibilitiesQueue = new GroupedThrottledQueue(async (organizationId: string) => {
94
+ console.log('(delayed) Updating setups steps for organization', organizationId, 'because of change in registrations');
95
+ SetupStepUpdater.updateForOrganizationId(organizationId, { types: [SetupStepType.Responsibilities] }).catch(console.error);
96
+ }, { maxDelay: 3_000 });
97
+
98
+ Model.modelEventBus.addListener({}, async (event) => {
99
+ if (!(event.model instanceof Registration)) {
100
+ return;
101
+ }
102
+
103
+ if ((event.type === 'updated' && ('registeredAt' in event.changedFields || 'deactivatedAt' in event.changedFields)) || (event.type === 'created' && event.model.registeredAt)) {
104
+ updateResponsibilitiesQueue.addItem(event.model.organizationId, 1);
105
+ }
106
+ });
88
107
  }
89
108
 
90
109
  private static readonly STEP_TYPE_OPERATIONS: Record<
@@ -170,7 +189,7 @@ export class SetupStepUpdater {
170
189
  });
171
190
  }
172
191
 
173
- static async updateForOrganizationId(id: string) {
192
+ static async updateForOrganizationId(id: string, options?: { types?: SetupStepType[] }) {
174
193
  const organization = await Organization.getByID(id);
175
194
  if (!organization) {
176
195
  throw new SimpleError({
@@ -180,7 +199,7 @@ export class SetupStepUpdater {
180
199
  });
181
200
  }
182
201
 
183
- await this.updateForOrganization(organization);
202
+ await this.updateForOrganization(organization, options);
184
203
  }
185
204
 
186
205
  static async updateForOrganization(
@@ -188,9 +207,11 @@ export class SetupStepUpdater {
188
207
  {
189
208
  platform,
190
209
  organizationRegistrationPeriod,
210
+ types,
191
211
  }: {
192
212
  platform?: PlatformStruct;
193
213
  organizationRegistrationPeriod?: OrganizationRegistrationPeriod;
214
+ types?: SetupStepType[];
194
215
  } = {},
195
216
  ) {
196
217
  if (!platform) {
@@ -222,6 +243,7 @@ export class SetupStepUpdater {
222
243
  organizationRegistrationPeriod,
223
244
  platform,
224
245
  organization,
246
+ types,
225
247
  );
226
248
  }
227
249
 
@@ -229,12 +251,13 @@ export class SetupStepUpdater {
229
251
  organizationRegistrationPeriod: OrganizationRegistrationPeriod,
230
252
  platform: PlatformStruct,
231
253
  organization: Organization,
254
+ types?: SetupStepType[],
232
255
  ) {
233
256
  console.log('Updating setup steps for organization', organization.id);
234
257
  const setupSteps = organizationRegistrationPeriod.setupSteps;
235
258
 
236
259
  await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
237
- for (const stepType of Object.values(SetupStepType)) {
260
+ for (const stepType of types ?? Object.values(SetupStepType)) {
238
261
  const operation = this.STEP_TYPE_OPERATIONS[stepType];
239
262
  await operation(setupSteps, organization, platform);
240
263
  }
@@ -309,15 +332,28 @@ export class SetupStepUpdater {
309
332
  private static updateStepCompanies(
310
333
  setupSteps: SetupSteps,
311
334
  organization: Organization,
312
- _platform: PlatformStruct,
335
+ platform: PlatformStruct,
313
336
  ) {
314
337
  const totalSteps = 1;
315
338
  let finishedSteps = 0;
316
339
 
317
340
  if (organization.meta.companies.length) {
318
341
  finishedSteps = 1;
342
+
343
+ try {
344
+ RecordCategory.validate(
345
+ platform.config.organizationLevelRecordsConfiguration.recordCategories,
346
+ organization.getBaseStructureWithPrivateMeta(), // private data needed for answers
347
+ );
348
+ }
349
+ catch (error) {
350
+ console.error('Error validating record categories for organization', organization.id, error);
351
+ finishedSteps = 0;
352
+ }
319
353
  }
320
354
 
355
+ console.log('Updating companies step for organization', organization.id, 'with total steps', totalSteps, 'and finished steps', finishedSteps);
356
+
321
357
  setupSteps.update(SetupStepType.Companies, {
322
358
  totalSteps,
323
359
  finishedSteps,
@@ -346,11 +382,14 @@ export class SetupStepUpdater {
346
382
  // Remove invalid responsibilities: members that are not registered in the current period
347
383
  const memberIds = Formatter.uniqueArray(allRecords.map(r => r.memberId));
348
384
  const members = await Member.getBlobByIds(...memberIds);
349
- const validMembers = members.filter(m => m.registrations.some(r => r.organizationId === organization.id && r.periodId === organization.periodId && r.group.type === GroupType.Membership && r.deactivatedAt === null && r.registeredAt !== null));
385
+ const validMembers = members.filter(m => m.registrations.some(r => r.organizationId === organization.id && r.periodId === organization.periodId && r.group.defaultAgeGroupId && r.group.type === GroupType.Membership && r.deactivatedAt === null && r.registeredAt !== null));
386
+ const invalidMembers = members.filter(m => !m.registrations.some(r => r.organizationId === organization.id && r.periodId === organization.periodId && r.group.defaultAgeGroupId && r.group.type === GroupType.Membership && r.deactivatedAt === null && r.registeredAt !== null));
350
387
 
351
388
  const validMembersIds = validMembers.map(m => m.id);
389
+ const invalidMembersIds = invalidMembers.map(m => m.id);
352
390
 
353
391
  const records = allRecords.filter(r => validMembersIds.includes(r.memberId));
392
+ const invalidRecords = allRecords.filter(r => invalidMembersIds.includes(r.memberId));
354
393
 
355
394
  let totalSteps = 0;
356
395
  let finishedSteps = 0;
@@ -381,6 +420,13 @@ export class SetupStepUpdater {
381
420
  for (const { responsibility, group } of flatResponsibilities) {
382
421
  const { minimumMembers: min, maximumMembers: max } = responsibility;
383
422
 
423
+ const invalidMembersWithThisResponsibility = !!invalidRecords.find(r => r.responsibilityId === responsibility.id && (group ? r.groupId === group.id : true));
424
+ if (invalidMembersWithThisResponsibility) {
425
+ totalSteps += Math.max(min ?? 1, 1);
426
+ // Step not okay: we have an invalid member with this responsibility
427
+ continue;
428
+ }
429
+
384
430
  if (min === null) {
385
431
  continue;
386
432
  }
@@ -406,13 +452,14 @@ export class SetupStepUpdater {
406
452
  }
407
453
 
408
454
  if (max !== null && totalRecordsWithThisResponsibility > max) {
409
- // Not added
455
+ // Not added (not okay)
410
456
  continue;
411
457
  }
412
458
 
413
459
  finishedSteps += Math.min(min, totalRecordsWithThisResponsibility);
414
460
  }
415
461
 
462
+ console.log('Updating responsibilities step for organization', organization.id, 'with total steps', totalSteps, 'and finished steps', finishedSteps);
416
463
  setupSteps.update(SetupStepType.Responsibilities, {
417
464
  totalSteps,
418
465
  finishedSteps,
@@ -83,7 +83,7 @@ export class UitpasTokenRepository {
83
83
  throw new SimpleError({
84
84
  code: 'uitpas_api_not_configured_for_platform',
85
85
  message: 'UiTPAS api is not configured for the platform',
86
- human: $t('UiTPAS is niet volledig geconfigureerd, contacteer de platformbeheerder.'),
86
+ human: $t('71a8218b-c58e-4e95-9626-551b80eb8367'),
87
87
  });
88
88
  }
89
89
  model = new UitpasClientCredential();
@@ -123,7 +123,7 @@ export class UitpasTokenRepository {
123
123
  throw new SimpleError({
124
124
  code: 'invalid_uitpas_client_credentials',
125
125
  message: `Invalid UiTPAS client credentials`,
126
- human: $t(`De opgegeven UiTPAS client credentials zijn ongeldig. Controleer ze en probeer opnieuw.`),
126
+ human: $t(`1086bb24-5df4-4faf-9dc0-ab5a955b0d8f`),
127
127
  });
128
128
  }
129
129
  console.error(`Unsuccessful response when fetching UiTPAS token for organization with id ${this.uitpasClientCredential.organizationId}:`, response.statusText);
@@ -193,7 +193,7 @@ export class UitpasTokenRepository {
193
193
  throw new SimpleError({
194
194
  code: 'uitpas_api_not_configured_for_this_organization',
195
195
  message: `UiTPAS api not configured for organization with id ${organizationId}`,
196
- human: $t('UiTPAS is nog niet volledig geconfigureerd voor deze organisatie.'),
196
+ human: $t('6b333cb4-21fd-42be-b0c9-6d899f2fd348'),
197
197
  });
198
198
  }
199
199
  repo = UitpasTokenRepository.setRepoInMemory(organizationId, new UitpasTokenRepository(model)); // store in memory
@@ -1,10 +1,14 @@
1
1
  import { Migration } from '@simonbackx/simple-database';
2
- import { Organization } from '@stamhoofd/models';
2
+ import { Organization, Webshop } from '@stamhoofd/models';
3
3
 
4
4
  export async function startRecordsConfigurationMigration() {
5
5
  for await (const organization of Organization.select().all()) {
6
6
  await organization.save();
7
7
  }
8
+
9
+ for await (const webshop of Webshop.select().all()) {
10
+ await webshop.save();
11
+ }
8
12
  }
9
13
 
10
14
  export default new Migration(async () => {