@stamhoofd/backend 2.135.0 → 2.136.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -17
- package/src/crons/cleanup-orphaned-cached-balances.test.ts +88 -0
- package/src/crons/cleanup-orphaned-cached-balances.ts +44 -0
- package/src/crons/index.ts +1 -0
- package/src/email-recipient-loaders/orders.ts +7 -9
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.test.ts +95 -0
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +7 -0
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -2
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +1 -1
- package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +131 -1
- package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +8 -3
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +287 -1
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +21 -2
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +119 -1
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +271 -3
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +15 -2
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +112 -0
- package/src/endpoints/organization/webshops/OrderConfirmationEmailLanguage.test.ts +87 -1
- package/src/excel-loaders/members.test.ts +59 -0
- package/src/excel-loaders/members.ts +17 -0
- package/src/helpers/AuthenticatedStructures.ts +9 -0
- package/src/helpers/MemberMerger.test.ts +70 -2
- package/src/helpers/MemberMerger.ts +43 -1
- package/src/helpers/MemberUserSyncer.ts +5 -0
- package/src/helpers/MembershipCharger.test.ts +110 -0
- package/src/helpers/MembershipCharger.ts +10 -32
- package/src/helpers/XlsxTransformerColumnHelper.test.ts +63 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +47 -1
- package/src/seeds/1784057557-fix-invisible-trial-registrations.ts +143 -0
- package/src/services/BalanceItemService.ts +1 -1
- package/src/sql-filters/members.ts +5 -0
- package/src/sql-filters/orders.ts +5 -0
- package/vitest.config.js +1 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { AutoEncoderPatchType } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { PatchMap } from '@simonbackx/simple-encoding';
|
|
2
3
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
4
|
import type { Organization, RegistrationPeriod, User } from '@stamhoofd/models';
|
|
4
5
|
import { Email, EmailRecipient, GroupFactory, MemberFactory, OrganizationFactory, RegistrationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
|
-
import { AccessRight, EmailRecipientFilter, EmailRecipientSubfilter, EmailStatus, Email as EmailStruct, OrganizationEmail, Parent, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserPermissions, Version } from '@stamhoofd/structures';
|
|
6
|
+
import { AccessRight, EmailContent, EmailRecipientFilter, EmailRecipientSubfilter, EmailStatus, Email as EmailStruct, OrganizationEmail, Parent, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserPermissions, Version } from '@stamhoofd/structures';
|
|
7
|
+
import { Language } from '@stamhoofd/types/Language';
|
|
6
8
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
7
9
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
8
10
|
import { PatchEmailEndpoint } from './PatchEmailEndpoint.js';
|
|
@@ -1199,4 +1201,288 @@ describe('Endpoint.PatchEmailEndpoint', () => {
|
|
|
1199
1201
|
expect(sentEmail.html).toContain('Identical Email');
|
|
1200
1202
|
}
|
|
1201
1203
|
});
|
|
1204
|
+
|
|
1205
|
+
describe('Translations', () => {
|
|
1206
|
+
const createDraftEmail = async (options: { language?: Language } = {}) => {
|
|
1207
|
+
const email = new Email();
|
|
1208
|
+
email.subject = 'Default subject';
|
|
1209
|
+
email.status = EmailStatus.Draft;
|
|
1210
|
+
email.text = 'Default text';
|
|
1211
|
+
email.html = '<p>Default html</p>';
|
|
1212
|
+
email.json = {};
|
|
1213
|
+
email.language = options.language ?? null;
|
|
1214
|
+
email.userId = user.id;
|
|
1215
|
+
email.organizationId = organization.id;
|
|
1216
|
+
email.senderId = sender.id;
|
|
1217
|
+
await email.save();
|
|
1218
|
+
return email;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
test('the first language can be set without creating a translation', async () => {
|
|
1222
|
+
const email = await createDraftEmail();
|
|
1223
|
+
|
|
1224
|
+
const body = EmailStruct.patch({
|
|
1225
|
+
id: email.id,
|
|
1226
|
+
language: Language.Dutch,
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
const response = await patchEmail(body, token, organization);
|
|
1230
|
+
expect(response.body.language).toBe(Language.Dutch);
|
|
1231
|
+
expect(response.body.translations.size).toBe(0);
|
|
1232
|
+
|
|
1233
|
+
const saved = await Email.getByID(email.id);
|
|
1234
|
+
expect(saved!.language).toBe(Language.Dutch);
|
|
1235
|
+
expect(saved!.translations.size).toBe(0);
|
|
1236
|
+
expect(saved!.subject).toBe('Default subject');
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
test('a translation can be added to a draft email', async () => {
|
|
1240
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1241
|
+
|
|
1242
|
+
const body = EmailStruct.patch({
|
|
1243
|
+
id: email.id,
|
|
1244
|
+
translations: new PatchMap([[Language.French, EmailContent.create({ subject: 'Sujet français', html: '<p>Français</p>', text: 'Français' })]]),
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
const response = await patchEmail(body, token, organization);
|
|
1248
|
+
expect(response.body.translations.get(Language.French)!.subject).toBe('Sujet français');
|
|
1249
|
+
|
|
1250
|
+
const saved = await Email.getByID(email.id);
|
|
1251
|
+
expect(saved!.translations.get(Language.French)!.subject).toBe('Sujet français');
|
|
1252
|
+
expect(saved!.subject).toBe('Default subject');
|
|
1253
|
+
expect(saved!.language).toBe(Language.Dutch);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
test('a translation can be removed from a draft email', async () => {
|
|
1257
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1258
|
+
email.translations = new Map([
|
|
1259
|
+
[Language.French, EmailContent.create({ subject: 'Sujet français' })],
|
|
1260
|
+
[Language.English, EmailContent.create({ subject: 'English subject' })],
|
|
1261
|
+
]);
|
|
1262
|
+
await email.save();
|
|
1263
|
+
|
|
1264
|
+
const body = EmailStruct.patch({
|
|
1265
|
+
id: email.id,
|
|
1266
|
+
translations: new PatchMap([[Language.French, null]]),
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
await patchEmail(body, token, organization);
|
|
1270
|
+
|
|
1271
|
+
const saved = await Email.getByID(email.id);
|
|
1272
|
+
expect(saved!.translations.size).toBe(1);
|
|
1273
|
+
expect(saved!.translations.get(Language.English)!.subject).toBe('English subject');
|
|
1274
|
+
expect(saved!.language).toBe(Language.Dutch);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test('removing the default language applies the move-over patch atomically', async () => {
|
|
1278
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1279
|
+
email.translations = new Map([
|
|
1280
|
+
[Language.French, EmailContent.create({ subject: 'Sujet français', html: '<p>Français</p>', text: 'Français' })],
|
|
1281
|
+
]);
|
|
1282
|
+
await email.save();
|
|
1283
|
+
|
|
1284
|
+
// The UI removes the default language by moving the first remaining translation
|
|
1285
|
+
// into the default content in a single patch
|
|
1286
|
+
const body = EmailStruct.patch({
|
|
1287
|
+
id: email.id,
|
|
1288
|
+
language: Language.French,
|
|
1289
|
+
subject: 'Sujet français',
|
|
1290
|
+
html: '<p>Français</p>',
|
|
1291
|
+
text: 'Français',
|
|
1292
|
+
translations: new PatchMap([[Language.French, null]]),
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
await patchEmail(body, token, organization);
|
|
1296
|
+
|
|
1297
|
+
const saved = await Email.getByID(email.id);
|
|
1298
|
+
expect(saved!.language).toBe(Language.French);
|
|
1299
|
+
expect(saved!.subject).toBe('Sujet français');
|
|
1300
|
+
expect(saved!.html).toBe('<p>Français</p>');
|
|
1301
|
+
expect(saved!.translations.size).toBe(0);
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
test('removing the last language keeps the content', async () => {
|
|
1305
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1306
|
+
|
|
1307
|
+
const body = EmailStruct.patch({
|
|
1308
|
+
id: email.id,
|
|
1309
|
+
language: null,
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
await patchEmail(body, token, organization);
|
|
1313
|
+
|
|
1314
|
+
const saved = await Email.getByID(email.id);
|
|
1315
|
+
expect(saved!.language).toBeNull();
|
|
1316
|
+
expect(saved!.subject).toBe('Default subject');
|
|
1317
|
+
expect(saved!.html).toBe('<p>Default html</p>');
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test('patching the subject keeps the translations', async () => {
|
|
1321
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1322
|
+
email.translations = new Map([
|
|
1323
|
+
[Language.French, EmailContent.create({ subject: 'Sujet français' })],
|
|
1324
|
+
]);
|
|
1325
|
+
await email.save();
|
|
1326
|
+
|
|
1327
|
+
const body = EmailStruct.patch({
|
|
1328
|
+
id: email.id,
|
|
1329
|
+
subject: 'New default subject',
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
await patchEmail(body, token, organization);
|
|
1333
|
+
|
|
1334
|
+
const saved = await Email.getByID(email.id);
|
|
1335
|
+
expect(saved!.subject).toBe('New default subject');
|
|
1336
|
+
expect(saved!.language).toBe(Language.Dutch);
|
|
1337
|
+
expect(saved!.translations.get(Language.French)!.subject).toBe('Sujet français');
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test('rejects a translation without a default language', async () => {
|
|
1341
|
+
const email = await createDraftEmail();
|
|
1342
|
+
|
|
1343
|
+
const body = EmailStruct.patch({
|
|
1344
|
+
id: email.id,
|
|
1345
|
+
translations: new PatchMap([[Language.French, EmailContent.create({ subject: 'Sujet français' })]]),
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
await expect(patchEmail(body, token, organization))
|
|
1349
|
+
.rejects
|
|
1350
|
+
.toThrow(STExpect.errorWithCode('invalid_translations'));
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test('rejects a translation for the default language itself', async () => {
|
|
1354
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1355
|
+
|
|
1356
|
+
const body = EmailStruct.patch({
|
|
1357
|
+
id: email.id,
|
|
1358
|
+
translations: new PatchMap([[Language.Dutch, EmailContent.create({ subject: 'Nederlands onderwerp' })]]),
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
await expect(patchEmail(body, token, organization))
|
|
1362
|
+
.rejects
|
|
1363
|
+
.toThrow(STExpect.errorWithCode('invalid_translations'));
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
test('rejects clearing the default language while translations remain', async () => {
|
|
1367
|
+
const email = await createDraftEmail({ language: Language.Dutch });
|
|
1368
|
+
email.translations = new Map([
|
|
1369
|
+
[Language.French, EmailContent.create({ subject: 'Sujet français' })],
|
|
1370
|
+
]);
|
|
1371
|
+
await email.save();
|
|
1372
|
+
|
|
1373
|
+
const body = EmailStruct.patch({
|
|
1374
|
+
id: email.id,
|
|
1375
|
+
language: null,
|
|
1376
|
+
});
|
|
1377
|
+
|
|
1378
|
+
await expect(patchEmail(body, token, organization))
|
|
1379
|
+
.rejects
|
|
1380
|
+
.toThrow(STExpect.errorWithCode('invalid_translations'));
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
const createSendableEmail = async () => {
|
|
1384
|
+
const email = new Email();
|
|
1385
|
+
email.subject = 'Default subject';
|
|
1386
|
+
email.status = EmailStatus.Draft;
|
|
1387
|
+
email.text = 'Default text {{unsubscribeUrl}}';
|
|
1388
|
+
email.html = '<!DOCTYPE html><html><body><p>Default html</p>{{unsubscribeUrl}}</body></html>';
|
|
1389
|
+
email.json = {};
|
|
1390
|
+
email.language = Language.Dutch;
|
|
1391
|
+
email.userId = user.id;
|
|
1392
|
+
email.organizationId = organization.id;
|
|
1393
|
+
email.senderId = sender.id;
|
|
1394
|
+
return email;
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
test('cannot send an email with an incomplete translation', async () => {
|
|
1398
|
+
const email = await createSendableEmail();
|
|
1399
|
+
email.translations = new Map([
|
|
1400
|
+
[Language.French, EmailContent.create({ subject: 'Sujet français', text: '', html: '' })],
|
|
1401
|
+
]);
|
|
1402
|
+
await email.save();
|
|
1403
|
+
|
|
1404
|
+
const body = EmailStruct.patch({ id: email.id, status: EmailStatus.Sending });
|
|
1405
|
+
|
|
1406
|
+
await expect(patchEmail(body, token, organization))
|
|
1407
|
+
.rejects
|
|
1408
|
+
.toThrow(STExpect.errorWithCode('invalid_field'));
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
test('cannot send an email when a translation misses the unsubscribe button', async () => {
|
|
1412
|
+
const email = await createSendableEmail();
|
|
1413
|
+
email.translations = new Map([
|
|
1414
|
+
[Language.French, EmailContent.create({
|
|
1415
|
+
subject: 'Sujet français',
|
|
1416
|
+
text: 'Texte français',
|
|
1417
|
+
html: '<!DOCTYPE html><html><body><p>Français</p></body></html>',
|
|
1418
|
+
})],
|
|
1419
|
+
]);
|
|
1420
|
+
await email.save();
|
|
1421
|
+
|
|
1422
|
+
const body = EmailStruct.patch({ id: email.id, status: EmailStatus.Sending });
|
|
1423
|
+
|
|
1424
|
+
await expect(patchEmail(body, token, organization))
|
|
1425
|
+
.rejects
|
|
1426
|
+
.toThrow(STExpect.errorWithCode('missing_unsubscribe_button'));
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
describe('Example recipients per language', () => {
|
|
1431
|
+
// The greeting translations looked up in shared/locales/dist/locales/digit/{nl,fr}-BE.json,
|
|
1432
|
+
// hardcoded on purpose so we don't verify $t with the same $t machinery we're testing
|
|
1433
|
+
const dutchGreeting = 'Dag';
|
|
1434
|
+
const frenchGreeting = 'Bonjour';
|
|
1435
|
+
|
|
1436
|
+
const createDraftEmail = async (options: { language?: Language; translations?: Map<Language, EmailContent> } = {}) => {
|
|
1437
|
+
const email = new Email();
|
|
1438
|
+
email.subject = 'Default subject';
|
|
1439
|
+
email.status = EmailStatus.Draft;
|
|
1440
|
+
email.text = 'Default text';
|
|
1441
|
+
email.html = '<p>Default html</p>';
|
|
1442
|
+
email.json = {};
|
|
1443
|
+
email.language = options.language ?? null;
|
|
1444
|
+
email.translations = options.translations ?? new Map();
|
|
1445
|
+
email.userId = user.id;
|
|
1446
|
+
email.organizationId = organization.id;
|
|
1447
|
+
email.senderId = sender.id;
|
|
1448
|
+
await email.save();
|
|
1449
|
+
return email;
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
const greetingOf = (recipient: { replacements: { token: string; value: string }[] }) => {
|
|
1453
|
+
return recipient.replacements.find(r => r.token === 'greeting')?.value;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
test('the same example recipient is returned with replacements in every supported language', async () => {
|
|
1457
|
+
TestUtils.setEnvironment('locales', { BE: [Language.Dutch, Language.French] });
|
|
1458
|
+
const email = await createDraftEmail();
|
|
1459
|
+
|
|
1460
|
+
const response = await patchEmail(EmailStruct.patch({ id: email.id }), token, organization);
|
|
1461
|
+
const preview = response.body;
|
|
1462
|
+
|
|
1463
|
+
// The deprecated single example recipient is still returned
|
|
1464
|
+
expect(preview.exampleRecipient).not.toBeNull();
|
|
1465
|
+
expect(greetingOf(preview.exampleRecipient!)).toBe(`${dutchGreeting} ${preview.exampleRecipient!.firstName},`);
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
test('every language the email has content for is included when it is supported', async () => {
|
|
1469
|
+
TestUtils.setEnvironment('locales', { BE: [Language.Dutch, Language.French, Language.English] });
|
|
1470
|
+
const email = await createDraftEmail({
|
|
1471
|
+
language: Language.Dutch,
|
|
1472
|
+
translations: new Map([[Language.English, EmailContent.create({ subject: 'English subject', html: '<p>English</p>', text: 'English' })]]),
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
const response = await patchEmail(EmailStruct.patch({ id: email.id }), token, organization);
|
|
1476
|
+
const preview = response.body;
|
|
1477
|
+
|
|
1478
|
+
expect([...preview.exampleRecipients.keys()].sort()).toEqual([Language.Dutch, Language.English].sort());
|
|
1479
|
+
const english = preview.exampleRecipients.get(Language.English)!;
|
|
1480
|
+
expect(english.language).toBe(Language.English);
|
|
1481
|
+
|
|
1482
|
+
// The exact English text is machine-generated and can change between locale builds:
|
|
1483
|
+
// assert it was generated in its own language (not in one of the other languages)
|
|
1484
|
+
expect(greetingOf(english)).toBeDefined();
|
|
1485
|
+
expect(greetingOf(english)).not.toBe(greetingOf(preview.exampleRecipients.get(Language.Dutch)!));
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1202
1488
|
});
|
|
@@ -2,7 +2,7 @@ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
|
2
2
|
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { Email, Platform } from '@stamhoofd/models';
|
|
4
4
|
import type { EmailPreview } from '@stamhoofd/structures';
|
|
5
|
-
import { EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
5
|
+
import { EmailRecipientsStatus, EmailStatus, Email as EmailStruct, PermissionLevel, validateEmailTranslations } from '@stamhoofd/structures';
|
|
6
6
|
|
|
7
7
|
import type { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
|
|
8
8
|
import { patchObject } from '@simonbackx/simple-encoding';
|
|
@@ -114,6 +114,25 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
114
114
|
model.json = request.body.json;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
if (request.body.translations !== undefined) {
|
|
118
|
+
const lang = [...model.translations.keys()];
|
|
119
|
+
model.translations = patchObject(model.translations, request.body.translations);
|
|
120
|
+
const after = [...model.translations.keys()];
|
|
121
|
+
|
|
122
|
+
if (JSON.stringify(lang.sort()) && JSON.stringify(after.sort())) {
|
|
123
|
+
rebuild = true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (request.body.language !== undefined) {
|
|
128
|
+
model.language = request.body.language;
|
|
129
|
+
rebuild = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (request.body.translations !== undefined || request.body.language !== undefined) {
|
|
133
|
+
validateEmailTranslations(model);
|
|
134
|
+
}
|
|
135
|
+
|
|
117
136
|
if (request.body.recipientFilter) {
|
|
118
137
|
if (model.status !== EmailStatus.Draft) {
|
|
119
138
|
throw new SimpleError({
|
|
@@ -192,6 +211,6 @@ export class PatchEmailEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
192
211
|
await model.queueForSending();
|
|
193
212
|
}
|
|
194
213
|
|
|
195
|
-
return new Response(await model.getPreviewStructure());
|
|
214
|
+
return new Response(await model.getPreviewStructure({ allLanguages: true }));
|
|
196
215
|
}
|
|
197
216
|
}
|
|
@@ -3,7 +3,7 @@ import { Request } from '@simonbackx/simple-endpoints';
|
|
|
3
3
|
import type { MemberWithUsersRegistrationsAndGroups, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
4
|
import { EventFactory, GroupFactory, MemberFactory, OrganizationFactory, RecordCategoryFactory, RegistrationFactory, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
|
|
5
5
|
import type { SortList } from '@stamhoofd/structures';
|
|
6
|
-
import { AccessRight, EventMeta, GroupType, LimitedFilteredRequest, NamedObject, PermissionLevel, PermissionRoleDetailed, Permissions, PermissionsResourceType, RecordAnswer, RecordTextAnswer, RecordType, ResourcePermissions, SortItemDirection } from '@stamhoofd/structures';
|
|
6
|
+
import { AccessRight, EventMeta, GroupType, LimitedFilteredRequest, NamedObject, PermissionLevel, PermissionRoleDetailed, Permissions, PermissionsResourceType, RecordAnswer, RecordDateAnswer, RecordTextAnswer, RecordType, ResourcePermissions, SortItemDirection } from '@stamhoofd/structures';
|
|
7
7
|
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
8
8
|
import { GetMembersEndpoint } from './GetMembersEndpoint.js';
|
|
9
9
|
import { testServer } from '../../../../tests/helpers/TestServer.js';
|
|
@@ -1446,6 +1446,124 @@ describe('Endpoint.GetMembersEndpoint', () => {
|
|
|
1446
1446
|
});
|
|
1447
1447
|
|
|
1448
1448
|
describe('Record answer filtering', () => {
|
|
1449
|
+
test('A user can filter members on the date of a date record answer', async () => {
|
|
1450
|
+
const resources = new Map();
|
|
1451
|
+
|
|
1452
|
+
const organization = await new OrganizationFactory({ period, roles: [] })
|
|
1453
|
+
.create();
|
|
1454
|
+
|
|
1455
|
+
const recordCategory = await new RecordCategoryFactory({
|
|
1456
|
+
records: [
|
|
1457
|
+
{
|
|
1458
|
+
type: RecordType.Date,
|
|
1459
|
+
},
|
|
1460
|
+
],
|
|
1461
|
+
}).create();
|
|
1462
|
+
|
|
1463
|
+
await initPlatformRecordCategory({ recordCategory });
|
|
1464
|
+
const record = recordCategory.records[0];
|
|
1465
|
+
|
|
1466
|
+
const user = await new UserFactory({
|
|
1467
|
+
organization,
|
|
1468
|
+
permissions: Permissions.create({
|
|
1469
|
+
level: PermissionLevel.None,
|
|
1470
|
+
roles: [],
|
|
1471
|
+
resources,
|
|
1472
|
+
}),
|
|
1473
|
+
})
|
|
1474
|
+
.create();
|
|
1475
|
+
|
|
1476
|
+
const token = await Token.createToken(user);
|
|
1477
|
+
|
|
1478
|
+
// The member we are looking for: answered with a time of day that is not midnight
|
|
1479
|
+
const member1 = await new MemberFactory({}).create();
|
|
1480
|
+
const answer1 = RecordDateAnswer.create({ settings: record });
|
|
1481
|
+
answer1.dateValue = new Date(2023, 5, 10, 14, 30, 15);
|
|
1482
|
+
member1.details.recordAnswers.set(record.id, answer1);
|
|
1483
|
+
await member1.save();
|
|
1484
|
+
|
|
1485
|
+
// Answered with a different date
|
|
1486
|
+
const member2 = await new MemberFactory({}).create();
|
|
1487
|
+
const answer2 = RecordDateAnswer.create({ settings: record });
|
|
1488
|
+
answer2.dateValue = new Date(2023, 5, 11, 14, 30, 15);
|
|
1489
|
+
member2.details.recordAnswers.set(record.id, answer2);
|
|
1490
|
+
await member2.save();
|
|
1491
|
+
|
|
1492
|
+
// Did not answer the question
|
|
1493
|
+
const member3 = await new MemberFactory({}).create();
|
|
1494
|
+
|
|
1495
|
+
const group = await new GroupFactory({ organization, period }).create();
|
|
1496
|
+
|
|
1497
|
+
resources.set(
|
|
1498
|
+
PermissionsResourceType.Groups, new Map([[
|
|
1499
|
+
group.id,
|
|
1500
|
+
ResourcePermissions.create({
|
|
1501
|
+
level: PermissionLevel.Read,
|
|
1502
|
+
accessRights: [],
|
|
1503
|
+
}),
|
|
1504
|
+
]]),
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
resources.set(
|
|
1508
|
+
PermissionsResourceType.RecordCategories, new Map([[
|
|
1509
|
+
recordCategory.id,
|
|
1510
|
+
ResourcePermissions.create({
|
|
1511
|
+
level: PermissionLevel.Read,
|
|
1512
|
+
accessRights: [],
|
|
1513
|
+
}),
|
|
1514
|
+
]]),
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
await user.save();
|
|
1518
|
+
|
|
1519
|
+
await new RegistrationFactory({ member: member1, group }).create();
|
|
1520
|
+
await new RegistrationFactory({ member: member2, group }).create();
|
|
1521
|
+
await new RegistrationFactory({ member: member3, group }).create();
|
|
1522
|
+
|
|
1523
|
+
const request = Request.get({
|
|
1524
|
+
path: baseUrl,
|
|
1525
|
+
host: organization.getApiHost(),
|
|
1526
|
+
query: new LimitedFilteredRequest({
|
|
1527
|
+
filter: {
|
|
1528
|
+
registrations: {
|
|
1529
|
+
$elemMatch: {
|
|
1530
|
+
groupId: group.id,
|
|
1531
|
+
},
|
|
1532
|
+
},
|
|
1533
|
+
details: {
|
|
1534
|
+
recordAnswers: {
|
|
1535
|
+
[record.id]: {
|
|
1536
|
+
// Same filter as the date filter in the UI builds for 'equals'
|
|
1537
|
+
$and: [
|
|
1538
|
+
{
|
|
1539
|
+
dateValue: {
|
|
1540
|
+
$gte: new Date(2023, 5, 10),
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
dateValue: {
|
|
1545
|
+
$lte: new Date(2023, 5, 10, 23, 59, 59, 999),
|
|
1546
|
+
},
|
|
1547
|
+
},
|
|
1548
|
+
],
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
},
|
|
1553
|
+
limit: 10,
|
|
1554
|
+
}),
|
|
1555
|
+
headers: {
|
|
1556
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
const response = await testServer.test(endpoint, request);
|
|
1561
|
+
expect(response.status).toBe(200);
|
|
1562
|
+
expect(response.body.results.members).toIncludeSameMembers([
|
|
1563
|
+
expect.objectContaining({ id: member1.id }),
|
|
1564
|
+
]);
|
|
1565
|
+
});
|
|
1566
|
+
|
|
1449
1567
|
test('[REGRESSION] A user with minimal access can also view platform record answers in platform scope', async () => {
|
|
1450
1568
|
/**
|
|
1451
1569
|
* When fetching members via the admin api, without organization scope, we need to calculate which records to return and which not.
|