@stamhoofd/backend 2.129.2 → 2.130.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 +17 -17
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.test.ts +324 -2
- package/src/endpoints/global/members/GetMembersEndpoint.ts +6 -5
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +4 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +1 -1
- package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +95 -76
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +61 -2
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +14 -1
- package/src/helpers/AdminPermissionChecker.ts +13 -11
- package/src/helpers/AuthenticatedStructures.ts +5 -7
- package/src/migrations/1783342463-remove-platform-uitpas-flag.ts +25 -0
- package/src/services/MollieService.ts +1 -1
- package/src/services/SSOService.ts +5 -1
- package/src/services/uitpas/searchUitpasEvents.ts +22 -18
- package/src/sql-filters/organizations.ts +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.130.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -57,20 +57,20 @@
|
|
|
57
57
|
"@simonbackx/simple-endpoints": "1.21.1",
|
|
58
58
|
"@simonbackx/simple-errors": "1.5.0",
|
|
59
59
|
"@simonbackx/simple-logging": "1.0.1",
|
|
60
|
-
"@stamhoofd/backend-env": "2.
|
|
61
|
-
"@stamhoofd/backend-i18n": "2.
|
|
62
|
-
"@stamhoofd/backend-middleware": "2.
|
|
63
|
-
"@stamhoofd/crons": "2.
|
|
64
|
-
"@stamhoofd/email": "2.
|
|
65
|
-
"@stamhoofd/excel-writer": "2.
|
|
66
|
-
"@stamhoofd/logging": "2.
|
|
67
|
-
"@stamhoofd/models": "2.
|
|
68
|
-
"@stamhoofd/object-differ": "2.
|
|
69
|
-
"@stamhoofd/queues": "2.
|
|
70
|
-
"@stamhoofd/sql": "2.
|
|
71
|
-
"@stamhoofd/structures": "2.
|
|
72
|
-
"@stamhoofd/types": "2.
|
|
73
|
-
"@stamhoofd/utility": "2.
|
|
60
|
+
"@stamhoofd/backend-env": "2.130.0",
|
|
61
|
+
"@stamhoofd/backend-i18n": "2.130.0",
|
|
62
|
+
"@stamhoofd/backend-middleware": "2.130.0",
|
|
63
|
+
"@stamhoofd/crons": "2.130.0",
|
|
64
|
+
"@stamhoofd/email": "2.130.0",
|
|
65
|
+
"@stamhoofd/excel-writer": "2.130.0",
|
|
66
|
+
"@stamhoofd/logging": "2.130.0",
|
|
67
|
+
"@stamhoofd/models": "2.130.0",
|
|
68
|
+
"@stamhoofd/object-differ": "2.130.0",
|
|
69
|
+
"@stamhoofd/queues": "2.130.0",
|
|
70
|
+
"@stamhoofd/sql": "2.130.0",
|
|
71
|
+
"@stamhoofd/structures": "2.130.0",
|
|
72
|
+
"@stamhoofd/types": "2.130.0",
|
|
73
|
+
"@stamhoofd/utility": "2.130.0",
|
|
74
74
|
"archiver": "7.0.1",
|
|
75
75
|
"axios": "1.16.0",
|
|
76
76
|
"base-x": "3.0.11",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"stripe": "16.12.0"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@stamhoofd/test-utils": "2.
|
|
94
|
+
"@stamhoofd/test-utils": "2.130.0",
|
|
95
95
|
"@types/cookie": "0.6.0",
|
|
96
96
|
"@types/luxon": "3.7.1",
|
|
97
97
|
"@types/mailparser": "3.4.6",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"publishConfig": {
|
|
108
108
|
"access": "public"
|
|
109
109
|
},
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "b87db6d1a28c82fb12e588f899bcc7dc68d4cd90"
|
|
111
111
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import type { Organization, Token } from '@stamhoofd/models';
|
|
3
|
-
import { OrganizationFactory, STPackageFactory, UserFactory } from '@stamhoofd/models';
|
|
4
|
-
import { LimitedFilteredRequest, OrganizationMetaData, OrganizationType, PermissionLevel, Permissions, STPackageBundle, type StamhoofdFilter, UmbrellaOrganization } from '@stamhoofd/structures';
|
|
3
|
+
import { DocumentTemplateFactory, OrganizationFactory, STPackageFactory, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { DocumentStatus, LimitedFilteredRequest, OrganizationMetaData, OrganizationRecordsConfiguration, OrganizationType, PermissionLevel, Permissions, RecordCategory, RecordSettings, RecordType, STPackageBundle, type StamhoofdFilter, TranslatedString, UmbrellaOrganization } from '@stamhoofd/structures';
|
|
5
5
|
import { Country } from '@stamhoofd/types/Country';
|
|
6
6
|
import { TestUtils } from '@stamhoofd/test-utils';
|
|
7
7
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
@@ -229,4 +229,326 @@ describe('Endpoint.GetOrganizationsEndpoint', () => {
|
|
|
229
229
|
expect(await filter({ createdAt: { $gt: anHourAgo } }, adminToken)).toContain(org.id);
|
|
230
230
|
expect(await filter({ createdAt: { $gt: inAnHour } }, adminToken)).not.toContain(org.id);
|
|
231
231
|
});
|
|
232
|
+
|
|
233
|
+
// Creates an organization with the given record categories (questionnaires) in its records configuration
|
|
234
|
+
const createOrganizationWithRecordCategories = async (name: string, recordCategories: RecordCategory[]) => {
|
|
235
|
+
const meta = OrganizationMetaData.create({
|
|
236
|
+
type: OrganizationType.Other,
|
|
237
|
+
umbrellaOrganization: null,
|
|
238
|
+
defaultEndDate: new Date(),
|
|
239
|
+
defaultStartDate: new Date(),
|
|
240
|
+
defaultPrices: [],
|
|
241
|
+
recordsConfiguration: OrganizationRecordsConfiguration.create({
|
|
242
|
+
recordCategories,
|
|
243
|
+
}),
|
|
244
|
+
});
|
|
245
|
+
return await new OrganizationFactory({ name, meta }).create();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const createTextRecord = (nameValue: string) => {
|
|
249
|
+
return RecordSettings.create({
|
|
250
|
+
name: TranslatedString.create(nameValue),
|
|
251
|
+
type: RecordType.Text,
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
test('Filters organizations by record category name', async () => {
|
|
256
|
+
const { adminToken } = await initPlatformAdmin();
|
|
257
|
+
|
|
258
|
+
const medicalOrg = await createOrganizationWithRecordCategories('Medical Questionnaire Club', [
|
|
259
|
+
RecordCategory.create({ name: TranslatedString.create('Medische fiche') }),
|
|
260
|
+
]);
|
|
261
|
+
const consentOrg = await createOrganizationWithRecordCategories('Consent Questionnaire Club', [
|
|
262
|
+
RecordCategory.create({ name: TranslatedString.create('Toestemmingen') }),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const results = await filter({ recordCategoryName: { $contains: 'Medische' } }, adminToken);
|
|
266
|
+
expect(results).toContain(medicalOrg.id);
|
|
267
|
+
expect(results).not.toContain(consentOrg.id);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('Matches questionnaire name regardless of the language of a translated name', async () => {
|
|
271
|
+
const { adminToken } = await initPlatformAdmin();
|
|
272
|
+
|
|
273
|
+
// A name that is stored as a multi-language object (not a plain string)
|
|
274
|
+
const org = await createOrganizationWithRecordCategories('Multilingual Questionnaire Club', [
|
|
275
|
+
RecordCategory.create({
|
|
276
|
+
name: TranslatedString.create({ nl: 'Gezondheidsvragen', en: 'Health questions' }),
|
|
277
|
+
}),
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
expect(await filter({ recordCategoryName: { $contains: 'Gezondheidsvragen' } }, adminToken)).toContain(org.id);
|
|
281
|
+
expect(await filter({ recordCategoryName: { $contains: 'Health questions' } }, adminToken)).toContain(org.id);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('Does not match a record name or child category name when filtering on record category name', async () => {
|
|
285
|
+
const { adminToken } = await initPlatformAdmin();
|
|
286
|
+
|
|
287
|
+
const org = await createOrganizationWithRecordCategories('Precise Questionnaire Club', [
|
|
288
|
+
RecordCategory.create({
|
|
289
|
+
name: TranslatedString.create('Algemeen'),
|
|
290
|
+
childCategories: [
|
|
291
|
+
RecordCategory.create({
|
|
292
|
+
name: TranslatedString.create('Subcategorie'),
|
|
293
|
+
records: [createTextRecord('Bloedgroep')],
|
|
294
|
+
}),
|
|
295
|
+
],
|
|
296
|
+
}),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
// The top-level questionnaire name matches
|
|
300
|
+
expect(await filter({ recordCategoryName: { $contains: 'Algemeen' } }, adminToken)).toContain(org.id);
|
|
301
|
+
// A child category name is not a questionnaire
|
|
302
|
+
expect(await filter({ recordCategoryName: { $contains: 'Subcategorie' } }, adminToken)).not.toContain(org.id);
|
|
303
|
+
// A record name is not a questionnaire
|
|
304
|
+
expect(await filter({ recordCategoryName: { $contains: 'Bloedgroep' } }, adminToken)).not.toContain(org.id);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('Filters organizations by a record name in a top-level record categories', async () => {
|
|
308
|
+
const { adminToken } = await initPlatformAdmin();
|
|
309
|
+
|
|
310
|
+
const org = await createOrganizationWithRecordCategories('Direct Question Club', [
|
|
311
|
+
RecordCategory.create({
|
|
312
|
+
name: TranslatedString.create('Algemeen'),
|
|
313
|
+
records: [createTextRecord('Bloedgroep')],
|
|
314
|
+
}),
|
|
315
|
+
]);
|
|
316
|
+
const otherOrg = await createOrganizationWithRecordCategories('Other Question Club', [
|
|
317
|
+
RecordCategory.create({
|
|
318
|
+
name: TranslatedString.create('Algemeen'),
|
|
319
|
+
records: [createTextRecord('Schoenmaat')],
|
|
320
|
+
}),
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
const results = await filter({ recordName: { $contains: 'Bloedgroep' } }, adminToken);
|
|
324
|
+
expect(results).toContain(org.id);
|
|
325
|
+
expect(results).not.toContain(otherOrg.id);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test('Filters organizations by a record name nested in deep child categories', async () => {
|
|
329
|
+
const { adminToken } = await initPlatformAdmin();
|
|
330
|
+
|
|
331
|
+
// Category -> child -> child -> child, with the record at the deepest level
|
|
332
|
+
const org = await createOrganizationWithRecordCategories('Deeply Nested Question Club', [
|
|
333
|
+
RecordCategory.create({
|
|
334
|
+
name: TranslatedString.create('Niveau 0'),
|
|
335
|
+
childCategories: [
|
|
336
|
+
RecordCategory.create({
|
|
337
|
+
name: TranslatedString.create('Niveau 1'),
|
|
338
|
+
childCategories: [
|
|
339
|
+
RecordCategory.create({
|
|
340
|
+
name: TranslatedString.create('Niveau 2'),
|
|
341
|
+
childCategories: [
|
|
342
|
+
RecordCategory.create({
|
|
343
|
+
name: TranslatedString.create('Niveau 3'),
|
|
344
|
+
records: [createTextRecord('Diepe vraag')],
|
|
345
|
+
}),
|
|
346
|
+
],
|
|
347
|
+
}),
|
|
348
|
+
],
|
|
349
|
+
}),
|
|
350
|
+
],
|
|
351
|
+
}),
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
const noMatchOrg = await createOrganizationWithRecordCategories('Shallow Question Club', [
|
|
355
|
+
RecordCategory.create({
|
|
356
|
+
name: TranslatedString.create('Niveau 0'),
|
|
357
|
+
records: [createTextRecord('Oppervlakkige vraag')],
|
|
358
|
+
}),
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const results = await filter({ recordName: { $contains: 'Diepe vraag' } }, adminToken);
|
|
362
|
+
expect(results).toContain(org.id);
|
|
363
|
+
expect(results).not.toContain(noMatchOrg.id);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('Filters organizations by a child (sub)category name', async () => {
|
|
367
|
+
const { adminToken } = await initPlatformAdmin();
|
|
368
|
+
|
|
369
|
+
const org = await createOrganizationWithRecordCategories('Child Category Club', [
|
|
370
|
+
RecordCategory.create({
|
|
371
|
+
name: TranslatedString.create('Algemeen'),
|
|
372
|
+
childCategories: [
|
|
373
|
+
RecordCategory.create({
|
|
374
|
+
name: TranslatedString.create('Medische subcategorie'),
|
|
375
|
+
records: [createTextRecord('Bloedgroep')],
|
|
376
|
+
}),
|
|
377
|
+
],
|
|
378
|
+
}),
|
|
379
|
+
]);
|
|
380
|
+
const otherOrg = await createOrganizationWithRecordCategories('Other Child Category Club', [
|
|
381
|
+
RecordCategory.create({
|
|
382
|
+
name: TranslatedString.create('Algemeen'),
|
|
383
|
+
childCategories: [
|
|
384
|
+
RecordCategory.create({
|
|
385
|
+
name: TranslatedString.create('Sportieve subcategorie'),
|
|
386
|
+
records: [createTextRecord('Bloedgroep')],
|
|
387
|
+
}),
|
|
388
|
+
],
|
|
389
|
+
}),
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
const results = await filter({ recordChildCategoryName: { $contains: 'Medische subcategorie' } }, adminToken);
|
|
393
|
+
expect(results).toContain(org.id);
|
|
394
|
+
expect(results).not.toContain(otherOrg.id);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('Does not match a top-level questionnaire name or record name when filtering on child category name', async () => {
|
|
398
|
+
const { adminToken } = await initPlatformAdmin();
|
|
399
|
+
|
|
400
|
+
const org = await createOrganizationWithRecordCategories('Precise Child Category Club', [
|
|
401
|
+
RecordCategory.create({
|
|
402
|
+
name: TranslatedString.create('Bovenliggende naam'),
|
|
403
|
+
childCategories: [
|
|
404
|
+
RecordCategory.create({
|
|
405
|
+
name: TranslatedString.create('Onderliggende naam'),
|
|
406
|
+
records: [createTextRecord('Vraagnaam')],
|
|
407
|
+
}),
|
|
408
|
+
],
|
|
409
|
+
}),
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
// The child category name matches
|
|
413
|
+
expect(await filter({ recordChildCategoryName: { $contains: 'Onderliggende naam' } }, adminToken)).toContain(org.id);
|
|
414
|
+
// A top-level questionnaire name is not a child category
|
|
415
|
+
expect(await filter({ recordChildCategoryName: { $contains: 'Bovenliggende naam' } }, adminToken)).not.toContain(org.id);
|
|
416
|
+
// A record name is not a child category
|
|
417
|
+
expect(await filter({ recordChildCategoryName: { $contains: 'Vraagnaam' } }, adminToken)).not.toContain(org.id);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('Filters organizations by a child category name nested in deep child categories', async () => {
|
|
421
|
+
const { adminToken } = await initPlatformAdmin();
|
|
422
|
+
|
|
423
|
+
// Category -> child -> child -> child, with the matching child category at the deepest level
|
|
424
|
+
const org = await createOrganizationWithRecordCategories('Deeply Nested Child Category Club', [
|
|
425
|
+
RecordCategory.create({
|
|
426
|
+
name: TranslatedString.create('Niveau 0'),
|
|
427
|
+
childCategories: [
|
|
428
|
+
RecordCategory.create({
|
|
429
|
+
name: TranslatedString.create('Niveau 1'),
|
|
430
|
+
childCategories: [
|
|
431
|
+
RecordCategory.create({
|
|
432
|
+
name: TranslatedString.create('Niveau 2'),
|
|
433
|
+
childCategories: [
|
|
434
|
+
RecordCategory.create({
|
|
435
|
+
name: TranslatedString.create('Diepe subcategorie'),
|
|
436
|
+
records: [createTextRecord('Diepe vraag')],
|
|
437
|
+
}),
|
|
438
|
+
],
|
|
439
|
+
}),
|
|
440
|
+
],
|
|
441
|
+
}),
|
|
442
|
+
],
|
|
443
|
+
}),
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
const noMatchOrg = await createOrganizationWithRecordCategories('Shallow Child Category Club', [
|
|
447
|
+
RecordCategory.create({
|
|
448
|
+
name: TranslatedString.create('Niveau 0'),
|
|
449
|
+
childCategories: [
|
|
450
|
+
RecordCategory.create({
|
|
451
|
+
name: TranslatedString.create('Oppervlakkige subcategorie'),
|
|
452
|
+
}),
|
|
453
|
+
],
|
|
454
|
+
}),
|
|
455
|
+
]);
|
|
456
|
+
|
|
457
|
+
const results = await filter({ recordChildCategoryName: { $contains: 'Diepe subcategorie' } }, adminToken);
|
|
458
|
+
expect(results).toContain(org.id);
|
|
459
|
+
expect(results).not.toContain(noMatchOrg.id);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('Filters organizations by document template type', async () => {
|
|
463
|
+
const { adminToken } = await initPlatformAdmin();
|
|
464
|
+
|
|
465
|
+
const fiscalOrg = await new OrganizationFactory({ name: 'Fiscal Document Club' }).create();
|
|
466
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: fiscalOrg.id, type: 'fiscal' }).create();
|
|
467
|
+
|
|
468
|
+
const participationOrg = await new OrganizationFactory({ name: 'Participation Document Club' }).create();
|
|
469
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: participationOrg.id, type: 'participation' }).create();
|
|
470
|
+
|
|
471
|
+
const results = await filter({ documentTemplates: { $elemMatch: { type: { $in: ['fiscal'] } } } }, adminToken);
|
|
472
|
+
expect(results).toContain(fiscalOrg.id);
|
|
473
|
+
expect(results).not.toContain(participationOrg.id);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
test('Filters organizations by document template year', async () => {
|
|
477
|
+
const { adminToken } = await initPlatformAdmin();
|
|
478
|
+
|
|
479
|
+
const org2024 = await new OrganizationFactory({ name: 'Document Year 2024 Club' }).create();
|
|
480
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: org2024.id, year: 2024 }).create();
|
|
481
|
+
|
|
482
|
+
const org2023 = await new OrganizationFactory({ name: 'Document Year 2023 Club' }).create();
|
|
483
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: org2023.id, year: 2023 }).create();
|
|
484
|
+
|
|
485
|
+
const results = await filter({ documentTemplates: { $elemMatch: { year: { $eq: 2024 } } } }, adminToken);
|
|
486
|
+
expect(results).toContain(org2024.id);
|
|
487
|
+
expect(results).not.toContain(org2023.id);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('Filters organizations by document template status', async () => {
|
|
491
|
+
const { adminToken } = await initPlatformAdmin();
|
|
492
|
+
|
|
493
|
+
const publishedOrg = await new OrganizationFactory({ name: 'Published Document Club' }).create();
|
|
494
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: publishedOrg.id, status: DocumentStatus.Published }).create();
|
|
495
|
+
|
|
496
|
+
const draftOrg = await new OrganizationFactory({ name: 'Draft Document Club' }).create();
|
|
497
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: draftOrg.id, status: DocumentStatus.Draft }).create();
|
|
498
|
+
|
|
499
|
+
const results = await filter({ documentTemplates: { $elemMatch: { status: { $in: [DocumentStatus.Published] } } } }, adminToken);
|
|
500
|
+
expect(results).toContain(publishedOrg.id);
|
|
501
|
+
expect(results).not.toContain(draftOrg.id);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test('Filters organizations by document template isLocked', async () => {
|
|
505
|
+
const { adminToken } = await initPlatformAdmin();
|
|
506
|
+
|
|
507
|
+
const lockedOrg = await new OrganizationFactory({ name: 'Locked Document Club' }).create();
|
|
508
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: lockedOrg.id, isLocked: true }).create();
|
|
509
|
+
|
|
510
|
+
const unlockedOrg = await new OrganizationFactory({ name: 'Unlocked Document Club' }).create();
|
|
511
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: unlockedOrg.id, isLocked: false }).create();
|
|
512
|
+
|
|
513
|
+
const results = await filter({ documentTemplates: { $elemMatch: { isLocked: { $eq: true } } } }, adminToken);
|
|
514
|
+
expect(results).toContain(lockedOrg.id);
|
|
515
|
+
expect(results).not.toContain(unlockedOrg.id);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('Filters organizations by document template updatesEnabled', async () => {
|
|
519
|
+
const { adminToken } = await initPlatformAdmin();
|
|
520
|
+
|
|
521
|
+
const noUpdatesOrg = await new OrganizationFactory({ name: 'No Updates Document Club' }).create();
|
|
522
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: noUpdatesOrg.id, updatesEnabled: false }).create();
|
|
523
|
+
|
|
524
|
+
const updatesOrg = await new OrganizationFactory({ name: 'Updates Document Club' }).create();
|
|
525
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: updatesOrg.id, updatesEnabled: true }).create();
|
|
526
|
+
|
|
527
|
+
const results = await filter({ documentTemplates: { $elemMatch: { updatesEnabled: { $eq: false } } } }, adminToken);
|
|
528
|
+
expect(results).toContain(noUpdatesOrg.id);
|
|
529
|
+
expect(results).not.toContain(updatesOrg.id);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('Combines multiple document template conditions in a single $elemMatch', async () => {
|
|
533
|
+
const { adminToken } = await initPlatformAdmin();
|
|
534
|
+
|
|
535
|
+
const matchOrg = await new OrganizationFactory({ name: 'Matching Document Club' }).create();
|
|
536
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: matchOrg.id, type: 'fiscal', year: 2025, status: DocumentStatus.Published }).create();
|
|
537
|
+
|
|
538
|
+
// Same org, but the conditions are split over two different templates: should not match a single-template $elemMatch
|
|
539
|
+
const splitOrg = await new OrganizationFactory({ name: 'Split Document Club' }).create();
|
|
540
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: splitOrg.id, type: 'fiscal', year: 2020, status: DocumentStatus.Published }).create();
|
|
541
|
+
await new DocumentTemplateFactory({ groups: [], organizationId: splitOrg.id, type: 'participation', year: 2025, status: DocumentStatus.Published }).create();
|
|
542
|
+
|
|
543
|
+
const results = await filter({
|
|
544
|
+
documentTemplates: {
|
|
545
|
+
$elemMatch: {
|
|
546
|
+
type: { $in: ['fiscal'] },
|
|
547
|
+
year: { $eq: 2025 },
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
}, adminToken);
|
|
551
|
+
expect(results).toContain(matchOrg.id);
|
|
552
|
+
expect(results).not.toContain(splitOrg.id);
|
|
553
|
+
});
|
|
232
554
|
});
|
|
@@ -44,8 +44,11 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
44
44
|
const organization = Context.organization;
|
|
45
45
|
let scopeFilter: StamhoofdFilter | undefined = undefined;
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (organization && STAMHOOFD.userMode === 'organization' && await Context.auth.hasFullAccess(organization.id)) {
|
|
48
|
+
// Don't add any scoping. Organization id filter is added automatically.
|
|
49
|
+
} else if (!Context.auth.canAccessAllPlatformMembers(permissionLevel) && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
|
|
50
|
+
// First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
|
|
51
|
+
|
|
49
52
|
if (!organization) {
|
|
50
53
|
const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
|
|
51
54
|
if (tags !== 'all' && tags.length === 0) {
|
|
@@ -69,9 +72,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
69
72
|
},
|
|
70
73
|
};
|
|
71
74
|
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (organization) {
|
|
75
|
+
} else {
|
|
75
76
|
// Add organization scope filter
|
|
76
77
|
if (await Context.auth.canAccessAllMembers(organization.id, permissionLevel)) {
|
|
77
78
|
if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
|
|
@@ -159,6 +159,10 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
|
|
|
159
159
|
.setMaxExecutionTime(15 * 1000)
|
|
160
160
|
.where('registeredAt', '!=', null);
|
|
161
161
|
|
|
162
|
+
if (organization && STAMHOOFD.userMode === 'organization') {
|
|
163
|
+
query.where('organizationId', organization.id);
|
|
164
|
+
}
|
|
165
|
+
|
|
162
166
|
if (scopeFilter) {
|
|
163
167
|
query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
|
|
164
168
|
}
|
|
@@ -3266,7 +3266,7 @@ describe('Endpoint.RegisterMembers', () => {
|
|
|
3266
3266
|
|
|
3267
3267
|
// #region act and assert
|
|
3268
3268
|
await post(body1, organization, token);
|
|
3269
|
-
await expect(post(body2, organization, token)).rejects.toThrow(/
|
|
3269
|
+
await expect(post(body2, organization, token)).rejects.toThrow(/Cannot delete inactive registration/);
|
|
3270
3270
|
// #endregion
|
|
3271
3271
|
});
|
|
3272
3272
|
});
|
package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { PaymentService } from '../../../../services/PaymentService.js';
|
|
|
12
12
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
13
13
|
import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
|
|
14
14
|
import { BalanceItemService } from '../../../../services/BalanceItemService.js';
|
|
15
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
15
16
|
|
|
16
17
|
type Params = Record<string, never>;
|
|
17
18
|
type Query = CountFilteredRequest;
|
|
@@ -37,98 +38,116 @@ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Bo
|
|
|
37
38
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
38
39
|
const sellingOrganization = await Context.setOrganizationScope();
|
|
39
40
|
const { user } = await Context.authenticate();
|
|
40
|
-
const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
message: 'Cannot charge a cached balance from a different organization',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
// todo
|
|
50
|
-
const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
|
|
42
|
+
if (!await Context.auth.canManageFinances(sellingOrganization.id)) {
|
|
43
|
+
throw Context.auth.error();
|
|
44
|
+
}
|
|
45
|
+
const queueId = 'charge-receivable-balances/' + sellingOrganization.id;
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
if (QueueHandler.isRunning(queueId)) {
|
|
48
|
+
throw new SimpleError({
|
|
49
|
+
code: 'charge_pending',
|
|
50
|
+
message: 'Already pending charge',
|
|
51
|
+
human: $t('%Zbh'),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const i of items) {
|
|
60
|
-
map.set(i, i.priceOpen);
|
|
61
|
-
total += i.priceOpen;
|
|
62
|
-
}
|
|
63
|
-
if (total <= 0) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
55
|
+
await QueueHandler.schedule(queueId, async () => {
|
|
56
|
+
const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
for await (const cachedBalance of query.all()) {
|
|
59
|
+
if (cachedBalance.organizationId !== sellingOrganization.id) {
|
|
60
|
+
throw new SimpleError({
|
|
61
|
+
code: 'wrong_organization',
|
|
62
|
+
message: 'Cannot charge a cached balance from a different organization',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// todo
|
|
66
|
+
const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
|
|
67
|
+
|
|
68
|
+
if (items.length === 0) {
|
|
69
|
+
console.log('Nothing to charge for', cachedBalance.id);
|
|
73
70
|
continue;
|
|
74
71
|
}
|
|
75
|
-
payingOrganization = p;
|
|
76
|
-
}
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
const map: Map<BalanceItem, number> = new Map();
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const i of items) {
|
|
76
|
+
map.set(i, i.priceOpen);
|
|
77
|
+
total += i.priceOpen;
|
|
78
|
+
}
|
|
79
|
+
if (total <= 0) {
|
|
82
80
|
continue;
|
|
83
81
|
}
|
|
84
|
-
customerUser = p;
|
|
85
|
-
}
|
|
86
82
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
let payingOrganization: Organization | null = null;
|
|
84
|
+
let customerUser: User | null = null;
|
|
85
|
+
if (cachedBalance.objectType === ReceivableBalanceType.organization) {
|
|
86
|
+
const p = await Organization.getByID(cachedBalance.objectId);
|
|
87
|
+
if (!p || !(await Context.auth.hasFullAccess(p))) {
|
|
88
|
+
console.error('Unexpected missing paying organization id', cachedBalance);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
payingOrganization = p;
|
|
92
|
+
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
if (cachedBalance.objectType === ReceivableBalanceType.user || cachedBalance.objectType === ReceivableBalanceType.userWithoutMembers) {
|
|
95
|
+
const p = await User.getByID(cachedBalance.objectId);
|
|
96
|
+
if (!p || !(Context.auth.checkScope(p.organizationId))) {
|
|
97
|
+
console.error('Unexpected missing customer user id', cachedBalance);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
customerUser = p;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mandates = await PaymentMandateService.getMandates({
|
|
104
|
+
sellingOrganization,
|
|
105
|
+
user: customerUser,
|
|
106
|
+
payingOrganization,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const mandate = mandates.find(m => m.isDefault);
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
if (!mandate) {
|
|
96
112
|
// Not possible
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
console.error('No mandates found for', cachedBalance.id);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
100
116
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
customerUser = customerUser ?? (payingOrganization ? (await payingOrganization.getFullAdmins())[0] : null);
|
|
118
|
+
const customer = PaymentCustomer.create({
|
|
119
|
+
firstName: customerUser?.firstName,
|
|
120
|
+
lastName: customerUser?.lastName,
|
|
121
|
+
email: customerUser?.email ?? (payingOrganization ? (await payingOrganization.getReplyEmails())[0].email : null),
|
|
122
|
+
company: payingOrganization?.defaultCompanies[0],
|
|
123
|
+
});
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
await PaymentService.createPayment({
|
|
126
|
+
balanceItems: map,
|
|
127
|
+
checkout: {
|
|
128
|
+
paymentMethod: PaymentMethod.Unknown,
|
|
129
|
+
totalPrice: null,
|
|
130
|
+
customer,
|
|
131
|
+
cancelUrl: null,
|
|
132
|
+
redirectUrl: null,
|
|
133
|
+
},
|
|
134
|
+
user: customerUser,
|
|
135
|
+
adminUserId: user.id,
|
|
136
|
+
organization: sellingOrganization,
|
|
137
|
+
payingOrganization,
|
|
138
|
+
serviceFeeType: 'system',
|
|
139
|
+
createMandate: null,
|
|
140
|
+
useMandate: mandate,
|
|
141
|
+
paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
|
|
142
|
+
privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Clear cache while looping, to update the table while we are charging
|
|
146
|
+
// Make sure the user can refresh data and see the updated cached amounts
|
|
147
|
+
await BalanceItemService.flushCaches(sellingOrganization.id);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
129
150
|
|
|
130
|
-
// Make sure the user can refresh data and see the updated cached amounts
|
|
131
|
-
await BalanceItemService.flushCaches(sellingOrganization.id);
|
|
132
151
|
return new Response(undefined, 201);
|
|
133
152
|
}
|
|
134
153
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
2
2
|
import type { Organization } from '@stamhoofd/models';
|
|
3
|
-
import { GroupFactory, OrganizationFactory, OrganizationRegistrationPeriod, OrganizationRegistrationPeriodFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
4
|
-
import { GroupSettings, Group as GroupStruct, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, Permissions, Version } from '@stamhoofd/structures';
|
|
3
|
+
import { Group, GroupFactory, OrganizationFactory, OrganizationRegistrationPeriod, OrganizationRegistrationPeriodFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
4
|
+
import { GroupCategory, GroupSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, Permissions, TranslatedString, Version } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import type { PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
|
|
7
7
|
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
@@ -153,6 +153,65 @@ describe('Endpoint.PatchOrganizationRegistrationPeriods', () => {
|
|
|
153
153
|
expect(response.body).toBeDefined();
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
test('should copy groups that reference a waiting list', async () => {
|
|
157
|
+
const organization = await new OrganizationFactory({ }).create();
|
|
158
|
+
|
|
159
|
+
// create period
|
|
160
|
+
const startDate = new Date(Date.now() + 5 * 24 * 60 * 60 * 1000);
|
|
161
|
+
const endDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
162
|
+
|
|
163
|
+
const newPeriod = await new RegistrationPeriodFactory({
|
|
164
|
+
organization,
|
|
165
|
+
startDate,
|
|
166
|
+
endDate,
|
|
167
|
+
}).create();
|
|
168
|
+
|
|
169
|
+
const user = await new UserFactory({
|
|
170
|
+
organization,
|
|
171
|
+
permissions: Permissions.create({ level: PermissionLevel.Full }),
|
|
172
|
+
}).create();
|
|
173
|
+
|
|
174
|
+
const token = await Token.createToken(user);
|
|
175
|
+
|
|
176
|
+
// A waiting list group and a regular group that references it. The regular group is
|
|
177
|
+
// listed BEFORE the waiting list, reproducing the ordering produced by Group.defaultSort
|
|
178
|
+
// when duplicating a period (groups with a maxAge sort before waiting lists).
|
|
179
|
+
const waitingListGroup = GroupStruct.create({
|
|
180
|
+
type: GroupType.WaitingList,
|
|
181
|
+
settings: GroupSettings.create({ name: TranslatedString.create('Wachtlijst') }),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const regularGroup = GroupStruct.create({
|
|
185
|
+
type: GroupType.Membership,
|
|
186
|
+
settings: GroupSettings.create({ name: TranslatedString.create('Kapoenen'), maxAge: 8 }),
|
|
187
|
+
waitingList: waitingListGroup,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const newOrganizationPeriod = OrganizationRegistrationPeriodStruct.create({
|
|
191
|
+
period: newPeriod.getStructure(),
|
|
192
|
+
});
|
|
193
|
+
newOrganizationPeriod.groups.push(regularGroup, waitingListGroup);
|
|
194
|
+
newOrganizationPeriod.settings.categories = [
|
|
195
|
+
GroupCategory.create({ id: 'root', groupIds: [regularGroup.id] }),
|
|
196
|
+
];
|
|
197
|
+
newOrganizationPeriod.settings.rootCategoryId = 'root';
|
|
198
|
+
|
|
199
|
+
const patch: PatchableArrayAutoEncoder<OrganizationRegistrationPeriodStruct> = new PatchableArray();
|
|
200
|
+
patch.addPut(newOrganizationPeriod);
|
|
201
|
+
|
|
202
|
+
const response = await patchOrganizationRegistrationPeriods({ patch, organization, token });
|
|
203
|
+
expect(response.body).toBeDefined();
|
|
204
|
+
|
|
205
|
+
// Both the regular group and its waiting list should have been created
|
|
206
|
+
const createdGroups = await Group.getAll(organization.id, newPeriod.id, true, [GroupType.Membership, GroupType.WaitingList]);
|
|
207
|
+
const regular = createdGroups.find(g => g.id === regularGroup.id);
|
|
208
|
+
const waitingList = createdGroups.find(g => g.id === waitingListGroup.id);
|
|
209
|
+
|
|
210
|
+
expect(waitingList).toBeDefined();
|
|
211
|
+
expect(regular).toBeDefined();
|
|
212
|
+
expect(regular!.waitingListId).toBe(waitingListGroup.id);
|
|
213
|
+
});
|
|
214
|
+
|
|
156
215
|
test('should not be able to patch groups of other organization', async () => {
|
|
157
216
|
const organization = await new OrganizationFactory({ }).create();
|
|
158
217
|
const otherOrganization = await new OrganizationFactory({ }).create();
|
|
@@ -342,7 +342,20 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
342
342
|
organizationPeriod.settings = struct.settings;
|
|
343
343
|
await organizationPeriod.save();
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
// Create waiting list groups first: a regular group referencing a waiting list requires
|
|
346
|
+
// that waiting list group to already exist in the same period (see createGroup), otherwise
|
|
347
|
+
// it fails with 'Waiting list not found' and gets silently skipped below.
|
|
348
|
+
const sortedGroups = [...struct.groups].sort((a, b) => {
|
|
349
|
+
if (a.type === GroupType.WaitingList && b.type !== GroupType.WaitingList) {
|
|
350
|
+
return -1;
|
|
351
|
+
}
|
|
352
|
+
if (b.type === GroupType.WaitingList && a.type !== GroupType.WaitingList) {
|
|
353
|
+
return 1;
|
|
354
|
+
}
|
|
355
|
+
return 0;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
for (const s of sortedGroups) {
|
|
346
359
|
s.settings.registeredMembers = 0;
|
|
347
360
|
s.settings.reservedMembers = 0;
|
|
348
361
|
try {
|
|
@@ -486,9 +486,21 @@ export class AdminPermissionChecker {
|
|
|
486
486
|
* Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
|
|
487
487
|
*/
|
|
488
488
|
async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read, checkMember: boolean | MemberWithUsersRegistrationsAndGroups = true) {
|
|
489
|
+
const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
|
|
490
|
+
|
|
491
|
+
if (!organizationPermissions) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
|
|
496
|
+
// Only full permissions; because non-full doesn't have access to other periods
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Non full admin logic
|
|
489
501
|
if (registration.deactivatedAt || !registration.registeredAt) {
|
|
490
502
|
if (!checkMember) {
|
|
491
|
-
// We can't grant access to a member because of a deactivated registration
|
|
503
|
+
// We can't grant access to a member because of a deactivated registration - unless full permission
|
|
492
504
|
return false;
|
|
493
505
|
}
|
|
494
506
|
|
|
@@ -499,16 +511,6 @@ export class AdminPermissionChecker {
|
|
|
499
511
|
}
|
|
500
512
|
}
|
|
501
513
|
|
|
502
|
-
const organizationPermissions = await this.getOrganizationPermissions(registration.organizationId);
|
|
503
|
-
|
|
504
|
-
if (!organizationPermissions) {
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (organizationPermissions.hasAccess(PermissionLevel.Full)) {
|
|
509
|
-
// Only full permissions; because non-full doesn't have access to other periods
|
|
510
|
-
return true;
|
|
511
|
-
}
|
|
512
514
|
const organization = await this.getOrganization(registration.organizationId);
|
|
513
515
|
|
|
514
516
|
if (registration.periodId !== organization.periodId) {
|
|
@@ -514,6 +514,9 @@ export class AuthenticatedStructures {
|
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
for (const member of members) {
|
|
517
|
+
if (member.organizationId) {
|
|
518
|
+
organizationIds.push(member.organizationId);
|
|
519
|
+
}
|
|
517
520
|
for (const registration of member.registrations) {
|
|
518
521
|
organizationIds.push(registration.organizationId);
|
|
519
522
|
}
|
|
@@ -620,14 +623,8 @@ export class AuthenticatedStructures {
|
|
|
620
623
|
group: Group;
|
|
621
624
|
})[] = [];
|
|
622
625
|
const userManager = Context.auth.isUserManager(member);
|
|
626
|
+
|
|
623
627
|
for (const registration of member.registrations) {
|
|
624
|
-
if (includeContextOrganization || registration.organizationId !== Context.auth.organization?.id) {
|
|
625
|
-
const found = organizations.get(registration.id);
|
|
626
|
-
if (!found) {
|
|
627
|
-
const organization = await Context.auth.getOrganization(registration.organizationId);
|
|
628
|
-
organizations.set(organization.id, organization);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
628
|
if (organizations.get(registration.organizationId)?.active || (Context.auth.organization && Context.auth.organization.active && registration.organizationId === Context.auth.organization.id) || await Context.auth.hasFullAccess(registration.organizationId)) {
|
|
632
629
|
if (
|
|
633
630
|
!!options?.forAdminCartCalculation
|
|
@@ -639,6 +636,7 @@ export class AuthenticatedStructures {
|
|
|
639
636
|
}
|
|
640
637
|
}
|
|
641
638
|
}
|
|
639
|
+
|
|
642
640
|
member.registrations = filtered;
|
|
643
641
|
const balancesPermission = (!!options?.forAdminCartCalculation) || await Context.auth.hasFinancialMemberAccess(member, PermissionLevel.Read, Context.organization?.id);
|
|
644
642
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Platform } 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
|
+
const platform = await Platform.getForEditing();
|
|
11
|
+
|
|
12
|
+
if (!platform.config.featureFlags.includes('uitpas')) {
|
|
13
|
+
console.log('Platform does not have the uitpas feature flag, nothing to migrate');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The uitpas (UiTPAS social tariff on webshops) feature flag is now controlled
|
|
18
|
+
// per organization instead of platform-wide. Remove it from the platform config
|
|
19
|
+
// so it no longer implicitly enables the feature for every organization.
|
|
20
|
+
platform.config.featureFlags = platform.config.featureFlags.filter(f => f !== 'uitpas');
|
|
21
|
+
await platform.save();
|
|
22
|
+
await Platform.clearCache();
|
|
23
|
+
|
|
24
|
+
console.log('Removed the uitpas feature flag from the platform config');
|
|
25
|
+
});
|
|
@@ -251,7 +251,7 @@ export class MollieService {
|
|
|
251
251
|
details: PaymentMandateDetails.create({
|
|
252
252
|
name: ('consumerName' in details ? details.consumerName : details.cardHolder) ?? undefined,
|
|
253
253
|
cardNumber: 'cardNumber' in details ? details.cardNumber : null,
|
|
254
|
-
iban: 'consumerAccount' in details ? details.consumerAccount : null,
|
|
254
|
+
iban: 'consumerAccount' in details ? Formatter.iban(details.consumerAccount) : null,
|
|
255
255
|
bic: ('consumerBic' in details ? details.consumerBic : undefined),
|
|
256
256
|
expiryDate: ('cardExpiryDate' in details ? DateTime.fromISO(details.cardExpiryDate, { zone: Formatter.timezone }).toJSDate() : null), // todo: parse date correctly in Brussels timezone!
|
|
257
257
|
brand: ('cardLabel' in details ? details.cardLabel : null),
|
|
@@ -61,6 +61,10 @@ export class SSOService {
|
|
|
61
61
|
this.provider = data.provider;
|
|
62
62
|
this.platform = data.platform;
|
|
63
63
|
this.organization = data.organization ?? null;
|
|
64
|
+
|
|
65
|
+
if (STAMHOOFD.userMode === 'platform' && this.organization) {
|
|
66
|
+
throw new Error('SSO provided by organization disabled in platform mode');
|
|
67
|
+
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
static async clearExpiredTokensOrFromUser(userId: string | null = null) {
|
|
@@ -371,7 +375,7 @@ export class SSOService {
|
|
|
371
375
|
if (token) {
|
|
372
376
|
user = await User.getByID(token.userId);
|
|
373
377
|
|
|
374
|
-
if (!user) {
|
|
378
|
+
if (!user || user.organizationId !== (this.organization?.id ?? null)) {
|
|
375
379
|
throw new SimpleError({
|
|
376
380
|
code: 'invalid_user',
|
|
377
381
|
message: 'User not found',
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { UitpasEventResponse, UitpasEventsResponse } from '@stamhoofd/structures';
|
|
2
2
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
3
3
|
|
|
4
|
+
/* Pick a translated string, prefer 'nl' */
|
|
5
|
+
function pickTranslation(value: unknown): string {
|
|
6
|
+
if (typeof value !== 'object' || value === null) {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
const entries = Object.entries(value as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === 'string');
|
|
10
|
+
if (entries.length === 0) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
const nl = entries.find(([key]) => key === 'nl');
|
|
14
|
+
return nl ? nl[1] : entries[0][1];
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
type EventsResponse = {
|
|
5
18
|
totalItems: number;
|
|
6
19
|
itemsPerPage: number;
|
|
7
20
|
member: Array<{
|
|
8
21
|
'@id': string;
|
|
9
|
-
'name':
|
|
10
|
-
nl: string;
|
|
11
|
-
};
|
|
22
|
+
'name': Record<string, string>;
|
|
12
23
|
'location': {
|
|
13
|
-
name:
|
|
24
|
+
name: Record<string, string>;
|
|
14
25
|
};
|
|
15
26
|
'startDate'?: string;
|
|
16
27
|
'endDate'?: string;
|
|
@@ -33,10 +44,7 @@ function assertIsEventsResponse(json: unknown): asserts json is EventsResponse {
|
|
|
33
44
|
|| !('@id' in member)
|
|
34
45
|
|| typeof member['@id'] !== 'string'
|
|
35
46
|
|| !('name' in member)
|
|
36
|
-
||
|
|
37
|
-
|| member.name === null
|
|
38
|
-
|| !('nl' in member.name)
|
|
39
|
-
|| typeof member.name.nl !== 'string'
|
|
47
|
+
|| pickTranslation(member.name) === ''
|
|
40
48
|
|| !('location' in member)
|
|
41
49
|
|| typeof member.location !== 'object'
|
|
42
50
|
|| member.location === null
|
|
@@ -111,6 +119,10 @@ export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: st
|
|
|
111
119
|
});
|
|
112
120
|
});
|
|
113
121
|
|
|
122
|
+
return parseUitpasEventsResponse(json);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function parseUitpasEventsResponse(json: unknown): UitpasEventsResponse {
|
|
114
126
|
assertIsEventsResponse(json);
|
|
115
127
|
const eventsResponse = new UitpasEventsResponse();
|
|
116
128
|
eventsResponse.totalItems = json.totalItems;
|
|
@@ -118,16 +130,8 @@ export async function searchUitpasEvents(clientId: string, uitpasOrganizerId: st
|
|
|
118
130
|
eventsResponse.member = json.member.map((member) => {
|
|
119
131
|
const event = new UitpasEventResponse();
|
|
120
132
|
event.url = member['@id'];
|
|
121
|
-
event.name = member.name
|
|
122
|
-
|
|
123
|
-
const entrs = Object.entries(locationName);
|
|
124
|
-
const hasNl = entrs.find(([key]) => key === 'nl');
|
|
125
|
-
if (hasNl) {
|
|
126
|
-
event.location = locationName.nl;
|
|
127
|
-
} else {
|
|
128
|
-
const lang = entrs[0];
|
|
129
|
-
event.location = lang ? lang[1] : '';
|
|
130
|
-
}
|
|
133
|
+
event.name = pickTranslation(member.name);
|
|
134
|
+
event.location = pickTranslation(member.location.name);
|
|
131
135
|
if (member.startDate) {
|
|
132
136
|
event.startDate = new Date(member.startDate);
|
|
133
137
|
}
|
|
@@ -69,6 +69,58 @@ export const organizationFilterCompilers: SQLFilterDefinitions = {
|
|
|
69
69
|
type: SQLValueType.JSONArray,
|
|
70
70
|
nullable: false,
|
|
71
71
|
}),
|
|
72
|
+
recordCategoryName: createColumnFilter({
|
|
73
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'meta'), '$.value.recordsConfiguration.recordCategories[*].name'),
|
|
74
|
+
type: SQLValueType.JSONArray,
|
|
75
|
+
nullable: true,
|
|
76
|
+
}),
|
|
77
|
+
// Name of a child (sub)category in any record category, at any nesting depth
|
|
78
|
+
recordChildCategoryName: createColumnFilter({
|
|
79
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'meta'), '$.value.recordsConfiguration.recordCategories**.childCategories[*].name'),
|
|
80
|
+
type: SQLValueType.JSONArray,
|
|
81
|
+
nullable: true,
|
|
82
|
+
}),
|
|
83
|
+
recordName: createColumnFilter({
|
|
84
|
+
expression: SQL.jsonExtract(SQL.column('organizations', 'meta'), '$.value.recordsConfiguration.recordCategories**.records[*].name'),
|
|
85
|
+
type: SQLValueType.JSONArray,
|
|
86
|
+
nullable: true,
|
|
87
|
+
}),
|
|
88
|
+
documentTemplates: createExistsFilter(
|
|
89
|
+
SQL.select()
|
|
90
|
+
.from(SQL.table('document_templates'))
|
|
91
|
+
.where(
|
|
92
|
+
SQL.column('document_templates', 'organizationId'),
|
|
93
|
+
SQL.column('organizations', 'id'),
|
|
94
|
+
),
|
|
95
|
+
{
|
|
96
|
+
...baseSQLFilterCompilers,
|
|
97
|
+
type: createColumnFilter({
|
|
98
|
+
expression: SQL.jsonExtract(SQL.column('document_templates', 'privateSettings'), '$.value.templateDefinition.type'),
|
|
99
|
+
type: SQLValueType.JSONString,
|
|
100
|
+
nullable: true,
|
|
101
|
+
}),
|
|
102
|
+
year: createColumnFilter({
|
|
103
|
+
expression: SQL.column('document_templates', 'year'),
|
|
104
|
+
type: SQLValueType.Number,
|
|
105
|
+
nullable: false,
|
|
106
|
+
}),
|
|
107
|
+
status: createColumnFilter({
|
|
108
|
+
expression: SQL.column('document_templates', 'status'),
|
|
109
|
+
type: SQLValueType.String,
|
|
110
|
+
nullable: false,
|
|
111
|
+
}),
|
|
112
|
+
isLocked: createColumnFilter({
|
|
113
|
+
expression: SQL.column('document_templates', 'isLocked'),
|
|
114
|
+
type: SQLValueType.Boolean,
|
|
115
|
+
nullable: false,
|
|
116
|
+
}),
|
|
117
|
+
updatesEnabled: createColumnFilter({
|
|
118
|
+
expression: SQL.column('document_templates', 'updatesEnabled'),
|
|
119
|
+
type: SQLValueType.Boolean,
|
|
120
|
+
nullable: false,
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
),
|
|
72
124
|
setupSteps: createExistsFilter(
|
|
73
125
|
SQL.select()
|
|
74
126
|
.from(SQL.table('organization_registration_periods'))
|