@stamhoofd/backend 2.136.3 → 2.137.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 +28 -28
- package/src/audit-logs/GroupLogger.ts +13 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +17 -1
- package/src/endpoints/organization/webshops/RetrieveUitpasSocialTariffPriceEndpoint.ts +2 -1
- package/src/helpers/AuthenticatedStructures.ts +6 -1
- package/src/helpers/UitpasTokenRepository.ts +88 -34
- package/src/seeds/1784795228-groups-event-id.ts +35 -0
- package/src/seeds/1784806109-update-cached-outstanding-balance-for-merged-members.ts +33 -0
- package/src/services/InvoicePdfService.ts +2 -3
- package/src/services/SSOService.ts +1 -0
- package/src/services/uitpas/UitpasService.test.ts +3 -3
- package/src/services/uitpas/UitpasService.ts +17 -12
- package/src/services/uitpas/checkPermissionsFor.ts +16 -15
- package/src/sql-filters/groups.ts +5 -0
- package/src/seeds/1760702454-update-cached-outstanding-balance-from-items.ts +0 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.137.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -46,34 +46,34 @@
|
|
|
46
46
|
"test:reset": "yarn -s build:full && yarn -s test"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@aws-sdk/client-s3": "3.
|
|
50
|
-
"@aws-sdk/client-ses": "3.
|
|
51
|
-
"@aws-sdk/client-sesv2": "3.
|
|
52
|
-
"@aws-sdk/client-sqs": "3.
|
|
53
|
-
"@aws-sdk/s3-request-presigner": "3.
|
|
49
|
+
"@aws-sdk/client-s3": "3.1090.0",
|
|
50
|
+
"@aws-sdk/client-ses": "3.1090.0",
|
|
51
|
+
"@aws-sdk/client-sesv2": "3.1090.0",
|
|
52
|
+
"@aws-sdk/client-sqs": "3.1090.0",
|
|
53
|
+
"@aws-sdk/s3-request-presigner": "3.1090.0",
|
|
54
54
|
"@bwip-js/node": "4.11.1",
|
|
55
|
-
"@mollie/api-client": "4.
|
|
55
|
+
"@mollie/api-client": "4.6.0",
|
|
56
56
|
"@simonbackx/simple-database": "1.37.1",
|
|
57
57
|
"@simonbackx/simple-encoding": "2.26.11",
|
|
58
58
|
"@simonbackx/simple-endpoints": "1.21.1",
|
|
59
59
|
"@simonbackx/simple-errors": "1.5.0",
|
|
60
60
|
"@simonbackx/simple-logging": "1.0.1",
|
|
61
|
-
"@stamhoofd/backend-env": "2.
|
|
62
|
-
"@stamhoofd/backend-i18n": "2.
|
|
63
|
-
"@stamhoofd/backend-middleware": "2.
|
|
64
|
-
"@stamhoofd/crons": "2.
|
|
65
|
-
"@stamhoofd/email": "2.
|
|
66
|
-
"@stamhoofd/excel-writer": "2.
|
|
67
|
-
"@stamhoofd/logging": "2.
|
|
68
|
-
"@stamhoofd/models": "2.
|
|
69
|
-
"@stamhoofd/object-differ": "2.
|
|
70
|
-
"@stamhoofd/queues": "2.
|
|
71
|
-
"@stamhoofd/sql": "2.
|
|
72
|
-
"@stamhoofd/structures": "2.
|
|
73
|
-
"@stamhoofd/types": "2.
|
|
74
|
-
"@stamhoofd/utility": "2.
|
|
61
|
+
"@stamhoofd/backend-env": "2.137.0",
|
|
62
|
+
"@stamhoofd/backend-i18n": "2.137.0",
|
|
63
|
+
"@stamhoofd/backend-middleware": "2.137.0",
|
|
64
|
+
"@stamhoofd/crons": "2.137.0",
|
|
65
|
+
"@stamhoofd/email": "2.137.0",
|
|
66
|
+
"@stamhoofd/excel-writer": "2.137.0",
|
|
67
|
+
"@stamhoofd/logging": "2.137.0",
|
|
68
|
+
"@stamhoofd/models": "2.137.0",
|
|
69
|
+
"@stamhoofd/object-differ": "2.137.0",
|
|
70
|
+
"@stamhoofd/queues": "2.137.0",
|
|
71
|
+
"@stamhoofd/sql": "2.137.0",
|
|
72
|
+
"@stamhoofd/structures": "2.137.0",
|
|
73
|
+
"@stamhoofd/types": "2.137.0",
|
|
74
|
+
"@stamhoofd/utility": "2.137.0",
|
|
75
75
|
"archiver": "7.0.1",
|
|
76
|
-
"axios": "1.
|
|
76
|
+
"axios": "1.18.0",
|
|
77
77
|
"base-x": "3.0.11",
|
|
78
78
|
"chalk": "5.6.2",
|
|
79
79
|
"cookie": "0.7.2",
|
|
@@ -86,18 +86,18 @@
|
|
|
86
86
|
"luxon": "3.7.2",
|
|
87
87
|
"mailparser": "3.9.9",
|
|
88
88
|
"mockdate": "3.0.5",
|
|
89
|
-
"mysql2": "3.
|
|
89
|
+
"mysql2": "3.23.1",
|
|
90
90
|
"node-rsa": "2.0.0",
|
|
91
91
|
"openid-client": "5.7.1",
|
|
92
|
-
"stripe": "16.12.0"
|
|
92
|
+
"stripe": "16.12.0",
|
|
93
|
+
"uuid": "14.0.0"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
|
-
"@stamhoofd/test-utils": "2.
|
|
96
|
+
"@stamhoofd/test-utils": "2.137.0",
|
|
96
97
|
"@types/cookie": "0.6.0",
|
|
97
98
|
"@types/luxon": "3.7.1",
|
|
98
99
|
"@types/mailparser": "3.4.6",
|
|
99
|
-
"@types/
|
|
100
|
-
"@types/node": "22.15.34",
|
|
100
|
+
"@types/node": "22.20.1",
|
|
101
101
|
"@types/uuid": "8.3.4",
|
|
102
102
|
"nock": "14.0.15",
|
|
103
103
|
"qs": "6.15.2",
|
|
@@ -108,5 +108,5 @@
|
|
|
108
108
|
"publishConfig": {
|
|
109
109
|
"access": "public"
|
|
110
110
|
},
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "f327bce84bd0c5ca7160e3e71ad03700d520b086"
|
|
112
112
|
}
|
|
@@ -45,6 +45,10 @@ export const GroupLogger = new ModelLogger(Group, {
|
|
|
45
45
|
if (model.settings.eventId) {
|
|
46
46
|
result.objectId = model.settings.eventId;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
if (model.eventId) {
|
|
50
|
+
result.objectId = model.eventId;
|
|
51
|
+
}
|
|
48
52
|
break;
|
|
49
53
|
case AuditLogType.GroupDeleted:
|
|
50
54
|
// do not log
|
|
@@ -75,6 +79,15 @@ export const GroupLogger = new ModelLogger(Group, {
|
|
|
75
79
|
})],
|
|
76
80
|
]);
|
|
77
81
|
}
|
|
82
|
+
if (model.eventId) {
|
|
83
|
+
return new Map([
|
|
84
|
+
['e', AuditLogReplacement.create({
|
|
85
|
+
id: model.eventId,
|
|
86
|
+
value: model.settings.name.toString(),
|
|
87
|
+
type: AuditLogReplacementType.Event,
|
|
88
|
+
})],
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
78
91
|
return new Map([
|
|
79
92
|
['e', AuditLogReplacement.create({
|
|
80
93
|
id: model.id,
|
|
@@ -527,7 +527,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
527
527
|
});
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
if (existingGroup.
|
|
530
|
+
if (existingGroup.eventId !== null && existingGroup.eventId !== event.id) {
|
|
531
531
|
throw new SimpleError({
|
|
532
532
|
code: 'invalid_group',
|
|
533
533
|
message: 'Group is already linked to another event',
|
|
@@ -544,7 +544,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
544
544
|
existingGroup.type = GroupType.EventRegistration;
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
existingGroup.
|
|
547
|
+
existingGroup.eventId = event.id;
|
|
548
548
|
|
|
549
549
|
// update group categories
|
|
550
550
|
const period = await RegistrationPeriod.getByID(existingGroup.periodId);
|
|
@@ -572,7 +572,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
572
572
|
} catch (e) {
|
|
573
573
|
// reset the group
|
|
574
574
|
existingGroup.type = originalGroupType;
|
|
575
|
-
existingGroup.
|
|
575
|
+
existingGroup.eventId = null;
|
|
576
576
|
await existingGroup.save();
|
|
577
577
|
|
|
578
578
|
// delete the event again if failed to link the group
|
|
@@ -54,7 +54,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
54
54
|
|
|
55
55
|
// Validate domains
|
|
56
56
|
|
|
57
|
-
if (request.body.registerDomain !== null && !request.body.registerDomain.match(/^(?:[a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/i)) {
|
|
57
|
+
if (request.body.registerDomain !== null && !request.body.registerDomain.match(/^(?:[a-z0-9-]+\.)?(?:[a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/i)) {
|
|
58
58
|
throw new SimpleError({
|
|
59
59
|
code: 'invalid_domain',
|
|
60
60
|
message: 'registerDomain is invalid',
|
|
@@ -389,6 +389,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
389
389
|
await event.save();
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
const groups = await Group.select().where('waitingListId', id).fetch();
|
|
393
|
+
for (const group of groups) {
|
|
394
|
+
group.waitingListId = null;
|
|
395
|
+
await group.save();
|
|
396
|
+
}
|
|
397
|
+
|
|
392
398
|
// delete invitations
|
|
393
399
|
await RegistrationInvitation.delete().where('groupId', id);
|
|
394
400
|
}
|
|
@@ -574,6 +580,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
574
580
|
};
|
|
575
581
|
|
|
576
582
|
const patch = struct;
|
|
583
|
+
let syncGroupWithEvent = false;
|
|
577
584
|
if (patch.waitingList !== undefined) {
|
|
578
585
|
if (patch.waitingList === null) {
|
|
579
586
|
// delete
|
|
@@ -586,7 +593,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
586
593
|
field: 'waitingList',
|
|
587
594
|
message: 'Cannot patch waitingList',
|
|
588
595
|
});
|
|
589
|
-
} else {
|
|
596
|
+
} else if (model.waitingListId !== patch.waitingList.id) {
|
|
590
597
|
if (model.waitingListId) {
|
|
591
598
|
model.waitingListId = null;
|
|
592
599
|
}
|
|
@@ -612,6 +619,8 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
612
619
|
}
|
|
613
620
|
|
|
614
621
|
model.waitingListId = existing.id;
|
|
622
|
+
existing.eventId = model.eventId;
|
|
623
|
+
syncGroupWithEvent = true;
|
|
615
624
|
} else {
|
|
616
625
|
throw new SimpleError({
|
|
617
626
|
code: 'invalid_waiting_list',
|
|
@@ -648,6 +657,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
648
657
|
await model.updateOccupancy();
|
|
649
658
|
await model.save();
|
|
650
659
|
|
|
660
|
+
if (model.eventId && syncGroupWithEvent) {
|
|
661
|
+
const event = await Event.getByID(model.eventId);
|
|
662
|
+
if (event) {
|
|
663
|
+
await event.syncGroupRequirements(model);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
651
667
|
return model;
|
|
652
668
|
}
|
|
653
669
|
|
|
@@ -75,7 +75,8 @@ export class RetrieveUitpasSocialTariffPricesEndpoint extends Endpoint<Params, Q
|
|
|
75
75
|
human: $t('%189'),
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
await
|
|
78
|
+
const organization = await Context.setOrganizationScope({ willAuthenticate: false });
|
|
79
|
+
await UitpasService.checkUitpasNumbers(organization.id, request.body.uitpasNumbers); // Throws if invalid
|
|
79
80
|
const uitpasPriceCheckResponse = UitpasPriceCheckResponse.create({
|
|
80
81
|
prices: request.body.uitpasNumbers.map(_ => reducedPrice), // All reduced prices are the same in this non-official flow
|
|
81
82
|
});
|
|
@@ -120,11 +120,15 @@ export class AuthenticatedStructures {
|
|
|
120
120
|
const structs: GroupStruct[] = [];
|
|
121
121
|
for (const group of groups) {
|
|
122
122
|
const waitingList = waitingLists.find(g => g.id === group.waitingListId) ?? null;
|
|
123
|
-
|
|
123
|
+
let waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null;
|
|
124
124
|
if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
|
|
125
125
|
waitingListStruct.privateSettings = null;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
if (waitingListStruct?.deletedAt) {
|
|
129
|
+
waitingListStruct = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
128
132
|
const struct = GroupStruct.create({
|
|
129
133
|
...group,
|
|
130
134
|
waitingList: waitingListStruct,
|
|
@@ -163,6 +167,7 @@ export class AuthenticatedStructures {
|
|
|
163
167
|
const whereWaitingList = SQL.where('organizationId', organizationIds)
|
|
164
168
|
.and('periodId', periodIds)
|
|
165
169
|
.and('type', GroupType.WaitingList)
|
|
170
|
+
.and('eventId', null)
|
|
166
171
|
.and('deletedAt', null);
|
|
167
172
|
|
|
168
173
|
if (groupIds.length) {
|
|
@@ -64,12 +64,15 @@ export class UitpasTokenRepository {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* organizationId (null means platform) -> UitpasTokenRepository
|
|
67
|
+
* organizationId (null means platform) -> UitpasTokenRepository or null if no creds are configured for this org/platform
|
|
68
68
|
*/
|
|
69
|
-
static
|
|
69
|
+
private static knownRepos: Map<string | null, UitpasTokenRepository | null> = new Map();
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* @returns null if no creds configured, undefined if not found in memory, UitpasTokenRepository if found in memory
|
|
73
|
+
*/
|
|
74
|
+
private static getRepoFromMemory(organizationId: string | null): UitpasTokenRepository | undefined | null {
|
|
75
|
+
return UitpasTokenRepository.knownRepos.get(organizationId);
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
private static async getModelFromDb(organizationId: string | null) {
|
|
@@ -154,9 +157,8 @@ export class UitpasTokenRepository {
|
|
|
154
157
|
return this.accessToken;
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
private static setRepoInMemory(organizationId: string | null, repo: UitpasTokenRepository) {
|
|
158
|
-
UitpasTokenRepository.
|
|
159
|
-
return repo;
|
|
160
|
+
private static setRepoInMemory(organizationId: string | null, repo: UitpasTokenRepository | null) {
|
|
161
|
+
UitpasTokenRepository.knownRepos.set(organizationId, repo);
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
private static async setModelInDb(organizationId: string | null, model: UitpasClientCredential) {
|
|
@@ -173,6 +175,17 @@ export class UitpasTokenRepository {
|
|
|
173
175
|
return model; // return new model
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
static async getAccessTokenForWithPlatformFallback(orgId: string, forceRefresh: boolean = false): Promise<string> {
|
|
179
|
+
try {
|
|
180
|
+
return await UitpasTokenRepository.getAccessTokenFor(orgId, forceRefresh);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (error instanceof SimpleError && error.code === 'uitpas_api_not_configured_for_this_organization') {
|
|
183
|
+
return await UitpasTokenRepository.getAccessTokenFor(null, forceRefresh); // fallback to platform
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
/**
|
|
177
190
|
* Get the access token for the organization or platform.
|
|
178
191
|
* @param organizationId the organization ID for which to get the access token. If null, it means the platform.
|
|
@@ -180,60 +193,101 @@ export class UitpasTokenRepository {
|
|
|
180
193
|
* @returns Promise<string> the access token for the organization or platform
|
|
181
194
|
* @throws SimpleError if the token cannot be obtained or the API is not configured
|
|
182
195
|
*/
|
|
183
|
-
static async getAccessTokenFor(organizationId: string | null
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
196
|
+
static async getAccessTokenFor(organizationId: string | null, forceRefresh: boolean = false): Promise<string> {
|
|
197
|
+
const notConfiguredError = new SimpleError({
|
|
198
|
+
code: 'uitpas_api_not_configured_for_this_organization',
|
|
199
|
+
message: `UiTPAS api not configured for organization with id ${organizationId}`,
|
|
200
|
+
human: $t('%1BJ'),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const tryFromMemory = (): string | UitpasTokenRepository | undefined => {
|
|
204
|
+
const cached = UitpasTokenRepository.getRepoFromMemory(organizationId);
|
|
205
|
+
if (cached === null) {
|
|
206
|
+
throw notConfiguredError;
|
|
207
|
+
}
|
|
208
|
+
if (cached && cached.accessToken && !forceRefresh && cached.expiresOn > new Date()) {
|
|
209
|
+
return cached.accessToken;
|
|
210
|
+
}
|
|
211
|
+
return cached; // not found in memory or expired or no access token or forceRefresh
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const memoryResult = tryFromMemory();
|
|
215
|
+
if (typeof memoryResult === 'string') {
|
|
216
|
+
return memoryResult; // access token found in memory
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
// Prevent multiple concurrent requests for the same organization, asking for an access token to the UiTPAS API.
|
|
190
220
|
// The queue can only run one at a time for the same organizationId
|
|
191
221
|
return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
|
|
192
222
|
// we re-search for the repo, as another call to this funcion might have added while we we're waiting in the queue
|
|
193
|
-
repo
|
|
194
|
-
|
|
195
|
-
|
|
223
|
+
let repo: UitpasTokenRepository;
|
|
224
|
+
const memoryResult = tryFromMemory();
|
|
225
|
+
if (typeof memoryResult === 'string') {
|
|
226
|
+
return memoryResult; // access token found in memory
|
|
196
227
|
}
|
|
197
|
-
if (!
|
|
228
|
+
if (!memoryResult) {
|
|
198
229
|
const model = await UitpasTokenRepository.getModelFromDb(organizationId);
|
|
199
230
|
if (!model) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
message: `UiTPAS api not configured for organization with id ${organizationId}`,
|
|
203
|
-
human: $t('%1BJ'),
|
|
204
|
-
});
|
|
231
|
+
UitpasTokenRepository.setRepoInMemory(organizationId, null);
|
|
232
|
+
throw notConfiguredError;
|
|
205
233
|
}
|
|
206
|
-
repo =
|
|
234
|
+
repo = new UitpasTokenRepository(model);
|
|
235
|
+
UitpasTokenRepository.setRepoInMemory(organizationId, repo);
|
|
236
|
+
} else {
|
|
237
|
+
repo = memoryResult;
|
|
207
238
|
}
|
|
208
239
|
// ask for a new access token
|
|
209
|
-
return repo.getNewAccessToken();
|
|
240
|
+
return await repo.getNewAccessToken();
|
|
210
241
|
});
|
|
211
242
|
}
|
|
212
243
|
|
|
213
|
-
static async getClientIdFor(organizationId: string
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
244
|
+
static async getClientIdFor(organizationId: string): Promise<string> {
|
|
245
|
+
const tryFromMemory = (): string | undefined => {
|
|
246
|
+
const cached = UitpasTokenRepository.getRepoFromMemory(organizationId);
|
|
247
|
+
if (cached === null) {
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
if (cached) {
|
|
251
|
+
return cached.uitpasClientCredential.clientId;
|
|
219
252
|
}
|
|
220
|
-
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const memoryResult = tryFromMemory();
|
|
256
|
+
if (typeof memoryResult === 'string') {
|
|
257
|
+
return memoryResult; // client ID found in memory
|
|
221
258
|
}
|
|
222
|
-
|
|
259
|
+
|
|
260
|
+
return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
|
|
261
|
+
// re-search memory, as another call to this function might have populated it while we were waiting in the queue
|
|
262
|
+
const memoryResult = tryFromMemory();
|
|
263
|
+
if (typeof memoryResult === 'string') {
|
|
264
|
+
return memoryResult; // client ID found in memory
|
|
265
|
+
}
|
|
266
|
+
const model = await UitpasTokenRepository.getModelFromDb(organizationId);
|
|
267
|
+
if (!model) {
|
|
268
|
+
UitpasTokenRepository.setRepoInMemory(organizationId, null); // remember there are no creds configured
|
|
269
|
+
return '';
|
|
270
|
+
}
|
|
271
|
+
UitpasTokenRepository.setRepoInMemory(organizationId, new UitpasTokenRepository(model)); // store in memory
|
|
272
|
+
return model.clientId; // client ID configured, now also in memory
|
|
273
|
+
});
|
|
223
274
|
}
|
|
224
275
|
|
|
225
276
|
static async clearClientCredentialsFor(organizationId: string | null): Promise<boolean> {
|
|
226
277
|
return await UitpasTokenRepository.handleInQueue(organizationId, async () => {
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
278
|
+
const cached = UitpasTokenRepository.getRepoFromMemory(organizationId);
|
|
279
|
+
if (cached) {
|
|
229
280
|
// in memory, thus also in db
|
|
230
|
-
await
|
|
231
|
-
UitpasTokenRepository.
|
|
281
|
+
await cached.uitpasClientCredential.delete(); // remove from db
|
|
282
|
+
UitpasTokenRepository.setRepoInMemory(organizationId, null); // remove from memory
|
|
232
283
|
return true;
|
|
233
284
|
}
|
|
234
285
|
// not in memory, maybe in db
|
|
235
286
|
const model = await UitpasTokenRepository.getModelFromDb(organizationId);
|
|
236
287
|
if (model) {
|
|
288
|
+
if (cached === null) {
|
|
289
|
+
console.warn(`Clearing UiTPAS client credentials for organization with id ${organizationId} although it was not in memory, but found in database. This is a bug, memory and database are out of sync.`);
|
|
290
|
+
}
|
|
237
291
|
await model.delete(); // remove from database
|
|
238
292
|
return true;
|
|
239
293
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Event, Group } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment === 'test') {
|
|
6
|
+
console.log('skipped in tests');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
await setEventIdOnGroups();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export async function setEventIdOnGroups() {
|
|
14
|
+
let count = 0;
|
|
15
|
+
|
|
16
|
+
// Loop all events and link the associated group (and its waiting list) back to the event.
|
|
17
|
+
for await (const event of Event.select().all()) {
|
|
18
|
+
if (!event.groupId) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const group = await Group.getByID(event.groupId);
|
|
23
|
+
if (!group) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (group.eventId !== event.id) {
|
|
28
|
+
group.eventId = event.id;
|
|
29
|
+
await event.syncGroupRequirements(group);
|
|
30
|
+
count++;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`Set eventId on ${count} groups`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { BalanceItem, CachedBalance, MergedMember } from '@stamhoofd/models';
|
|
3
|
+
import { SQL } from '@stamhoofd/sql';
|
|
4
|
+
import { SeedTools } from '../helpers/SeedTools.js';
|
|
5
|
+
|
|
6
|
+
export default new Migration(async () => {
|
|
7
|
+
if (STAMHOOFD.environment === 'test') {
|
|
8
|
+
console.log('skipped in tests');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (STAMHOOFD.platformName === 'stamhoofd') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Loop all members
|
|
17
|
+
await SeedTools.loop({
|
|
18
|
+
query: MergedMember.select(),
|
|
19
|
+
batchSize: 100,
|
|
20
|
+
action: async (mergedMember) => {
|
|
21
|
+
const items = await SQL.select('organizationId')
|
|
22
|
+
.from(BalanceItem.table)
|
|
23
|
+
.where('memberId', mergedMember.id)
|
|
24
|
+
.groupBy(SQL.column('organizationId'))
|
|
25
|
+
.fetch();
|
|
26
|
+
for (const item of items) {
|
|
27
|
+
await CachedBalance.updateForMembers(item[BalanceItem.table].organizationId as string, [mergedMember.id]);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return Promise.resolve();
|
|
33
|
+
});
|
|
@@ -5,10 +5,9 @@ import type { Invoice } from '@stamhoofd/models';
|
|
|
5
5
|
import { Image, InvoicedBalanceItem, Organization, Payment, Platform } from '@stamhoofd/models';
|
|
6
6
|
import { render } from '@stamhoofd/models/helpers/Handlebars.js';
|
|
7
7
|
import type { Address } from '@stamhoofd/structures';
|
|
8
|
-
import { CountryHelper, File, getVATExcemptInvoiceNote, getVATExcemptReasonName, PaymentMethod, PaymentMethodHelper, PaymentStatus } from '@stamhoofd/structures';
|
|
8
|
+
import { CountryHelper, File, getVATExcemptInvoiceNote, getVATExcemptReasonName, PaymentMethod, PaymentMethodHelper, PaymentStatus, Version } from '@stamhoofd/structures';
|
|
9
9
|
import { Formatter } from '@stamhoofd/utility';
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
|
-
import { VERSION } from 'luxon';
|
|
12
11
|
import { v4 as uuidv4 } from 'uuid';
|
|
13
12
|
|
|
14
13
|
export class InvoicePdfService {
|
|
@@ -260,7 +259,7 @@ export class InvoicePdfService {
|
|
|
260
259
|
|
|
261
260
|
try {
|
|
262
261
|
// Issue with system trusted CA in development
|
|
263
|
-
const result = await fetch((STAMHOOFD.environment === 'development' ? 'http://' : 'https://') + STAMHOOFD.domains.rendererApi + '/v' +
|
|
262
|
+
const result = await fetch((STAMHOOFD.environment === 'development' ? 'http://' : 'https://') + STAMHOOFD.domains.rendererApi + '/v' + Version + '/html-to-pdf', {
|
|
264
263
|
method: 'POST',
|
|
265
264
|
body: form,
|
|
266
265
|
signal: controller.signal,
|
|
@@ -4,19 +4,19 @@ import { UitpasService } from './UitpasService.js';
|
|
|
4
4
|
describe.skip('UitpasService', () => {
|
|
5
5
|
it('should validate a correct Uitpas number with kansentarief', async () => {
|
|
6
6
|
const validNumbers = ['0900000067513'];
|
|
7
|
-
await expect(UitpasService.checkUitpasNumbers(validNumbers)).resolves.toBeUndefined();
|
|
7
|
+
await expect(UitpasService.checkUitpasNumbers('unexisting-org-id', validNumbers)).resolves.toBeUndefined();
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
it('should throw an error for an invalid Uitpas number', async () => {
|
|
11
11
|
const invalidNumbers = ['1234567890123'];
|
|
12
|
-
await expect(UitpasService.checkUitpasNumbers(invalidNumbers)).rejects.toThrow(
|
|
12
|
+
await expect(UitpasService.checkUitpasNumbers('unexisting-org-id', invalidNumbers)).rejects.toThrow(
|
|
13
13
|
STExpect.simpleError({ code: 'unsuccessful_but_expected_response_retrieving_pass_by_uitpas_number' }),
|
|
14
14
|
);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it('should throw an error for a Uitpas number with kansentarief expired', async () => {
|
|
18
18
|
const expiredNumbers = ['0900000058918'];
|
|
19
|
-
await expect(UitpasService.checkUitpasNumbers(expiredNumbers)).rejects.toThrow(
|
|
19
|
+
await expect(UitpasService.checkUitpasNumbers('unexisting-org-id', expiredNumbers)).rejects.toThrow(
|
|
20
20
|
STExpect.simpleError({ code: 'uitpas_number_issue' }),
|
|
21
21
|
);
|
|
22
22
|
});
|
|
@@ -309,7 +309,7 @@ export class UitpasService {
|
|
|
309
309
|
|
|
310
310
|
static async searchUitpasOrganizers(name: string): Promise<UitpasOrganizersResponse> {
|
|
311
311
|
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/list-organizers
|
|
312
|
-
const access_token = await UitpasTokenRepository.getAccessTokenFor(); //
|
|
312
|
+
const access_token = await UitpasTokenRepository.getAccessTokenFor(null); // always use platform credentials
|
|
313
313
|
return await searchUitpasOrganizers(access_token, name);
|
|
314
314
|
}
|
|
315
315
|
|
|
@@ -327,7 +327,7 @@ export class UitpasService {
|
|
|
327
327
|
* @param organizationId
|
|
328
328
|
* @returns clientId or empty string if not configured
|
|
329
329
|
*/
|
|
330
|
-
static async getClientIdFor(organizationId: string
|
|
330
|
+
static async getClientIdFor(organizationId: string): Promise<string> {
|
|
331
331
|
// Get the uitpas client credentials for the organization
|
|
332
332
|
return await UitpasTokenRepository.getClientIdFor(organizationId);
|
|
333
333
|
}
|
|
@@ -338,9 +338,9 @@ export class UitpasService {
|
|
|
338
338
|
* The field of the error will be the index of the uitpas number in the array.
|
|
339
339
|
* @param uitpasNumbers The uitpas numbers to check
|
|
340
340
|
*/
|
|
341
|
-
static async checkUitpasNumbers(uitpasNumbers: string[]) {
|
|
341
|
+
static async checkUitpasNumbers(organizationId: string, uitpasNumbers: string[]) {
|
|
342
342
|
// https://docs.publiq.be/docs/uitpas/uitpas-api/reference/operations/get-a-pass
|
|
343
|
-
const access_token = await UitpasTokenRepository.
|
|
343
|
+
const access_token = await UitpasTokenRepository.getAccessTokenForWithPlatformFallback(organizationId);
|
|
344
344
|
return await checkUitpasNumbers(access_token, uitpasNumbers);
|
|
345
345
|
}
|
|
346
346
|
|
|
@@ -350,7 +350,7 @@ export class UitpasService {
|
|
|
350
350
|
static async getPassByUitpasNumber(uitpasNumber: string): Promise<GetPassResponse> {
|
|
351
351
|
let access_token!: string;
|
|
352
352
|
try {
|
|
353
|
-
access_token = await UitpasTokenRepository.getAccessTokenFor(); // use platform credentials
|
|
353
|
+
access_token = await UitpasTokenRepository.getAccessTokenFor(null); // always use platform credentials (for now)
|
|
354
354
|
} catch (e) {
|
|
355
355
|
if (isSimpleError(e) && e.hasCode('uitpas_api_not_configured_for_platform')) {
|
|
356
356
|
console.error('UiTPAS API has not been configured for this platform, so defaulting to an unknown status for number ', uitpasNumber);
|
|
@@ -387,8 +387,8 @@ export class UitpasService {
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
static async validateCart(organizationId: string, webshopId: string, cart: Cart, exisitingOrderId?: string): Promise<Cart> {
|
|
390
|
-
let
|
|
391
|
-
|
|
390
|
+
let access_token: string | undefined;
|
|
391
|
+
const unofficialsToBeChecked: string[][] = [];
|
|
392
392
|
for (const item of cart.items) {
|
|
393
393
|
if (item.uitpasNumbers.length === 0) {
|
|
394
394
|
continue;
|
|
@@ -416,8 +416,8 @@ export class UitpasService {
|
|
|
416
416
|
});
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
|
|
420
|
-
const verified = await getSocialTariffForUitpasNumbers(
|
|
419
|
+
access_token = access_token ?? await UitpasTokenRepository.getAccessTokenFor(organizationId);
|
|
420
|
+
const verified = await getSocialTariffForUitpasNumbers(access_token, item.uitpasNumbers.map(p => p.uitpasNumber), basePrice, item.product.uitpasEvent.url);
|
|
421
421
|
if (verified.length < item.uitpasNumbers.length) {
|
|
422
422
|
throw new SimpleError({
|
|
423
423
|
code: 'uitpas_social_tariff_price_mismatch',
|
|
@@ -441,9 +441,14 @@ export class UitpasService {
|
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
} else {
|
|
444
|
-
// non-official flow
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
// non-official flow: do all checks after official flows (for maximum token re-usage), but retaining index numbering for error reporting
|
|
445
|
+
unofficialsToBeChecked.push(item.uitpasNumbers.map(p => p.uitpasNumber));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (unofficialsToBeChecked.length > 0) {
|
|
449
|
+
const token = access_token ?? await UitpasTokenRepository.getAccessTokenForWithPlatformFallback(organizationId);
|
|
450
|
+
for (const uitpasNumbers of unofficialsToBeChecked) {
|
|
451
|
+
await checkUitpasNumbers(token, uitpasNumbers);
|
|
447
452
|
}
|
|
448
453
|
}
|
|
449
454
|
return cart;
|
|
@@ -74,21 +74,22 @@ export async function checkPermissionsFor(access_token: string, organizationId:
|
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
assertIsPermissionsResponse(json);
|
|
77
|
-
const neededPermissions =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
const neededPermissions = [{
|
|
78
|
+
permission: 'PASSES_READ',
|
|
79
|
+
human: 'Basis UiTPAS informatie ophalen met UiTPAS nummer',
|
|
80
|
+
}];
|
|
81
|
+
if (organizationId) {
|
|
82
|
+
neededPermissions.push(...[{
|
|
83
|
+
permission: 'TARIFFS_READ',
|
|
84
|
+
human: 'Tarieven opvragen',
|
|
85
|
+
}, {
|
|
86
|
+
permission: 'TICKETSALES_REGISTER',
|
|
87
|
+
human: 'Ticketsales registreren',
|
|
88
|
+
}, {
|
|
89
|
+
permission: 'TICKETSALES_SEARCH',
|
|
90
|
+
human: 'Ticketsales zoeken',
|
|
91
|
+
}]);
|
|
92
|
+
}
|
|
92
93
|
const item = json.find(item => item.organizer.id === uitpasOrganizerId);
|
|
93
94
|
if (!item) {
|
|
94
95
|
const organizers = Formatter.joinLast(json.map(i => i.organizer.name), ', ', ' ' + $t('%1BM') + ' ');
|
|
@@ -14,6 +14,11 @@ export const groupFilterCompilers: SQLFilterDefinitions = {
|
|
|
14
14
|
type: SQLValueType.String,
|
|
15
15
|
nullable: false,
|
|
16
16
|
}),
|
|
17
|
+
eventId: createColumnFilter({
|
|
18
|
+
expression: SQL.column('eventId'),
|
|
19
|
+
type: SQLValueType.String,
|
|
20
|
+
nullable: true,
|
|
21
|
+
}),
|
|
17
22
|
periodId: createColumnFilter({
|
|
18
23
|
expression: SQL.column('periodId'),
|
|
19
24
|
type: SQLValueType.String,
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
-
import { CachedBalance, Member, Organization, Platform, Registration, User } from '@stamhoofd/models';
|
|
3
|
-
import { SeedTools } from '../helpers/SeedTools.js';
|
|
4
|
-
|
|
5
|
-
export default new Migration(async () => {
|
|
6
|
-
if (STAMHOOFD.environment === 'test') {
|
|
7
|
-
console.log('skipped in tests');
|
|
8
|
-
return;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (STAMHOOFD.platformName !== 'stamhoofd') {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (STAMHOOFD.userMode !== 'organization') {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Loop all members
|
|
20
|
-
await SeedTools.loopBatched({
|
|
21
|
-
query: Organization.select(),
|
|
22
|
-
// useTransactionPerBatch: true,
|
|
23
|
-
batchSize: 100,
|
|
24
|
-
batchAction: async (organizations) => {
|
|
25
|
-
for (const organization of organizations) {
|
|
26
|
-
for await (const batch of Member.select('id').where('organizationId', organization.id).limit(1000).allBatched()) {
|
|
27
|
-
await CachedBalance.updateForMembers(organization.id, batch.map(m => m.id));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
for await (const batch of User.select('id').where('organizationId', organization.id).limit(1000).allBatched()) {
|
|
31
|
-
await CachedBalance.updateForUsers(organization.id, batch.map(m => m.id));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
for await (const batch of Registration.select('id').where('organizationId', organization.id).limit(1000).allBatched()) {
|
|
35
|
-
await CachedBalance.updateForRegistrations(organization.id, batch.map(m => m.id));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const membershipOrganizationId = (await Platform.getShared()).membershipOrganizationId;
|
|
42
|
-
if (membershipOrganizationId) {
|
|
43
|
-
const membershipOrganization = await Organization.getByID(membershipOrganizationId, true);
|
|
44
|
-
await SeedTools.loopBatched({
|
|
45
|
-
query: Organization.select('id').where('id', '!=', membershipOrganization.id),
|
|
46
|
-
// useTransactionPerBatch: true,
|
|
47
|
-
batchSize: 1000,
|
|
48
|
-
batchAction: async (organizations) => {
|
|
49
|
-
await CachedBalance.updateForOrganizations(membershipOrganization.id, organizations.map(m => m.id));
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
|
|
55
|
-
// for await (const items of BalanceItem.select().limit(10_000).allBatched()) {
|
|
56
|
-
// await BalanceItemService.updatePaidAndPending(items);
|
|
57
|
-
// await BalanceItemService.flushAll();
|
|
58
|
-
//
|
|
59
|
-
// progressLogger.update(items.length);
|
|
60
|
-
// }
|
|
61
|
-
// });
|
|
62
|
-
|
|
63
|
-
// console.log('Updated outstanding balance for ' + progressLogger.total + ' items');
|
|
64
|
-
|
|
65
|
-
// Do something here
|
|
66
|
-
return Promise.resolve();
|
|
67
|
-
});
|