@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.6.0",
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": "7a3f9f6c08058dc8b671befbfad73184afdc6d7c"
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.groupIds'),
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, SQLJSONValue, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
7
- import { CountFilteredRequest, EmailRecipientFilterType, GroupStatus, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter, mergeFilters } from '@stamhoofd/structures';
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
- scopeFilter = {
401
- registrations: {
402
- $elemMatch: {
403
- organizationId: organization.id,
404
- registeredAt: {
405
- $neq: null
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
- if (struct.registrations.length === 0) {
122
- // We risk creating a new member without being able to access it manually afterwards
123
-
124
- if ((organization && !await Context.auth.hasFullAccess(organization.id)) || (!organization && !Context.auth.hasPlatformFullAccess())) {
125
- throw new SimpleError({
126
- code: "missing_group",
127
- message: "Missing group",
128
- human: "Je moet hoofdbeheerder zijn om een lid toe te voegen zonder inschrijving in het systeem",
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
- let group = groups[0];
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 addRegistration(member: Member & Record<"registrations", (Registration & {group: Group})[]> & Record<"users", User[]>, registrationStruct: RegistrationStruct, group: Group) {
749
- // Check if this member has this registration already.
750
- // Note: we cannot use the relation here, because invalid ones or reserved ones are not loaded in there
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), ...request.body.cart.deleteRegistrations.map(i => i.member.id)]
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 memberBalanceItems: MemberBalanceItem[] = []
179
- let balanceItems: BalanceItem[] = []
180
+ let memberBalanceItemsStructs: MemberBalanceItem[] = []
181
+ let balanceItemsModels: BalanceItem[] = []
180
182
  if (balanceItemIds.length > 0) {
181
- balanceItems = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
182
- if (balanceItems.length != balanceItemIds.length) {
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
- memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
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
- registration = existingRegistration
241
- .setRelation(registrationMemberRelation, member as Member)
242
- .setRelation(Registration.group, group)
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: "Dit lid is reeds ingeschreven. Herlaad de pagina en probeer opnieuw."
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
- if (!registration) {
254
- registration = new Registration()
255
- .setRelation(registrationMemberRelation, member as Member)
256
- .setRelation(Registration.group, group)
257
- registration.organizationId = organization.id
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.cycle = group.cycle
264
- registration.price = item.calculatedPrice
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 items: BalanceItem[] = []
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 items array because we don't want to add this to the payment if we create a payment
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
- items.push(balanceItem)
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
- items.push(balanceItem)
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
- items.push(balanceItem)
483
+ createdBalanceItems.push(balanceItem);
428
484
  }
429
485
 
430
486
  if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
431
- throw new Error('Not possible to pay balance items when whoWillPayNow is nobody')
432
- }
433
-
434
- // Create negative balance items
435
- for (const registrationStruct of checkout.cart.deleteRegistrations) {
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 items) {
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 = balanceItems.find(i => i.id === item.item.id)
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
- items.push(balanceItem)
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(members),
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 (const [id] of organization.meta.recordsConfiguration.inheritedRecordCategories) {
792
- if (recordCategories.find(c => c.id === id)) {
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
- const category = this.platform.config.recordsConfiguration.recordCategories.find(c => c.id === id)
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
- if (data.details.recordAnswers) {
959
- if (!(data.details.recordAnswers instanceof PatchMap)) {
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
- for (const [key, value] of data.details.recordAnswers.entries()) {
970
- let name: string | undefined = undefined
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
- const id = value.settings.id
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 (id !== key) {
991
+ if (!isUserManager && !records.has(key)) {
983
992
  throw new SimpleError({
984
- code: 'invalid_request',
985
- message: 'Record answer key does not match record id',
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
- if (!isUserManager && !records.has(key)) {
994
- throw new SimpleError({
995
- code: 'permission_denied',
996
- message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
997
- statusCode: 400
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',
@@ -228,7 +228,6 @@ export class AuthenticatedStructures {
228
228
  }
229
229
  }
230
230
 
231
-
232
231
  const blob = member.getStructureWithRegistrations()
233
232
  memberBlobs.push(
234
233
  await Context.auth.filterMemberData(member, blob)
@@ -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 = [...member.details.alternativeEmails]
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 parentAndUncategorizedEmails) {
28
- // Link parents and uncategorized emails
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
- // Remove access of users that are not in this list
33
- for (const user of member.users) {
34
- if (!userEmails.includes(user.email) && !parentAndUncategorizedEmails.includes(user.email)) {
35
- await this.unlinkUser(user, member)
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)