@stamhoofd/backend 2.105.0 → 2.106.1

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 (28) hide show
  1. package/package.json +10 -10
  2. package/src/crons.ts +39 -5
  3. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +953 -47
  4. package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -1
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +142 -0
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +1 -1
  7. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +163 -8
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -0
  9. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.test.ts +108 -0
  10. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +40 -0
  11. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
  12. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -1
  13. package/src/helpers/AdminPermissionChecker.ts +30 -6
  14. package/src/helpers/AuthenticatedStructures.ts +2 -2
  15. package/src/helpers/MemberUserSyncer.test.ts +400 -1
  16. package/src/helpers/MemberUserSyncer.ts +15 -10
  17. package/src/helpers/ServiceFeeHelper.ts +63 -0
  18. package/src/helpers/StripeHelper.ts +7 -4
  19. package/src/helpers/StripePayoutChecker.ts +1 -1
  20. package/src/seeds/0000000001-development-user.ts +2 -2
  21. package/src/seeds/0000000004-single-organization.ts +60 -0
  22. package/src/seeds/1754560914-groups-prices.test.ts +3023 -0
  23. package/src/seeds/1754560914-groups-prices.ts +408 -0
  24. package/src/seeds/{1722344162-sync-member-users.ts → 1761665607-sync-member-users.ts} +1 -1
  25. package/src/sql-filters/members.ts +1 -1
  26. package/tests/init/initAdmin.ts +19 -5
  27. package/tests/init/initPermissionRole.ts +14 -4
  28. package/tests/init/initPlatformRecordCategory.ts +8 -0
@@ -434,8 +434,16 @@ export class AdminPermissionChecker {
434
434
  */
435
435
  async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read, checkMember: boolean | MemberWithRegistrations = true) {
436
436
  if (registration.deactivatedAt || !registration.registeredAt) {
437
+ if (!checkMember) {
438
+ // We can't grant access to a member because of a deactivated registration
439
+ return false;
440
+ }
441
+
437
442
  // No full access: cannot access deactivated registrations
438
- return false;
443
+ if (permissionLevel !== PermissionLevel.Read) {
444
+ // Not allowed to edit registrations that are deleted
445
+ return false;
446
+ }
439
447
  }
440
448
 
441
449
  const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
@@ -1293,6 +1301,14 @@ export class AdminPermissionChecker {
1293
1301
  };
1294
1302
  }
1295
1303
 
1304
+ // All member access, means also having access to record categories of non-registered members
1305
+ if (this.canAccessAllPlatformMembers(PermissionLevel.Read)) {
1306
+ return {
1307
+ canAccess: true,
1308
+ record: record.record,
1309
+ };
1310
+ }
1311
+
1296
1312
  return {
1297
1313
  canAccess: false,
1298
1314
  record: record.record,
@@ -1362,6 +1378,14 @@ export class AdminPermissionChecker {
1362
1378
  };
1363
1379
  }
1364
1380
 
1381
+ // All member access, means also having access to record categories of non-registered members
1382
+ if (this.canAccessAllPlatformMembers(level)) { // needs to be full to also inherit record category access
1383
+ return {
1384
+ canAccess: true,
1385
+ record: record.record,
1386
+ };
1387
+ }
1388
+
1365
1389
  if (hasTemporaryMemberAccess(this.user.id, member.id, PermissionLevel.Full)) {
1366
1390
  // You created this member, so temporary can read all records in order to set the member up correctly
1367
1391
  return {
@@ -1371,13 +1395,13 @@ export class AdminPermissionChecker {
1371
1395
  }
1372
1396
 
1373
1397
  // It is possible that this is a platform admin (or an admin that has access to multiple organizations), and inherits automatic permissions for tags. So'll need to loop all the organizations where this member has an active registration for
1374
- if (!record.organizationId && !this.organization && this.hasSomePlatformAccess()) {
1398
+ if (!record.organizationId && !this.organization) {
1375
1399
  const checkedOrganizations = new Map<string, boolean>();
1376
1400
  for (const registration of member.registrations) {
1377
1401
  const permissions = checkedOrganizations.get(registration.organizationId);
1378
1402
 
1379
1403
  // Checking the organization permissions is faster (and less data lookups required), so we do that first before doing the more expensive registration access check
1380
- if (permissions !== null) {
1404
+ if (permissions !== undefined) {
1381
1405
  if (permissions === true && await this.canAccessRegistration(registration, level)) {
1382
1406
  return {
1383
1407
  canAccess: true,
@@ -1648,8 +1672,8 @@ export class AdminPermissionChecker {
1648
1672
  return data;
1649
1673
  }
1650
1674
 
1651
- canAccessAllPlatformMembers(): boolean {
1652
- return !!this.platformPermissions && !!this.platformPermissions.hasAccessRight(AccessRight.PlatformLoginAs);
1675
+ canAccessAllPlatformMembers(level: PermissionLevel): boolean {
1676
+ return this.getPlatformAccessibleOrganizationTags(level) === 'all';
1653
1677
  }
1654
1678
 
1655
1679
  canAccess(accessRight: AccessRight): boolean {
@@ -1682,7 +1706,7 @@ export class AdminPermissionChecker {
1682
1706
  }
1683
1707
  }
1684
1708
 
1685
- if (tags.length === allTags.length) {
1709
+ if (tags.length > 0 && tags.length === allTags.length) {
1686
1710
  return 'all';
1687
1711
  }
1688
1712
 
@@ -937,7 +937,7 @@ export class AuthenticatedStructures {
937
937
  ]
938
938
  : []),
939
939
 
940
- ...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
940
+ ...((member.details.calculatedParentsHaveAccess || member.details.getMemberEmails().length === 0)
941
941
  ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
942
942
  firstName: p.firstName ?? '',
943
943
  lastName: p.lastName ?? '',
@@ -980,7 +980,7 @@ export class AuthenticatedStructures {
980
980
  ]
981
981
  : []),
982
982
 
983
- ...((member.details.defaultAge <= 18 || member.details.getMemberEmails().length === 0)
983
+ ...((member.details.calculatedParentsHaveAccess || member.details.getMemberEmails().length === 0)
984
984
  ? member.details.parents.filter(p => p.getEmails().length > 0).map(p => ReceivableBalanceObjectContact.create({
985
985
  firstName: p.firstName ?? '',
986
986
  lastName: p.lastName ?? '',
@@ -1,6 +1,6 @@
1
1
  import { Database } from '@simonbackx/simple-database';
2
2
  import { Member, MemberFactory, MemberResponsibilityRecordFactory, User, UserFactory } from '@stamhoofd/models';
3
- import { MemberDetails, Parent, UserPermissions } from '@stamhoofd/structures';
3
+ import { BooleanStatus, MemberDetails, Parent, UserPermissions } from '@stamhoofd/structures';
4
4
  import { TestUtils } from '@stamhoofd/test-utils';
5
5
  import { MemberUserSyncer } from './MemberUserSyncer';
6
6
 
@@ -19,6 +19,143 @@ describe('Helpers.MemberUserSyncer', () => {
19
19
  details: MemberDetails.create({
20
20
  firstName: 'John',
21
21
  lastName: 'Doe',
22
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
23
+ parents: [
24
+ Parent.create({
25
+ firstName: 'Linda',
26
+ lastName: 'Potter',
27
+ email: 'linda@example.com',
28
+ }),
29
+ Parent.create({
30
+ firstName: 'Peter',
31
+ lastName: 'Doe',
32
+ email: 'peter@example.com',
33
+ alternativeEmails: [
34
+ 'peter@work.com',
35
+ ],
36
+ }),
37
+ ],
38
+ email: 'john@example.com',
39
+ alternativeEmails: ['john@work.com'],
40
+ unverifiedEmails: ['untitled@example.com', 'peter@example.com'], // Last one should be ignored
41
+ }),
42
+ }).create();
43
+
44
+ await MemberUserSyncer.onChangeMember(member);
45
+
46
+ const users = await Member.users.load(member);
47
+ expect(users).toIncludeSameMembers([
48
+ // Member
49
+ expect.objectContaining({
50
+ firstName: 'John',
51
+ lastName: 'Doe',
52
+ email: 'john@example.com',
53
+ memberId: member.id,
54
+ }),
55
+ expect.objectContaining({
56
+ firstName: 'John',
57
+ lastName: 'Doe',
58
+ email: 'john@work.com',
59
+ memberId: member.id,
60
+ }),
61
+
62
+ // Parents
63
+ expect.objectContaining({
64
+ firstName: 'Linda',
65
+ lastName: 'Potter',
66
+ email: 'linda@example.com',
67
+ memberId: null,
68
+ permissions: null,
69
+ }),
70
+ expect.objectContaining({
71
+ firstName: 'Peter',
72
+ lastName: 'Doe',
73
+ email: 'peter@example.com',
74
+ memberId: null,
75
+ permissions: null,
76
+ }),
77
+ expect.objectContaining({
78
+ firstName: 'Peter',
79
+ lastName: 'Doe',
80
+ email: 'peter@work.com',
81
+ memberId: null,
82
+ permissions: null,
83
+ }),
84
+
85
+ // Unverified
86
+ expect.objectContaining({
87
+ firstName: null,
88
+ lastName: null,
89
+ email: 'untitled@example.com',
90
+ memberId: null,
91
+ permissions: null,
92
+ }),
93
+ ]);
94
+ });
95
+
96
+ test('Parents do not get access by default for adult members', async () => {
97
+ const member = await new MemberFactory({
98
+ details: MemberDetails.create({
99
+ firstName: 'John',
100
+ lastName: 'Doe',
101
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 366 * 18), // 18 years old + compensation for leap years
102
+ parents: [
103
+ Parent.create({
104
+ firstName: 'Linda',
105
+ lastName: 'Potter',
106
+ email: 'linda@example.com',
107
+ }),
108
+ Parent.create({
109
+ firstName: 'Peter',
110
+ lastName: 'Doe',
111
+ email: 'peter@example.com',
112
+ alternativeEmails: [
113
+ 'peter@work.com',
114
+ ],
115
+ }),
116
+ ],
117
+ email: 'john@example.com',
118
+ alternativeEmails: ['john@work.com'],
119
+ unverifiedEmails: ['untitled@example.com', 'peter@example.com'], // Last one should be ignored
120
+ }),
121
+ }).create();
122
+
123
+ await MemberUserSyncer.onChangeMember(member);
124
+
125
+ const users = await Member.users.load(member);
126
+ expect(users).toIncludeSameMembers([
127
+ // Member
128
+ expect.objectContaining({
129
+ firstName: 'John',
130
+ lastName: 'Doe',
131
+ email: 'john@example.com',
132
+ memberId: member.id,
133
+ }),
134
+ expect.objectContaining({
135
+ firstName: 'John',
136
+ lastName: 'Doe',
137
+ email: 'john@work.com',
138
+ memberId: member.id,
139
+ }),
140
+
141
+ // Unverified
142
+ expect.objectContaining({
143
+ firstName: null,
144
+ lastName: null,
145
+ email: 'untitled@example.com',
146
+ memberId: null,
147
+ permissions: null,
148
+ }),
149
+ ]);
150
+ });
151
+
152
+ test('Parents can get access for adult members if explicitly set', async () => {
153
+ const member = await new MemberFactory({
154
+ details: MemberDetails.create({
155
+ firstName: 'John',
156
+ lastName: 'Doe',
157
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 366 * 18), // 18 years old + compensation for leap years
158
+ parentsHaveAccess: BooleanStatus.create({ value: true }),
22
159
  parents: [
23
160
  Parent.create({
24
161
  firstName: 'Linda',
@@ -104,6 +241,7 @@ describe('Helpers.MemberUserSyncer', () => {
104
241
  details: MemberDetails.create({
105
242
  firstName: 'John',
106
243
  lastName: 'Doe',
244
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
107
245
  parents: [
108
246
  Parent.create({
109
247
  firstName: 'Linda',
@@ -192,6 +330,7 @@ describe('Helpers.MemberUserSyncer', () => {
192
330
  details: MemberDetails.create({
193
331
  firstName: 'John',
194
332
  lastName: 'Doe',
333
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
195
334
  parents: [
196
335
  Parent.create({
197
336
  firstName: 'Linda',
@@ -225,6 +364,7 @@ describe('Helpers.MemberUserSyncer', () => {
225
364
  details: MemberDetails.create({
226
365
  firstName: 'John',
227
366
  lastName: 'Doe',
367
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
228
368
  parents: [
229
369
  Parent.create({
230
370
  firstName: 'Linda',
@@ -258,6 +398,7 @@ describe('Helpers.MemberUserSyncer', () => {
258
398
  details: MemberDetails.create({
259
399
  firstName: 'John',
260
400
  lastName: 'Doe',
401
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
261
402
  parents: [
262
403
  Parent.create({
263
404
  firstName: 'Linda',
@@ -291,6 +432,7 @@ describe('Helpers.MemberUserSyncer', () => {
291
432
  details: MemberDetails.create({
292
433
  firstName: 'John',
293
434
  lastName: 'Doe',
435
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
294
436
  parents: [
295
437
  Parent.create({
296
438
  firstName: 'John',
@@ -322,6 +464,7 @@ describe('Helpers.MemberUserSyncer', () => {
322
464
  details: MemberDetails.create({
323
465
  firstName: 'John',
324
466
  lastName: 'Doe',
467
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
325
468
  parents: [
326
469
  Parent.create({
327
470
  firstName: 'John',
@@ -353,6 +496,7 @@ describe('Helpers.MemberUserSyncer', () => {
353
496
  details: MemberDetails.create({
354
497
  firstName: 'John',
355
498
  lastName: 'Doe',
499
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
356
500
  parents: [
357
501
  Parent.create({
358
502
  firstName: 'Linda',
@@ -466,6 +610,170 @@ describe('Helpers.MemberUserSyncer', () => {
466
610
  });
467
611
  });
468
612
 
613
+ test('Parents are removed if access is revoked', async () => {
614
+ const member = await new MemberFactory({
615
+ details: MemberDetails.create({
616
+ firstName: 'John',
617
+ lastName: 'Doe',
618
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
619
+ parents: [
620
+ Parent.create({
621
+ firstName: 'Linda',
622
+ lastName: 'Potter',
623
+ email: 'linda@example.com',
624
+ }),
625
+ Parent.create({
626
+ firstName: 'Peter',
627
+ lastName: 'Doe',
628
+ email: 'peter@example.com',
629
+ alternativeEmails: [
630
+ 'peter@work.com',
631
+ ],
632
+ }),
633
+ ],
634
+ email: 'john@example.com',
635
+ }),
636
+ }).create();
637
+ await new MemberResponsibilityRecordFactory({
638
+ member,
639
+ }).create();
640
+
641
+ await MemberUserSyncer.onChangeMember(member);
642
+
643
+ let users = await Member.users.load(member);
644
+ expect(users).toIncludeSameMembers([
645
+ // Member
646
+ expect.objectContaining({
647
+ firstName: 'John',
648
+ lastName: 'Doe',
649
+ email: 'john@example.com',
650
+ memberId: member.id,
651
+ permissions: expect.any(UserPermissions),
652
+ }),
653
+
654
+ // Parents
655
+ expect.objectContaining({
656
+ firstName: 'Linda',
657
+ lastName: 'Potter',
658
+ email: 'linda@example.com',
659
+ memberId: null,
660
+ permissions: null,
661
+ }),
662
+ expect.objectContaining({
663
+ firstName: 'Peter',
664
+ lastName: 'Doe',
665
+ email: 'peter@example.com',
666
+ memberId: null,
667
+ permissions: null,
668
+ }),
669
+ expect.objectContaining({
670
+ firstName: 'Peter',
671
+ lastName: 'Doe',
672
+ email: 'peter@work.com',
673
+ memberId: null,
674
+ permissions: null,
675
+ }),
676
+ ]);
677
+
678
+ // Revoke parents access
679
+ member.details.parentsHaveAccess = BooleanStatus.create({ value: false });
680
+ await MemberUserSyncer.onChangeMember(member);
681
+
682
+ users = await Member.users.load(member);
683
+ expect(users).toIncludeSameMembers([
684
+ // Member
685
+ expect.objectContaining({
686
+ firstName: 'John',
687
+ lastName: 'Doe',
688
+ email: 'john@example.com',
689
+ memberId: member.id,
690
+ permissions: expect.any(UserPermissions),
691
+ }),
692
+ ]);
693
+ });
694
+
695
+ test('Parents are removed if member turns 18', async () => {
696
+ const member = await new MemberFactory({
697
+ details: MemberDetails.create({
698
+ firstName: 'John',
699
+ lastName: 'Doe',
700
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
701
+ parents: [
702
+ Parent.create({
703
+ firstName: 'Linda',
704
+ lastName: 'Potter',
705
+ email: 'linda@example.com',
706
+ }),
707
+ Parent.create({
708
+ firstName: 'Peter',
709
+ lastName: 'Doe',
710
+ email: 'peter@example.com',
711
+ alternativeEmails: [
712
+ 'peter@work.com',
713
+ ],
714
+ }),
715
+ ],
716
+ email: 'john@example.com',
717
+ }),
718
+ }).create();
719
+ await new MemberResponsibilityRecordFactory({
720
+ member,
721
+ }).create();
722
+
723
+ await MemberUserSyncer.onChangeMember(member);
724
+
725
+ let users = await Member.users.load(member);
726
+ expect(users).toIncludeSameMembers([
727
+ // Member
728
+ expect.objectContaining({
729
+ firstName: 'John',
730
+ lastName: 'Doe',
731
+ email: 'john@example.com',
732
+ memberId: member.id,
733
+ permissions: expect.any(UserPermissions),
734
+ }),
735
+
736
+ // Parents
737
+ expect.objectContaining({
738
+ firstName: 'Linda',
739
+ lastName: 'Potter',
740
+ email: 'linda@example.com',
741
+ memberId: null,
742
+ permissions: null,
743
+ }),
744
+ expect.objectContaining({
745
+ firstName: 'Peter',
746
+ lastName: 'Doe',
747
+ email: 'peter@example.com',
748
+ memberId: null,
749
+ permissions: null,
750
+ }),
751
+ expect.objectContaining({
752
+ firstName: 'Peter',
753
+ lastName: 'Doe',
754
+ email: 'peter@work.com',
755
+ memberId: null,
756
+ permissions: null,
757
+ }),
758
+ ]);
759
+
760
+ // Revoke parents access
761
+ member.details.birthDay = new Date(Date.now() - 1000 * 60 * 60 * 24 * 366 * 18); // 18 years old + compensation for leap years
762
+ await MemberUserSyncer.onChangeMember(member);
763
+
764
+ users = await Member.users.load(member);
765
+ expect(users).toIncludeSameMembers([
766
+ // Member
767
+ expect.objectContaining({
768
+ firstName: 'John',
769
+ lastName: 'Doe',
770
+ email: 'john@example.com',
771
+ memberId: member.id,
772
+ permissions: expect.any(UserPermissions),
773
+ }),
774
+ ]);
775
+ });
776
+
469
777
  test('Old emails with account are not removed', async () => {
470
778
  const user_m1 = await new UserFactory({ email: 'john@example.com' }).create();
471
779
  const user_m2 = await new UserFactory({ email: 'john@work.com' }).create();
@@ -478,6 +786,7 @@ describe('Helpers.MemberUserSyncer', () => {
478
786
  details: MemberDetails.create({
479
787
  firstName: 'John',
480
788
  lastName: 'Doe',
789
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
481
790
  parents: [
482
791
  Parent.create({
483
792
  firstName: 'Linda',
@@ -634,12 +943,97 @@ describe('Helpers.MemberUserSyncer', () => {
634
943
  });
635
944
  });
636
945
 
946
+ describe('Linking', () => {
947
+ test('Parents are added if access is added', async () => {
948
+ const member = await new MemberFactory({
949
+ details: MemberDetails.create({
950
+ firstName: 'John',
951
+ lastName: 'Doe',
952
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 366 * 18), // 18 years old + leap year margin
953
+ parents: [
954
+ Parent.create({
955
+ firstName: 'Linda',
956
+ lastName: 'Potter',
957
+ email: 'linda@example.com',
958
+ }),
959
+ Parent.create({
960
+ firstName: 'Peter',
961
+ lastName: 'Doe',
962
+ email: 'peter@example.com',
963
+ alternativeEmails: [
964
+ 'peter@work.com',
965
+ ],
966
+ }),
967
+ ],
968
+ email: 'john@example.com',
969
+ }),
970
+ }).create();
971
+ await new MemberResponsibilityRecordFactory({
972
+ member,
973
+ }).create();
974
+
975
+ await MemberUserSyncer.onChangeMember(member);
976
+
977
+ let users = await Member.users.load(member);
978
+ expect(users).toIncludeSameMembers([
979
+ // Member
980
+ expect.objectContaining({
981
+ firstName: 'John',
982
+ lastName: 'Doe',
983
+ email: 'john@example.com',
984
+ memberId: member.id,
985
+ permissions: expect.any(UserPermissions),
986
+ }),
987
+ ]);
988
+
989
+ // Revoke parents access
990
+ member.details.parentsHaveAccess = BooleanStatus.create({ value: true });
991
+ await MemberUserSyncer.onChangeMember(member);
992
+
993
+ users = await Member.users.load(member);
994
+ expect(users).toIncludeSameMembers([
995
+ // Member
996
+ expect.objectContaining({
997
+ firstName: 'John',
998
+ lastName: 'Doe',
999
+ email: 'john@example.com',
1000
+ memberId: member.id,
1001
+ permissions: expect.any(UserPermissions),
1002
+ }),
1003
+
1004
+ // Parents
1005
+ expect.objectContaining({
1006
+ firstName: 'Linda',
1007
+ lastName: 'Potter',
1008
+ email: 'linda@example.com',
1009
+ memberId: null,
1010
+ permissions: null,
1011
+ }),
1012
+ expect.objectContaining({
1013
+ firstName: 'Peter',
1014
+ lastName: 'Doe',
1015
+ email: 'peter@example.com',
1016
+ memberId: null,
1017
+ permissions: null,
1018
+ }),
1019
+ expect.objectContaining({
1020
+ firstName: 'Peter',
1021
+ lastName: 'Doe',
1022
+ email: 'peter@work.com',
1023
+ memberId: null,
1024
+ permissions: null,
1025
+ }),
1026
+ ]);
1027
+ });
1028
+ });
1029
+
637
1030
  describe('Members with the same email addresses', () => {
638
1031
  test('The most recent member is linked to a user if both do not have responsibilities', async () => {
639
1032
  const member1 = await new MemberFactory({
640
1033
  details: MemberDetails.create({
641
1034
  firstName: 'John',
642
1035
  lastName: 'Doe',
1036
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
643
1037
  email: 'john@example.com',
644
1038
  }),
645
1039
  }).create();
@@ -663,6 +1057,7 @@ describe('Helpers.MemberUserSyncer', () => {
663
1057
  details: MemberDetails.create({
664
1058
  firstName: 'Other',
665
1059
  lastName: 'Doe',
1060
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
666
1061
  email: 'john@example.com',
667
1062
  }),
668
1063
  }).create();
@@ -698,6 +1093,7 @@ describe('Helpers.MemberUserSyncer', () => {
698
1093
  details: MemberDetails.create({
699
1094
  firstName: 'John',
700
1095
  lastName: 'Doe',
1096
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
701
1097
  email: 'john@example.com',
702
1098
  }),
703
1099
  }).create();
@@ -724,6 +1120,7 @@ describe('Helpers.MemberUserSyncer', () => {
724
1120
  details: MemberDetails.create({
725
1121
  firstName: 'Other',
726
1122
  lastName: 'Doe',
1123
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
727
1124
  email: 'john@example.com',
728
1125
  }),
729
1126
  }).create();
@@ -764,6 +1161,7 @@ describe('Helpers.MemberUserSyncer', () => {
764
1161
  details: MemberDetails.create({
765
1162
  firstName: 'John',
766
1163
  lastName: 'Doe',
1164
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
767
1165
  email: 'john@example.com',
768
1166
  }),
769
1167
  }).create();
@@ -784,6 +1182,7 @@ describe('Helpers.MemberUserSyncer', () => {
784
1182
  details: MemberDetails.create({
785
1183
  firstName: 'Other',
786
1184
  lastName: 'Doe',
1185
+ birthDay: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 8), // 8 years old
787
1186
  email: 'john@example.com',
788
1187
  }),
789
1188
  }).create();
@@ -49,17 +49,19 @@ export class MemberUserSyncerStatic {
49
49
  });
50
50
  }
51
51
 
52
- for (const parent of member.details.parents) {
53
- for (const email of parent.getEmails()) {
54
- if (userEmails.includes(email.toLocaleLowerCase())) {
55
- continue;
52
+ if (member.details.calculatedParentsHaveAccess) {
53
+ for (const parent of member.details.parents) {
54
+ for (const email of parent.getEmails()) {
55
+ if (userEmails.includes(email.toLocaleLowerCase())) {
56
+ continue;
57
+ }
58
+
59
+ // Link parents and unverified emails
60
+ await this.linkUser(email, member, true, {
61
+ firstName: parent.firstName,
62
+ lastName: parent.lastName,
63
+ });
56
64
  }
57
-
58
- // Link parents and unverified emails
59
- await this.linkUser(email, member, true, {
60
- firstName: parent.firstName,
61
- lastName: parent.lastName,
62
- });
63
65
  }
64
66
  }
65
67
 
@@ -89,6 +91,9 @@ export class MemberUserSyncerStatic {
89
91
  await this.linkUser(user.email, member, true);
90
92
  }
91
93
  }
94
+ else if (!member.details.calculatedParentsHaveAccess && parentEmails.includes(user.email.toLocaleLowerCase()) && !userEmails.includes(user.email.toLocaleLowerCase())) {
95
+ await this.unlinkUser(user, member);
96
+ }
92
97
  }
93
98
 
94
99
  if (member.details.securityCode === null) {