@stamhoofd/backend 2.77.0 → 2.77.2
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/package.json +10 -10
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +12 -3
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +44 -17
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +3 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +4 -1
- package/src/services/PlatformMembershipService.ts +19 -3
- package/src/sql-filters/members.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.77.
|
|
3
|
+
"version": "2.77.2",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@simonbackx/simple-encoding": "2.20.0",
|
|
38
38
|
"@simonbackx/simple-endpoints": "1.19.1",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.77.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.77.
|
|
42
|
-
"@stamhoofd/email": "2.77.
|
|
43
|
-
"@stamhoofd/models": "2.77.
|
|
44
|
-
"@stamhoofd/queues": "2.77.
|
|
45
|
-
"@stamhoofd/sql": "2.77.
|
|
46
|
-
"@stamhoofd/structures": "2.77.
|
|
47
|
-
"@stamhoofd/utility": "2.77.
|
|
40
|
+
"@stamhoofd/backend-i18n": "2.77.2",
|
|
41
|
+
"@stamhoofd/backend-middleware": "2.77.2",
|
|
42
|
+
"@stamhoofd/email": "2.77.2",
|
|
43
|
+
"@stamhoofd/models": "2.77.2",
|
|
44
|
+
"@stamhoofd/queues": "2.77.2",
|
|
45
|
+
"@stamhoofd/sql": "2.77.2",
|
|
46
|
+
"@stamhoofd/structures": "2.77.2",
|
|
47
|
+
"@stamhoofd/utility": "2.77.2",
|
|
48
48
|
"archiver": "^7.0.1",
|
|
49
49
|
"aws-sdk": "^2.885.0",
|
|
50
50
|
"axios": "1.6.8",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "8106bfebe63c39050eb31749e0c65ea9ddae08d7"
|
|
68
68
|
}
|
|
@@ -42,9 +42,18 @@ export class GetAuditLogsEndpoint extends Endpoint<Params, Query, Body, Response
|
|
|
42
42
|
if (!await Context.auth.hasFullAccess(organization.id)) {
|
|
43
43
|
throw Context.auth.error();
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
46
|
+
scopeFilter = {
|
|
47
|
+
organizationId: organization.id,
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
if (!q.filter || typeof q.filter !== 'object' || !('objectId' in q.filter)) {
|
|
51
|
+
scopeFilter = {
|
|
52
|
+
organizationId: organization.id,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
else {
|
|
50
59
|
if (!Context.auth.hasPlatformFullAccess()) {
|
|
@@ -2,8 +2,8 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
|
|
|
2
2
|
import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
-
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
6
|
-
import { GroupType, MembersBlob, MemberWithRegistrationsBlob, PermissionLevel } from '@stamhoofd/structures';
|
|
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';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { Email } from '@stamhoofd/email';
|
|
@@ -17,6 +17,7 @@ import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
|
|
|
17
17
|
import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
|
|
18
18
|
import { RegistrationService } from '../../../services/RegistrationService';
|
|
19
19
|
import { shouldCheckIfMemberIsDuplicateForPatch, shouldCheckIfMemberIsDuplicateForPut } from './shouldCheckIfMemberIsDuplicate';
|
|
20
|
+
import { AuditLogService } from '../../../services/AuditLogService';
|
|
20
21
|
|
|
21
22
|
type Params = Record<string, never>;
|
|
22
23
|
type Query = undefined;
|
|
@@ -491,7 +492,22 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
491
492
|
});
|
|
492
493
|
}
|
|
493
494
|
|
|
494
|
-
|
|
495
|
+
const membership = new MemberPlatformMembership();
|
|
496
|
+
membership.id = put.id;
|
|
497
|
+
membership.memberId = member.id;
|
|
498
|
+
membership.membershipTypeId = put.membershipTypeId;
|
|
499
|
+
membership.organizationId = put.organizationId;
|
|
500
|
+
membership.periodId = put.periodId;
|
|
501
|
+
|
|
502
|
+
membership.startDate = put.startDate;
|
|
503
|
+
membership.endDate = put.endDate;
|
|
504
|
+
membership.expireDate = put.expireDate;
|
|
505
|
+
membership.locked = false;
|
|
506
|
+
|
|
507
|
+
// Correct price and dates
|
|
508
|
+
await membership.calculatePrice(member);
|
|
509
|
+
|
|
510
|
+
// Check duplicate memberships after correcting the dates
|
|
495
511
|
const existing = await MemberPlatformMembership.select()
|
|
496
512
|
.where('memberId', member.id)
|
|
497
513
|
.where('membershipTypeId', put.membershipTypeId)
|
|
@@ -522,19 +538,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
522
538
|
});
|
|
523
539
|
}
|
|
524
540
|
|
|
525
|
-
|
|
526
|
-
membership.id = put.id;
|
|
527
|
-
membership.memberId = member.id;
|
|
528
|
-
membership.membershipTypeId = put.membershipTypeId;
|
|
529
|
-
membership.organizationId = put.organizationId;
|
|
530
|
-
membership.periodId = put.periodId;
|
|
531
|
-
|
|
532
|
-
membership.startDate = new Date(Math.max(Date.now(), put.startDate.getTime()));
|
|
533
|
-
membership.endDate = put.endDate;
|
|
534
|
-
membership.expireDate = put.expireDate;
|
|
535
|
-
membership.locked = false;
|
|
536
|
-
|
|
537
|
-
await membership.calculatePrice(member);
|
|
541
|
+
// Save if okay
|
|
538
542
|
await membership.save();
|
|
539
543
|
|
|
540
544
|
updateMembershipMemberIds.add(member.id);
|
|
@@ -671,7 +675,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
671
675
|
}
|
|
672
676
|
|
|
673
677
|
if (updateMembershipsForOrganizations.size) {
|
|
674
|
-
QueueHandler.schedule('update-membership-prices', async () => {
|
|
678
|
+
QueueHandler.schedule('update-organization-membership-prices', async () => {
|
|
675
679
|
for (const id of updateMembershipsForOrganizations) {
|
|
676
680
|
await MembershipCharger.updatePrices(id);
|
|
677
681
|
}
|
|
@@ -794,6 +798,29 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
794
798
|
|
|
795
799
|
// Grant temporary access to this member without needing to enter the security code again
|
|
796
800
|
await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.Write);
|
|
801
|
+
|
|
802
|
+
const log = new AuditLog();
|
|
803
|
+
|
|
804
|
+
// 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
|
+
|
|
807
|
+
log.type = AuditLogType.MemberSecurityCodeUsed;
|
|
808
|
+
log.source = AuditLogSource.Anonymous;
|
|
809
|
+
|
|
810
|
+
if (Context.user) {
|
|
811
|
+
log.userId = Context.user.id;
|
|
812
|
+
log.source = AuditLogSource.User;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
log.objectId = member.id;
|
|
816
|
+
log.replacements = new Map([
|
|
817
|
+
['m', AuditLogReplacement.create({
|
|
818
|
+
value: member.details.name,
|
|
819
|
+
type: AuditLogReplacementType.Member,
|
|
820
|
+
id: member.id,
|
|
821
|
+
})],
|
|
822
|
+
]);
|
|
823
|
+
await log.save();
|
|
797
824
|
}
|
|
798
825
|
else {
|
|
799
826
|
throw new SimpleError({
|
|
@@ -213,7 +213,10 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
213
213
|
if (shouldUpdateMemberships) {
|
|
214
214
|
if (!QueueHandler.isRunning('update-membership-prices')) {
|
|
215
215
|
QueueHandler.schedule('update-membership-prices', async () => {
|
|
216
|
+
// Update all membership prices (required to update temporary memberships)
|
|
216
217
|
await MembershipCharger.updatePrices();
|
|
218
|
+
|
|
219
|
+
// Update memberships of all members (note: this only updates non-day memberships)
|
|
217
220
|
await PlatformMembershipService.updateAll();
|
|
218
221
|
}).catch(console.error);
|
|
219
222
|
}
|
|
@@ -87,6 +87,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
|
|
|
87
87
|
expect(member.details.lastName).toBe(lastName);
|
|
88
88
|
expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
|
|
89
89
|
expect(member.details.email).toBe('anewemail@example.com'); // this has been merged
|
|
90
|
+
expect(member.details.alternativeEmails).toHaveLength(0);
|
|
90
91
|
});
|
|
91
92
|
|
|
92
93
|
test('A duplicate member with existing registrations returns those registrations after a merge', async () => {
|
|
@@ -96,6 +97,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
|
|
|
96
97
|
firstName,
|
|
97
98
|
lastName,
|
|
98
99
|
securityCode: 'ABC-123',
|
|
100
|
+
email: 'original@example.com',
|
|
99
101
|
parents: [
|
|
100
102
|
Parent.create({
|
|
101
103
|
firstName: 'Jane',
|
|
@@ -146,7 +148,8 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
|
|
|
146
148
|
expect(member.details.firstName).toBe(firstName);
|
|
147
149
|
expect(member.details.lastName).toBe(lastName);
|
|
148
150
|
expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
|
|
149
|
-
expect(member.details.email).toBe('
|
|
151
|
+
expect(member.details.email).toBe('original@example.com'); // this has been merged
|
|
152
|
+
expect(member.details.alternativeEmails).toEqual(['anewemail@example.com']); // this has been merged
|
|
150
153
|
|
|
151
154
|
// Check the registration is still there
|
|
152
155
|
expect(member.registrations.length).toBe(1);
|
|
@@ -164,7 +164,6 @@ export class PlatformMembershipService {
|
|
|
164
164
|
// Every (not-locked) period can have a generated membership
|
|
165
165
|
for (const period of periods) {
|
|
166
166
|
const registrations = me.registrations.filter(r => r.group.periodId === period.id && r.registeredAt && !r.deactivatedAt);
|
|
167
|
-
const now = new Date();
|
|
168
167
|
|
|
169
168
|
const defaultMemberships = registrations.flatMap((r) => {
|
|
170
169
|
if (!r.group.defaultAgeGroupId) {
|
|
@@ -248,12 +247,26 @@ export class PlatformMembershipService {
|
|
|
248
247
|
|
|
249
248
|
const shouldApplyReducedPrice = me.details.shouldApplyReducedPrice;
|
|
250
249
|
|
|
250
|
+
// We'll by default give this member the most cheap membership it can get, and if the price is the same, the membership associated with the first registration
|
|
251
251
|
const cheapestMembership = defaultMembershipsWithOrganization.sort((a, b) => {
|
|
252
252
|
const tagIdsA = a.organization?.meta.tags ?? [];
|
|
253
253
|
const tagIdsB = b.organization?.meta.tags ?? [];
|
|
254
|
-
const
|
|
254
|
+
const aPrice = a.membership.getPrice(
|
|
255
|
+
period.id,
|
|
256
|
+
a.registration.startDate ?? a.registration.registeredAt ?? a.registration.createdAt,
|
|
257
|
+
tagIdsA,
|
|
258
|
+
shouldApplyReducedPrice
|
|
259
|
+
) ?? 10000000;
|
|
260
|
+
const bPrice = b.membership.getPrice(
|
|
261
|
+
period.id,
|
|
262
|
+
b.registration.startDate ?? b.registration.registeredAt ?? b.registration.createdAt,
|
|
263
|
+
tagIdsB,
|
|
264
|
+
shouldApplyReducedPrice
|
|
265
|
+
) ?? 10000000;
|
|
266
|
+
|
|
267
|
+
const diff = aPrice - bPrice;
|
|
255
268
|
if (diff === 0) {
|
|
256
|
-
return Sorter.byDateValue(b.registration.startDate ?? b.registration.createdAt, a.registration.startDate ?? a.registration.createdAt);
|
|
269
|
+
return Sorter.byDateValue(b.registration.startDate ?? b.registration.registeredAt ?? b.registration.createdAt, a.registration.startDate ?? a.registration.registeredAt ?? a.registration.createdAt);
|
|
257
270
|
}
|
|
258
271
|
return diff;
|
|
259
272
|
})[0];
|
|
@@ -264,6 +277,7 @@ export class PlatformMembershipService {
|
|
|
264
277
|
}
|
|
265
278
|
|
|
266
279
|
// Check if already have the same membership
|
|
280
|
+
// if that is the case, we'll keep that one and update the price + dates if the organization matches the cheapest/earliest membership
|
|
267
281
|
let didFind = false;
|
|
268
282
|
for (const m of activeMemberships) {
|
|
269
283
|
if (m.membershipTypeId === cheapestMembership.membership.id && m.organizationId === cheapestMembership.registration.organizationId) {
|
|
@@ -303,6 +317,8 @@ export class PlatformMembershipService {
|
|
|
303
317
|
membership.membershipTypeId = cheapestMembership.membership.id;
|
|
304
318
|
membership.organizationId = cheapestMembership.registration.organizationId;
|
|
305
319
|
membership.periodId = period.id;
|
|
320
|
+
|
|
321
|
+
// Note: the dates will get modified in the price calculation
|
|
306
322
|
membership.startDate = periodConfig.startDate;
|
|
307
323
|
membership.endDate = periodConfig.endDate;
|
|
308
324
|
membership.expireDate = periodConfig.expireDate;
|
|
@@ -257,6 +257,7 @@ export const memberFilterCompilers: SQLFilterDefinitions = {
|
|
|
257
257
|
organizationId: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'organizationId')),
|
|
258
258
|
periodId: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'periodId')),
|
|
259
259
|
price: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'price')),
|
|
260
|
+
priceWithoutDiscount: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'priceWithoutDiscount')),
|
|
260
261
|
startDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'startDate')),
|
|
261
262
|
endDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'endDate')),
|
|
262
263
|
expireDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'expireDate')),
|