@stamhoofd/backend 2.6.0 → 2.8.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/package.json +2 -2
- package/src/endpoints/global/events/GetEventsEndpoint.ts +5 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +39 -16
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +32 -365
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -11
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +100 -88
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -3
- package/src/helpers/AdminPermissionChecker.ts +70 -55
- package/src/helpers/AuthenticatedStructures.ts +0 -1
- package/src/helpers/MemberUserSyncer.ts +44 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"postmark": "4.0.2",
|
|
51
51
|
"stripe": "^16.6.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "1e0e1853f8dea6b47718433b2890907042c17ddc"
|
|
54
54
|
}
|
|
@@ -23,7 +23,7 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
23
23
|
startDate: createSQLColumnFilterCompiler('startDate'),
|
|
24
24
|
endDate: createSQLColumnFilterCompiler('endDate'),
|
|
25
25
|
groupIds: createSQLExpressionFilterCompiler(
|
|
26
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.
|
|
26
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.groups[*].id'),
|
|
27
27
|
undefined,
|
|
28
28
|
true,
|
|
29
29
|
true
|
|
@@ -35,7 +35,10 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
35
35
|
true
|
|
36
36
|
),
|
|
37
37
|
organizationTagIds: createSQLExpressionFilterCompiler(
|
|
38
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds')
|
|
38
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds'),
|
|
39
|
+
undefined,
|
|
40
|
+
true,
|
|
41
|
+
true
|
|
39
42
|
)
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -3,8 +3,8 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Email, Member, MemberWithRegistrations, Platform } from '@stamhoofd/models';
|
|
6
|
-
import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions,
|
|
7
|
-
import { CountFilteredRequest, EmailRecipientFilterType,
|
|
6
|
+
import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
|
|
7
|
+
import { CountFilteredRequest, EmailRecipientFilterType, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter, mergeFilters } from '@stamhoofd/structures';
|
|
8
8
|
import { DataValidator, Formatter } from '@stamhoofd/utility';
|
|
9
9
|
|
|
10
10
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -36,6 +36,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
|
|
|
36
36
|
return await q.count();
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
+
|
|
39
40
|
Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
|
|
40
41
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
41
42
|
const result = await GetMembersEndpoint.buildData(query)
|
|
@@ -54,6 +55,24 @@ Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
|
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.MemberUnverified, {
|
|
59
|
+
fetch: async (query: LimitedFilteredRequest) => {
|
|
60
|
+
const result = await GetMembersEndpoint.buildData(query)
|
|
61
|
+
|
|
62
|
+
return new PaginatedResponse({
|
|
63
|
+
results: result.results.members.flatMap(m => m.getEmailRecipients(['unverified'])),
|
|
64
|
+
next: result.next
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
count: async (query: LimitedFilteredRequest) => {
|
|
69
|
+
const q = await GetMembersEndpoint.buildQuery(query)
|
|
70
|
+
return await q.sum(
|
|
71
|
+
SQL.jsonLength(SQL.column('details'), '$.value.unverifiedEmails')
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
57
76
|
const registrationFilterCompilers: SQLFilterDefinitions = {
|
|
58
77
|
...baseSQLFilterCompilers,
|
|
59
78
|
"price": createSQLColumnFilterCompiler('price'),
|
|
@@ -378,9 +397,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
378
397
|
}
|
|
379
398
|
},
|
|
380
399
|
periodId: platform.periodId,
|
|
381
|
-
registeredAt: {
|
|
382
|
-
$neq: null
|
|
383
|
-
},
|
|
384
400
|
group: {
|
|
385
401
|
defaultAgeGroupId: {
|
|
386
402
|
$neq: null
|
|
@@ -397,16 +413,26 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
397
413
|
const groups = await Context.auth.getAccessibleGroups(organization.id)
|
|
398
414
|
|
|
399
415
|
if (groups === 'all') {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
417
|
+
// Can access full history for now
|
|
418
|
+
scopeFilter = {
|
|
419
|
+
registrations: {
|
|
420
|
+
$elemMatch: {
|
|
421
|
+
organizationId: organization.id,
|
|
406
422
|
}
|
|
407
423
|
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
424
|
+
};
|
|
425
|
+
} else {
|
|
426
|
+
// Can only access current period
|
|
427
|
+
scopeFilter = {
|
|
428
|
+
registrations: {
|
|
429
|
+
$elemMatch: {
|
|
430
|
+
organizationId: organization.id,
|
|
431
|
+
periodId: organization.periodId,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
410
436
|
} else {
|
|
411
437
|
scopeFilter = {
|
|
412
438
|
registrations: {
|
|
@@ -415,9 +441,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
415
441
|
periodId: organization.periodId,
|
|
416
442
|
groupId: {
|
|
417
443
|
$in: groups
|
|
418
|
-
},
|
|
419
|
-
registeredAt: {
|
|
420
|
-
$neq: null
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
}
|
|
@@ -76,15 +76,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
76
76
|
const balanceItemRegistrationIdsPerOrganization: Map<string, string[]> = new Map()
|
|
77
77
|
const updateMembershipMemberIds = new Set<string>()
|
|
78
78
|
|
|
79
|
-
function addBalanceItemRegistrationId(organizationId: string, registrationId: string) {
|
|
80
|
-
const existing = balanceItemRegistrationIdsPerOrganization.get(organizationId);
|
|
81
|
-
if (existing) {
|
|
82
|
-
existing.push(registrationId)
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
balanceItemRegistrationIdsPerOrganization.set(organizationId, [registrationId])
|
|
86
|
-
}
|
|
87
|
-
|
|
88
79
|
// Loop all members one by one
|
|
89
80
|
for (const put of request.body.getPuts()) {
|
|
90
81
|
const struct = put.put
|
|
@@ -118,38 +109,14 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
118
109
|
}
|
|
119
110
|
}
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
statusCode: 400
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Throw early
|
|
135
|
-
for (const registrationStruct of struct.registrations) {
|
|
136
|
-
const group = await getGroup(registrationStruct.groupId)
|
|
137
|
-
if (!group || group.organizationId !== registrationStruct.organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
|
|
138
|
-
throw Context.auth.notFoundOrNoAccess("Je hebt niet voldoende rechten om leden toe te voegen in deze groep")
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const period = await RegistrationPeriod.getByID(group.periodId)
|
|
142
|
-
if (!period || period.locked) {
|
|
143
|
-
throw new SimpleError({
|
|
144
|
-
code: "period_locked",
|
|
145
|
-
message: "Deze inschrijvingsperiode is afgesloten en staat geen wijzigingen meer toe.",
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Set organization id of member based on registrations
|
|
150
|
-
if (!organization && STAMHOOFD.userMode !== 'platform' && !member.organizationId) {
|
|
151
|
-
member.organizationId = group.organizationId
|
|
152
|
-
}
|
|
112
|
+
// We risk creating a new member without being able to access it manually afterwards
|
|
113
|
+
if ((organization && !await Context.auth.hasFullAccess(organization.id)) || (!organization && !Context.auth.hasPlatformFullAccess())) {
|
|
114
|
+
throw new SimpleError({
|
|
115
|
+
code: "missing_group",
|
|
116
|
+
message: "Missing group",
|
|
117
|
+
human: "Je moet hoofdbeheerder zijn om een lid toe te voegen in het systeem",
|
|
118
|
+
statusCode: 400
|
|
119
|
+
})
|
|
153
120
|
}
|
|
154
121
|
|
|
155
122
|
if (STAMHOOFD.userMode !== 'platform' && !member.organizationId) {
|
|
@@ -167,16 +134,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
167
134
|
if ((STAMHOOFD.environment == "development" || STAMHOOFD.environment == "staging") && organization) {
|
|
168
135
|
if (member.details.firstName.toLocaleLowerCase() == "create" && parseInt(member.details.lastName) > 0) {
|
|
169
136
|
const count = parseInt(member.details.lastName);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const registrationStruct of struct.registrations) {
|
|
173
|
-
const g = await getGroup(registrationStruct.groupId)
|
|
174
|
-
if (g) {
|
|
175
|
-
group = g
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
await this.createDummyMembers(organization, group, count)
|
|
137
|
+
await this.createDummyMembers(organization, count)
|
|
180
138
|
|
|
181
139
|
// Skip creating this member
|
|
182
140
|
continue;
|
|
@@ -188,20 +146,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
188
146
|
balanceItemMemberIds.push(member.id)
|
|
189
147
|
updateMembershipMemberIds.add(member.id)
|
|
190
148
|
|
|
191
|
-
// Add registrations
|
|
192
|
-
for (const registrationStruct of struct.registrations) {
|
|
193
|
-
const group = await getGroup(registrationStruct.groupId)
|
|
194
|
-
if (!group || group.organizationId !== registrationStruct.organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
|
|
195
|
-
throw Context.auth.notFoundOrNoAccess("Je hebt niet voldoende rechten om leden toe te voegen in deze groep")
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const reg = await this.addRegistration(member, registrationStruct, group)
|
|
199
|
-
addBalanceItemRegistrationId(reg.organizationId, reg.id)
|
|
200
|
-
|
|
201
|
-
// Update occupancy at the end of the call
|
|
202
|
-
updateGroups.set(group.id, group)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
149
|
// Auto link users based on data
|
|
206
150
|
await MemberUserSyncer.onChangeMember(member)
|
|
207
151
|
}
|
|
@@ -233,192 +177,6 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
233
177
|
// Update documents
|
|
234
178
|
await Document.updateForMember(member.id)
|
|
235
179
|
|
|
236
|
-
// Update registrations
|
|
237
|
-
for (const patchRegistration of patch.registrations.getPatches()) {
|
|
238
|
-
const registration = member.registrations.find(r => r.id === patchRegistration.id)
|
|
239
|
-
if (!registration || registration.memberId != member.id || (!await Context.auth.canAccessRegistration(registration, PermissionLevel.Write))) {
|
|
240
|
-
throw new SimpleError({
|
|
241
|
-
code: "permission_denied",
|
|
242
|
-
message: "You don't have permissions to access this endpoint",
|
|
243
|
-
human: "Je hebt geen toegang om deze registratie te wijzigen"
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let group: Group | null = null
|
|
248
|
-
|
|
249
|
-
console.log('Patch registration', patchRegistration)
|
|
250
|
-
|
|
251
|
-
if (patchRegistration.group) {
|
|
252
|
-
patchRegistration.groupId = patchRegistration.group.id
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (patchRegistration.groupId) {
|
|
256
|
-
group = await getGroup(patchRegistration.groupId)
|
|
257
|
-
if (group) {
|
|
258
|
-
// We need to update group occupancy because we moved a member to it
|
|
259
|
-
updateGroups.set(group.id, group)
|
|
260
|
-
}
|
|
261
|
-
const oldGroup = await getGroup(registration.groupId)
|
|
262
|
-
if (oldGroup) {
|
|
263
|
-
// We need to update this group occupancy because we moved one member away from it
|
|
264
|
-
updateGroups.set(oldGroup.id, oldGroup)
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
group = await getGroup(registration.groupId)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (!group || group.organizationId !== (patchRegistration.organizationId ?? registration.organizationId)) {
|
|
271
|
-
throw new SimpleError({
|
|
272
|
-
code: "invalid_field",
|
|
273
|
-
message: "Group doesn't exist",
|
|
274
|
-
human: "De groep naarwaar je dit lid wilt verplaatsen bestaat niet",
|
|
275
|
-
field: "groupId"
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (!await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
|
|
280
|
-
throw Context.auth.error("Je hebt niet voldoende rechten om leden te verplaatsen naar deze groep")
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (patchRegistration.cycle && patchRegistration.cycle > group.cycle) {
|
|
284
|
-
throw new SimpleError({
|
|
285
|
-
code: "invalid_field",
|
|
286
|
-
message: "Invalid cycle",
|
|
287
|
-
human: "Je kan een lid niet inschrijven voor een groep die nog moet starten",
|
|
288
|
-
field: "cycle"
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const period = await RegistrationPeriod.getByID(group.periodId)
|
|
293
|
-
if (!period || period.locked) {
|
|
294
|
-
throw new SimpleError({
|
|
295
|
-
code: "period_locked",
|
|
296
|
-
message: "Deze inschrijvingsperiode is afgesloten en staat geen wijzigingen meer toe.",
|
|
297
|
-
})
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// TODO: allow group changes
|
|
301
|
-
registration.waitingList = patchRegistration.waitingList ?? registration.waitingList
|
|
302
|
-
|
|
303
|
-
if (!registration.waitingList && registration.registeredAt === null) {
|
|
304
|
-
registration.registeredAt = new Date()
|
|
305
|
-
}
|
|
306
|
-
registration.canRegister = patchRegistration.canRegister ?? registration.canRegister
|
|
307
|
-
if (!registration.waitingList) {
|
|
308
|
-
registration.canRegister = false
|
|
309
|
-
}
|
|
310
|
-
registration.cycle = patchRegistration.cycle ?? registration.cycle
|
|
311
|
-
registration.groupId = patchRegistration.groupId ?? registration.groupId
|
|
312
|
-
registration.group = group
|
|
313
|
-
registration.organizationId = patchRegistration.organizationId ?? registration.organizationId
|
|
314
|
-
|
|
315
|
-
// Check if we should create a placeholder payment?
|
|
316
|
-
|
|
317
|
-
if (patchRegistration.cycle !== undefined || patchRegistration.waitingList !== undefined || patchRegistration.canRegister !== undefined) {
|
|
318
|
-
// We need to update occupancy (because cycle / waitlist change)
|
|
319
|
-
updateGroups.set(group.id, group)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (patchRegistration.price) {
|
|
323
|
-
// Create balance item
|
|
324
|
-
const balanceItem = new BalanceItem();
|
|
325
|
-
balanceItem.registrationId = registration.id;
|
|
326
|
-
balanceItem.price = patchRegistration.price
|
|
327
|
-
balanceItem.description = group ? `Inschrijving ${group.settings.name}` : `Inschrijving`
|
|
328
|
-
balanceItem.pricePaid = patchRegistration.pricePaid ?? 0
|
|
329
|
-
balanceItem.memberId = registration.memberId;
|
|
330
|
-
balanceItem.userId = member.users[0]?.id ?? null
|
|
331
|
-
balanceItem.organizationId = group.organizationId
|
|
332
|
-
balanceItem.status = BalanceItemStatus.Pending;
|
|
333
|
-
await balanceItem.save();
|
|
334
|
-
|
|
335
|
-
addBalanceItemRegistrationId(registration.organizationId, registration.id)
|
|
336
|
-
balanceItemMemberIds.push(member.id)
|
|
337
|
-
|
|
338
|
-
if (balanceItem.pricePaid > 0) {
|
|
339
|
-
// Create an Unknown payment and attach it to the balance item
|
|
340
|
-
const payment = new Payment();
|
|
341
|
-
payment.userId = member.users[0]?.id ?? null
|
|
342
|
-
payment.organizationId = member.organizationId
|
|
343
|
-
payment.method = PaymentMethod.Unknown
|
|
344
|
-
payment.status = PaymentStatus.Succeeded
|
|
345
|
-
payment.price = balanceItem.pricePaid;
|
|
346
|
-
payment.paidAt = new Date()
|
|
347
|
-
payment.provider = null
|
|
348
|
-
await payment.save()
|
|
349
|
-
|
|
350
|
-
const balanceItemPayment = new BalanceItemPayment()
|
|
351
|
-
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
352
|
-
balanceItemPayment.paymentId = payment.id;
|
|
353
|
-
balanceItemPayment.organizationId = group.organizationId
|
|
354
|
-
balanceItemPayment.price = payment.price;
|
|
355
|
-
await balanceItemPayment.save();
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
await registration.save()
|
|
360
|
-
updateMembershipMemberIds.add(member.id)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
for (const deleteId of patch.registrations.getDeletes()) {
|
|
364
|
-
const registration = member.registrations.find(r => r.id === deleteId)
|
|
365
|
-
if (!registration || registration.memberId != member.id) {
|
|
366
|
-
throw new SimpleError({
|
|
367
|
-
code: "permission_denied",
|
|
368
|
-
message: "You don't have permissions to access this endpoint",
|
|
369
|
-
human: "Je hebt geen toegang om deze registratie te wijzigen"
|
|
370
|
-
})
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (!await Context.auth.canAccessRegistration(registration, PermissionLevel.Write)) {
|
|
374
|
-
throw Context.auth.error("Je hebt niet voldoende rechten om deze inschrijving te verwijderen")
|
|
375
|
-
}
|
|
376
|
-
const oldGroup = await getGroup(registration.groupId)
|
|
377
|
-
const period = oldGroup && await RegistrationPeriod.getByID(oldGroup.periodId)
|
|
378
|
-
if (!period || period.locked) {
|
|
379
|
-
throw new SimpleError({
|
|
380
|
-
code: "period_locked",
|
|
381
|
-
message: "Deze inschrijvingsperiode is afgesloten en staat geen wijzigingen meer toe.",
|
|
382
|
-
})
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
balanceItemMemberIds.push(member.id)
|
|
386
|
-
updateMembershipMemberIds.add(member.id)
|
|
387
|
-
await BalanceItem.deleteForDeletedRegistration(registration.id)
|
|
388
|
-
await registration.delete()
|
|
389
|
-
member.registrations = member.registrations.filter(r => r.id !== deleteId)
|
|
390
|
-
|
|
391
|
-
if (oldGroup) {
|
|
392
|
-
// We need to update this group occupancy because we moved one member away from it
|
|
393
|
-
updateGroups.set(oldGroup.id, oldGroup)
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Add registrations
|
|
398
|
-
for (const registrationStruct of patch.registrations.getPuts()) {
|
|
399
|
-
const struct = registrationStruct.put
|
|
400
|
-
const group = await getGroup(struct.groupId)
|
|
401
|
-
|
|
402
|
-
if (!group || group.organizationId !== struct.organizationId || !await Context.auth.canAccessGroup(group, PermissionLevel.Write)) {
|
|
403
|
-
throw Context.auth.error("Je hebt niet voldoende rechten om inschrijvingen in deze groep te maken")
|
|
404
|
-
}
|
|
405
|
-
const period = await RegistrationPeriod.getByID(group.periodId)
|
|
406
|
-
if (!period || period.locked) {
|
|
407
|
-
throw new SimpleError({
|
|
408
|
-
code: "period_locked",
|
|
409
|
-
message: "Deze inschrijvingsperiode is afgesloten en staat geen wijzigingen meer toe.",
|
|
410
|
-
})
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const reg = await this.addRegistration(member, struct, group)
|
|
414
|
-
balanceItemMemberIds.push(member.id)
|
|
415
|
-
updateMembershipMemberIds.add(member.id)
|
|
416
|
-
addBalanceItemRegistrationId(reg.organizationId, reg.id)
|
|
417
|
-
|
|
418
|
-
// We need to update this group occupancy because we moved one member away from it
|
|
419
|
-
updateGroups.set(group.id, group)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
180
|
// Update responsibilities
|
|
423
181
|
for (const patchResponsibility of patch.responsibilities.getPatches()) {
|
|
424
182
|
if (!Context.auth.hasPlatformFullAccess() && !(organization && await Context.auth.hasFullAccess(organization.id))) {
|
|
@@ -570,6 +328,26 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
570
328
|
// Auto link users based on data
|
|
571
329
|
await MemberUserSyncer.onChangeMember(member)
|
|
572
330
|
|
|
331
|
+
// Allow to remove access for certain users
|
|
332
|
+
for (const id of patch.users.getDeletes()) {
|
|
333
|
+
const user = member.users.find(u => u.id === id)
|
|
334
|
+
if (!user) {
|
|
335
|
+
// Ignore silently
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (MemberUserSyncer.doesEmailHaveAccess(member.details, user.email)) {
|
|
340
|
+
throw new SimpleError({
|
|
341
|
+
code: "invalid_field",
|
|
342
|
+
message: "Invalid email",
|
|
343
|
+
human: "Je kan een account niet de toegang ontzetten tot een lid als het e-mailadres nog steeds is opgeslagen als onderdeel van de gegevens van dat lid. Verwijder eerst het e-mailadres uit de gegevens van het lid en ontkoppel daarna het account."
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Remove access
|
|
348
|
+
await MemberUserSyncer.unlinkUser(user, member)
|
|
349
|
+
}
|
|
350
|
+
|
|
573
351
|
// Add platform memberships
|
|
574
352
|
for (const {put} of patch.platformMemberships.getPuts()) {
|
|
575
353
|
if (put.periodId !== platform.periodId) {
|
|
@@ -745,120 +523,9 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
745
523
|
}
|
|
746
524
|
}
|
|
747
525
|
|
|
748
|
-
async
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const existings = await Registration.where({
|
|
752
|
-
memberId: member.id,
|
|
753
|
-
groupId: registrationStruct.groupId,
|
|
754
|
-
cycle: registrationStruct.cycle
|
|
755
|
-
}, { limit: 1 })
|
|
756
|
-
const existing = existings.length > 0 ? existings[0] : null
|
|
757
|
-
|
|
758
|
-
// If the existing is invalid, delete it.
|
|
759
|
-
if (existing && !existing.registeredAt && !existing.waitingList) {
|
|
760
|
-
console.log('Deleting invalid registration', existing.id)
|
|
761
|
-
await existing.delete()
|
|
762
|
-
} else if (existing) {
|
|
763
|
-
throw new SimpleError({
|
|
764
|
-
code: "invalid_field",
|
|
765
|
-
message: "Registration already exists",
|
|
766
|
-
human: existing.waitingList ? "Dit lid staat al op de wachtlijst voor deze groep" : "Dit lid is al ingeschreven voor deze groep",
|
|
767
|
-
field: "groupId"
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (!group) {
|
|
772
|
-
throw new SimpleError({
|
|
773
|
-
code: 'invalid_field',
|
|
774
|
-
field: 'groupId',
|
|
775
|
-
message: 'Invalid groupId',
|
|
776
|
-
human: 'Deze inschrijvingsgroep is ongeldig'
|
|
777
|
-
})
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const registration = new Registration()
|
|
781
|
-
registration.groupId = registrationStruct.groupId
|
|
782
|
-
registration.organizationId = group.organizationId
|
|
783
|
-
registration.periodId = group.periodId
|
|
784
|
-
registration.cycle = registrationStruct.cycle
|
|
785
|
-
registration.memberId = member.id
|
|
786
|
-
registration.registeredAt = registrationStruct.registeredAt
|
|
787
|
-
registration.waitingList = registrationStruct.waitingList
|
|
788
|
-
registration.createdAt = registrationStruct.createdAt ?? new Date()
|
|
789
|
-
|
|
790
|
-
if (registration.waitingList) {
|
|
791
|
-
registration.registeredAt = null
|
|
792
|
-
}
|
|
793
|
-
registration.canRegister = registrationStruct.canRegister
|
|
794
|
-
|
|
795
|
-
if (!registration.waitingList) {
|
|
796
|
-
registration.canRegister = false
|
|
797
|
-
}
|
|
798
|
-
registration.deactivatedAt = registrationStruct.deactivatedAt
|
|
799
|
-
|
|
800
|
-
await registration.save()
|
|
801
|
-
member.registrations.push(registration.setRelation(Registration.group, group))
|
|
802
|
-
|
|
803
|
-
if (registrationStruct.price) {
|
|
804
|
-
// Create balance item
|
|
805
|
-
const balanceItem = new BalanceItem();
|
|
806
|
-
balanceItem.registrationId = registration.id;
|
|
807
|
-
balanceItem.price = registrationStruct.price
|
|
808
|
-
balanceItem.description = group ? `Inschrijving ${group.settings.name}` : `Inschrijving`
|
|
809
|
-
balanceItem.pricePaid = registrationStruct.pricePaid ?? 0
|
|
810
|
-
balanceItem.memberId = registration.memberId;
|
|
811
|
-
balanceItem.userId = member.users[0]?.id ?? null
|
|
812
|
-
balanceItem.organizationId = group.organizationId
|
|
813
|
-
balanceItem.status = BalanceItemStatus.Pending;
|
|
814
|
-
await balanceItem.save();
|
|
815
|
-
|
|
816
|
-
if (balanceItem.pricePaid > 0) {
|
|
817
|
-
// Create an Unknown payment and attach it to the balance item
|
|
818
|
-
const payment = new Payment();
|
|
819
|
-
payment.userId = member.users[0]?.id ?? null
|
|
820
|
-
payment.organizationId = member.organizationId
|
|
821
|
-
payment.method = PaymentMethod.Unknown
|
|
822
|
-
payment.status = PaymentStatus.Succeeded
|
|
823
|
-
payment.price = balanceItem.pricePaid;
|
|
824
|
-
payment.paidAt = new Date()
|
|
825
|
-
payment.provider = null
|
|
826
|
-
await payment.save()
|
|
827
|
-
|
|
828
|
-
const balanceItemPayment = new BalanceItemPayment()
|
|
829
|
-
balanceItemPayment.balanceItemId = balanceItem.id;
|
|
830
|
-
balanceItemPayment.paymentId = payment.id;
|
|
831
|
-
balanceItemPayment.organizationId = group.organizationId
|
|
832
|
-
balanceItemPayment.price = payment.price;
|
|
833
|
-
await balanceItemPayment.save();
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
return registration
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
async createDummyMembers(organization: Organization, group: Group, count: number) {
|
|
841
|
-
const members = await new MemberFactory({
|
|
842
|
-
organization,
|
|
843
|
-
minAge: group.settings.minAge ?? undefined,
|
|
844
|
-
maxAge: group.settings.maxAge ?? undefined
|
|
526
|
+
async createDummyMembers(organization: Organization, count: number) {
|
|
527
|
+
await new MemberFactory({
|
|
528
|
+
organization
|
|
845
529
|
}).createMultiple(count)
|
|
846
|
-
|
|
847
|
-
for (const m of members) {
|
|
848
|
-
const member = m.setManyRelation(Member.registrations as unknown as OneToManyRelation<"registrations", Member, Registration>, []).setManyRelation(Member.users, [])
|
|
849
|
-
const d = new Date(new Date().getTime() - Math.random() * 60 * 1000 * 60 * 24 * 60)
|
|
850
|
-
|
|
851
|
-
// Create a registration for this member for thisg roup
|
|
852
|
-
const registration = new Registration()
|
|
853
|
-
registration.organizationId = organization.id
|
|
854
|
-
registration.memberId = member.id
|
|
855
|
-
registration.groupId = group.id
|
|
856
|
-
registration.periodId = group.periodId
|
|
857
|
-
registration.cycle = group.cycle
|
|
858
|
-
registration.registeredAt = d
|
|
859
|
-
|
|
860
|
-
member.registrations.push(registration)
|
|
861
|
-
await registration.save()
|
|
862
|
-
}
|
|
863
530
|
}
|
|
864
531
|
}
|
|
@@ -4,10 +4,10 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
4
4
|
import { Document, Member } from '@stamhoofd/models';
|
|
5
5
|
import { MemberWithRegistrationsBlob, MembersBlob } from "@stamhoofd/structures";
|
|
6
6
|
|
|
7
|
-
import { Context } from '../../../helpers/Context';
|
|
8
|
-
import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
|
|
9
7
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
8
|
+
import { Context } from '../../../helpers/Context';
|
|
10
9
|
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
10
|
+
import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
|
|
11
11
|
type Params = Record<string, never>;
|
|
12
12
|
type Query = undefined;
|
|
13
13
|
type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>
|
|
@@ -48,15 +48,6 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
48
48
|
struct.details.cleanData()
|
|
49
49
|
member.details = struct.details
|
|
50
50
|
|
|
51
|
-
if (!struct.details) {
|
|
52
|
-
throw new SimpleError({
|
|
53
|
-
code: "invalid_data",
|
|
54
|
-
message: "No details provided",
|
|
55
|
-
human: "Opgelet! Je gebruikt een oudere versie van de inschrijvingspagina die niet langer wordt ondersteund. Herlaad de website grondig en wis je browser cache.",
|
|
56
|
-
field: "details"
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
51
|
// Check for duplicates and prevent creating a duplicate member by a user
|
|
61
52
|
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member);
|
|
62
53
|
if (duplicate) {
|
|
@@ -85,6 +76,12 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
85
76
|
if (updatedMember) {
|
|
86
77
|
// Make sure we also give access to other parents
|
|
87
78
|
await MemberUserSyncer.onChangeMember(updatedMember)
|
|
79
|
+
|
|
80
|
+
if (!updatedMember.users.find(u => u.id === user.id)) {
|
|
81
|
+
// Also link the user to the member if the email address is missing in the details
|
|
82
|
+
await MemberUserSyncer.linkUser(user.email, updatedMember, true)
|
|
83
|
+
}
|
|
84
|
+
|
|
88
85
|
await Document.updateForMember(updatedMember.id)
|
|
89
86
|
}
|
|
90
87
|
}
|
|
@@ -13,7 +13,6 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
|
|
|
13
13
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
14
14
|
import { Context } from '../../../helpers/Context';
|
|
15
15
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
16
|
-
import { ExchangePaymentEndpoint } from '../../organization/shared/ExchangePaymentEndpoint';
|
|
17
16
|
type Params = Record<string, never>;
|
|
18
17
|
type Query = undefined;
|
|
19
18
|
type Body = IDRegisterCheckout
|
|
@@ -99,8 +98,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
|
|
101
|
+
const deleteRegistrationIds = request.body.cart.deleteRegistrationIds
|
|
102
|
+
const deleteRegistrationModels = (deleteRegistrationIds.length ? (await Registration.getByIDs(...deleteRegistrationIds)) : []).filter(r => r.organizationId === organization.id)
|
|
103
|
+
|
|
102
104
|
const memberIds = Formatter.uniqueArray(
|
|
103
|
-
[...request.body.cart.items.map(i => i.memberId), ...
|
|
105
|
+
[...request.body.cart.items.map(i => i.memberId), ...deleteRegistrationModels.map(i => i.memberId)]
|
|
104
106
|
)
|
|
105
107
|
const members = await Member.getBlobByIds(...memberIds)
|
|
106
108
|
const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
|
|
@@ -175,23 +177,23 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
175
177
|
|
|
176
178
|
// Validate balance items (can only happen serverside)
|
|
177
179
|
const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id)
|
|
178
|
-
let
|
|
179
|
-
let
|
|
180
|
+
let memberBalanceItemsStructs: MemberBalanceItem[] = []
|
|
181
|
+
let balanceItemsModels: BalanceItem[] = []
|
|
180
182
|
if (balanceItemIds.length > 0) {
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
+
balanceItemsModels = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
|
|
184
|
+
if (balanceItemsModels.length != balanceItemIds.length) {
|
|
183
185
|
throw new SimpleError({
|
|
184
186
|
code: "invalid_data",
|
|
185
187
|
message: "Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw."
|
|
186
188
|
})
|
|
187
189
|
}
|
|
188
|
-
|
|
190
|
+
memberBalanceItemsStructs = await BalanceItem.getMemberStructure(balanceItemsModels)
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
|
|
192
194
|
|
|
193
195
|
// Validate the cart
|
|
194
|
-
checkout.validate({memberBalanceItems})
|
|
196
|
+
checkout.validate({memberBalanceItems: memberBalanceItemsStructs})
|
|
195
197
|
|
|
196
198
|
// Recalculate the price
|
|
197
199
|
checkout.updatePrices()
|
|
@@ -234,34 +236,37 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
234
236
|
|
|
235
237
|
// Check if this member is already registered in this group?
|
|
236
238
|
const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle })
|
|
237
|
-
let registration: RegistrationWithMemberAndGroup | undefined = undefined;
|
|
238
239
|
|
|
239
240
|
for (const existingRegistration of existingRegistrations) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
if (item.replaceRegistrations.some(r => r.id === existingRegistration.id)) {
|
|
242
|
+
// Safe
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (checkout.cart.deleteRegistrations.some(r => r.id === existingRegistration.id)) {
|
|
247
|
+
// Safe
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
244
250
|
|
|
245
251
|
if (existingRegistration.registeredAt !== null && existingRegistration.deactivatedAt === null) {
|
|
246
252
|
throw new SimpleError({
|
|
247
253
|
code: "already_registered",
|
|
248
|
-
message:
|
|
254
|
+
message: `${member.firstName} is al ingeschreven voor ${group.settings.name}. Mogelijks heb je meerdere keren proberen in te schrijven en is het intussen wel gelukt. Herlaad de pagina best even om zeker te zijn.`
|
|
249
255
|
})
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
registration.periodId = group.periodId
|
|
259
|
-
}
|
|
259
|
+
const registration = new Registration()
|
|
260
|
+
.setRelation(registrationMemberRelation, member as Member)
|
|
261
|
+
.setRelation(Registration.group, group)
|
|
262
|
+
registration.organizationId = organization.id
|
|
263
|
+
registration.periodId = group.periodId
|
|
260
264
|
|
|
261
265
|
registration.memberId = member.id
|
|
262
266
|
registration.groupId = group.id
|
|
263
|
-
registration.
|
|
264
|
-
registration.
|
|
267
|
+
registration.price = 0 // will get filled by balance items themselves
|
|
268
|
+
registration.groupPrice = item.groupPrice;
|
|
269
|
+
registration.options = item.options
|
|
265
270
|
|
|
266
271
|
payRegistrations.push({
|
|
267
272
|
registration,
|
|
@@ -306,8 +311,59 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
306
311
|
|
|
307
312
|
console.log('Registering members using whoWillPayNow', whoWillPayNow, checkout.paymentMethod, totalPrice)
|
|
308
313
|
|
|
309
|
-
const
|
|
310
|
-
const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale
|
|
314
|
+
const createdBalanceItems: BalanceItem[] = []
|
|
315
|
+
const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale || checkout.paymentMethod === PaymentMethod.Unknown
|
|
316
|
+
|
|
317
|
+
// Create negative balance items
|
|
318
|
+
for (const registrationStruct of [...checkout.cart.deleteRegistrations, ...checkout.cart.items.flatMap(i => i.replaceRegistrations)]) {
|
|
319
|
+
if (whoWillPayNow !== 'nobody') {
|
|
320
|
+
// this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
|
|
321
|
+
throw new SimpleError({
|
|
322
|
+
code: "forbidden",
|
|
323
|
+
message: "Permission denied: you are not allowed to delete registrations",
|
|
324
|
+
human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
|
|
325
|
+
statusCode: 403
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const existingRegistration = await Registration.getByID(registrationStruct.id)
|
|
330
|
+
if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
|
|
331
|
+
throw new SimpleError({
|
|
332
|
+
code: "invalid_data",
|
|
333
|
+
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
|
|
338
|
+
throw new SimpleError({
|
|
339
|
+
code: "forbidden",
|
|
340
|
+
message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
|
|
341
|
+
statusCode: 403
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
|
|
346
|
+
throw new SimpleError({
|
|
347
|
+
code: "invalid_data",
|
|
348
|
+
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
|
|
353
|
+
// Find all balance items of this registration and set them to zero
|
|
354
|
+
await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
|
|
355
|
+
|
|
356
|
+
// Clear the registration
|
|
357
|
+
await existingRegistration.deactivate()
|
|
358
|
+
|
|
359
|
+
const group = groups.find(g => g.id === existingRegistration.groupId)
|
|
360
|
+
if (!group) {
|
|
361
|
+
const g = await Group.getByID(existingRegistration.groupId)
|
|
362
|
+
if (g) {
|
|
363
|
+
groups.push(g)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
311
367
|
|
|
312
368
|
// Save registrations and add extra data if needed
|
|
313
369
|
for (const bundle of payRegistrations) {
|
|
@@ -367,7 +423,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
367
423
|
balanceItem2.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
368
424
|
await balanceItem2.save();
|
|
369
425
|
|
|
370
|
-
// do not add to
|
|
426
|
+
// do not add to createdBalanceItems array because we don't want to add this to the payment if we create a payment
|
|
371
427
|
} else {
|
|
372
428
|
balanceItem.memberId = registration.memberId;
|
|
373
429
|
balanceItem.userId = user.id
|
|
@@ -380,7 +436,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
380
436
|
balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null
|
|
381
437
|
|
|
382
438
|
await balanceItem.save();
|
|
383
|
-
|
|
439
|
+
createdBalanceItems.push(balanceItem)
|
|
384
440
|
}
|
|
385
441
|
|
|
386
442
|
const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
|
|
@@ -400,7 +456,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
400
456
|
}
|
|
401
457
|
balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
402
458
|
await balanceItem.save();
|
|
403
|
-
|
|
459
|
+
createdBalanceItems.push(balanceItem)
|
|
404
460
|
}
|
|
405
461
|
|
|
406
462
|
if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
|
|
@@ -424,64 +480,15 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
424
480
|
balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
425
481
|
await balanceItem.save();
|
|
426
482
|
|
|
427
|
-
|
|
483
|
+
createdBalanceItems.push(balanceItem);
|
|
428
484
|
}
|
|
429
485
|
|
|
430
486
|
if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
|
|
431
|
-
throw new
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (whoWillPayNow !== 'nobody') {
|
|
437
|
-
// this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
|
|
438
|
-
throw new SimpleError({
|
|
439
|
-
code: "forbidden",
|
|
440
|
-
message: "Permission denied: you are not allowed to delete registrations",
|
|
441
|
-
human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
|
|
442
|
-
statusCode: 403
|
|
443
|
-
})
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const existingRegistration = await Registration.getByID(registrationStruct.id)
|
|
447
|
-
if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
|
|
448
|
-
throw new SimpleError({
|
|
449
|
-
code: "invalid_data",
|
|
450
|
-
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
451
|
-
})
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
|
|
455
|
-
throw new SimpleError({
|
|
456
|
-
code: "forbidden",
|
|
457
|
-
message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
|
|
458
|
-
statusCode: 403
|
|
459
|
-
})
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
|
|
463
|
-
throw new SimpleError({
|
|
464
|
-
code: "invalid_data",
|
|
465
|
-
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
|
|
466
|
-
})
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
|
|
470
|
-
// Find all balance items of this registration and set them to zero
|
|
471
|
-
await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
|
|
472
|
-
|
|
473
|
-
// Clear the registration
|
|
474
|
-
existingRegistration.deactivatedAt = new Date()
|
|
475
|
-
await existingRegistration.save()
|
|
476
|
-
existingRegistration.scheduleStockUpdate()
|
|
477
|
-
|
|
478
|
-
const group = groups.find(g => g.id === existingRegistration.groupId)
|
|
479
|
-
if (!group) {
|
|
480
|
-
const g = await Group.getByID(existingRegistration.groupId)
|
|
481
|
-
if (g) {
|
|
482
|
-
groups.push(g)
|
|
483
|
-
}
|
|
484
|
-
}
|
|
487
|
+
throw new SimpleError({
|
|
488
|
+
code: 'invalid_data',
|
|
489
|
+
message: 'Not possible to pay balance items as the organization',
|
|
490
|
+
statusCode: 400
|
|
491
|
+
})
|
|
485
492
|
}
|
|
486
493
|
|
|
487
494
|
let paymentUrl: string | null = null
|
|
@@ -490,19 +497,21 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
490
497
|
if (whoWillPayNow !== 'nobody') {
|
|
491
498
|
const mappedBalanceItems = new Map<BalanceItem, number>()
|
|
492
499
|
|
|
493
|
-
for (const item of
|
|
500
|
+
for (const item of createdBalanceItems) {
|
|
494
501
|
mappedBalanceItems.set(item, item.price)
|
|
495
502
|
}
|
|
496
503
|
|
|
497
504
|
for (const item of checkout.cart.balanceItems) {
|
|
498
|
-
const balanceItem =
|
|
505
|
+
const balanceItem = balanceItemsModels.find(i => i.id === item.item.id)
|
|
499
506
|
if (!balanceItem) {
|
|
500
507
|
throw new Error('Balance item not found')
|
|
501
508
|
}
|
|
502
509
|
mappedBalanceItems.set(balanceItem, item.price)
|
|
503
|
-
|
|
510
|
+
createdBalanceItems.push(balanceItem)
|
|
504
511
|
}
|
|
505
512
|
|
|
513
|
+
// Make sure every price is accurate before creating a payment
|
|
514
|
+
await BalanceItem.updateOutstanding(createdBalanceItems, organization.id)
|
|
506
515
|
const response = await this.createPayment({
|
|
507
516
|
balanceItems: mappedBalanceItems,
|
|
508
517
|
organization,
|
|
@@ -515,9 +524,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
515
524
|
paymentUrl = response.paymentUrl
|
|
516
525
|
payment = response.payment
|
|
517
526
|
}
|
|
527
|
+
} else {
|
|
528
|
+
await BalanceItem.updateOutstanding(createdBalanceItems, organization.id)
|
|
518
529
|
}
|
|
519
530
|
|
|
520
|
-
await BalanceItem.updateOutstanding(items, organization.id)
|
|
521
531
|
|
|
522
532
|
// Update occupancy
|
|
523
533
|
for (const group of groups) {
|
|
@@ -527,9 +537,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
527
537
|
}
|
|
528
538
|
}
|
|
529
539
|
|
|
540
|
+
const updatedMembers = await Member.getBlobByIds(...memberIds)
|
|
541
|
+
|
|
530
542
|
return new Response(RegisterResponse.create({
|
|
531
543
|
payment: payment ? PaymentStruct.create(payment) : null,
|
|
532
|
-
members: await AuthenticatedStructures.membersBlob(
|
|
544
|
+
members: await AuthenticatedStructures.membersBlob(updatedMembers),
|
|
533
545
|
registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
|
|
534
546
|
paymentUrl
|
|
535
547
|
}));
|
|
@@ -2,10 +2,9 @@ import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, Patch
|
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { RegistrationPeriod as RegistrationPeriodStruct } from "@stamhoofd/structures";
|
|
4
4
|
|
|
5
|
-
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
6
|
-
import { Context } from '../../../helpers/Context';
|
|
7
|
-
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
8
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
7
|
+
import { Context } from '../../../helpers/Context';
|
|
9
8
|
|
|
10
9
|
type Params = Record<string, never>;
|
|
11
10
|
type Query = undefined;
|
|
@@ -207,10 +207,11 @@ export class AdminPermissionChecker {
|
|
|
207
207
|
return await this.hasFullAccess(organizationId)
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
/**
|
|
211
|
-
* Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
|
|
212
|
-
*/
|
|
213
210
|
async canAccessMember(member: MemberWithRegistrations, permissionLevel: PermissionLevel = PermissionLevel.Read) {
|
|
211
|
+
if (this.isUserManager(member) && permissionLevel !== PermissionLevel.Full) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
214
215
|
// Check user has permissions
|
|
215
216
|
if (!this.user.permissions) {
|
|
216
217
|
return false
|
|
@@ -761,18 +762,6 @@ export class AdminPermissionChecker {
|
|
|
761
762
|
for (const category of organization.meta.recordsConfiguration.recordCategories) {
|
|
762
763
|
recordCategories.push(category)
|
|
763
764
|
}
|
|
764
|
-
|
|
765
|
-
for (const [id] of organization.meta.recordsConfiguration.inheritedRecordCategories) {
|
|
766
|
-
if (recordCategories.find(c => c.id === id)) {
|
|
767
|
-
// Already added
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const category = this.platform.config.recordsConfiguration.recordCategories.find(c => c.id === id)
|
|
772
|
-
if (category) {
|
|
773
|
-
recordCategories.push(category)
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
765
|
continue;
|
|
777
766
|
}
|
|
778
767
|
|
|
@@ -788,17 +777,15 @@ export class AdminPermissionChecker {
|
|
|
788
777
|
}
|
|
789
778
|
}
|
|
790
779
|
|
|
791
|
-
for
|
|
792
|
-
|
|
780
|
+
// Platform ones where we have been given permissions for in this organization
|
|
781
|
+
for (const category of this.platform.config.recordsConfiguration.recordCategories) {
|
|
782
|
+
if (recordCategories.find(c => c.id === category.id)) {
|
|
793
783
|
// Already added
|
|
794
784
|
continue;
|
|
795
785
|
}
|
|
796
786
|
|
|
797
|
-
if (permissions.hasResourceAccess(PermissionsResourceType.RecordCategories, id, level)) {
|
|
798
|
-
|
|
799
|
-
if (category) {
|
|
800
|
-
recordCategories.push(category)
|
|
801
|
-
}
|
|
787
|
+
if (permissions.hasResourceAccess(PermissionsResourceType.RecordCategories, category.id, level)) {
|
|
788
|
+
recordCategories.push(category)
|
|
802
789
|
}
|
|
803
790
|
}
|
|
804
791
|
}
|
|
@@ -916,6 +903,10 @@ export class AdminPermissionChecker {
|
|
|
916
903
|
const isUserManager = this.isUserManager(member)
|
|
917
904
|
if (isUserManager) {
|
|
918
905
|
// For the user manager, we don't delete data, because when registering a new member, it doesn't have any organizations yet...
|
|
906
|
+
|
|
907
|
+
// Notes are not visible for the member.
|
|
908
|
+
data.details.notes = null;
|
|
909
|
+
|
|
919
910
|
return data;
|
|
920
911
|
}
|
|
921
912
|
|
|
@@ -932,6 +923,7 @@ export class AdminPermissionChecker {
|
|
|
932
923
|
// Has financial read access?
|
|
933
924
|
if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
|
|
934
925
|
cloned.details.requiresFinancialSupport = null
|
|
926
|
+
cloned.details.uitpasNumber = null
|
|
935
927
|
cloned.outstandingBalance = 0
|
|
936
928
|
|
|
937
929
|
for (const registration of cloned.registrations) {
|
|
@@ -955,48 +947,63 @@ export class AdminPermissionChecker {
|
|
|
955
947
|
})
|
|
956
948
|
}
|
|
957
949
|
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
throw new SimpleError({
|
|
961
|
-
code: 'invalid_request',
|
|
962
|
-
message: 'Cannot PUT recordAnswers',
|
|
963
|
-
statusCode: 400
|
|
964
|
-
})
|
|
965
|
-
}
|
|
966
|
-
const isUserManager = this.isUserManager(member)
|
|
967
|
-
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
950
|
+
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
951
|
+
const hasNotes = data.details.notes !== undefined;
|
|
968
952
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (value) {
|
|
972
|
-
if (value.isPatch()) {
|
|
973
|
-
throw new SimpleError({
|
|
974
|
-
code: 'invalid_request',
|
|
975
|
-
message: 'Cannot PATCH a record answer object',
|
|
976
|
-
statusCode: 400
|
|
977
|
-
})
|
|
978
|
-
}
|
|
953
|
+
if(hasRecordAnswers || hasNotes) {
|
|
954
|
+
const isUserManager = this.isUserManager(member);
|
|
979
955
|
|
|
980
|
-
|
|
956
|
+
if (hasRecordAnswers) {
|
|
957
|
+
if (!(data.details.recordAnswers instanceof PatchMap)) {
|
|
958
|
+
throw new SimpleError({
|
|
959
|
+
code: 'invalid_request',
|
|
960
|
+
message: 'Cannot PUT recordAnswers',
|
|
961
|
+
statusCode: 400
|
|
962
|
+
})
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
966
|
+
|
|
967
|
+
for (const [key, value] of data.details.recordAnswers.entries()) {
|
|
968
|
+
let name: string | undefined = undefined
|
|
969
|
+
if (value) {
|
|
970
|
+
if (value.isPatch()) {
|
|
971
|
+
throw new SimpleError({
|
|
972
|
+
code: 'invalid_request',
|
|
973
|
+
message: 'Cannot PATCH a record answer object',
|
|
974
|
+
statusCode: 400
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const id = value.settings.id
|
|
979
|
+
|
|
980
|
+
if (id !== key) {
|
|
981
|
+
throw new SimpleError({
|
|
982
|
+
code: 'invalid_request',
|
|
983
|
+
message: 'Record answer key does not match record id',
|
|
984
|
+
statusCode: 400
|
|
985
|
+
})
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
name = value.settings.name
|
|
989
|
+
}
|
|
981
990
|
|
|
982
|
-
if (
|
|
991
|
+
if (!isUserManager && !records.has(key)) {
|
|
983
992
|
throw new SimpleError({
|
|
984
|
-
code: '
|
|
985
|
-
message: '
|
|
993
|
+
code: 'permission_denied',
|
|
994
|
+
message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
|
|
986
995
|
statusCode: 400
|
|
987
996
|
})
|
|
988
997
|
}
|
|
989
|
-
|
|
990
|
-
name = value.settings.name
|
|
991
998
|
}
|
|
999
|
+
}
|
|
992
1000
|
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
}
|
|
1001
|
+
if(hasNotes && isUserManager) {
|
|
1002
|
+
throw new SimpleError({
|
|
1003
|
+
code: 'permission_denied',
|
|
1004
|
+
message: 'Cannot edit notes',
|
|
1005
|
+
statusCode: 400
|
|
1006
|
+
})
|
|
1000
1007
|
}
|
|
1001
1008
|
}
|
|
1002
1009
|
|
|
@@ -1010,6 +1017,14 @@ export class AdminPermissionChecker {
|
|
|
1010
1017
|
})
|
|
1011
1018
|
}
|
|
1012
1019
|
|
|
1020
|
+
if (data.details.uitpasNumber) {
|
|
1021
|
+
throw new SimpleError({
|
|
1022
|
+
code: 'permission_denied',
|
|
1023
|
+
message: 'Je hebt geen toegangsrechten om het UiTPAS-nummer van dit lid aan te passen',
|
|
1024
|
+
statusCode: 400
|
|
1025
|
+
})
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1013
1028
|
if (data.outstandingBalance) {
|
|
1014
1029
|
throw new SimpleError({
|
|
1015
1030
|
code: 'permission_denied',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Member, MemberResponsibilityRecord, MemberWithRegistrations, User } from "@stamhoofd/models";
|
|
2
2
|
import { SQL } from "@stamhoofd/sql";
|
|
3
|
-
import { Permissions, UserPermissions } from "@stamhoofd/structures";
|
|
3
|
+
import { MemberDetails, Permissions, UserPermissions } from "@stamhoofd/structures";
|
|
4
4
|
|
|
5
5
|
export class MemberUserSyncerStatic {
|
|
6
6
|
/**
|
|
@@ -8,15 +8,8 @@ export class MemberUserSyncerStatic {
|
|
|
8
8
|
* - responsibilities have changed
|
|
9
9
|
* - email addresses have changed
|
|
10
10
|
*/
|
|
11
|
-
async onChangeMember(member: MemberWithRegistrations) {
|
|
12
|
-
const userEmails =
|
|
13
|
-
|
|
14
|
-
if (member.details.email) {
|
|
15
|
-
userEmails.push(member.details.email)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const uncategorizedEmails: string[] = member.details.uncategorizedEmails;
|
|
19
|
-
const parentAndUncategorizedEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(uncategorizedEmails) : []
|
|
11
|
+
async onChangeMember(member: MemberWithRegistrations, unlinkUsers: boolean = false) {
|
|
12
|
+
const {userEmails, parentAndUnverifiedEmails} = this.getMemberAccessEmails(member.details)
|
|
20
13
|
|
|
21
14
|
// Make sure all these users have access to the member
|
|
22
15
|
for (const email of userEmails) {
|
|
@@ -24,19 +17,54 @@ export class MemberUserSyncerStatic {
|
|
|
24
17
|
await this.linkUser(email, member, false)
|
|
25
18
|
}
|
|
26
19
|
|
|
27
|
-
for (const email of
|
|
28
|
-
// Link parents and
|
|
20
|
+
for (const email of parentAndUnverifiedEmails) {
|
|
21
|
+
// Link parents and unverified emails
|
|
29
22
|
await this.linkUser(email, member, true)
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
if (unlinkUsers && !member.details.parentsHaveAccess) {
|
|
26
|
+
// Remove access of users that are not in this list
|
|
27
|
+
// NOTE: we should only do this once a year (preferably on the birthday of the member)
|
|
28
|
+
// 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
|
|
29
|
+
// users can regain access to a member after they have lost control by using the normal verification flow when detecting duplicate members
|
|
30
|
+
|
|
31
|
+
for (const user of member.users) {
|
|
32
|
+
if (!userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
|
|
33
|
+
await this.unlinkUser(user, member)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
// Only auto unlink users that do not have an account
|
|
38
|
+
for (const user of member.users) {
|
|
39
|
+
if (!user.hasAccount() && !userEmails.includes(user.email) && !parentAndUnverifiedEmails.includes(user.email)) {
|
|
40
|
+
await this.unlinkUser(user, member)
|
|
41
|
+
}
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
getMemberAccessEmails(details: MemberDetails) {
|
|
47
|
+
const userEmails = [...details.alternativeEmails]
|
|
48
|
+
|
|
49
|
+
if (details.email) {
|
|
50
|
+
userEmails.push(details.email)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const unverifiedEmails: string[] = details.unverifiedEmails;
|
|
54
|
+
const parentAndUnverifiedEmails = details.parentsHaveAccess ? details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : []
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
userEmails,
|
|
58
|
+
parentAndUnverifiedEmails,
|
|
59
|
+
emails: userEmails.concat(parentAndUnverifiedEmails)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
doesEmailHaveAccess(details: MemberDetails, email: string) {
|
|
64
|
+
const {emails} = this.getMemberAccessEmails(details)
|
|
65
|
+
return emails.includes(email)
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
async onDeleteMember(member: MemberWithRegistrations) {
|
|
41
69
|
for (const u of member.users) {
|
|
42
70
|
console.log("Unlinking user "+u.email+" from deleted member "+member.id)
|