@stamhoofd/backend 2.7.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.7.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": "319a4de9fac39a31110cddbfa9a580d1b9c8f730"
53
+ "gitHead": "1e0e1853f8dea6b47718433b2890907042c17ddc"
54
54
  }
@@ -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
  }
@@ -98,8 +98,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
98
98
  }
99
99
  }
100
100
 
101
+ const deleteRegistrationIds = request.body.cart.deleteRegistrationIds
102
+ const deleteRegistrationModels = (deleteRegistrationIds.length ? (await Registration.getByIDs(...deleteRegistrationIds)) : []).filter(r => r.organizationId === organization.id)
103
+
101
104
  const memberIds = Formatter.uniqueArray(
102
- [...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)]
103
106
  )
104
107
  const members = await Member.getBlobByIds(...memberIds)
105
108
  const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
@@ -174,23 +177,23 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
174
177
 
175
178
  // Validate balance items (can only happen serverside)
176
179
  const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id)
177
- let memberBalanceItems: MemberBalanceItem[] = []
178
- let balanceItems: BalanceItem[] = []
180
+ let memberBalanceItemsStructs: MemberBalanceItem[] = []
181
+ let balanceItemsModels: BalanceItem[] = []
179
182
  if (balanceItemIds.length > 0) {
180
- balanceItems = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
181
- if (balanceItems.length != balanceItemIds.length) {
183
+ balanceItemsModels = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
184
+ if (balanceItemsModels.length != balanceItemIds.length) {
182
185
  throw new SimpleError({
183
186
  code: "invalid_data",
184
187
  message: "Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw."
185
188
  })
186
189
  }
187
- memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
190
+ memberBalanceItemsStructs = await BalanceItem.getMemberStructure(balanceItemsModels)
188
191
  }
189
192
 
190
193
  console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
191
194
 
192
195
  // Validate the cart
193
- checkout.validate({memberBalanceItems})
196
+ checkout.validate({memberBalanceItems: memberBalanceItemsStructs})
194
197
 
195
198
  // Recalculate the price
196
199
  checkout.updatePrices()
@@ -233,34 +236,37 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
233
236
 
234
237
  // Check if this member is already registered in this group?
235
238
  const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle })
236
- let registration: RegistrationWithMemberAndGroup | undefined = undefined;
237
239
 
238
240
  for (const existingRegistration of existingRegistrations) {
239
- registration = existingRegistration
240
- .setRelation(registrationMemberRelation, member as Member)
241
- .setRelation(Registration.group, group)
242
-
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
+ }
243
250
 
244
251
  if (existingRegistration.registeredAt !== null && existingRegistration.deactivatedAt === null) {
245
252
  throw new SimpleError({
246
253
  code: "already_registered",
247
- 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.`
248
255
  })
249
256
  }
250
257
  }
251
258
 
252
- if (!registration) {
253
- registration = new Registration()
254
- .setRelation(registrationMemberRelation, member as Member)
255
- .setRelation(Registration.group, group)
256
- registration.organizationId = organization.id
257
- registration.periodId = group.periodId
258
- }
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
259
264
 
260
265
  registration.memberId = member.id
261
266
  registration.groupId = group.id
262
- registration.cycle = group.cycle
263
- 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
264
270
 
265
271
  payRegistrations.push({
266
272
  registration,
@@ -305,8 +311,59 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
305
311
 
306
312
  console.log('Registering members using whoWillPayNow', whoWillPayNow, checkout.paymentMethod, totalPrice)
307
313
 
308
- const items: BalanceItem[] = []
309
- 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
+ }
310
367
 
311
368
  // Save registrations and add extra data if needed
312
369
  for (const bundle of payRegistrations) {
@@ -366,7 +423,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
366
423
  balanceItem2.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
367
424
  await balanceItem2.save();
368
425
 
369
- // 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
370
427
  } else {
371
428
  balanceItem.memberId = registration.memberId;
372
429
  balanceItem.userId = user.id
@@ -379,7 +436,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
379
436
  balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null
380
437
 
381
438
  await balanceItem.save();
382
- items.push(balanceItem)
439
+ createdBalanceItems.push(balanceItem)
383
440
  }
384
441
 
385
442
  const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
@@ -399,7 +456,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
399
456
  }
400
457
  balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
401
458
  await balanceItem.save();
402
- items.push(balanceItem)
459
+ createdBalanceItems.push(balanceItem)
403
460
  }
404
461
 
405
462
  if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
@@ -423,62 +480,15 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
423
480
  balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
424
481
  await balanceItem.save();
425
482
 
426
- items.push(balanceItem);
483
+ createdBalanceItems.push(balanceItem);
427
484
  }
428
485
 
429
486
  if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
430
- throw new Error('Not possible to pay balance items when whoWillPayNow is nobody')
431
- }
432
-
433
- // Create negative balance items
434
- for (const registrationStruct of checkout.cart.deleteRegistrations) {
435
- if (whoWillPayNow !== 'nobody') {
436
- // this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
437
- throw new SimpleError({
438
- code: "forbidden",
439
- message: "Permission denied: you are not allowed to delete registrations",
440
- human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
441
- statusCode: 403
442
- })
443
- }
444
-
445
- const existingRegistration = await Registration.getByID(registrationStruct.id)
446
- if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
447
- throw new SimpleError({
448
- code: "invalid_data",
449
- message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
450
- })
451
- }
452
-
453
- if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
454
- throw new SimpleError({
455
- code: "forbidden",
456
- message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
457
- statusCode: 403
458
- })
459
- }
460
-
461
- if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
462
- throw new SimpleError({
463
- code: "invalid_data",
464
- message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
465
- })
466
- }
467
-
468
- // We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
469
- // Find all balance items of this registration and set them to zero
470
- await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
471
-
472
- // Clear the registration
473
- await existingRegistration.deactivate()
474
-
475
- const group = groups.find(g => g.id === existingRegistration.groupId)
476
- if (!group) {
477
- const g = await Group.getByID(existingRegistration.groupId)
478
- if (g) {
479
- groups.push(g)
480
- }
481
- }
487
+ throw new SimpleError({
488
+ code: 'invalid_data',
489
+ message: 'Not possible to pay balance items as the organization',
490
+ statusCode: 400
491
+ })
482
492
  }
483
493
 
484
494
  let paymentUrl: string | null = null
@@ -487,19 +497,21 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
487
497
  if (whoWillPayNow !== 'nobody') {
488
498
  const mappedBalanceItems = new Map<BalanceItem, number>()
489
499
 
490
- for (const item of items) {
500
+ for (const item of createdBalanceItems) {
491
501
  mappedBalanceItems.set(item, item.price)
492
502
  }
493
503
 
494
504
  for (const item of checkout.cart.balanceItems) {
495
- const balanceItem = balanceItems.find(i => i.id === item.item.id)
505
+ const balanceItem = balanceItemsModels.find(i => i.id === item.item.id)
496
506
  if (!balanceItem) {
497
507
  throw new Error('Balance item not found')
498
508
  }
499
509
  mappedBalanceItems.set(balanceItem, item.price)
500
- items.push(balanceItem)
510
+ createdBalanceItems.push(balanceItem)
501
511
  }
502
512
 
513
+ // Make sure every price is accurate before creating a payment
514
+ await BalanceItem.updateOutstanding(createdBalanceItems, organization.id)
503
515
  const response = await this.createPayment({
504
516
  balanceItems: mappedBalanceItems,
505
517
  organization,
@@ -512,9 +524,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
512
524
  paymentUrl = response.paymentUrl
513
525
  payment = response.payment
514
526
  }
527
+ } else {
528
+ await BalanceItem.updateOutstanding(createdBalanceItems, organization.id)
515
529
  }
516
530
 
517
- await BalanceItem.updateOutstanding(items, organization.id)
518
531
 
519
532
  // Update occupancy
520
533
  for (const group of groups) {
@@ -524,9 +537,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
524
537
  }
525
538
  }
526
539
 
540
+ const updatedMembers = await Member.getBlobByIds(...memberIds)
541
+
527
542
  return new Response(RegisterResponse.create({
528
543
  payment: payment ? PaymentStruct.create(payment) : null,
529
- members: await AuthenticatedStructures.membersBlob(members),
544
+ members: await AuthenticatedStructures.membersBlob(updatedMembers),
530
545
  registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
531
546
  paymentUrl
532
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
  }
@@ -936,6 +923,7 @@ export class AdminPermissionChecker {
936
923
  // Has financial read access?
937
924
  if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
938
925
  cloned.details.requiresFinancialSupport = null
926
+ cloned.details.uitpasNumber = null
939
927
  cloned.outstandingBalance = 0
940
928
 
941
929
  for (const registration of cloned.registrations) {
@@ -1029,6 +1017,14 @@ export class AdminPermissionChecker {
1029
1017
  })
1030
1018
  }
1031
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
+
1032
1028
  if (data.outstandingBalance) {
1033
1029
  throw new SimpleError({
1034
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 unverifiedEmails: string[] = member.details.unverifiedEmails;
19
- const parentAndUnverifiedEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : []
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) {
@@ -29,14 +22,49 @@ export class MemberUserSyncerStatic {
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) && !parentAndUnverifiedEmails.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)