@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.
- package/.env.ci.json +19 -8
- package/index.ts +8 -1
- package/package.json +13 -12
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +1 -1
- package/src/endpoints/auth/CreateAdminEndpoint.ts +1 -1
- package/src/endpoints/auth/CreateTokenEndpoint.ts +1 -1
- package/src/endpoints/auth/ForgotPasswordEndpoint.ts +1 -1
- package/src/endpoints/auth/PatchUserEndpoint.ts +1 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +2686 -0
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +206 -20
- package/src/endpoints/global/members/shouldCheckIfMemberIsDuplicate.ts +9 -21
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +1 -1
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +2 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +15 -14
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +3 -23
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -0
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +164 -0
- package/src/helpers/AdminPermissionChecker.ts +41 -2
- package/src/helpers/AuthenticatedStructures.ts +77 -20
- package/src/helpers/ForwardHandler.test.ts +16 -5
- package/src/helpers/ForwardHandler.ts +21 -9
- package/src/helpers/MemberUserSyncer.test.ts +822 -0
- package/src/helpers/MemberUserSyncer.ts +137 -108
- package/src/helpers/TagHelper.ts +3 -3
- package/src/seeds/1734596144-fill-previous-period-id.ts +1 -1
- package/src/seeds/1741008870-fix-auditlog-description.ts +50 -0
- package/src/seeds/1741011468-fix-auditlog-description-uft16.ts +88 -0
- package/src/services/FileSignService.ts +1 -1
- package/src/services/PlatformMembershipService.ts +7 -2
- package/src/services/SSOService.ts +1 -1
- 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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
if (duplicate) {
|
|
111
|
+
const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
|
|
112
|
+
if (duplicate) {
|
|
115
113
|
// Merge data
|
|
116
|
-
|
|
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.
|
|
159
|
+
throw Context.auth.memberNotFoundOrNoAccess();
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
|
|
166
|
-
|
|
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
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
});
|