@stamhoofd/backend 2.58.0 → 2.60.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 (56) hide show
  1. package/index.ts +10 -1
  2. package/package.json +12 -12
  3. package/src/audit-logs/DocumentTemplateLogger.ts +22 -0
  4. package/src/audit-logs/EventLogger.ts +30 -0
  5. package/src/audit-logs/GroupLogger.ts +95 -0
  6. package/src/audit-logs/MemberLogger.ts +24 -0
  7. package/src/audit-logs/MemberPlatformMembershipLogger.ts +60 -0
  8. package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
  9. package/src/audit-logs/ModelLogger.ts +219 -0
  10. package/src/audit-logs/OrderLogger.ts +57 -0
  11. package/src/audit-logs/OrganizationLogger.ts +16 -0
  12. package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
  13. package/src/audit-logs/PaymentLogger.ts +43 -0
  14. package/src/audit-logs/PlatformLogger.ts +13 -0
  15. package/src/audit-logs/RegistrationLogger.ts +53 -0
  16. package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
  17. package/src/audit-logs/StripeAccountLogger.ts +47 -0
  18. package/src/audit-logs/WebshopLogger.ts +35 -0
  19. package/src/crons/updateSetupSteps.ts +1 -1
  20. package/src/crons.ts +2 -1
  21. package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
  22. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +7 -21
  23. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -13
  24. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +5 -12
  25. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
  26. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +43 -27
  27. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +0 -19
  28. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -13
  29. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +20 -43
  30. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +0 -6
  31. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +0 -6
  32. package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +5 -14
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
  34. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
  35. package/src/helpers/AuthenticatedStructures.ts +16 -1
  36. package/src/helpers/Context.ts +8 -2
  37. package/src/helpers/MemberUserSyncer.ts +45 -40
  38. package/src/helpers/PeriodHelper.ts +5 -5
  39. package/src/helpers/SetupStepUpdater.ts +503 -0
  40. package/src/helpers/TagHelper.ts +23 -20
  41. package/src/seeds/1722344162-update-membership.ts +2 -2
  42. package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
  43. package/src/seeds/1726847064-setup-steps.ts +1 -1
  44. package/src/seeds/1733319079-fill-paying-organization-ids.ts +68 -0
  45. package/src/services/AuditLogService.ts +81 -296
  46. package/src/services/BalanceItemPaymentService.ts +1 -1
  47. package/src/services/BalanceItemService.ts +14 -5
  48. package/src/services/DocumentService.ts +43 -0
  49. package/src/services/MemberNumberService.ts +120 -0
  50. package/src/services/PaymentService.ts +199 -193
  51. package/src/services/PlatformMembershipService.ts +284 -0
  52. package/src/services/RegistrationService.ts +78 -27
  53. package/src/services/diff.ts +512 -0
  54. package/src/sql-filters/events.ts +13 -1
  55. package/src/helpers/MembershipHelper.ts +0 -54
  56. package/src/services/explainPatch.ts +0 -782
@@ -1,8 +1,9 @@
1
1
  import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from '@stamhoofd/models';
2
2
  import { SQL } from '@stamhoofd/sql';
3
- import { MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
3
+ import { AuditLogSource, MemberDetails, Permissions, UserPermissions } from '@stamhoofd/structures';
4
4
  import crypto from 'crypto';
5
5
  import basex from 'base-x';
6
+ import { AuditLogService } from '../services/AuditLogService';
6
7
 
7
8
  const ALPHABET = '123456789ABCDEFGHJKMNPQRSTUVWXYZ'; // Note: we removed 0, O, I and l to make it easier for humans
8
9
  const customBase = basex(ALPHABET);
@@ -26,61 +27,63 @@ export class MemberUserSyncerStatic {
26
27
  * - email addresses have changed
27
28
  */
28
29
  async onChangeMember(member: MemberWithRegistrations, unlinkUsers: boolean = false) {
29
- const { userEmails, parentAndUnverifiedEmails } = this.getMemberAccessEmails(member.details);
30
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
31
+ const { userEmails, parentAndUnverifiedEmails } = this.getMemberAccessEmails(member.details);
30
32
 
31
- // Make sure all these users have access to the member
32
- for (const email of userEmails) {
33
+ // Make sure all these users have access to the member
34
+ for (const email of userEmails) {
33
35
  // Link users that are found with these email addresses.
34
- await this.linkUser(email, member, false, true);
35
- }
36
-
37
- for (const email of parentAndUnverifiedEmails) {
38
- if (userEmails.includes(email)) {
39
- continue;
36
+ await this.linkUser(email, member, false, true);
40
37
  }
41
38
 
42
- // Link parents and unverified emails
43
- // Now we add the responsibility permissions to the parent if there are no userEmails
44
- const asParent = userEmails.length > 0 || !member.details.unverifiedEmails.includes(email) || member.details.defaultAge < 16;
45
- await this.linkUser(email, member, asParent, true);
46
- }
39
+ for (const email of parentAndUnverifiedEmails) {
40
+ if (userEmails.includes(email)) {
41
+ continue;
42
+ }
43
+
44
+ // Link parents and unverified emails
45
+ // Now we add the responsibility permissions to the parent if there are no userEmails
46
+ const asParent = userEmails.length > 0 || !member.details.unverifiedEmails.includes(email) || member.details.defaultAge < 16;
47
+ await this.linkUser(email, member, asParent, true);
48
+ }
47
49
 
48
- if (unlinkUsers && !member.details.parentsHaveAccess) {
50
+ if (unlinkUsers && !member.details.parentsHaveAccess) {
49
51
  // Remove access of users that are not in this list
50
52
  // NOTE: we should only do this once a year (preferably on the birthday of the member)
51
53
  // only once because otherwise users loose the access to a member during the creation of the member, or when they have changed their email address
52
54
  // users can regain access to a member after they have lost control by using the normal verification flow when detecting duplicate members
53
55
 
54
- for (const user of member.users) {
55
- if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
56
- await this.unlinkUser(user, member);
56
+ for (const user of member.users) {
57
+ if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
58
+ await this.unlinkUser(user, member);
59
+ }
57
60
  }
58
61
  }
59
- }
60
- else {
62
+ else {
61
63
  // Only auto unlink users that do not have an account
62
- for (const user of member.users) {
63
- if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
64
- if (!user.hasAccount()) {
65
- await this.unlinkUser(user, member);
66
- }
67
- else {
64
+ for (const user of member.users) {
65
+ if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
66
+ if (!user.hasAccount()) {
67
+ await this.unlinkUser(user, member);
68
+ }
69
+ else {
68
70
  // Make sure only linked as a parent, not as user self
69
71
  // This makes sure we don't inherit permissions and aren't counted as 'being' the member
70
- await this.linkUser(user.email, member, true);
72
+ await this.linkUser(user.email, member, true);
73
+ }
71
74
  }
72
75
  }
73
76
  }
74
- }
75
77
 
76
- if (member.details.securityCode === null) {
77
- console.log('Generating security code for member ' + member.id);
78
+ if (member.details.securityCode === null) {
79
+ console.log('Generating security code for member ' + member.id);
78
80
 
79
- const length = 16;
80
- const code = customBase.encode(await randomBytes(100)).toUpperCase().substring(0, length);
81
- member.details.securityCode = code;
82
- await member.save();
83
- }
81
+ const length = 16;
82
+ const code = customBase.encode(await randomBytes(100)).toUpperCase().substring(0, length);
83
+ member.details.securityCode = code;
84
+ await member.save();
85
+ }
86
+ });
84
87
  }
85
88
 
86
89
  getMemberAccessEmails(details: MemberDetails) {
@@ -106,10 +109,12 @@ export class MemberUserSyncerStatic {
106
109
  }
107
110
 
108
111
  async onDeleteMember(member: MemberWithRegistrations) {
109
- for (const u of member.users) {
110
- console.log('Unlinking user ' + u.email + ' from deleted member ' + member.id);
111
- await this.unlinkUser(u, member);
112
- }
112
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
113
+ for (const u of member.users) {
114
+ console.log('Unlinking user ' + u.email + ' from deleted member ' + member.id);
115
+ await this.unlinkUser(u, member);
116
+ }
117
+ });
113
118
  }
114
119
 
115
120
  async getResponsibilitiesForMembers(memberIds: string[]) {
@@ -1,17 +1,17 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod, SetupStepUpdater } from '@stamhoofd/models';
2
+ import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
3
3
  import { QueueHandler } from '@stamhoofd/queues';
4
- import { Group as GroupStruct, PermissionLevel } from '@stamhoofd/structures';
4
+ import { AuditLogSource, Group as GroupStruct, PermissionLevel } from '@stamhoofd/structures';
5
5
  import { PatchOrganizationRegistrationPeriodsEndpoint } from '../endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
6
6
  import { AuthenticatedStructures } from './AuthenticatedStructures';
7
7
  import { MemberUserSyncer } from './MemberUserSyncer';
8
8
  import { AuditLogService } from '../services/AuditLogService';
9
+ import { SetupStepUpdater } from './SetupStepUpdater';
9
10
 
10
11
  export class PeriodHelper {
11
12
  static async moveOrganizationToPeriod(organization: Organization, period: RegistrationPeriod) {
12
- await AuditLogService.disable(async () => {
13
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
13
14
  console.log('moveOrganizationToPeriod', organization.id, period.id);
14
-
15
15
  await this.createOrganizationPeriodForPeriod(organization, period);
16
16
  organization.periodId = period.id;
17
17
  await organization.save();
@@ -172,7 +172,7 @@ export class PeriodHelper {
172
172
 
173
173
  const batchSize = 100;
174
174
  await QueueHandler.schedule(tag, async () => {
175
- await AuditLogService.disable(async () => {
175
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
176
176
  let lastId = '';
177
177
 
178
178
  while (true) {
@@ -0,0 +1,503 @@
1
+ import { Model } from '@simonbackx/simple-database';
2
+ import { SimpleError } from '@simonbackx/simple-errors';
3
+ import {
4
+ Group,
5
+ Member,
6
+ MemberResponsibilityRecord,
7
+ Organization,
8
+ OrganizationRegistrationPeriod,
9
+ Platform,
10
+ } from '@stamhoofd/models';
11
+ import { QueueHandler } from '@stamhoofd/queues';
12
+ import { SQL, SQLWhereSign } from '@stamhoofd/sql';
13
+ import {
14
+ AuditLogSource,
15
+ GroupType,
16
+ MemberResponsibility,
17
+ Platform as PlatformStruct,
18
+ SetupStepType,
19
+ SetupSteps,
20
+ } from '@stamhoofd/structures';
21
+ import { Formatter } from '@stamhoofd/utility';
22
+ import { AuditLogService } from '../services/AuditLogService';
23
+
24
+ type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void | Promise<void>;
25
+
26
+ /**
27
+ * Helper function to detect whether we should update the setup steps if a group is changed
28
+ */
29
+ async function getGroupMemberCompletion(group: Group) {
30
+ const defaultAgeGroupId = group.defaultAgeGroupId;
31
+
32
+ if (!defaultAgeGroupId) {
33
+ // Not included = no step
34
+ return null;
35
+ }
36
+
37
+ if (group.deletedAt) {
38
+ // Not included = no step
39
+ return null;
40
+ }
41
+
42
+ const platform = await Platform.getSharedStruct();
43
+
44
+ if (group.periodId !== platform.period.id) {
45
+ // Not included = no step
46
+ return null;
47
+ }
48
+
49
+ const defaultAgeGroup = platform.config.defaultAgeGroups.find(g => g.id === defaultAgeGroupId);
50
+
51
+ if (!defaultAgeGroup) {
52
+ // Not included = no step
53
+ return null;
54
+ }
55
+
56
+ const required = defaultAgeGroup.minimumRequiredMembers;
57
+ if (required === 0) {
58
+ // Not included = no step
59
+ return null;
60
+ }
61
+
62
+ return Math.min(required, group.settings.registeredMembers ?? 0);
63
+ }
64
+
65
+ export class SetupStepUpdater {
66
+ static isListening = false;
67
+
68
+ static listen() {
69
+ if (this.isListening) {
70
+ return;
71
+ }
72
+ this.isListening = true;
73
+ Model.modelEventBus.addListener({}, async (event) => {
74
+ if (!(event.model instanceof Group)) {
75
+ return;
76
+ }
77
+
78
+ const before = (event.type === 'updated' ? await getGroupMemberCompletion(event.getOldModel() as Group) : (event.type === 'deleted' ? await getGroupMemberCompletion(event.model) : null));
79
+ const after = (event.type === 'updated' ? await getGroupMemberCompletion(event.model) : (event.type === 'created' ? await getGroupMemberCompletion(event.model) : null));
80
+
81
+ if (before !== after) {
82
+ 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
+ // We need to do a recalculation
84
+ SetupStepUpdater.updateForOrganizationId(event.model.organizationId)
85
+ .catch(console.error);
86
+ }
87
+ });
88
+ }
89
+
90
+ private static readonly STEP_TYPE_OPERATIONS: Record<
91
+ SetupStepType,
92
+ SetupStepOperation
93
+ > = {
94
+ [SetupStepType.Responsibilities]: this.updateStepResponsibilities,
95
+ [SetupStepType.Companies]: this.updateStepCompanies,
96
+ [SetupStepType.Groups]: this.updateStepGroups,
97
+ [SetupStepType.Premises]: this.updateStepPremises,
98
+ [SetupStepType.Emails]: this.updateStepEmails,
99
+ [SetupStepType.Payment]: this.updateStepPayment,
100
+ [SetupStepType.Registrations]: this.updateStepRegistrations,
101
+ };
102
+
103
+ static async updateSetupStepsForAllOrganizationsInCurrentPeriod({
104
+ batchSize,
105
+ }: { batchSize?: number } = {}) {
106
+ const tag = 'updateSetupStepsForAllOrganizationsInCurrentPeriod';
107
+ QueueHandler.cancel(tag);
108
+
109
+ await QueueHandler.schedule(tag, async () => {
110
+ const platform = (await Platform.getSharedPrivateStruct()).clone();
111
+
112
+ const periodId = platform.period.id;
113
+
114
+ let lastId = '';
115
+
116
+ while (true) {
117
+ const organizationRegistrationPeriods
118
+ = await OrganizationRegistrationPeriod.where(
119
+ {
120
+ id: { sign: '>', value: lastId },
121
+ periodId: periodId,
122
+ },
123
+ { limit: batchSize ?? 10, sort: ['id'] },
124
+ );
125
+
126
+ if (organizationRegistrationPeriods.length === 0) {
127
+ lastId = '';
128
+ break;
129
+ }
130
+
131
+ const organizationPeriodMap = new Map(
132
+ organizationRegistrationPeriods.map((period) => {
133
+ return [period.organizationId, period];
134
+ }),
135
+ );
136
+
137
+ const organizations = await Organization.getByIDs(
138
+ ...organizationPeriodMap.keys(),
139
+ );
140
+
141
+ for (const organization of organizations) {
142
+ const organizationId = organization.id;
143
+ const organizationRegistrationPeriod
144
+ = organizationPeriodMap.get(organizationId);
145
+
146
+ if (!organizationRegistrationPeriod) {
147
+ console.error(
148
+ `[FLAG-MOMENT] organizationRegistrationPeriod not found for organization with id ${organizationId}`,
149
+ );
150
+ continue;
151
+ }
152
+
153
+ console.log(
154
+ '[FLAG-MOMENT] checking flag moments for '
155
+ + organizationId,
156
+ );
157
+
158
+ await SetupStepUpdater.updateFor(
159
+ organizationRegistrationPeriod,
160
+ platform,
161
+ organization,
162
+ );
163
+ }
164
+
165
+ lastId
166
+ = organizationRegistrationPeriods[
167
+ organizationRegistrationPeriods.length - 1
168
+ ].id;
169
+ }
170
+ });
171
+ }
172
+
173
+ static async updateForOrganizationId(id: string) {
174
+ const organization = await Organization.getByID(id);
175
+ if (!organization) {
176
+ throw new SimpleError({
177
+ code: 'not_found',
178
+ message: 'Organization not found',
179
+ human: 'De organisatie werd niet gevonden',
180
+ });
181
+ }
182
+
183
+ await this.updateForOrganization(organization);
184
+ }
185
+
186
+ static async updateForOrganization(
187
+ organization: Organization,
188
+ {
189
+ platform,
190
+ organizationRegistrationPeriod,
191
+ }: {
192
+ platform?: PlatformStruct;
193
+ organizationRegistrationPeriod?: OrganizationRegistrationPeriod;
194
+ } = {},
195
+ ) {
196
+ if (!platform) {
197
+ platform = await Platform.getSharedPrivateStruct();
198
+ if (!platform) {
199
+ console.error('No platform not found');
200
+ return;
201
+ }
202
+ }
203
+
204
+ if (!organizationRegistrationPeriod) {
205
+ const periodId = platform.period.id;
206
+ organizationRegistrationPeriod = (
207
+ await OrganizationRegistrationPeriod.where({
208
+ organizationId: organization.id,
209
+ periodId: periodId,
210
+ })
211
+ )[0];
212
+
213
+ if (!organizationRegistrationPeriod) {
214
+ console.error(
215
+ `OrganizationRegistrationPeriod with organizationId ${organization.id} and periodId ${periodId} not found`,
216
+ );
217
+ return;
218
+ }
219
+ }
220
+
221
+ await this.updateFor(
222
+ organizationRegistrationPeriod,
223
+ platform,
224
+ organization,
225
+ );
226
+ }
227
+
228
+ private static async updateFor(
229
+ organizationRegistrationPeriod: OrganizationRegistrationPeriod,
230
+ platform: PlatformStruct,
231
+ organization: Organization,
232
+ ) {
233
+ console.log('Updating setup steps for organization', organization.id);
234
+ const setupSteps = organizationRegistrationPeriod.setupSteps;
235
+
236
+ await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
237
+ for (const stepType of Object.values(SetupStepType)) {
238
+ const operation = this.STEP_TYPE_OPERATIONS[stepType];
239
+ await operation(setupSteps, organization, platform);
240
+ }
241
+
242
+ await organizationRegistrationPeriod.save();
243
+ });
244
+ }
245
+
246
+ private static updateStepPremises(
247
+ setupSteps: SetupSteps,
248
+ organization: Organization,
249
+ platform: PlatformStruct,
250
+ ) {
251
+ let totalSteps = 0;
252
+ let finishedSteps = 0;
253
+
254
+ const premiseTypes = platform.config.premiseTypes;
255
+
256
+ for (const premiseType of premiseTypes) {
257
+ const { min, max } = premiseType;
258
+
259
+ // only add step if premise type has restrictions
260
+ if (min === null && max === null) {
261
+ continue;
262
+ }
263
+
264
+ totalSteps++;
265
+
266
+ const premiseTypeId = premiseType.id;
267
+ let totalPremisesOfThisType = 0;
268
+
269
+ for (const premise of organization.privateMeta.premises) {
270
+ if (premise.premiseTypeIds.includes(premiseTypeId)) {
271
+ totalPremisesOfThisType++;
272
+ }
273
+ }
274
+
275
+ if (max !== null && totalPremisesOfThisType > max) {
276
+ continue;
277
+ }
278
+
279
+ if (min !== null && totalPremisesOfThisType < min) {
280
+ continue;
281
+ }
282
+
283
+ finishedSteps++;
284
+ }
285
+
286
+ setupSteps.update(SetupStepType.Premises, {
287
+ totalSteps,
288
+ finishedSteps,
289
+ });
290
+ }
291
+
292
+ private static updateStepGroups(
293
+ setupSteps: SetupSteps,
294
+ _organization: Organization,
295
+ _platform: PlatformStruct,
296
+ ) {
297
+ setupSteps.update(SetupStepType.Groups, {
298
+ totalSteps: 0,
299
+ finishedSteps: 0,
300
+ });
301
+ }
302
+
303
+ private static updateStepCompanies(
304
+ setupSteps: SetupSteps,
305
+ organization: Organization,
306
+ _platform: PlatformStruct,
307
+ ) {
308
+ const totalSteps = 1;
309
+ let finishedSteps = 0;
310
+
311
+ if (organization.meta.companies.length) {
312
+ finishedSteps = 1;
313
+ }
314
+
315
+ setupSteps.update(SetupStepType.Companies, {
316
+ totalSteps,
317
+ finishedSteps,
318
+ });
319
+ }
320
+
321
+ private static async updateStepResponsibilities(
322
+ setupSteps: SetupSteps,
323
+ organization: Organization,
324
+ platform: PlatformStruct,
325
+ ) {
326
+ const now = new Date();
327
+ const organizationBasedResponsibilitiesWithRestriction = platform.config.responsibilities
328
+ .filter(r => r.organizationBased && (r.minimumMembers || r.maximumMembers));
329
+
330
+ const responsibilityIds = organizationBasedResponsibilitiesWithRestriction.map(r => r.id);
331
+
332
+ const allRecords = responsibilityIds.length === 0
333
+ ? []
334
+ : await MemberResponsibilityRecord.select()
335
+ .where('responsibilityId', responsibilityIds)
336
+ .where('organizationId', organization.id)
337
+ .where(SQL.where('endDate', SQLWhereSign.Greater, now).or('endDate', null))
338
+ .fetch();
339
+
340
+ // Remove invalid responsibilities: members that are not registered in the current period
341
+ const memberIds = Formatter.uniqueArray(allRecords.map(r => r.memberId));
342
+ const members = await Member.getBlobByIds(...memberIds);
343
+ 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));
344
+
345
+ const validMembersIds = validMembers.map(m => m.id);
346
+
347
+ const records = allRecords.filter(r => validMembersIds.includes(r.memberId));
348
+
349
+ let totalSteps = 0;
350
+ let finishedSteps = 0;
351
+
352
+ const groups = await Group.getAll(organization.id, organization.periodId);
353
+
354
+ const flatResponsibilities: { responsibility: MemberResponsibility; group: Group | null }[] = organizationBasedResponsibilitiesWithRestriction
355
+ .flatMap((responsibility) => {
356
+ const defaultAgeGroupIds = responsibility.defaultAgeGroupIds;
357
+ if (defaultAgeGroupIds === null) {
358
+ const item: { responsibility: MemberResponsibility; group: Group | null } = {
359
+ responsibility,
360
+ group: null,
361
+ };
362
+ return [item];
363
+ }
364
+
365
+ return groups
366
+ .filter(g => g.defaultAgeGroupId !== null && defaultAgeGroupIds.includes(g.defaultAgeGroupId))
367
+ .map((group) => {
368
+ return {
369
+ responsibility,
370
+ group,
371
+ };
372
+ });
373
+ });
374
+
375
+ for (const { responsibility, group } of flatResponsibilities) {
376
+ const { minimumMembers: min, maximumMembers: max } = responsibility;
377
+
378
+ if (min === null) {
379
+ continue;
380
+ }
381
+
382
+ totalSteps += min;
383
+
384
+ const responsibilityId = responsibility.id;
385
+ let totalRecordsWithThisResponsibility = 0;
386
+
387
+ if (group === null) {
388
+ for (const record of records) {
389
+ if (record.responsibilityId === responsibilityId) {
390
+ totalRecordsWithThisResponsibility++;
391
+ }
392
+ }
393
+ }
394
+ else {
395
+ for (const record of records) {
396
+ if (record.responsibilityId === responsibilityId && record.groupId === group.id) {
397
+ totalRecordsWithThisResponsibility++;
398
+ }
399
+ }
400
+ }
401
+
402
+ if (max !== null && totalRecordsWithThisResponsibility > max) {
403
+ // Not added
404
+ continue;
405
+ }
406
+
407
+ finishedSteps += Math.min(min, totalRecordsWithThisResponsibility);
408
+ }
409
+
410
+ setupSteps.update(SetupStepType.Responsibilities, {
411
+ totalSteps,
412
+ finishedSteps,
413
+ });
414
+ }
415
+
416
+ private static updateStepEmails(setupSteps: SetupSteps,
417
+ organization: Organization,
418
+ _platform: PlatformStruct) {
419
+ const totalSteps = 1;
420
+ let finishedSteps = 0;
421
+
422
+ const emails = organization.privateMeta.emails;
423
+
424
+ // organization should have 1 default email
425
+ if (emails.some(e => e.default)) {
426
+ finishedSteps = 1;
427
+ }
428
+
429
+ setupSteps.update(SetupStepType.Emails, {
430
+ totalSteps,
431
+ finishedSteps,
432
+ });
433
+
434
+ if (finishedSteps >= totalSteps) {
435
+ setupSteps.markReviewed(SetupStepType.Emails, { userId: 'backend', userName: 'backend' });
436
+ }
437
+ else {
438
+ setupSteps.resetReviewed(SetupStepType.Emails);
439
+ }
440
+ }
441
+
442
+ private static updateStepPayment(setupSteps: SetupSteps,
443
+ _organization: Organization,
444
+ _platform: PlatformStruct) {
445
+ setupSteps.update(SetupStepType.Payment, {
446
+ totalSteps: 0,
447
+ finishedSteps: 0,
448
+ });
449
+ }
450
+
451
+ private static async updateStepRegistrations(setupSteps: SetupSteps,
452
+ organization: Organization,
453
+ platform: PlatformStruct) {
454
+ const defaultAgeGroupIds = platform.config.defaultAgeGroups.filter(g => g.minimumRequiredMembers > 0).map(x => x.id);
455
+
456
+ const groupsWithDefaultAgeGroups = defaultAgeGroupIds.length > 0
457
+ ? await Group.select()
458
+ .where('organizationId', organization.id)
459
+ .where('periodId', platform.period.id)
460
+ .where('defaultAgeGroupId', defaultAgeGroupIds)
461
+ .where('deletedAt', null)
462
+ .fetch()
463
+ : [];
464
+
465
+ let totalSteps = 0;
466
+
467
+ // Count per default age group, (e.g. minmium 10 means the total across all members with the same age group id should be 10, not 10 each)
468
+ const processedDefaultAgeGroupIds: Map<string, number> = new Map();
469
+
470
+ for (const group of groupsWithDefaultAgeGroups) {
471
+ const defaultAgeGroup = platform.config.defaultAgeGroups.find(g => g.id === group.defaultAgeGroupId);
472
+ if (!defaultAgeGroup) {
473
+ continue;
474
+ }
475
+ if (!processedDefaultAgeGroupIds.has(defaultAgeGroup.id)) {
476
+ totalSteps += defaultAgeGroup.minimumRequiredMembers;
477
+ processedDefaultAgeGroupIds.set(defaultAgeGroup.id, Math.min(defaultAgeGroup.minimumRequiredMembers, group.settings.registeredMembers ?? 0));
478
+ }
479
+ else {
480
+ processedDefaultAgeGroupIds.set(defaultAgeGroup.id,
481
+ Math.min(
482
+ defaultAgeGroup.minimumRequiredMembers,
483
+ processedDefaultAgeGroupIds.get(defaultAgeGroup.id)! + (group.settings.registeredMembers ?? 0),
484
+ ),
485
+ );
486
+ }
487
+ }
488
+
489
+ const finishedSteps = Array.from(processedDefaultAgeGroupIds.values()).reduce((a, b) => a + b, 0);
490
+
491
+ setupSteps.update(SetupStepType.Registrations, {
492
+ totalSteps,
493
+ finishedSteps,
494
+ });
495
+
496
+ if (finishedSteps >= totalSteps) {
497
+ setupSteps.markReviewed(SetupStepType.Registrations, { userId: 'backend', userName: 'backend' });
498
+ }
499
+ else {
500
+ setupSteps.resetReviewed(SetupStepType.Registrations);
501
+ }
502
+ }
503
+ }