@stamhoofd/backend 2.78.3 → 2.79.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 (33) hide show
  1. package/.env.ci.json +19 -8
  2. package/index.ts +8 -1
  3. package/package.json +13 -12
  4. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +1 -1
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +1 -1
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
  7. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +1 -1
  9. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +2686 -0
  10. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +206 -20
  11. package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +9 -21
  12. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +1 -1
  13. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
  14. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +15 -14
  15. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +3 -23
  16. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -0
  17. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -3
  18. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
  19. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +164 -0
  20. package/src/helpers/AdminPermissionChecker.ts +41 -2
  21. package/src/helpers/AuthenticatedStructures.ts +77 -20
  22. package/src/helpers/ForwardHandler.test.ts +16 -5
  23. package/src/helpers/ForwardHandler.ts +21 -9
  24. package/src/helpers/MemberUserSyncer.test.ts +822 -0
  25. package/src/helpers/MemberUserSyncer.ts +137 -108
  26. package/src/helpers/TagHelper.ts +3 -3
  27. package/src/seeds/1734596144-fill-previous-period-id.ts +1 -1
  28. package/src/seeds/1741008870-fix-auditlog-description.ts +50 -0
  29. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +88 -0
  30. package/src/services/FileSignService.ts +1 -1
  31. package/src/services/PlatformMembershipService.ts +7 -2
  32. package/src/services/SSOService.ts +1 -1
  33. package/tests/e2e/register.test.ts +2 -2
@@ -1,9 +1,9 @@
1
1
  import { OneToManyRelation } from '@simonbackx/simple-database';
2
- import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
+ import { AutoEncoderPatchType, ConvertArrayToPatchableArray, Decoder, isEmptyPatch, isPatchableArray, PatchableArray, 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
5
  import { AuditLog, BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
6
- import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, GroupType, MembersBlob, MemberWithRegistrationsBlob, PermissionLevel } from '@stamhoofd/structures';
6
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLogSource, AuditLogType, EmergencyContact, GroupType, MemberDetails, MemberResponsibility, MembersBlob, MemberWithRegistrationsBlob, Parent, PermissionLevel } from '@stamhoofd/structures';
7
7
  import { Formatter } from '@stamhoofd/utility';
8
8
 
9
9
  import { Email } from '@stamhoofd/email';
@@ -16,8 +16,7 @@ import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
16
16
  import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
17
17
  import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
18
18
  import { RegistrationService } from '../../../services/RegistrationService';
19
- import { shouldCheckIfMemberIsDuplicateForPatch, shouldCheckIfMemberIsDuplicateForPut } from './shouldCheckIfMemberIsDuplicate';
20
- import { AuditLogService } from '../../../services/AuditLogService';
19
+ import { shouldCheckIfMemberIsDuplicateForPatch } from './shouldCheckIfMemberIsDuplicate';
21
20
 
22
21
  type Params = Record<string, never>;
23
22
  type Query = undefined;
@@ -109,12 +108,10 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
109
108
  struct.details.cleanData();
110
109
  member.details = struct.details;
111
110
 
112
- if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
113
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
114
- if (duplicate) {
111
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
112
+ if (duplicate) {
115
113
  // Merge data
116
- member = duplicate;
117
- }
114
+ member = duplicate;
118
115
  }
119
116
 
120
117
  // We risk creating a new member without being able to access it manually afterwards
@@ -159,12 +156,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
159
156
  const securityCode = patch.details?.securityCode; // will get cleared after the filter
160
157
 
161
158
  if (!member) {
162
- throw Context.auth.notFoundOrNoAccess('Je hebt geen toegang tot dit lid of het bestaat niet');
159
+ throw Context.auth.memberNotFoundOrNoAccess();
163
160
  }
164
161
 
165
- if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
166
- // Still allowed if you provide a security code
167
- await PatchOrganizationMembersEndpoint.checkSecurityCode(member, securityCode);
162
+ if (!(await Context.auth.canAccessMember(member, PermissionLevel.Write))) {
163
+ await PatchOrganizationMembersEndpoint.checkSecurityCode(member, securityCode, 'patch');
168
164
  }
169
165
 
170
166
  patch = await Context.auth.filterMemberPatch(member, patch);
@@ -194,7 +190,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
194
190
  }
195
191
 
196
192
  if (shouldCheckDuplicate) {
197
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
193
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode, 'patch');
198
194
 
199
195
  if (duplicate) {
200
196
  // Remove the member from the list
@@ -211,6 +207,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
211
207
 
212
208
  await member.save();
213
209
 
210
+ // If parents changed or emergeny contacts: fetch family and merge data
211
+ if (patch.details && (!isEmptyPatch(patch.details?.parents) || !isEmptyPatch(patch.details?.emergencyContacts))) {
212
+ await PatchOrganizationMembersEndpoint.mergeDuplicateRelations(member, patch.details);
213
+ }
214
+
214
215
  // Update documents
215
216
  await Document.updateForMember(member.id);
216
217
 
@@ -261,6 +262,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
261
262
  responsibilityRecord.startDate = patchResponsibility.startDate;
262
263
  }
263
264
 
265
+ // Check maximum
266
+ if (responsibility) {
267
+ await this.checkResponsbilityLimits(responsibilityRecord, responsibility);
268
+ }
269
+
264
270
  await responsibilityRecord.save();
265
271
  shouldUpdateSetupSteps = true;
266
272
  }
@@ -393,6 +399,9 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
393
399
 
394
400
  model.startDate = put.startDate;
395
401
 
402
+ // Check maximum
403
+ await this.checkResponsbilityLimits(model, responsibility);
404
+
396
405
  await model.save();
397
406
  shouldUpdateSetupSteps = true;
398
407
  }
@@ -735,6 +744,129 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
735
744
  }
736
745
  }
737
746
 
747
+ static async mergeDuplicateRelations(member: MemberWithRegistrations, patch: AutoEncoderPatchType<MemberDetails> | MemberDetails) {
748
+ const _familyMembers = await Member.getFamilyWithRegistrations(member.id);
749
+ const familyMembers: typeof _familyMembers = [];
750
+ // Only modify members if we have write access to them (this avoids issues with overriding data)
751
+ for (const member of _familyMembers) {
752
+ if (await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
753
+ familyMembers.push(member);
754
+ }
755
+ }
756
+
757
+ // Replace member with member
758
+ const memberIndex = familyMembers.findIndex(m => m.id === member.id);
759
+ if (memberIndex !== -1 && familyMembers.length >= 2) {
760
+ familyMembers[memberIndex] = member;
761
+ const parentMergeMap = MemberDetails.mergeParents(
762
+ familyMembers.map(m => m.details),
763
+ true, // Allow deletes
764
+ );
765
+ const contactsMergeMap = MemberDetails.mergeEmergencyContacts(
766
+ familyMembers.map(m => m.details),
767
+ true, // Allow deletes
768
+ );
769
+
770
+ // If there were patches or puts of parents or emergency contacts
771
+ // Make sure that those patches have been applied even after a potential merge
772
+ // E.g. you only changed the email, but there was a more recent parent object in a different member
773
+ // -> avoid losing the email change
774
+ const parentPatches = isPatchableArray(patch.parents) ? patch.parents.getPatches() : [];
775
+
776
+ // Add puts
777
+ const parentPuts = isPatchableArray(patch.parents) ? patch.parents.getPuts().map(p => p.put) : patch.parents;
778
+ for (const put of parentPuts) {
779
+ if (!parentMergeMap.get(put.id)) {
780
+ // This one has not been merged
781
+ continue;
782
+ }
783
+
784
+ const alternativeEmailsArr = new PatchableArray() as PatchableArray<string, string, string>;
785
+ for (const alternativeEmail of put.alternativeEmails) {
786
+ alternativeEmailsArr.addPut(alternativeEmail);
787
+ }
788
+
789
+ const p = Parent.patch({
790
+ ...put,
791
+ alternativeEmails: alternativeEmailsArr,
792
+ createdAt: undefined, // Not allowed to change (should have already happened + the merge method will already chose the right value)
793
+ updatedAt: undefined, // Not allowed to change (should have already happened + the merge method will already chose the right value)
794
+ });
795
+
796
+ // Delete null values or empty strings
797
+ for (const key in p) {
798
+ if (p[key] === null || p[key] === '') {
799
+ delete p[key];
800
+ }
801
+ }
802
+
803
+ parentPatches.push(p);
804
+ }
805
+
806
+ // Same for emergency contacts
807
+ const contactsPatches = isPatchableArray(patch.emergencyContacts) ? patch.emergencyContacts.getPatches() : [];
808
+
809
+ // Add puts
810
+ const contactsPuts = isPatchableArray(patch.emergencyContacts) ? patch.emergencyContacts.getPuts().map(p => p.put) : patch.emergencyContacts;
811
+ for (const put of contactsPuts) {
812
+ if (!contactsMergeMap.get(put.id)) {
813
+ // This one has not been merged
814
+ continue;
815
+ }
816
+ const p = EmergencyContact.patch({
817
+ ...put,
818
+ createdAt: undefined, // Not allowed to change (should have already happened + the merge method will already chose the right value)
819
+ updatedAt: undefined, // Not allowed to change (should have already happened + the merge method will already chose the right value)
820
+ });
821
+
822
+ // Delete null values or empty strings
823
+ for (const key in p) {
824
+ if (p[key] === null || p[key] === '') {
825
+ delete p[key];
826
+ }
827
+ }
828
+
829
+ contactsPatches.push(p);
830
+ }
831
+
832
+ // Apply patches
833
+ for (const parentPatch of parentPatches) {
834
+ for (const m of familyMembers) {
835
+ const arr = new PatchableArray() as PatchableArrayAutoEncoder<Parent>;
836
+ parentPatch.id = parentMergeMap.get(parentPatch.id) ?? parentPatch.id;
837
+ arr.addPatch(parentPatch);
838
+ m.details = m.details.patch({
839
+ parents: arr,
840
+ });
841
+ }
842
+ }
843
+
844
+ // Apply patches
845
+ for (const contactPatch of contactsPatches) {
846
+ for (const m of familyMembers) {
847
+ const arr = new PatchableArray() as PatchableArrayAutoEncoder<EmergencyContact>;
848
+ contactPatch.id = contactsMergeMap.get(contactPatch.id) ?? contactPatch.id;
849
+ arr.addPatch(contactPatch);
850
+ m.details = m.details.patch({
851
+ emergencyContacts: arr,
852
+ });
853
+ }
854
+ }
855
+
856
+ for (const m of familyMembers) {
857
+ m.details.cleanData();
858
+
859
+ if (await m.save() && m.id !== member.id) {
860
+ // Auto link users based on data
861
+ await MemberUserSyncer.onChangeMember(m);
862
+
863
+ // Update documents
864
+ await Document.updateForMember(m.id);
865
+ }
866
+ }
867
+ }
868
+ }
869
+
738
870
  static async findExistingMember(member: Member) {
739
871
  if (!member.details.birthDay) {
740
872
  return;
@@ -759,8 +891,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
759
891
  }
760
892
  }
761
893
 
762
- static async checkSecurityCode(member: MemberWithRegistrations, securityCode: string | null | undefined) {
763
- if (await member.isSafeToMergeDuplicateWithoutSecurityCode() || await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
894
+ static async checkSecurityCode(member: MemberWithRegistrations, securityCode: string | null | undefined, type: 'put' | 'patch') {
895
+ if ((type === 'put' && await member.isSafeToMergeDuplicateWithoutSecurityCode()) || await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
764
896
  console.log('checkSecurityCode: without security code: allowed for ' + member.id);
765
897
  }
766
898
  else if (securityCode) {
@@ -802,8 +934,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
802
934
  const log = new AuditLog();
803
935
 
804
936
  // a member has multiple organizations, so this is difficult to determine - for now it is only visible in the admin panel
805
- log.organizationId = member.organizationId;
806
-
937
+ log.organizationId = member.organizationId;
938
+
807
939
  log.type = AuditLogType.MemberSecurityCodeUsed;
808
940
  log.source = AuditLogSource.Anonymous;
809
941
 
@@ -823,6 +955,9 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
823
955
  await log.save();
824
956
  }
825
957
  else {
958
+ if (type === 'patch') {
959
+ throw Context.auth.memberNotFoundOrNoAccess();
960
+ }
826
961
  throw new SimpleError({
827
962
  code: 'known_member_missing_rights',
828
963
  message: 'Creating known member without sufficient access rights',
@@ -832,11 +967,25 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
832
967
  }
833
968
  }
834
969
 
835
- static async checkDuplicate(member: Member, securityCode: string | null | undefined) {
970
+ static shouldCheckIfMemberIsDuplicate(put: Member): boolean {
971
+ if (put.details.firstName.length <= 3 && put.details.lastName.length <= 3) {
972
+ return false;
973
+ }
974
+
975
+ const age = put.details.age;
976
+ // do not check if member is duplicate for historical members
977
+ return age !== null && age < 81;
978
+ }
979
+
980
+ static async checkDuplicate(member: Member, securityCode: string | null | undefined, type: 'put' | 'patch') {
981
+ if (!this.shouldCheckIfMemberIsDuplicate(member)) {
982
+ return;
983
+ }
984
+
836
985
  // Check for duplicates and prevent creating a duplicate member by a user
837
986
  const duplicate = await this.findExistingMember(member);
838
987
  if (duplicate) {
839
- await this.checkSecurityCode(duplicate, securityCode);
988
+ await this.checkSecurityCode(duplicate, securityCode, type);
840
989
 
841
990
  // Merge data
842
991
  // NOTE: We use mergeTwoMembers instead of mergeMultipleMembers, because we should never safe 'member' , because that one does not exist in the database
@@ -850,4 +999,41 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
850
999
  organization,
851
1000
  }).createMultiple(count);
852
1001
  }
1002
+
1003
+ async checkResponsbilityLimits(model: MemberResponsibilityRecord, responsibility: MemberResponsibility) {
1004
+ if (responsibility.maximumMembers !== null) {
1005
+ if (!model.getBaseStructure().isActive) {
1006
+ return;
1007
+ }
1008
+
1009
+ const query = MemberResponsibilityRecord.select()
1010
+ .where('responsibilityId', responsibility.id)
1011
+ .andWhere('organizationId', model.organizationId)
1012
+ .andWhere('groupId', model.groupId)
1013
+ .andWhere(MemberResponsibilityRecord.whereActive);
1014
+
1015
+ if (model.existsInDatabase) {
1016
+ query.andWhere('id', '!=', model.id);
1017
+ }
1018
+
1019
+ const count = (await query.count()) + 1;
1020
+
1021
+ // Because it should be possible to move around responsibilities, we allow 1 extra
1022
+ const actualLimit = responsibility.maximumMembers <= 1 ? 2 : responsibility.maximumMembers;
1023
+
1024
+ if (count > actualLimit) {
1025
+ throw new SimpleError({
1026
+ code: 'invalid_field',
1027
+ message: 'Maximum members reached',
1028
+ human: responsibility.maximumMembers === 1
1029
+ ? (model.groupId
1030
+ ? $t('Je kan maar één lid hebben met de functie {responsibility} in deze leeftijdsgroep', { responsibility: responsibility.name })
1031
+ : $t('Je kan maar één lid hebben met de functie {responsibility}', { responsibility: responsibility.name }))
1032
+ : (model.groupId
1033
+ ? $t('Je kan maximum {count} leden hebben met de functie {responsibility} in deze leeftijdsgroep', { count: responsibility.maximumMembers.toFixed(), responsibility: responsibility.name })
1034
+ : $t('Je kan maximum {count} leden hebben met de functie {responsibility}', { count: responsibility.maximumMembers.toFixed(), responsibility: responsibility.name })),
1035
+ });
1036
+ }
1037
+ }
1038
+ }
853
1039
  }
@@ -1,34 +1,22 @@
1
1
  import { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
2
2
  import { MemberDetails, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
3
3
 
4
+ /**
5
+ * Returns true when either the firstname, lastname or birthday has changed
6
+ */
4
7
  export function shouldCheckIfMemberIsDuplicateForPatch(patch: { details: MemberDetails | AutoEncoderPatchType<MemberDetails> | undefined }, originalDetails: MemberDetails): boolean {
5
8
  if (patch.details === undefined) {
6
9
  return false;
7
10
  }
8
11
 
9
- return (
10
- // has long first name
11
- ((patch.details.firstName !== undefined && patch.details.firstName.length > 3) || (patch.details.firstName === undefined && originalDetails.firstName.length > 3))
12
- // or has long last name
13
- || ((patch.details.lastName !== undefined && patch.details.lastName.length > 3) || (patch.details.lastName === undefined && originalDetails.lastName.length > 3))
14
- )
15
- // has name change or birthday change
16
- && (
17
- // has first name change
12
+ // name or birthday has changed
13
+ if (
18
14
  (patch.details.firstName !== undefined && patch.details.firstName !== originalDetails.firstName)
19
- // has last name change
20
15
  || (patch.details.lastName !== undefined && patch.details.lastName !== originalDetails.lastName)
21
- // has birth day change
22
- || (patch.details.birthDay !== undefined && patch.details.birthDay?.getTime() !== originalDetails.birthDay?.getTime())
23
- );
24
- }
25
-
26
- export function shouldCheckIfMemberIsDuplicateForPut(put: MemberWithRegistrationsBlob): boolean {
27
- if (put.details.firstName.length <= 3 && put.details.lastName.length <= 3) {
28
- return false;
16
+ || (patch.details.birthDay !== undefined && patch.details.birthDay !== originalDetails.birthDay)
17
+ ) {
18
+ return true;
29
19
  }
30
20
 
31
- const age = put.details.age;
32
- // do not check if member is duplicate for historical members
33
- return age !== null && age < 81;
21
+ return false;
34
22
  }
@@ -49,7 +49,7 @@ export class PatchPlatformEndpoint extends Endpoint<
49
49
  throw Context.auth.error();
50
50
  }
51
51
 
52
- const platform = await Platform.getShared();
52
+ const platform = await Platform.getForEditing();
53
53
  let shouldUpdateUserPermissions = false;
54
54
 
55
55
  if (request.body.privateConfig) {
@@ -295,7 +295,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
295
295
  ],
296
296
  });
297
297
 
298
- const platform = await Platform.getShared();
298
+ const platform = await Platform.getForEditing();
299
299
  platform.config.recordsConfiguration.recordCategories.push(recordCategory);
300
300
  await platform.save();
301
301
 
@@ -355,7 +355,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
355
355
  ],
356
356
  });
357
357
 
358
- const platform = await Platform.getShared();
358
+ const platform = await Platform.getForEditing();
359
359
  platform.config.recordsConfiguration.recordCategories.push(recordCategory);
360
360
  await platform.save();
361
361
 
@@ -1,4 +1,4 @@
1
- import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoderPatchType, Decoder, isEmptyPatch, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Document, Member, RateLimiter } from '@stamhoofd/models';
@@ -8,7 +8,7 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
8
8
  import { Context } from '../../../helpers/Context';
9
9
  import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
10
10
  import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
11
- import { shouldCheckIfMemberIsDuplicateForPatch, shouldCheckIfMemberIsDuplicateForPut } from '../members/shouldCheckIfMemberIsDuplicate';
11
+ import { shouldCheckIfMemberIsDuplicateForPatch } from '../members/shouldCheckIfMemberIsDuplicate';
12
12
  type Params = Record<string, never>;
13
13
  type Query = undefined;
14
14
  type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
@@ -61,12 +61,10 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
61
61
 
62
62
  this.throwIfInvalidDetails(member.details);
63
63
 
64
- if (shouldCheckIfMemberIsDuplicateForPut(struct)) {
65
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode);
66
- if (duplicate) {
67
- addedMembers.push(duplicate);
68
- continue;
69
- }
64
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
65
+ if (duplicate) {
66
+ addedMembers.push(duplicate);
67
+ continue;
70
68
  }
71
69
 
72
70
  await member.save();
@@ -79,12 +77,9 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
79
77
  for (let struct of request.body.getPatches()) {
80
78
  const member = members.find(m => m.id === struct.id);
81
79
  if (!member) {
82
- throw new SimpleError({
83
- code: 'invalid_member',
84
- message: "This member does not exist or you don't have permissions to modify this member",
85
- human: 'Je probeert een lid aan te passen die niet (meer) bestaat. Er ging ergens iets mis.',
86
- });
80
+ throw Context.auth.memberNotFoundOrNoAccess();
87
81
  }
82
+
88
83
  const securityCode = struct.details?.securityCode; // will get cleared after the filter
89
84
  struct = await Context.auth.filterMemberPatch(member, struct);
90
85
 
@@ -117,7 +112,7 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
117
112
  }
118
113
 
119
114
  if (shouldCheckDuplicate) {
120
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode);
115
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode, 'patch');
121
116
  if (duplicate) {
122
117
  // Remove the member from the list
123
118
  members.splice(members.findIndex(m => m.id === member.id), 1);
@@ -129,6 +124,12 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
129
124
  }
130
125
 
131
126
  await member.save();
127
+
128
+ // If parents changed or emergeny contacts: fetch family and merge data
129
+ if (struct.details && (!isEmptyPatch(struct.details?.parents) || !isEmptyPatch(struct.details?.emergencyContacts))) {
130
+ await PatchOrganizationMembersEndpoint.mergeDuplicateRelations(member, struct.details);
131
+ }
132
+
132
133
  await MemberUserSyncer.onChangeMember(member);
133
134
 
134
135
  // Update documents
@@ -33,10 +33,8 @@ describe('Endpoint.RegisterMembers', () => {
33
33
  period = await new RegistrationPeriodFactory({
34
34
  startDate: new Date(2023, 0, 1),
35
35
  endDate: new Date(2023, 11, 31),
36
+ previousPeriodId: previousPeriod.id,
36
37
  }).create();
37
-
38
- period.previousPeriodId = previousPeriod.id;
39
- await period.save();
40
38
  });
41
39
 
42
40
  afterEach(() => {
@@ -2199,34 +2197,16 @@ describe('Endpoint.RegisterMembers', () => {
2199
2197
  groupPrice: groupPrice1,
2200
2198
  }).create();
2201
2199
 
2202
- const group2 = await new GroupFactory({
2203
- organization,
2204
- price: 30,
2205
- stock: 5,
2206
- }).create();
2207
-
2208
- const groupPrice = group2.settings.prices[0];
2209
-
2210
2200
  const body = IDRegisterCheckout.create({
2211
2201
  cart: IDRegisterCart.create({
2212
- items: [
2213
- IDRegisterItem.create({
2214
- id: uuidv4(),
2215
- replaceRegistrationIds: [],
2216
- options: [],
2217
- groupPrice,
2218
- organizationId: organization.id,
2219
- groupId: group2.id,
2220
- memberId: member.id,
2221
- }),
2222
- ],
2202
+ items: [],
2223
2203
  balanceItems: [],
2224
2204
  deleteRegistrationIds: [registration.id],
2225
2205
  }),
2226
2206
  administrationFee: 0,
2227
2207
  freeContribution: 0,
2228
2208
  paymentMethod: PaymentMethod.PointOfSale,
2229
- totalPrice: 30,
2209
+ totalPrice: 0,
2230
2210
  customer: null,
2231
2211
  asOrganizationId: organization.id,
2232
2212
  });
@@ -241,6 +241,18 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
241
241
  });
242
242
  }
243
243
 
244
+ if (request.body.asOrganizationId) {
245
+ // Do you have write access to this group?
246
+ if (!await Context.auth.canRegisterMembersInGroup(group, request.body.asOrganizationId)) {
247
+ throw new SimpleError({
248
+ code: 'forbidden',
249
+ message: 'No permission to register in this group',
250
+ human: $t('Je hebt geen toegangsrechten om een lid in te schrijven voor {group}', { group: group.settings.name }),
251
+ statusCode: 403,
252
+ });
253
+ }
254
+ }
255
+
244
256
  // Check if this member is already registered in this group?
245
257
  const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle, periodId: group.periodId, registeredAt: { sign: '!=', value: null } });
246
258
 
@@ -285,7 +285,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
285
285
  tags: request.body.meta.tags as any,
286
286
  });
287
287
 
288
- const platform: Platform = await Platform.getShared();
288
+ const platform = await Platform.getShared();
289
289
  const patchedMeta: OrganizationMetaData = organization.meta.patch(cleanedPatch);
290
290
  for (const tag of patchedMeta.tags) {
291
291
  if (!platform.config.tags.find(t => t.id === tag)) {
@@ -293,8 +293,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
293
293
  }
294
294
  }
295
295
 
296
- const platformConfig: PlatformConfig = platform.config;
297
- organization.meta.tags = TagHelper.getAllTagsFromHierarchy(patchedMeta.tags, platformConfig.tags);
296
+ organization.meta.tags = TagHelper.getAllTagsFromHierarchy(patchedMeta.tags, platform.config.tags);
298
297
 
299
298
  updateTags = true;
300
299
  }
@@ -250,7 +250,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
250
250
  if (!member || !(await Context.auth.canLinkBalanceItemToMember(member))) {
251
251
  throw new SimpleError({
252
252
  code: 'permission_denied',
253
- message: 'No permission to link balanace items to this member',
253
+ message: 'No permission to link balance items to this member',
254
254
  human: 'Je hebt geen toegang om aanrekeningen te maken verbonden met dit lid',
255
255
  field: 'memberId',
256
256
  });
@@ -264,7 +264,7 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
264
264
  if (!user || !await Context.auth.canLinkBalanceItemToUser(balanceItem, user)) {
265
265
  throw new SimpleError({
266
266
  code: 'permission_denied',
267
- message: 'No permission to link balanace items to this user',
267
+ message: 'No permission to link balance items to this user',
268
268
  human: 'Je hebt geen toegang om aanrekeningen te maken verbonden met deze gebruiker',
269
269
  field: 'userId',
270
270
  });