@stamhoofd/backend 2.78.4 → 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 (29) hide show
  1. package/index.ts +1 -1
  2. package/package.json +13 -12
  3. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +1 -1
  4. package/src/endpoints/auth/CreateAdminEndpoint.ts +1 -1
  5. package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
  6. package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
  7. package/src/endpoints/auth/PatchUserEndpoint.ts +1 -1
  8. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +1964 -4
  9. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +175 -2
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +1 -1
  11. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
  12. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +7 -1
  13. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +3 -23
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -0
  15. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -3
  16. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +164 -0
  17. package/src/helpers/AdminPermissionChecker.ts +22 -1
  18. package/src/helpers/AuthenticatedStructures.ts +77 -20
  19. package/src/helpers/ForwardHandler.test.ts +16 -5
  20. package/src/helpers/ForwardHandler.ts +21 -9
  21. package/src/helpers/MemberUserSyncer.test.ts +822 -0
  22. package/src/helpers/MemberUserSyncer.ts +137 -108
  23. package/src/helpers/TagHelper.ts +3 -3
  24. package/src/seeds/1734596144-fill-previous-period-id.ts +1 -1
  25. package/src/seeds/1741008870-fix-auditlog-description.ts +50 -0
  26. package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +88 -0
  27. package/src/services/PlatformMembershipService.ts +7 -2
  28. package/src/services/SSOService.ts +1 -1
  29. 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';
@@ -207,6 +207,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
207
207
 
208
208
  await member.save();
209
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
+
210
215
  // Update documents
211
216
  await Document.updateForMember(member.id);
212
217
 
@@ -257,6 +262,11 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
257
262
  responsibilityRecord.startDate = patchResponsibility.startDate;
258
263
  }
259
264
 
265
+ // Check maximum
266
+ if (responsibility) {
267
+ await this.checkResponsbilityLimits(responsibilityRecord, responsibility);
268
+ }
269
+
260
270
  await responsibilityRecord.save();
261
271
  shouldUpdateSetupSteps = true;
262
272
  }
@@ -389,6 +399,9 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
389
399
 
390
400
  model.startDate = put.startDate;
391
401
 
402
+ // Check maximum
403
+ await this.checkResponsbilityLimits(model, responsibility);
404
+
392
405
  await model.save();
393
406
  shouldUpdateSetupSteps = true;
394
407
  }
@@ -731,6 +744,129 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
731
744
  }
732
745
  }
733
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
+
734
870
  static async findExistingMember(member: Member) {
735
871
  if (!member.details.birthDay) {
736
872
  return;
@@ -863,4 +999,41 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
863
999
  organization,
864
1000
  }).createMultiple(count);
865
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
+ }
866
1039
  }
@@ -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';
@@ -124,6 +124,12 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
124
124
  }
125
125
 
126
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
+
127
133
  await MemberUserSyncer.onChangeMember(member);
128
134
 
129
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
  }
@@ -0,0 +1,164 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { GroupFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { GroupType, PermissionLevel, Permissions, Version } from '@stamhoofd/structures';
4
+ import { testServer } from '../../../../../tests/helpers/TestServer';
5
+ import { PatchRegistrationPeriodsEndpoint } from './GetOrganizationRegistrationPeriodsEndpoint';
6
+
7
+ const baseUrl = `/v${Version}/organization/registration-periods`;
8
+
9
+ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
10
+ const endpoint = new PatchRegistrationPeriodsEndpoint();
11
+
12
+ let registrationPeriod1: RegistrationPeriod;
13
+ let registrationPeriod2: RegistrationPeriod;
14
+
15
+ const get = async (organization: Organization, token: Token) => {
16
+ const request = Request.buildJson('GET', baseUrl, organization.getApiHost());
17
+ request.headers.authorization = 'Bearer ' + token.accessToken;
18
+ return await testServer.test(endpoint, request);
19
+ };
20
+
21
+ beforeAll(async () => {
22
+ registrationPeriod2 = await new RegistrationPeriodFactory({
23
+ startDate: new Date(2022, 0, 1),
24
+ endDate: new Date(2022, 11, 31),
25
+ }).create();
26
+
27
+ registrationPeriod1 = await new RegistrationPeriodFactory({
28
+ startDate: new Date(2023, 0, 1),
29
+ endDate: new Date(2023, 11, 31),
30
+ previousPeriodId: registrationPeriod2.id,
31
+ }).create();
32
+ });
33
+
34
+ const initOrganization = async (registrationPeriod: RegistrationPeriod) => {
35
+ return await new OrganizationFactory({ period: registrationPeriod })
36
+ .create();
37
+ };
38
+
39
+ describe('groups', () => {
40
+ test('Should contain waiting lists', async () => {
41
+ // arrange
42
+ const organization = await initOrganization(registrationPeriod1);
43
+ await new OrganizationRegistrationPeriodFactory({
44
+ organization,
45
+ period: registrationPeriod1,
46
+ }).create();
47
+
48
+ const user = await new UserFactory({
49
+ organization,
50
+ permissions: Permissions.create({
51
+ level: PermissionLevel.Full,
52
+ }),
53
+ })
54
+ .create();
55
+ const token = await Token.createToken(user);
56
+
57
+ const otherOrganization = await initOrganization(registrationPeriod1);
58
+
59
+ const waitingList1 = await new GroupFactory({
60
+ organization,
61
+ type: GroupType.WaitingList,
62
+ }).create();
63
+
64
+ const waitingList2 = await new GroupFactory({
65
+ organization,
66
+ type: GroupType.WaitingList,
67
+ }).create();
68
+
69
+ // waiting list of other organization
70
+ await new GroupFactory({
71
+ organization: otherOrganization,
72
+ type: GroupType.WaitingList,
73
+ }).create();
74
+
75
+ const groupWithWaitingList = await new GroupFactory({
76
+ organization,
77
+ }).create();
78
+
79
+ groupWithWaitingList.waitingListId = waitingList1.id;
80
+ await groupWithWaitingList.save();
81
+
82
+ // act
83
+ const result = await get(organization, token);
84
+
85
+ // assert
86
+ expect(result.body).toBeDefined();
87
+ expect(result.body.organizationPeriods.length).toBe(1);
88
+ const organizationPeriod = result.body.organizationPeriods[0];
89
+ const groups = organizationPeriod.groups;
90
+ expect(groups.length).toBe(3);
91
+ expect(groups).toEqual(expect.arrayContaining([
92
+ expect.objectContaining({
93
+ id: groupWithWaitingList.id,
94
+ }),
95
+ expect.objectContaining({
96
+ id: waitingList1.id,
97
+ }),
98
+ expect.objectContaining({
99
+ id: waitingList2.id,
100
+ }),
101
+ ]));
102
+ expect(organizationPeriod.waitingLists.length).toBe(2);
103
+ });
104
+
105
+ test('Should contain only groups of the organization registration period', async () => {
106
+ // arrange
107
+ const organization = await initOrganization(registrationPeriod1);
108
+ await new OrganizationRegistrationPeriodFactory({
109
+ organization,
110
+ period: registrationPeriod1,
111
+ }).create();
112
+
113
+ const user = await new UserFactory({
114
+ organization,
115
+ permissions: Permissions.create({
116
+ level: PermissionLevel.Full,
117
+ }),
118
+ })
119
+ .create();
120
+
121
+ const token = await Token.createToken(user);
122
+
123
+ const group1 = await new GroupFactory({
124
+ organization,
125
+ period: registrationPeriod1,
126
+ }).create();
127
+
128
+ const group2 = await new GroupFactory({
129
+ organization,
130
+ period: registrationPeriod1,
131
+ }).create();
132
+
133
+ // group list of other organization
134
+ const otherOrganization = await initOrganization(registrationPeriod1);
135
+
136
+ await new GroupFactory({
137
+ organization: otherOrganization,
138
+ }).create();
139
+
140
+ // group of other period
141
+ await new GroupFactory({
142
+ organization,
143
+ period: registrationPeriod2,
144
+ }).create();
145
+
146
+ // act
147
+ const result = await get(organization, token);
148
+
149
+ // assert
150
+ expect(result.body).toBeDefined();
151
+ expect(result.body.organizationPeriods.length).toBe(1);
152
+ const groups = result.body.organizationPeriods[0].groups;
153
+ expect(groups.length).toBe(2);
154
+ expect(groups).toEqual(expect.arrayContaining([
155
+ expect.objectContaining({
156
+ id: group1.id,
157
+ }),
158
+ expect.objectContaining({
159
+ id: group2.id,
160
+ }),
161
+ ]));
162
+ });
163
+ });
164
+ });
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { BalanceItem, CachedBalance, Document, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
4
- import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory, RecordSettings } from '@stamhoofd/structures';
4
+ import { AccessRight, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordCategory, RecordSettings } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
7
7
  import { MemberRecordStore } from '../services/MemberRecordStore';
@@ -185,6 +185,15 @@ export class AdminPermissionChecker {
185
185
  return true;
186
186
  }
187
187
 
188
+ if (group.type === GroupType.EventRegistration) {
189
+ // Check if we can access the event
190
+ const event = await Event.select().where('groupId', group.id).first(false);
191
+
192
+ if (event && event.organizationId === group.organizationId && await this.canAccessEvent(event)) {
193
+ return true;
194
+ }
195
+ }
196
+
188
197
  // Check parent categories
189
198
  const organizationPeriod = await this.getOrganizationCurrentPeriod(organization);
190
199
  const parentCategories = group.getParentCategories(organizationPeriod.settings.categories);
@@ -197,6 +206,18 @@ export class AdminPermissionChecker {
197
206
  return false;
198
207
  }
199
208
 
209
+ async canRegisterMembersInGroup(group: Group, asOrganizationId: string | null) {
210
+ if (await this.canAccessGroup(group, PermissionLevel.Write)) {
211
+ return true;
212
+ }
213
+ if (asOrganizationId) {
214
+ if (group.settings.allowRegistrationsByOrganization) {
215
+ return await this.hasFullAccess(asOrganizationId);
216
+ }
217
+ }
218
+ return false;
219
+ }
220
+
200
221
  /**
201
222
  * Will throw error if not allowed to edit/add/delete this event
202
223
  * @param event