@stamhoofd/backend 2.76.0 → 2.77.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.76.0",
3
+ "version": "2.77.1",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -35,16 +35,16 @@
35
35
  "@mollie/api-client": "3.7.0",
36
36
  "@simonbackx/simple-database": "1.29.0",
37
37
  "@simonbackx/simple-encoding": "2.20.0",
38
- "@simonbackx/simple-endpoints": "1.19.0",
38
+ "@simonbackx/simple-endpoints": "1.19.1",
39
39
  "@simonbackx/simple-logging": "^1.0.1",
40
- "@stamhoofd/backend-i18n": "2.76.0",
41
- "@stamhoofd/backend-middleware": "2.76.0",
42
- "@stamhoofd/email": "2.76.0",
43
- "@stamhoofd/models": "2.76.0",
44
- "@stamhoofd/queues": "2.76.0",
45
- "@stamhoofd/sql": "2.76.0",
46
- "@stamhoofd/structures": "2.76.0",
47
- "@stamhoofd/utility": "2.76.0",
40
+ "@stamhoofd/backend-i18n": "2.77.1",
41
+ "@stamhoofd/backend-middleware": "2.77.1",
42
+ "@stamhoofd/email": "2.77.1",
43
+ "@stamhoofd/models": "2.77.1",
44
+ "@stamhoofd/queues": "2.77.1",
45
+ "@stamhoofd/sql": "2.77.1",
46
+ "@stamhoofd/structures": "2.77.1",
47
+ "@stamhoofd/utility": "2.77.1",
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": "85e0625ba777d83d92ad582443df1666955d51f6"
67
+ "gitHead": "e6d9dce8ddfdfaf5380e98f3a0ffd113a3826955"
68
68
  }
@@ -1,10 +1,9 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { Platform } from '@stamhoofd/models';
3
+ import { QueueHandler } from '@stamhoofd/queues';
2
4
  import { SQL, SQLAlias, SQLCount, SQLDistinct, SQLSelectAs, SQLSum, SQLWhereSign } from '@stamhoofd/sql';
3
5
  import { ChargeMembershipsSummary, ChargeMembershipsTypeSummary } from '@stamhoofd/structures';
4
6
  import { Context } from '../../../helpers/Context';
5
- import { QueueHandler } from '@stamhoofd/queues';
6
- import { Platform } from '@stamhoofd/models';
7
- import { SimpleError } from '@simonbackx/simple-errors';
8
7
 
9
8
  type Params = Record<string, never>;
10
9
  type Query = Record<string, never>;
@@ -102,6 +101,7 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
102
101
  .from('member_platform_memberships')
103
102
  .where('balanceItemId', null)
104
103
  .where('deletedAt', null)
104
+ .where('locked', false)
105
105
  .whereNot('organizationId', chargeVia);
106
106
 
107
107
  if (!trial) {
@@ -164,6 +164,7 @@ export class GetChargeMembershipsSummaryEndpoint extends Endpoint<Params, Query,
164
164
  .from('member_platform_memberships')
165
165
  .where('balanceItemId', null)
166
166
  .where('deletedAt', null)
167
+ .where('locked', false)
167
168
  .where(trialQ)
168
169
  .whereNot('organizationId', chargeVia)
169
170
  .groupBy(
@@ -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;
@@ -532,7 +533,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
532
533
  membership.startDate = new Date(Math.max(Date.now(), put.startDate.getTime()));
533
534
  membership.endDate = put.endDate;
534
535
  membership.expireDate = put.expireDate;
535
- membership.locked = put.locked;
536
+ membership.locked = false;
536
537
 
537
538
  await membership.calculatePrice(member);
538
539
  await membership.save();
@@ -540,6 +541,63 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
540
541
  updateMembershipMemberIds.add(member.id);
541
542
  }
542
543
 
544
+ for (const p of patch.platformMemberships.getPatches()) {
545
+ const membership = await MemberPlatformMembership.getByID(p.id);
546
+
547
+ if (!membership || membership.memberId !== member.id) {
548
+ throw new SimpleError({
549
+ code: 'invalid_field',
550
+ field: 'id',
551
+ message: 'Invalid id',
552
+ human: 'Deze aansluiting bestaat niet',
553
+ });
554
+ }
555
+
556
+ if (!await Context.auth.hasFullAccess(membership.organizationId)) {
557
+ throw Context.auth.error('Je hebt niet voldoende rechten om deze aansluiting aan te passen');
558
+ }
559
+
560
+ if (membership.periodId !== platform.periodId) {
561
+ const period = await RegistrationPeriod.getByID(membership.periodId);
562
+
563
+ if (!period) {
564
+ throw new SimpleError({
565
+ code: 'invalid_field',
566
+ message: 'Invalid period',
567
+ human: $t(`82af2364-c711-4e44-a871-9346c2cab66a`),
568
+ field: 'periodId',
569
+ });
570
+ }
571
+
572
+ if (period?.locked) {
573
+ throw new SimpleError({
574
+ code: 'invalid_field',
575
+ message: 'Invalid period',
576
+ human: $t(`92a41b40-9841-4326-abaf-a8a7d97e5d55`),
577
+ field: 'periodId',
578
+ });
579
+ }
580
+ }
581
+
582
+ // For now only alter 'locked'
583
+ if (Context.auth.hasPlatformFullAccess()) {
584
+ membership.locked = p.locked ?? membership.locked;
585
+ await membership.save();
586
+ }
587
+ else {
588
+ if (p.locked === true) {
589
+ throw Context.auth.error($t('bbc639c8-abdb-42d8-b5ed-f58084886ad9'));
590
+ }
591
+
592
+ if (p.locked === false) {
593
+ throw Context.auth.error($t('c6494677-86f0-4d2e-b9ac-bedfc9e87187'));
594
+ }
595
+ }
596
+
597
+ updateMembershipsForOrganizations.add(membership.organizationId); // can influence free memberships in other members of same organization
598
+ updateMembershipMemberIds.add(member.id);
599
+ }
600
+
543
601
  // Delete platform memberships
544
602
  for (const id of patch.platformMemberships.getDeletes()) {
545
603
  const membership = await MemberPlatformMembership.getByID(id);
@@ -737,6 +795,29 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
737
795
 
738
796
  // Grant temporary access to this member without needing to enter the security code again
739
797
  await Context.auth.temporarilyGrantMemberAccess(member, PermissionLevel.Write);
798
+
799
+ const log = new AuditLog();
800
+
801
+ // a member has multiple organizations, so this is difficult to determine - for now it is only visible in the admin panel
802
+ log.organizationId = member.organizationId;
803
+
804
+ log.type = AuditLogType.MemberSecurityCodeUsed;
805
+ log.source = AuditLogSource.Anonymous;
806
+
807
+ if (Context.user) {
808
+ log.userId = Context.user.id;
809
+ log.source = AuditLogSource.User;
810
+ }
811
+
812
+ log.objectId = member.id;
813
+ log.replacements = new Map([
814
+ ['m', AuditLogReplacement.create({
815
+ value: member.details.name,
816
+ type: AuditLogReplacementType.Member,
817
+ id: member.id,
818
+ })],
819
+ ]);
820
+ await log.save();
740
821
  }
741
822
  else {
742
823
  throw new SimpleError({
@@ -0,0 +1,163 @@
1
+ import { PatchableArray } from '@simonbackx/simple-encoding';
2
+ import { Endpoint, Request } from '@simonbackx/simple-endpoints';
3
+ import { GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import { MemberDetails, MemberWithRegistrationsBlob, Parent } from '@stamhoofd/structures';
5
+ import { testServer } from '../../../../tests/helpers/TestServer';
6
+ import { PatchUserMembersEndpoint } from './PatchUserMembersEndpoint';
7
+
8
+ const baseUrl = `/members`;
9
+ const endpoint = new PatchUserMembersEndpoint();
10
+ type EndpointType = typeof endpoint;
11
+ type Body = EndpointType extends Endpoint<any, any, infer B, any> ? B : never;
12
+
13
+ const firstName = 'John';
14
+ const lastName = 'Doe';
15
+ const birthDay = { year: 1993, month: 4, day: 5 };
16
+
17
+ const errorWithCode = (code: string) => expect.objectContaining({ code }) as jest.Constructable;
18
+
19
+ describe('Endpoint.PatchUserMembersEndpoint', () => {
20
+ describe('Duplicate members', () => {
21
+ test('The security code should be a requirement', async () => {
22
+ const organization = await new OrganizationFactory({ }).create();
23
+ const user = await new UserFactory({ organization }).create();
24
+ const existingMember = await new MemberFactory({
25
+ organization,
26
+ firstName,
27
+ lastName,
28
+ birthDay,
29
+ generateData: true,
30
+ }).create();
31
+
32
+ const token = await Token.createToken(user);
33
+
34
+ const arr: Body = new PatchableArray();
35
+ const put = MemberWithRegistrationsBlob.create({
36
+ details: MemberDetails.create({
37
+ firstName,
38
+ lastName,
39
+ birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
40
+ }),
41
+ });
42
+ arr.addPut(put);
43
+
44
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
45
+ request.headers.authorization = 'Bearer ' + token.accessToken;
46
+ await expect(testServer.test(endpoint, request))
47
+ .rejects
48
+ .toThrow(errorWithCode('known_member_missing_rights'));
49
+ });
50
+
51
+ test('The security code is not a requirement for members without additional data', async () => {
52
+ const organization = await new OrganizationFactory({ }).create();
53
+ const user = await new UserFactory({ organization }).create();
54
+ const existingMember = await new MemberFactory({
55
+ organization,
56
+ firstName,
57
+ lastName,
58
+ birthDay,
59
+ generateData: false,
60
+ }).create();
61
+
62
+ const token = await Token.createToken(user);
63
+
64
+ const arr: Body = new PatchableArray();
65
+ const put = MemberWithRegistrationsBlob.create({
66
+ details: MemberDetails.create({
67
+ firstName,
68
+ lastName,
69
+ birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
70
+ email: 'anewemail@example.com',
71
+ }),
72
+ });
73
+ arr.addPut(put);
74
+
75
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
76
+ request.headers.authorization = 'Bearer ' + token.accessToken;
77
+ const response = await testServer.test(endpoint, request);
78
+ expect(response.status).toBe(200);
79
+
80
+ // Check id of the returned memebr matches the existing member
81
+ expect(response.body.members.length).toBe(1);
82
+ expect(response.body.members[0].id).toBe(existingMember.id);
83
+
84
+ // Check data matches the original data + changes from the put
85
+ const member = response.body.members[0];
86
+ expect(member.details.firstName).toBe(firstName);
87
+ expect(member.details.lastName).toBe(lastName);
88
+ expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
89
+ expect(member.details.email).toBe('anewemail@example.com'); // this has been merged
90
+ expect(member.details.alternativeEmails).toHaveLength(0);
91
+ });
92
+
93
+ test('A duplicate member with existing registrations returns those registrations after a merge', async () => {
94
+ const organization = await new OrganizationFactory({ }).create();
95
+ const user = await new UserFactory({ organization }).create();
96
+ const details = MemberDetails.create({
97
+ firstName,
98
+ lastName,
99
+ securityCode: 'ABC-123',
100
+ email: 'original@example.com',
101
+ parents: [
102
+ Parent.create({
103
+ firstName: 'Jane',
104
+ lastName: 'Doe',
105
+ email: 'jane.doe@example.com',
106
+ }),
107
+ ],
108
+ });
109
+
110
+ const existingMember = await new MemberFactory({
111
+ organization,
112
+ birthDay,
113
+ details,
114
+ }).create();
115
+
116
+ // Create a registration for this member
117
+ const group = await new GroupFactory({ organization }).create();
118
+ const registration = await new RegistrationFactory({
119
+ member: existingMember,
120
+ group,
121
+ }).create();
122
+
123
+ const token = await Token.createToken(user);
124
+
125
+ const arr: Body = new PatchableArray();
126
+ const put = MemberWithRegistrationsBlob.create({
127
+ details: MemberDetails.create({
128
+ firstName,
129
+ lastName,
130
+ birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
131
+ securityCode: existingMember.details.securityCode,
132
+ email: 'anewemail@example.com',
133
+ }),
134
+ });
135
+ arr.addPut(put);
136
+
137
+ const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
138
+ request.headers.authorization = 'Bearer ' + token.accessToken;
139
+ const response = await testServer.test(endpoint, request);
140
+ expect(response.status).toBe(200);
141
+
142
+ // Check id of the returned memebr matches the existing member
143
+ expect(response.body.members.length).toBe(1);
144
+ expect(response.body.members[0].id).toBe(existingMember.id);
145
+
146
+ // Check data matches the original data + changes from the put
147
+ const member = response.body.members[0];
148
+ expect(member.details.firstName).toBe(firstName);
149
+ expect(member.details.lastName).toBe(lastName);
150
+ expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
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
153
+
154
+ // Check the registration is still there
155
+ expect(member.registrations.length).toBe(1);
156
+ expect(member.registrations[0].id).toBe(registration.id);
157
+
158
+ // Check parent is still there
159
+ expect(member.details.parents.length).toBe(1);
160
+ expect(member.details.parents[0]).toEqual(existingMember.details.parents[0]);
161
+ });
162
+ });
163
+ });
@@ -147,6 +147,32 @@ function getBalanceItemColumns(): XlsxTransformerColumn<PaymentWithItem>[] {
147
147
  value: object.balanceItemPayment.balanceItem.description,
148
148
  }),
149
149
  },
150
+ {
151
+ id: 'balanceItem.createdAt',
152
+ name: 'Aangerekend op',
153
+ width: 16,
154
+ getValue: (object: PaymentWithItem) => ({
155
+ value: object.balanceItemPayment.balanceItem.createdAt,
156
+ style: {
157
+ numberFormat: {
158
+ id: XlsxBuiltInNumberFormat.DateTimeSlash,
159
+ },
160
+ },
161
+ }),
162
+ },
163
+ {
164
+ id: 'balanceItem.dueAt',
165
+ name: 'Verschuldigd vanaf',
166
+ width: 16,
167
+ getValue: (object: PaymentWithItem) => ({
168
+ value: object.balanceItemPayment.balanceItem.dueAt,
169
+ style: {
170
+ numberFormat: {
171
+ id: XlsxBuiltInNumberFormat.DateTimeSlash,
172
+ },
173
+ },
174
+ }),
175
+ },
150
176
  {
151
177
  match: (id) => {
152
178
  if (id.startsWith('balanceItem.relations.')) {
@@ -1,6 +1,7 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
2
  import { AuditLog, BalanceItem, CachedBalance, Document, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
3
3
  import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
+ import { Sorter } from '@stamhoofd/utility';
4
5
 
5
6
  import { SQL } from '@stamhoofd/sql';
6
7
  import { Formatter } from '@stamhoofd/utility';
@@ -254,7 +255,7 @@ export class AuthenticatedStructures {
254
255
  ...baseStruct,
255
256
  period,
256
257
  privateMeta: organization.privateMeta,
257
- webshops: webshopPreviews.get(organization.id),
258
+ webshops: webshopPreviews.get(organization.id)?.sort((a, b) => Sorter.byDateValue(b.createdAt, a.createdAt)),
258
259
  });
259
260
  }
260
261
  else {
@@ -35,7 +35,7 @@ export const MembershipCharger = {
35
35
  .where('id', SQLWhereSign.Greater, lastId)
36
36
  .where('balanceItemId', null)
37
37
  .where('deletedAt', null)
38
- .whereNot('organizationId', chargeVia)
38
+ .where('locked', false)
39
39
  .where(SQL.where('trialUntil', null).or('trialUntil', SQLWhereSign.LessEqual, new Date()))
40
40
  .limit(chunkSize)
41
41
  .orderBy(
@@ -66,10 +66,6 @@ export const MembershipCharger = {
66
66
  continue;
67
67
  }
68
68
 
69
- if (membership.organizationId === chargeVia) {
70
- continue;
71
- }
72
-
73
69
  const member = members.find(m => m.id === membership.memberId);
74
70
 
75
71
  if (!member) {
@@ -86,6 +82,15 @@ export const MembershipCharger = {
86
82
  continue;
87
83
  }
88
84
 
85
+ // Lock membership
86
+ membership.locked = true;
87
+
88
+ if (membership.organizationId === chargeVia) {
89
+ // Do not charge
90
+ await membership.save();
91
+ continue;
92
+ }
93
+
89
94
  const balanceItem = new BalanceItem();
90
95
  balanceItem.unitPrice = membership.price;
91
96
  balanceItem.amount = 1;
@@ -1,7 +1,7 @@
1
1
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
2
2
  import { Member, MemberPlatformMembership, Organization } from '@stamhoofd/models';
3
3
  import { QueueHandler } from '@stamhoofd/queues';
4
- import { SQL, SQLWhereLike, scalarToSQLExpression } from '@stamhoofd/sql';
4
+ import { scalarToSQLExpression, SQL, SQLCharLength, SQLWhereLike } from '@stamhoofd/sql';
5
5
 
6
6
  export class MemberNumberService {
7
7
  static async assignMemberNumber(member: Member, membership: MemberPlatformMembership) {
@@ -10,9 +10,20 @@ export class MemberNumberService {
10
10
  return;
11
11
  }
12
12
 
13
+ if (!STAMHOOFD.MEMBER_NUMBER_ALGORITHM) {
14
+ console.warn('No member number algorithm set. Please set STAMHOOFD.MEMBER_NUMBER_ALGORITHM in the environment.');
15
+ return;
16
+ }
17
+
13
18
  return await QueueHandler.schedule('assignMemberNumber', async function (this: undefined) {
14
19
  try {
15
20
  const memberNumber = await MemberNumberService.createMemberNumber(member, membership);
21
+
22
+ if (memberNumber === undefined) {
23
+ console.warn('No valid member number algorithm set. Please set a valid STAMHOOFD.MEMBER_NUMBER_ALGORITHM in the environment.');
24
+ return;
25
+ }
26
+
16
27
  member.details.memberNumber = memberNumber;
17
28
  await member.save();
18
29
  }
@@ -32,7 +43,16 @@ export class MemberNumberService {
32
43
  });
33
44
  }
34
45
 
35
- static async createMemberNumber(member: Member, membership: MemberPlatformMembership): Promise<string> {
46
+ private static async createMemberNumber(member: Member, membership: MemberPlatformMembership): Promise<string | undefined> {
47
+ if (STAMHOOFD.MEMBER_NUMBER_ALGORITHM === MemberNumberAlgorithm.Incremental) {
48
+ return this.createIncrementalMemberNumber();
49
+ }
50
+ else if (STAMHOOFD.MEMBER_NUMBER_ALGORITHM === MemberNumberAlgorithm.KSA) {
51
+ return this.createKSAMemberNumber(member, membership);
52
+ }
53
+ }
54
+
55
+ private static async createKSAMemberNumber(member: Member, membership: MemberPlatformMembership): Promise<string> {
36
56
  // example: 5301-101012-1
37
57
 
38
58
  // #region get birth date part (ddmmjj)
@@ -117,4 +137,51 @@ export class MemberNumberService {
117
137
 
118
138
  return memberNumber;
119
139
  }
140
+
141
+ private static largestMemberNumberCache: number | null = null;
142
+
143
+ private static async createIncrementalMemberNumber(): Promise<string> {
144
+ const requiredLength = STAMHOOFD.MEMBER_NUMBER_ALGORITHM_LENGTH; // Required for reliable sorting (sorting strings with different length will cause unexpected results)
145
+
146
+ if (!requiredLength || typeof requiredLength !== 'number') {
147
+ throw new Error('When using the Incremental member number algorithm, you need to set STAMHOOFD.MEMBER_NUMBER_ALGORITHM_LENGTH in the environment.');
148
+ }
149
+
150
+ let nextNumber = 1;
151
+
152
+ if (this.largestMemberNumberCache !== null) {
153
+ nextNumber = this.largestMemberNumberCache + 1;
154
+ }
155
+ else {
156
+ // Find largest member number in the database
157
+ // The required length prevents string-sorting with different lengths, causing an unexpected order
158
+ const query = await SQL.select('memberNumber')
159
+ .from(SQL.table('members'))
160
+ .where(
161
+ new SQLCharLength(SQL.column('memberNumber')),
162
+ requiredLength,
163
+ )
164
+ .limit(1)
165
+ .orderBy('memberNumber', 'DESC')
166
+ .fetch();
167
+
168
+ const largestMemberNumber = query[0]?.['members']?.['memberNumber'];
169
+
170
+ if (largestMemberNumber && typeof largestMemberNumber === 'string') {
171
+ const parsed = parseInt(largestMemberNumber);
172
+ if (!isNaN(parsed)) {
173
+ nextNumber = parsed + 1;
174
+ }
175
+ }
176
+ }
177
+
178
+ this.largestMemberNumberCache = nextNumber;
179
+ const str = nextNumber.toString().padStart(requiredLength, '0');
180
+
181
+ if (str.length !== requiredLength) {
182
+ throw new Error('Reached maximum member number length ' + str);
183
+ }
184
+
185
+ return str;
186
+ }
120
187
  }
@@ -613,6 +613,7 @@ export class SSOServiceWithSession {
613
613
  if (!user.lastName || !user.hasPasswordBasedAccount()) {
614
614
  user.lastName = lastName;
615
615
  }
616
+ user.verified = true;
616
617
  user.linkLoginProvider(this.service.provider, claims.sub, !!this.session.userId);
617
618
  await user.save();
618
619
  }
@@ -257,11 +257,13 @@ 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
- invoiceId: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'invoiceId')),
261
260
  startDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'startDate')),
262
261
  endDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'endDate')),
263
262
  expireDate: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'expireDate')),
264
263
  trialUntil: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'trialUntil')),
264
+ locked: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'locked')),
265
+ balanceItemId: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'balanceItemId')),
266
+ generated: createSQLColumnFilterCompiler(SQL.column('member_platform_memberships', 'generated')),
265
267
  },
266
268
  ),
267
269