@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.77.0",
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.0",
41
- "@stamhoofd/backend-middleware": "2.77.0",
42
- "@stamhoofd/email": "2.77.0",
43
- "@stamhoofd/models": "2.77.0",
44
- "@stamhoofd/queues": "2.77.0",
45
- "@stamhoofd/sql": "2.77.0",
46
- "@stamhoofd/structures": "2.77.0",
47
- "@stamhoofd/utility": "2.77.0",
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": "209036d412e24fefe961eeeeaa5a8618297bfcd7"
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
- scopeFilter = {
46
- organizationId: organization.id,
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
- // Check duplicate memberships
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
- const membership = new MemberPlatformMembership();
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('anewemail@example.com'); // this has been merged
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 diff = a.membership.getPrice(period.id, a.registration.startDate ?? a.registration.registeredAt ?? now, tagIdsA, shouldApplyReducedPrice)! - b.membership.getPrice(period.id, a.registration.startDate ?? a.registration.registeredAt ?? now, tagIdsB, shouldApplyReducedPrice)!;
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')),