@stamhoofd/backend 2.23.0 → 2.25.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/.env.template.json +5 -0
- package/package.json +10 -10
- package/src/crons.ts +9 -14
- package/src/endpoints/auth/CreateAdminEndpoint.ts +43 -31
- package/src/endpoints/auth/SignupEndpoint.ts +2 -2
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +2 -7
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +28 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +8 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +3 -4
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +60 -23
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -3
- package/src/helpers/AuthenticatedStructures.ts +31 -2
- package/src/helpers/ForwardHandler.ts +30 -28
- package/src/helpers/MemberUserSyncer.ts +3 -3
- package/src/helpers/PeriodHelper.ts +166 -0
- package/src/helpers/SetupStepsUpdater.ts +17 -10
package/.env.template.json
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"BE": "www.be.stamhoofd",
|
|
13
13
|
"NL": "www.nl.stamhoofd"
|
|
14
14
|
},
|
|
15
|
+
"documentation": {
|
|
16
|
+
"": "www.be.stamhoofd/docs",
|
|
17
|
+
"BE": "www.be.stamhoofd/docs",
|
|
18
|
+
"NL": "www.nl.stamhoofd/docs"
|
|
19
|
+
},
|
|
15
20
|
"webshop": {
|
|
16
21
|
"": "shop.be.stamhoofd",
|
|
17
22
|
"BE": "shop.be.stamhoofd",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@simonbackx/simple-encoding": "2.15.0",
|
|
37
37
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
38
38
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
39
|
-
"@stamhoofd/backend-i18n": "2.
|
|
40
|
-
"@stamhoofd/backend-middleware": "2.
|
|
41
|
-
"@stamhoofd/email": "2.
|
|
42
|
-
"@stamhoofd/models": "2.
|
|
43
|
-
"@stamhoofd/queues": "2.
|
|
44
|
-
"@stamhoofd/sql": "2.
|
|
45
|
-
"@stamhoofd/structures": "2.
|
|
46
|
-
"@stamhoofd/utility": "2.
|
|
39
|
+
"@stamhoofd/backend-i18n": "2.25.0",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.25.0",
|
|
41
|
+
"@stamhoofd/email": "2.25.0",
|
|
42
|
+
"@stamhoofd/models": "2.25.0",
|
|
43
|
+
"@stamhoofd/queues": "2.25.0",
|
|
44
|
+
"@stamhoofd/sql": "2.25.0",
|
|
45
|
+
"@stamhoofd/structures": "2.25.0",
|
|
46
|
+
"@stamhoofd/utility": "2.25.0",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
49
49
|
"axios": "1.6.8",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "4.0.2",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "48963030dd9e90bef4ea1c96db2cf6ddb6e1908a"
|
|
64
64
|
}
|
package/src/crons.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { Database } from '@simonbackx/simple-database';
|
|
2
2
|
import { logger, StyledText } from "@simonbackx/simple-logging";
|
|
3
|
-
import { I18n } from '@stamhoofd/backend-i18n';
|
|
4
3
|
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
5
|
-
import { Group, Organization, Payment, Registration, STPackage,
|
|
6
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
|
+
import { Group, Organization, Payment, Registration, STPackage, Webshop } from '@stamhoofd/models';
|
|
7
5
|
import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
8
6
|
import { Formatter, sleep } from '@stamhoofd/utility';
|
|
9
7
|
import AWS from 'aws-sdk';
|
|
@@ -123,9 +121,7 @@ async function checkWebshopDNS() {
|
|
|
123
121
|
console.log("[DNS] Checking webshop DNS...")
|
|
124
122
|
|
|
125
123
|
for (const webshop of webshops) {
|
|
126
|
-
|
|
127
|
-
console.log("[DNS] Webshop "+webshop.meta.name+" ("+webshop.id+")"+" ("+webshop.domain+")")
|
|
128
|
-
}
|
|
124
|
+
console.log("[DNS] Webshop "+webshop.meta.name+" ("+webshop.id+")"+" ("+webshop.domain+")")
|
|
129
125
|
await webshop.updateDNSRecords()
|
|
130
126
|
}
|
|
131
127
|
|
|
@@ -133,10 +129,10 @@ async function checkWebshopDNS() {
|
|
|
133
129
|
}
|
|
134
130
|
|
|
135
131
|
async function checkReplies() {
|
|
136
|
-
if (STAMHOOFD.environment !== "production") {
|
|
137
|
-
return
|
|
132
|
+
if (STAMHOOFD.environment !== "production" || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
133
|
+
return
|
|
138
134
|
}
|
|
139
|
-
|
|
135
|
+
|
|
140
136
|
console.log("Checking replies from AWS SQS")
|
|
141
137
|
const sqs = new AWS.SQS();
|
|
142
138
|
const messages = await sqs.receiveMessage({ QueueUrl: "https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding", MaxNumberOfMessages: 10 }).promise()
|
|
@@ -261,7 +257,7 @@ async function checkPostmarkBounces() {
|
|
|
261
257
|
}
|
|
262
258
|
|
|
263
259
|
async function checkBounces() {
|
|
264
|
-
if (STAMHOOFD.environment !== "production") {
|
|
260
|
+
if (STAMHOOFD.environment !== "production" || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
265
261
|
return
|
|
266
262
|
}
|
|
267
263
|
|
|
@@ -343,7 +339,7 @@ async function checkBounces() {
|
|
|
343
339
|
}
|
|
344
340
|
|
|
345
341
|
async function checkComplaints() {
|
|
346
|
-
if (STAMHOOFD.environment !== "production") {
|
|
342
|
+
if (STAMHOOFD.environment !== "production" || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
347
343
|
return
|
|
348
344
|
}
|
|
349
345
|
|
|
@@ -396,11 +392,10 @@ async function checkComplaints() {
|
|
|
396
392
|
console.error("[AWS COMPLAINTS] Received virus / fraud complaint!")
|
|
397
393
|
console.error("[AWS COMPLAINTS]", complaint)
|
|
398
394
|
if (STAMHOOFD.environment === "production") {
|
|
399
|
-
Email.
|
|
400
|
-
to: "simon@stamhoofd.be",
|
|
395
|
+
Email.sendWebmaster({
|
|
401
396
|
subject: "Received a "+type+" email notification",
|
|
402
397
|
text: "We received a "+type+" notification for an e-mail from the organization: "+organization?.name+". Please check and adjust if needed.\n"
|
|
403
|
-
}
|
|
398
|
+
})
|
|
404
399
|
}
|
|
405
400
|
}
|
|
406
401
|
} else {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Decoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { User as UserStruct,UserPermissions, UserWithMembers } from "@stamhoofd/structures";
|
|
4
|
+
import { PasswordToken, sendEmailTemplate, User } from '@stamhoofd/models';
|
|
5
|
+
import { EmailTemplateType, Recipient, Replacement, UserPermissions, User as UserStruct, UserWithMembers } from "@stamhoofd/structures";
|
|
7
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
7
|
|
|
9
|
-
import { Context } from '../../helpers/Context';
|
|
10
8
|
import { AuthenticatedStructures } from '../../helpers/AuthenticatedStructures';
|
|
9
|
+
import { Context } from '../../helpers/Context';
|
|
11
10
|
type Params = Record<string, never>;
|
|
12
11
|
type Query = undefined;
|
|
13
12
|
type Body = UserStruct
|
|
@@ -95,11 +94,6 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
95
94
|
|
|
96
95
|
await admin.save();
|
|
97
96
|
|
|
98
|
-
const { from, replyTo } = {
|
|
99
|
-
from: organization ? organization.getStrongEmail(request.i18n) : Email.getInternalEmailFor(request.i18n),
|
|
100
|
-
replyTo: undefined
|
|
101
|
-
}
|
|
102
|
-
|
|
103
97
|
// Create a password token that is valid for 7 days
|
|
104
98
|
const validUntil = new Date();
|
|
105
99
|
validUntil.setTime(validUntil.getTime() + 7 * 24 * 3600 * 1000);
|
|
@@ -110,28 +104,46 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
110
104
|
const name = organization?.name ?? request.i18n.t("shared.platformName")
|
|
111
105
|
const what = organization ? `de vereniging ${name} op ${request.i18n.t("shared.platformName")}` : `${request.i18n.t("shared.platformName")}`
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
107
|
+
const emailTo = admin.getEmailTo();
|
|
108
|
+
const email: string = typeof emailTo === 'string' ? emailTo : emailTo[0]?.email;
|
|
109
|
+
|
|
110
|
+
await sendEmailTemplate(organization, {
|
|
111
|
+
recipients: [
|
|
112
|
+
Recipient.create({
|
|
113
|
+
email,
|
|
114
|
+
replacements: [
|
|
115
|
+
Replacement.create({
|
|
116
|
+
token: 'greeting',
|
|
117
|
+
value: admin.firstName ? `Dag ${admin.firstName},` : 'Hallo!'
|
|
118
|
+
}),
|
|
119
|
+
Replacement.create({
|
|
120
|
+
token: 'resetUrl',
|
|
121
|
+
value: recoveryUrl
|
|
122
|
+
}),
|
|
123
|
+
Replacement.create({
|
|
124
|
+
token: 'platformOrOrganizationName',
|
|
125
|
+
value: what
|
|
126
|
+
}),
|
|
127
|
+
Replacement.create({
|
|
128
|
+
token: 'inviterName',
|
|
129
|
+
value: user.firstName ?? 'Iemand'
|
|
130
|
+
}),
|
|
131
|
+
Replacement.create({
|
|
132
|
+
token: 'validUntil',
|
|
133
|
+
value: dateTime
|
|
134
|
+
}),
|
|
135
|
+
Replacement.create({
|
|
136
|
+
token: 'email',
|
|
137
|
+
value: admin.email
|
|
138
|
+
})
|
|
139
|
+
]
|
|
140
|
+
})
|
|
141
|
+
],
|
|
142
|
+
template: {
|
|
143
|
+
type: admin.hasAccount() ? EmailTemplateType.AdminInvitation : EmailTemplateType.AdminInvitationNewUser
|
|
144
|
+
},
|
|
145
|
+
type: 'transactional'
|
|
146
|
+
});
|
|
135
147
|
|
|
136
148
|
return new Response(
|
|
137
149
|
await AuthenticatedStructures.userWithMembers(admin)
|
|
@@ -60,11 +60,11 @@ export class SignupEndpoint extends Endpoint<Params, Query, Body, ResponseBody>
|
|
|
60
60
|
// Send an e-mail to say you already have an account + follow password forgot flow
|
|
61
61
|
const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(user, organization, request.i18n)
|
|
62
62
|
const { from, replyTo } = {
|
|
63
|
-
from: (user.permissions || !organization ? Email.getInternalEmailFor(request.i18n) : organization.
|
|
63
|
+
from: (user.permissions || !organization ? Email.getInternalEmailFor(request.i18n) : organization.getDefaultFrom(request.i18n)),
|
|
64
64
|
replyTo: undefined
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const footer = (!user.permissions && organization ? "\n\n—\n\nOnze ledenadministratie werkt via het Stamhoofd platform, op maat van verenigingen. Probeer het ook via https://"+request.i18n
|
|
67
|
+
const footer = (!user.permissions && organization ? "\n\n—\n\nOnze ledenadministratie werkt via het Stamhoofd platform, op maat van verenigingen. Probeer het ook via https://"+request.i18n.localizedDomains.marketing()+"/ledenadministratie\n\n" : '')
|
|
68
68
|
|
|
69
69
|
const name = organization ? organization.name : 'Stamhoofd'
|
|
70
70
|
// Send email
|
|
@@ -107,19 +107,14 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
107
107
|
return url;
|
|
108
108
|
}).catch(async (error) => {
|
|
109
109
|
if (sendEmail) {
|
|
110
|
-
|
|
110
|
+
await sendEmailTemplate(null, {
|
|
111
111
|
template: {
|
|
112
112
|
type: EmailTemplateType.ExcelExportFailed
|
|
113
113
|
},
|
|
114
114
|
recipients: [
|
|
115
115
|
user.createRecipient()
|
|
116
|
-
]
|
|
117
|
-
from: Email.getInternalEmailFor(Context.i18n)
|
|
116
|
+
]
|
|
118
117
|
})
|
|
119
|
-
|
|
120
|
-
if (builder) {
|
|
121
|
-
Email.schedule(builder)
|
|
122
|
-
}
|
|
123
118
|
}
|
|
124
119
|
throw error
|
|
125
120
|
}),
|
|
@@ -3,7 +3,7 @@ import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, Patch
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
4
4
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
5
5
|
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Platform, Registration, User } from '@stamhoofd/models';
|
|
6
|
-
import { MemberWithRegistrationsBlob, MembersBlob, PermissionLevel } from "@stamhoofd/structures";
|
|
6
|
+
import { GroupType, MemberWithRegistrationsBlob, MembersBlob, PermissionLevel } from "@stamhoofd/structures";
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -267,6 +267,33 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
267
267
|
})
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
const hasRegistration = member.registrations.some(registration => {
|
|
271
|
+
if (platformResponsibility) {
|
|
272
|
+
if (registration.group.defaultAgeGroupId === null) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (org) {
|
|
278
|
+
if (registration.periodId !== org.periodId) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
if (registration.periodId !== platform.periodId) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return registration.deactivatedAt === null && registration.registeredAt !== null && registration.group.type === GroupType.Membership
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (!hasRegistration) {
|
|
290
|
+
throw new SimpleError({
|
|
291
|
+
code: "invalid_field",
|
|
292
|
+
message: "Invalid organization",
|
|
293
|
+
human: "Je kan een functie enkel toekennen aan leden die zijn ingeschreven in het huidige werkjaar",
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
270
297
|
const model = new MemberResponsibilityRecord()
|
|
271
298
|
model.memberId = member.id
|
|
272
299
|
model.responsibilityId = responsibility.id
|
|
@@ -6,6 +6,7 @@ import { MemberResponsibility, PlatformConfig, PlatformPremiseType, Platform as
|
|
|
6
6
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
7
7
|
import { Context } from "../../../helpers/Context";
|
|
8
8
|
import { SetupStepUpdater } from "../../../helpers/SetupStepsUpdater";
|
|
9
|
+
import { PeriodHelper } from "../../../helpers/PeriodHelper";
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -73,6 +74,7 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
let shouldUpdateSetupSteps = false;
|
|
77
|
+
let shouldMoveToPeriod: RegistrationPeriod | null = null;
|
|
76
78
|
|
|
77
79
|
if (request.body.config) {
|
|
78
80
|
if (!Context.auth.hasPlatformFullAccess()) {
|
|
@@ -113,6 +115,8 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
113
115
|
});
|
|
114
116
|
}
|
|
115
117
|
platform.periodId = period.id;
|
|
118
|
+
shouldUpdateSetupSteps = true;
|
|
119
|
+
shouldMoveToPeriod = period;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
if (request.body.membershipOrganizationId !== undefined) {
|
|
@@ -155,7 +159,10 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
155
159
|
|
|
156
160
|
await platform.save();
|
|
157
161
|
|
|
158
|
-
if(
|
|
162
|
+
if (shouldMoveToPeriod) {
|
|
163
|
+
PeriodHelper.moveAllOrganizationsToPeriod(shouldMoveToPeriod).catch(console.error)
|
|
164
|
+
} else if(shouldUpdateSetupSteps) {
|
|
165
|
+
// Do not call this right away when moving to a period, because this needs to happen AFTER moving to the period
|
|
159
166
|
SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod().catch(console.error);
|
|
160
167
|
}
|
|
161
168
|
|
|
@@ -83,11 +83,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
83
83
|
try {
|
|
84
84
|
limiter.track(organization.id, 1);
|
|
85
85
|
} catch (e) {
|
|
86
|
-
Email.
|
|
87
|
-
to: "hallo@stamhoofd.be",
|
|
86
|
+
Email.sendWebmaster({
|
|
88
87
|
subject: "[Limiet] Limiet bereikt voor aantal inschrijvingen",
|
|
89
88
|
text: "Beste, \nDe limiet werd bereikt voor het aantal inschrijvingen per dag. \nVereniging: "+organization.id+" ("+organization.name+")" + "\n\n" + e.message + "\n\nStamhoofd"
|
|
90
|
-
}
|
|
89
|
+
})
|
|
91
90
|
|
|
92
91
|
throw new SimpleError({
|
|
93
92
|
code: "too_many_emails_period",
|
|
@@ -95,11 +95,10 @@ export class EmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
95
95
|
try {
|
|
96
96
|
limiter.track(organization.id, request.body.recipients.length);
|
|
97
97
|
} catch (e) {
|
|
98
|
-
Email.
|
|
99
|
-
to: "hallo@stamhoofd.be",
|
|
98
|
+
Email.sendWebmaster({
|
|
100
99
|
subject: "[Limiet] Limiet bereikt voor aantal e-mails",
|
|
101
100
|
text: "Beste, \nDe limiet werd bereikt voor het aantal e-mails per dag. \nVereniging: "+organization.id+" ("+organization.name+")" + "\n\n" + e.message + "\n\nStamhoofd"
|
|
102
|
-
}
|
|
101
|
+
})
|
|
103
102
|
|
|
104
103
|
throw new SimpleError({
|
|
105
104
|
code: "too_many_emails_period",
|
|
@@ -178,7 +177,7 @@ export class EmailEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
|
178
177
|
}
|
|
179
178
|
})
|
|
180
179
|
|
|
181
|
-
let from = organization.
|
|
180
|
+
let from = organization.getDefaultFrom(request.i18n, false, 'broadcast');
|
|
182
181
|
let replyTo: string | undefined = sender.email;
|
|
183
182
|
|
|
184
183
|
// Can we send from this e-mail or reply-to?
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
2
|
-
import { Group as GroupStruct,
|
|
2
|
+
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from "@stamhoofd/structures";
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from "@simonbackx/simple-encoding";
|
|
5
|
-
import { Context } from "../../../../helpers/Context";
|
|
6
|
-
import { Group, Member, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
7
5
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
6
|
+
import { Group, Member, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
8
7
|
import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
|
|
8
|
+
import { Context } from "../../../../helpers/Context";
|
|
9
9
|
|
|
10
10
|
type Params = Record<string, never>;
|
|
11
11
|
type Query = undefined;
|
|
@@ -46,9 +46,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
46
46
|
if (!await Context.auth.hasFullAccess(organization.id)) {
|
|
47
47
|
throw Context.auth.error()
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
periods.push(await PatchOrganizationRegistrationPeriodsEndpoint.createOrganizationPeriod(organization, put));
|
|
50
|
+
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
for (const patch of request.body.getPatches()) {
|
|
53
|
+
const organizationPeriod = await OrganizationRegistrationPeriod.getByID(patch.id);
|
|
54
|
+
if (!organizationPeriod || organizationPeriod.organizationId !== organization.id) {
|
|
52
55
|
throw new SimpleError({
|
|
53
56
|
code: "not_found",
|
|
54
57
|
message: "Period not found",
|
|
@@ -56,38 +59,33 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
56
59
|
})
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
const
|
|
60
|
-
organizationPeriod.id = put.id;
|
|
61
|
-
organizationPeriod.organizationId = organization.id;
|
|
62
|
-
organizationPeriod.periodId = put.period.id;
|
|
63
|
-
organizationPeriod.settings = put.settings;
|
|
64
|
-
await organizationPeriod.save();
|
|
62
|
+
const period = await RegistrationPeriod.getByID(organizationPeriod.periodId);
|
|
65
63
|
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
if (!period) {
|
|
65
|
+
throw new SimpleError({
|
|
66
|
+
code: "not_found",
|
|
67
|
+
message: "Period not found",
|
|
68
|
+
statusCode: 404
|
|
69
|
+
})
|
|
68
70
|
}
|
|
69
|
-
const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
|
|
70
|
-
|
|
71
|
-
// Delete unreachable categories first
|
|
72
|
-
await organizationPeriod.cleanCategories(groups);
|
|
73
|
-
await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
|
|
74
|
-
periods.push(organizationPeriod);
|
|
75
|
-
}
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
const organizationPeriod = await OrganizationRegistrationPeriod.getByID(patch.id);
|
|
79
|
-
if (!organizationPeriod || organizationPeriod.organizationId !== organization.id) {
|
|
72
|
+
if (period.locked) {
|
|
80
73
|
throw new SimpleError({
|
|
81
74
|
code: "not_found",
|
|
82
75
|
message: "Period not found",
|
|
76
|
+
human: 'Je kan geen wijzigingen meer aanbrengen in ' + period.getStructure().name + ' omdat deze is afgesloten',
|
|
83
77
|
statusCode: 404
|
|
84
78
|
})
|
|
85
79
|
}
|
|
80
|
+
|
|
86
81
|
let deleteUnreachable = false
|
|
87
82
|
const allowedIds: string[] = []
|
|
88
83
|
|
|
89
84
|
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
90
85
|
if (patch.settings) {
|
|
86
|
+
if(patch.settings.categories) {
|
|
87
|
+
deleteUnreachable = true;
|
|
88
|
+
}
|
|
91
89
|
organizationPeriod.settings.patchOrPut(patch.settings);
|
|
92
90
|
}
|
|
93
91
|
} else {
|
|
@@ -174,6 +172,36 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
174
172
|
})
|
|
175
173
|
}
|
|
176
174
|
|
|
175
|
+
static async createOrganizationPeriod(organization: Organization, struct: OrganizationRegistrationPeriodStruct) {
|
|
176
|
+
const period = await RegistrationPeriod.getByID(struct.period.id);
|
|
177
|
+
|
|
178
|
+
if (!period || period.locked) {
|
|
179
|
+
throw new SimpleError({
|
|
180
|
+
code: "not_found",
|
|
181
|
+
message: "Period not found",
|
|
182
|
+
statusCode: 404
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const organizationPeriod = new OrganizationRegistrationPeriod();
|
|
187
|
+
organizationPeriod.id = struct.id;
|
|
188
|
+
organizationPeriod.organizationId = organization.id;
|
|
189
|
+
organizationPeriod.periodId = struct.period.id;
|
|
190
|
+
organizationPeriod.settings = struct.settings;
|
|
191
|
+
await organizationPeriod.save();
|
|
192
|
+
|
|
193
|
+
for (const s of struct.groups) {
|
|
194
|
+
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(s, organization.id, organizationPeriod.periodId)
|
|
195
|
+
}
|
|
196
|
+
const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
|
|
197
|
+
|
|
198
|
+
// Delete unreachable categories first
|
|
199
|
+
await organizationPeriod.cleanCategories(groups);
|
|
200
|
+
await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
|
|
201
|
+
|
|
202
|
+
return organizationPeriod
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
static async deleteGroup(id: string) {
|
|
178
206
|
const model = await Group.getByID(id)
|
|
179
207
|
if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
@@ -263,6 +291,15 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
263
291
|
})
|
|
264
292
|
}
|
|
265
293
|
|
|
294
|
+
if (existing.periodId !== model.periodId) {
|
|
295
|
+
throw new SimpleError({
|
|
296
|
+
code: 'invalid_field',
|
|
297
|
+
field: 'waitingList',
|
|
298
|
+
message: 'Waiting list group is already used in another period',
|
|
299
|
+
human: 'Een wachtlijst kan momenteel niet gedeeld worden tussen verschillende werkjaren'
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
266
303
|
model.waitingListId = existing.id
|
|
267
304
|
} else {
|
|
268
305
|
const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
|
|
@@ -86,11 +86,10 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
86
86
|
try {
|
|
87
87
|
limiter.track(organization.id, 1);
|
|
88
88
|
} catch (e) {
|
|
89
|
-
Email.
|
|
90
|
-
to: "hallo@stamhoofd.be",
|
|
89
|
+
Email.sendWebmaster({
|
|
91
90
|
subject: "[Limiet] Limiet bereikt voor aantal bestellingen",
|
|
92
91
|
text: "Beste, \nDe limiet werd bereikt voor het aantal bestellingen per dag. \nVereniging: "+organization.id+" ("+organization.name+")" + "\n\n" + e.message + "\n\nStamhoofd"
|
|
93
|
-
}
|
|
92
|
+
})
|
|
94
93
|
|
|
95
94
|
throw new SimpleError({
|
|
96
95
|
code: "too_many_emails_period",
|
|
@@ -262,14 +262,43 @@ export class AuthenticatedStructures {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
const organizationStructs = await Promise.all([...organizations.values()].filter(o => o.active).map(o => this.organization(o)))
|
|
266
|
+
|
|
267
|
+
// Load missing groups
|
|
268
|
+
const allGroups = new Map<string, GroupStruct>()
|
|
269
|
+
for (const organization of organizationStructs) {
|
|
270
|
+
for (const group of organization.period.groups) {
|
|
271
|
+
allGroups.set(group.id, group)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
265
275
|
for (const blob of memberBlobs) {
|
|
266
|
-
|
|
276
|
+
for (const registration of blob.registrations) {
|
|
277
|
+
if (registration.group) {
|
|
278
|
+
allGroups.set(registration.group.id, registration.group)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const groupIds = Formatter.uniqueArray(responsibilities.map(r => r.groupId).filter(id => id !== null)).filter(id => !allGroups.has(id))
|
|
284
|
+
const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : []
|
|
285
|
+
const groupStructs = await this.groups(groups)
|
|
286
|
+
|
|
287
|
+
for (const group of groupStructs) {
|
|
288
|
+
allGroups.set(group.id, group)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const blob of memberBlobs) {
|
|
292
|
+
blob.responsibilities = responsibilities.filter(r => r.memberId == blob.id).map(r => {
|
|
293
|
+
const group = allGroups.get(r.groupId ?? '') ?? null
|
|
294
|
+
return r.getStructure(group)
|
|
295
|
+
})
|
|
267
296
|
blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r))
|
|
268
297
|
}
|
|
269
298
|
|
|
270
299
|
return MembersBlob.create({
|
|
271
300
|
members: memberBlobs,
|
|
272
|
-
organizations:
|
|
301
|
+
organizations: organizationStructs
|
|
273
302
|
})
|
|
274
303
|
}
|
|
275
304
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EmailAddress, EmailInterfaceRecipient } from "@stamhoofd/email";
|
|
1
|
+
import { Email, EmailAddress, EmailInterfaceRecipient } from "@stamhoofd/email";
|
|
2
2
|
import { Organization } from "@stamhoofd/models";
|
|
3
3
|
import { Formatter } from "@stamhoofd/utility";
|
|
4
4
|
import { simpleParser } from "mailparser";
|
|
@@ -26,32 +26,34 @@ export class ForwardHandler {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// Unsubscribe email?
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
for (const domain of Object.values(STAMHOOFD.domains.defaultBroadcastEmail ?? {})) {
|
|
30
|
+
if (email && email?.startsWith("unsubscribe+") && email.endsWith('@' + domain)) {
|
|
31
|
+
// Get id
|
|
32
|
+
const id = email.substring("unsubscribe+".length, email.indexOf('@' + domain))
|
|
33
|
+
const model = await EmailAddress.getByID(id)
|
|
34
|
+
|
|
35
|
+
if (model) {
|
|
36
|
+
console.log('[Unsubscribe] Received an unsubscribe request for ' + model.email + ' from ' + from)
|
|
37
|
+
if (model.unsubscribedAll) {
|
|
38
|
+
// Ignore
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
model.unsubscribedAll = true
|
|
42
|
+
await model.save()
|
|
43
|
+
} else {
|
|
44
|
+
console.error('[Unsubscribe] Received an unsubscribe request for unknown ID ' + id + ' from ' + from)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
// Forward
|
|
47
|
+
return {
|
|
48
|
+
from: Email.getWebmasterFromEmail(),
|
|
49
|
+
to: Email.getWebmasterToEmail(),
|
|
50
|
+
subject: "E-mail unsubscribe mislukt",
|
|
51
|
+
text: "Beste,\n\nEr werd een unsubscribe gemeld op "+email+" die niet kon worden verwerkt. Gelieve dit na te kijken.\n\nStamhoofd"
|
|
52
|
+
}
|
|
52
53
|
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
53
56
|
}
|
|
54
|
-
return;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
if (receipt.spamVerdict.status != "PASS" || receipt.virusVerdict.status != "PASS" || !(receipt.spfVerdict.status == "PASS" || receipt.dkimVerdict.status == "PASS")) {
|
|
@@ -60,7 +62,7 @@ export class ForwardHandler {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// Send a new e-mail
|
|
63
|
-
let defaultEmail: EmailInterfaceRecipient[]|string =
|
|
65
|
+
let defaultEmail: EmailInterfaceRecipient[]|string = Email.getWebmasterToEmail()
|
|
64
66
|
let organizationEmails: EmailInterfaceRecipient[] = []
|
|
65
67
|
const extraDescription = "Dit bericht werd verstuurd naar "+email+", en werd automatisch doorgestuurd naar alle beheerders. Stel in Stamhoofd de e-mailadressen in om ervoor te zorgen dat antwoorden naar een specifiek e-mailadres worden verstuurd."
|
|
66
68
|
|
|
@@ -76,7 +78,7 @@ export class ForwardHandler {
|
|
|
76
78
|
|
|
77
79
|
// Send back to receiver without including the original message to avoid spam
|
|
78
80
|
return {
|
|
79
|
-
from: email ??
|
|
81
|
+
from: email ?? Email.getWebmasterToEmail(),
|
|
80
82
|
to: from,
|
|
81
83
|
subject: "Ongeldig e-mailadres",
|
|
82
84
|
text: "Beste,\n\nDe vereniging die je probeert te bereiken via "+email+" is helaas niet bereikbaar via dit e-mailadres. Dit e-mailadres wordt enkel gebruikt voor het versturen van automatische e-mails in naam van een vereniging. Probeer de vereniging te contacteren via een ander e-mailadres.\n\nBedankt."
|
|
@@ -116,7 +118,7 @@ export class ForwardHandler {
|
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
const options = {
|
|
119
|
-
from: email ??
|
|
121
|
+
from: email ?? Email.getWebmasterToEmail(),
|
|
120
122
|
to: defaultEmail,
|
|
121
123
|
replyTo: parsed.from?.text,
|
|
122
124
|
subject: parsed.subject ?? "Doorgestuurd bericht",
|
|
@@ -137,4 +139,4 @@ export class ForwardHandler {
|
|
|
137
139
|
|
|
138
140
|
return options
|
|
139
141
|
}
|
|
140
|
-
}
|
|
142
|
+
}
|
|
@@ -116,14 +116,14 @@ export class MemberUserSyncerStatic {
|
|
|
116
116
|
if (organizationId === null) {
|
|
117
117
|
const patch = user.permissions.convertPlatformPatch(
|
|
118
118
|
Permissions.patch({
|
|
119
|
-
responsibilities: (responsibilitiesByOrganization.get(organizationId) ?? []).map(r => r.
|
|
119
|
+
responsibilities: (responsibilitiesByOrganization.get(organizationId) ?? []).map(r => r.getBaseStructure()) as any
|
|
120
120
|
})
|
|
121
121
|
)
|
|
122
122
|
user.permissions = user.permissions.patch(patch)
|
|
123
123
|
} else {
|
|
124
124
|
const patch = user.permissions.convertPatch(
|
|
125
125
|
Permissions.patch({
|
|
126
|
-
responsibilities: (responsibilitiesByOrganization.get(organizationId) ?? []).map(r => r.
|
|
126
|
+
responsibilities: (responsibilitiesByOrganization.get(organizationId) ?? []).map(r => r.getBaseStructure()) as any
|
|
127
127
|
}),
|
|
128
128
|
organizationId
|
|
129
129
|
)
|
|
@@ -164,7 +164,7 @@ export class MemberUserSyncerStatic {
|
|
|
164
164
|
let user = member.users.find(u => u.email.toLocaleLowerCase() === email.toLocaleLowerCase()) ?? await User.getForAuthentication(member.organizationId, email, {allowWithoutAccount: true})
|
|
165
165
|
|
|
166
166
|
if (user) {
|
|
167
|
-
console.log("Giving an existing user access to a member: " + user.id + ' - ' + member.id)
|
|
167
|
+
//console.log("Giving an existing user access to a member: " + user.id + ' - ' + member.id)
|
|
168
168
|
if (!asParent) {
|
|
169
169
|
if (user.memberId && user.memberId !== member.id) {
|
|
170
170
|
console.error('Found conflicting user with multiple members', user.id, 'members', user.memberId, 'to', member.id)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
|
|
2
|
+
import { Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
3
|
+
import { AuthenticatedStructures } from "./AuthenticatedStructures";
|
|
4
|
+
import { PatchOrganizationRegistrationPeriodsEndpoint } from "../endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint";
|
|
5
|
+
import { QueueHandler } from "@stamhoofd/queues";
|
|
6
|
+
import { SetupStepUpdater } from "./SetupStepsUpdater";
|
|
7
|
+
import { PermissionLevel } from "@stamhoofd/structures";
|
|
8
|
+
import { MemberUserSyncer } from "./MemberUserSyncer";
|
|
9
|
+
import { SimpleError } from "@simonbackx/simple-errors";
|
|
10
|
+
|
|
11
|
+
export class PeriodHelper {
|
|
12
|
+
static async moveOrganizationToPeriod(organization: Organization, period: RegistrationPeriod) {
|
|
13
|
+
console.log('moveOrganizationToPeriod', organization.id, period.id)
|
|
14
|
+
|
|
15
|
+
await this.createOrganizationPeriodForPeriod(organization, period)
|
|
16
|
+
organization.periodId = period.id
|
|
17
|
+
await organization.save()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static async stopAllResponsibilities() {
|
|
21
|
+
console.log('Stopping all responsibilities')
|
|
22
|
+
const platform = await Platform.getSharedPrivateStruct()
|
|
23
|
+
const keepPlatformResponsibilityIds = platform.config.responsibilities.filter(r => !r.organizationBased).map(r => r.id)
|
|
24
|
+
const keepResponsibilityIds = platform.config.responsibilities.filter(r => !r.organizationBased || r.permissions?.level === PermissionLevel.Full).map(r => r.id)
|
|
25
|
+
const batchSize = 100;
|
|
26
|
+
|
|
27
|
+
let lastId = "";
|
|
28
|
+
let c = 0;
|
|
29
|
+
|
|
30
|
+
while (true) {
|
|
31
|
+
const records = await MemberResponsibilityRecord.where(
|
|
32
|
+
{
|
|
33
|
+
id: { sign: ">", value: lastId },
|
|
34
|
+
endDate: null
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
limit: batchSize,
|
|
38
|
+
sort: ["id"]
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
for (const record of records) {
|
|
43
|
+
lastId = record.id;
|
|
44
|
+
|
|
45
|
+
const invalid = keepPlatformResponsibilityIds.includes(record.responsibilityId) && record.organizationId
|
|
46
|
+
|
|
47
|
+
if (!keepResponsibilityIds.includes(record.responsibilityId) || invalid) {
|
|
48
|
+
record.endDate = new Date()
|
|
49
|
+
await record.save()
|
|
50
|
+
c++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (records.length < batchSize) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
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
|
+
static async createOrganizationPeriodForPeriod(organization: Organization, period: RegistrationPeriod) {
|
|
104
|
+
const oPeriods = await OrganizationRegistrationPeriod.where({ periodId: period.id, organizationId: organization.id }, {limit: 1})
|
|
105
|
+
|
|
106
|
+
if (oPeriods.length) {
|
|
107
|
+
// Already created
|
|
108
|
+
return oPeriods[0]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const currentPeriod = await organization.getPeriod()
|
|
112
|
+
if (currentPeriod.periodId === period.id) {
|
|
113
|
+
return currentPeriod
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const struct = await AuthenticatedStructures.organizationRegistrationPeriod(currentPeriod)
|
|
117
|
+
|
|
118
|
+
const duplicate = struct.duplicate(period.getStructure())
|
|
119
|
+
return await PatchOrganizationRegistrationPeriodsEndpoint.createOrganizationPeriod(organization, duplicate)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static async moveAllOrganizationsToPeriod(period: RegistrationPeriod) {
|
|
123
|
+
const tag = "moveAllOrganizationsToPeriod";
|
|
124
|
+
if (QueueHandler.isRunning(tag)) {
|
|
125
|
+
throw new SimpleError({
|
|
126
|
+
code: 'move_period_pending',
|
|
127
|
+
message: 'Er is al een jaarovergang bezig. Wacht tot deze klaar is.'
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const batchSize = 10;
|
|
132
|
+
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
|
+
await this.moveOrganizationToPeriod(organization, period);
|
|
148
|
+
lastId = organization.id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (organizations.length < batchSize) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await this.stopAllResponsibilities()
|
|
158
|
+
await this.syncAllMemberUsers()
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// When done: update setup steps
|
|
162
|
+
await SetupStepUpdater.updateSetupStepsForAllOrganizationsInCurrentPeriod()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Group,
|
|
3
|
+
Member,
|
|
3
4
|
MemberResponsibilityRecord,
|
|
4
5
|
Organization,
|
|
5
6
|
OrganizationRegistrationPeriod,
|
|
@@ -8,11 +9,13 @@ import {
|
|
|
8
9
|
import { QueueHandler } from "@stamhoofd/queues";
|
|
9
10
|
import { SQL, SQLWhereSign } from "@stamhoofd/sql";
|
|
10
11
|
import {
|
|
12
|
+
GroupType,
|
|
11
13
|
MemberResponsibility,
|
|
12
14
|
Platform as PlatformStruct,
|
|
13
15
|
SetupStepType,
|
|
14
16
|
SetupSteps
|
|
15
17
|
} from "@stamhoofd/structures";
|
|
18
|
+
import { Formatter } from "@stamhoofd/utility";
|
|
16
19
|
|
|
17
20
|
type SetupStepOperation = (setupSteps: SetupSteps, organization: Organization, platform: PlatformStruct) => void | Promise<void>;
|
|
18
21
|
|
|
@@ -148,9 +151,7 @@ export class SetupStepUpdater {
|
|
|
148
151
|
) {
|
|
149
152
|
const setupSteps = organizationRegistrationPeriod.setupSteps;
|
|
150
153
|
|
|
151
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
152
154
|
for (const stepType of Object.values(SetupStepType)) {
|
|
153
|
-
console.log(`[STEP TYPE] ${stepType}`);
|
|
154
155
|
const operation = this.STEP_TYPE_OPERATIONS[stepType];
|
|
155
156
|
await operation(setupSteps, organization, platform);
|
|
156
157
|
}
|
|
@@ -244,12 +245,21 @@ export class SetupStepUpdater {
|
|
|
244
245
|
|
|
245
246
|
const responsibilityIds = organizationBasedResponsibilitiesWithRestriction.map(r => r.id);
|
|
246
247
|
|
|
247
|
-
const
|
|
248
|
+
const allRecords = await MemberResponsibilityRecord.select()
|
|
248
249
|
.where('responsibilityId', responsibilityIds)
|
|
249
250
|
.where('organizationId', organization.id)
|
|
250
251
|
.where(SQL.where('endDate', SQLWhereSign.Greater, now).or('endDate', null))
|
|
251
252
|
.fetch();
|
|
252
253
|
|
|
254
|
+
// Remove invalid responsibilities: members that are not registered in the current period
|
|
255
|
+
const memberIds = Formatter.uniqueArray(allRecords.map(r => r.memberId));
|
|
256
|
+
const members = await Member.getBlobByIds(...memberIds);
|
|
257
|
+
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));
|
|
258
|
+
|
|
259
|
+
const validMembersIds = validMembers.map(m => m.id);
|
|
260
|
+
|
|
261
|
+
const records = allRecords.filter(r => validMembersIds.includes(r.memberId));
|
|
262
|
+
|
|
253
263
|
let totalSteps = 0;
|
|
254
264
|
let finishedSteps = 0;
|
|
255
265
|
|
|
@@ -279,11 +289,11 @@ export class SetupStepUpdater {
|
|
|
279
289
|
for(const {responsibility, group} of flatResponsibilities) {
|
|
280
290
|
const { minimumMembers: min, maximumMembers: max } = responsibility;
|
|
281
291
|
|
|
282
|
-
if (min === null
|
|
292
|
+
if (min === null) {
|
|
283
293
|
continue;
|
|
284
294
|
}
|
|
285
295
|
|
|
286
|
-
totalSteps
|
|
296
|
+
totalSteps += min;
|
|
287
297
|
|
|
288
298
|
const responsibilityId = responsibility.id;
|
|
289
299
|
let totalRecordsWithThisResponsibility = 0;
|
|
@@ -303,14 +313,11 @@ export class SetupStepUpdater {
|
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
if (max !== null && totalRecordsWithThisResponsibility > max) {
|
|
316
|
+
// Not added
|
|
306
317
|
continue;
|
|
307
318
|
}
|
|
308
319
|
|
|
309
|
-
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
finishedSteps++;
|
|
320
|
+
finishedSteps += Math.min(min, totalRecordsWithThisResponsibility);
|
|
314
321
|
}
|
|
315
322
|
|
|
316
323
|
setupSteps.update(SetupStepType.Responsibilities, {
|