@stamhoofd/backend 2.76.0 → 2.77.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/package.json +11 -11
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +4 -3
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +58 -1
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +160 -0
- package/src/excel-loaders/payments.ts +26 -0
- package/src/helpers/AuthenticatedStructures.ts +2 -1
- package/src/helpers/MembershipCharger.ts +10 -5
- package/src/services/MemberNumberService.ts +69 -2
- package/src/services/SSOService.ts +1 -0
- package/src/sql-filters/members.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.77.0",
|
|
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.
|
|
38
|
+
"@simonbackx/simple-endpoints": "1.19.1",
|
|
39
39
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
40
|
-
"@stamhoofd/backend-i18n": "2.
|
|
41
|
-
"@stamhoofd/backend-middleware": "2.
|
|
42
|
-
"@stamhoofd/email": "2.
|
|
43
|
-
"@stamhoofd/models": "2.
|
|
44
|
-
"@stamhoofd/queues": "2.
|
|
45
|
-
"@stamhoofd/sql": "2.
|
|
46
|
-
"@stamhoofd/structures": "2.
|
|
47
|
-
"@stamhoofd/utility": "2.
|
|
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",
|
|
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": "209036d412e24fefe961eeeeaa5a8618297bfcd7"
|
|
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(
|
|
@@ -532,7 +532,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
532
532
|
membership.startDate = new Date(Math.max(Date.now(), put.startDate.getTime()));
|
|
533
533
|
membership.endDate = put.endDate;
|
|
534
534
|
membership.expireDate = put.expireDate;
|
|
535
|
-
membership.locked =
|
|
535
|
+
membership.locked = false;
|
|
536
536
|
|
|
537
537
|
await membership.calculatePrice(member);
|
|
538
538
|
await membership.save();
|
|
@@ -540,6 +540,63 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
540
540
|
updateMembershipMemberIds.add(member.id);
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
for (const p of patch.platformMemberships.getPatches()) {
|
|
544
|
+
const membership = await MemberPlatformMembership.getByID(p.id);
|
|
545
|
+
|
|
546
|
+
if (!membership || membership.memberId !== member.id) {
|
|
547
|
+
throw new SimpleError({
|
|
548
|
+
code: 'invalid_field',
|
|
549
|
+
field: 'id',
|
|
550
|
+
message: 'Invalid id',
|
|
551
|
+
human: 'Deze aansluiting bestaat niet',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!await Context.auth.hasFullAccess(membership.organizationId)) {
|
|
556
|
+
throw Context.auth.error('Je hebt niet voldoende rechten om deze aansluiting aan te passen');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (membership.periodId !== platform.periodId) {
|
|
560
|
+
const period = await RegistrationPeriod.getByID(membership.periodId);
|
|
561
|
+
|
|
562
|
+
if (!period) {
|
|
563
|
+
throw new SimpleError({
|
|
564
|
+
code: 'invalid_field',
|
|
565
|
+
message: 'Invalid period',
|
|
566
|
+
human: $t(`82af2364-c711-4e44-a871-9346c2cab66a`),
|
|
567
|
+
field: 'periodId',
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (period?.locked) {
|
|
572
|
+
throw new SimpleError({
|
|
573
|
+
code: 'invalid_field',
|
|
574
|
+
message: 'Invalid period',
|
|
575
|
+
human: $t(`92a41b40-9841-4326-abaf-a8a7d97e5d55`),
|
|
576
|
+
field: 'periodId',
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// For now only alter 'locked'
|
|
582
|
+
if (Context.auth.hasPlatformFullAccess()) {
|
|
583
|
+
membership.locked = p.locked ?? membership.locked;
|
|
584
|
+
await membership.save();
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
if (p.locked === true) {
|
|
588
|
+
throw Context.auth.error($t('bbc639c8-abdb-42d8-b5ed-f58084886ad9'));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (p.locked === false) {
|
|
592
|
+
throw Context.auth.error($t('c6494677-86f0-4d2e-b9ac-bedfc9e87187'));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
updateMembershipsForOrganizations.add(membership.organizationId); // can influence free memberships in other members of same organization
|
|
597
|
+
updateMembershipMemberIds.add(member.id);
|
|
598
|
+
}
|
|
599
|
+
|
|
543
600
|
// Delete platform memberships
|
|
544
601
|
for (const id of patch.platformMemberships.getDeletes()) {
|
|
545
602
|
const membership = await MemberPlatformMembership.getByID(id);
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
});
|
|
91
|
+
|
|
92
|
+
test('A duplicate member with existing registrations returns those registrations after a merge', async () => {
|
|
93
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
94
|
+
const user = await new UserFactory({ organization }).create();
|
|
95
|
+
const details = MemberDetails.create({
|
|
96
|
+
firstName,
|
|
97
|
+
lastName,
|
|
98
|
+
securityCode: 'ABC-123',
|
|
99
|
+
parents: [
|
|
100
|
+
Parent.create({
|
|
101
|
+
firstName: 'Jane',
|
|
102
|
+
lastName: 'Doe',
|
|
103
|
+
email: 'jane.doe@example.com',
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const existingMember = await new MemberFactory({
|
|
109
|
+
organization,
|
|
110
|
+
birthDay,
|
|
111
|
+
details,
|
|
112
|
+
}).create();
|
|
113
|
+
|
|
114
|
+
// Create a registration for this member
|
|
115
|
+
const group = await new GroupFactory({ organization }).create();
|
|
116
|
+
const registration = await new RegistrationFactory({
|
|
117
|
+
member: existingMember,
|
|
118
|
+
group,
|
|
119
|
+
}).create();
|
|
120
|
+
|
|
121
|
+
const token = await Token.createToken(user);
|
|
122
|
+
|
|
123
|
+
const arr: Body = new PatchableArray();
|
|
124
|
+
const put = MemberWithRegistrationsBlob.create({
|
|
125
|
+
details: MemberDetails.create({
|
|
126
|
+
firstName,
|
|
127
|
+
lastName,
|
|
128
|
+
birthDay: new Date(existingMember.details.birthDay!.getTime() + 1),
|
|
129
|
+
securityCode: existingMember.details.securityCode,
|
|
130
|
+
email: 'anewemail@example.com',
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
arr.addPut(put);
|
|
134
|
+
|
|
135
|
+
const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
|
|
136
|
+
request.headers.authorization = 'Bearer ' + token.accessToken;
|
|
137
|
+
const response = await testServer.test(endpoint, request);
|
|
138
|
+
expect(response.status).toBe(200);
|
|
139
|
+
|
|
140
|
+
// Check id of the returned memebr matches the existing member
|
|
141
|
+
expect(response.body.members.length).toBe(1);
|
|
142
|
+
expect(response.body.members[0].id).toBe(existingMember.id);
|
|
143
|
+
|
|
144
|
+
// Check data matches the original data + changes from the put
|
|
145
|
+
const member = response.body.members[0];
|
|
146
|
+
expect(member.details.firstName).toBe(firstName);
|
|
147
|
+
expect(member.details.lastName).toBe(lastName);
|
|
148
|
+
expect(member.details.birthDay).toEqual(existingMember.details.birthDay);
|
|
149
|
+
expect(member.details.email).toBe('anewemail@example.com'); // this has been merged
|
|
150
|
+
|
|
151
|
+
// Check the registration is still there
|
|
152
|
+
expect(member.registrations.length).toBe(1);
|
|
153
|
+
expect(member.registrations[0].id).toBe(registration.id);
|
|
154
|
+
|
|
155
|
+
// Check parent is still there
|
|
156
|
+
expect(member.details.parents.length).toBe(1);
|
|
157
|
+
expect(member.details.parents[0]).toEqual(existingMember.details.parents[0]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -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
|
-
.
|
|
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,
|
|
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
|
|