@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.
- package/index.ts +10 -1
- package/package.json +12 -12
- package/src/audit-logs/DocumentTemplateLogger.ts +22 -0
- package/src/audit-logs/EventLogger.ts +30 -0
- package/src/audit-logs/GroupLogger.ts +95 -0
- package/src/audit-logs/MemberLogger.ts +24 -0
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +60 -0
- package/src/audit-logs/MemberResponsibilityRecordLogger.ts +69 -0
- package/src/audit-logs/ModelLogger.ts +219 -0
- package/src/audit-logs/OrderLogger.ts +57 -0
- package/src/audit-logs/OrganizationLogger.ts +16 -0
- package/src/audit-logs/OrganizationRegistrationPeriodLogger.ts +77 -0
- package/src/audit-logs/PaymentLogger.ts +43 -0
- package/src/audit-logs/PlatformLogger.ts +13 -0
- package/src/audit-logs/RegistrationLogger.ts +53 -0
- package/src/audit-logs/RegistrationPeriodLogger.ts +21 -0
- package/src/audit-logs/StripeAccountLogger.ts +47 -0
- package/src/audit-logs/WebshopLogger.ts +35 -0
- package/src/crons/updateSetupSteps.ts +1 -1
- package/src/crons.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +12 -24
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +7 -21
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +5 -13
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +5 -12
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +0 -15
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +43 -27
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +0 -19
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -13
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +20 -43
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/stripe/UpdateStripeAccountEndpoint.ts +5 -14
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -2
- package/src/helpers/AuthenticatedStructures.ts +16 -1
- package/src/helpers/Context.ts +8 -2
- package/src/helpers/MemberUserSyncer.ts +45 -40
- package/src/helpers/PeriodHelper.ts +5 -5
- package/src/helpers/SetupStepUpdater.ts +503 -0
- package/src/helpers/TagHelper.ts +23 -20
- package/src/seeds/1722344162-update-membership.ts +2 -2
- package/src/seeds/1726572303-schedule-stock-updates.ts +2 -1
- package/src/seeds/1726847064-setup-steps.ts +1 -1
- package/src/seeds/1733319079-fill-paying-organization-ids.ts +68 -0
- package/src/services/AuditLogService.ts +81 -296
- package/src/services/BalanceItemPaymentService.ts +1 -1
- package/src/services/BalanceItemService.ts +14 -5
- package/src/services/DocumentService.ts +43 -0
- package/src/services/MemberNumberService.ts +120 -0
- package/src/services/PaymentService.ts +199 -193
- package/src/services/PlatformMembershipService.ts +284 -0
- package/src/services/RegistrationService.ts +78 -27
- package/src/services/diff.ts +512 -0
- package/src/sql-filters/events.ts +13 -1
- package/src/helpers/MembershipHelper.ts +0 -54
- 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
|
-
|
|
30
|
+
await AuditLogService.setContext({ source: AuditLogSource.System }, async () => {
|
|
31
|
+
const { userEmails, parentAndUnverifiedEmails } = this.getMemberAccessEmails(member.details);
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
+
await this.linkUser(user.email, member, true);
|
|
73
|
+
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
}
|
|
74
|
-
}
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
if (member.details.securityCode === null) {
|
|
79
|
+
console.log('Generating security code for member ' + member.id);
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
+
}
|