@stamhoofd/backend 2.7.0 → 2.9.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.
Files changed (32) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +3 -3
  3. package/src/crons.ts +3 -3
  4. package/src/decoders/StringArrayDecoder.ts +24 -0
  5. package/src/decoders/StringNullableDecoder.ts +18 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +14 -0
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  9. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  10. package/src/endpoints/global/members/GetMembersEndpoint.ts +0 -31
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +34 -367
  12. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  13. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -11
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +205 -110
  15. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -3
  16. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  17. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  18. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -2
  19. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -40
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  24. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  25. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  26. package/src/helpers/AdminPermissionChecker.ts +35 -24
  27. package/src/helpers/AuthenticatedStructures.ts +16 -7
  28. package/src/helpers/Context.ts +21 -0
  29. package/src/helpers/EmailResumer.ts +22 -2
  30. package/src/helpers/MemberUserSyncer.ts +42 -14
  31. package/src/seeds/1722344160-update-membership.ts +19 -22
  32. package/src/seeds/1722344161-sync-member-users.ts +60 -0
@@ -2,8 +2,8 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
2
2
  import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
4
  import { SimpleError } from "@simonbackx/simple-errors";
5
- import { BalanceItem, BalanceItemPayment, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Payment, Platform, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
- import { BalanceItemStatus, MemberWithRegistrationsBlob, MembersBlob, PaymentMethod, PaymentStatus, PermissionLevel, Registration as RegistrationStruct, User as UserStruct } from "@stamhoofd/structures";
5
+ import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, Platform, Registration, User } from '@stamhoofd/models';
6
+ import { MemberWithRegistrationsBlob, MembersBlob, PermissionLevel } from "@stamhoofd/structures";
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -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
  }
@@ -1,13 +1,13 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
2
  import { BalanceItem, Member } from "@stamhoofd/models";
3
- import { MemberBalanceItem } from "@stamhoofd/structures";
3
+ import { BalanceItemWithPayments } from "@stamhoofd/structures";
4
4
 
5
5
  import { Context } from "../../../helpers/Context";
6
6
 
7
7
  type Params = Record<string, never>;
8
8
  type Query = undefined
9
9
  type Body = undefined
10
- type ResponseBody = MemberBalanceItem[]
10
+ type ResponseBody = BalanceItemWithPayments[]
11
11
 
12
12
  export class GetUserBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
13
13
  protected doesMatch(request: Request): [true, Params] | [false] {
@@ -33,7 +33,7 @@ export class GetUserBalanceEndpoint extends Endpoint<Params, Query, Body, Respon
33
33
  const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization?.id ?? null, [user.id], members.map(m => m.id))
34
34
 
35
35
  return new Response(
36
- await BalanceItem.getMemberStructure(balanceItems)
36
+ await BalanceItem.getStructureWithPayments(balanceItems)
37
37
  );
38
38
  }
39
39
  }
@@ -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
  }