@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.
Files changed (34) hide show
  1. package/package.json +18 -17
  2. package/src/crons/cleanup-orphaned-cached-balances.test.ts +88 -0
  3. package/src/crons/cleanup-orphaned-cached-balances.ts +44 -0
  4. package/src/crons/index.ts +1 -0
  5. package/src/email-recipient-loaders/orders.ts +7 -9
  6. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.test.ts +95 -0
  7. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +7 -0
  8. package/src/endpoints/global/email/CreateEmailEndpoint.ts +5 -2
  9. package/src/endpoints/global/email/GetAdminEmailsEndpoint.ts +1 -1
  10. package/src/endpoints/global/email/GetEmailEndpoint.ts +1 -1
  11. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +131 -1
  12. package/src/endpoints/global/email/GetUserEmailsEndpoint.ts +8 -3
  13. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +287 -1
  14. package/src/endpoints/global/email/PatchEmailEndpoint.ts +21 -2
  15. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +119 -1
  16. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +271 -3
  17. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +15 -2
  18. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +112 -0
  19. package/src/endpoints/organization/webshops/OrderConfirmationEmailLanguage.test.ts +87 -1
  20. package/src/excel-loaders/members.test.ts +59 -0
  21. package/src/excel-loaders/members.ts +17 -0
  22. package/src/helpers/AuthenticatedStructures.ts +9 -0
  23. package/src/helpers/MemberMerger.test.ts +70 -2
  24. package/src/helpers/MemberMerger.ts +43 -1
  25. package/src/helpers/MemberUserSyncer.ts +5 -0
  26. package/src/helpers/MembershipCharger.test.ts +110 -0
  27. package/src/helpers/MembershipCharger.ts +10 -32
  28. package/src/helpers/XlsxTransformerColumnHelper.test.ts +63 -0
  29. package/src/helpers/XlsxTransformerColumnHelper.ts +47 -1
  30. package/src/seeds/1784057557-fix-invisible-trial-registrations.ts +143 -0
  31. package/src/services/BalanceItemService.ts +1 -1
  32. package/src/sql-filters/members.ts +5 -0
  33. package/src/sql-filters/orders.ts +5 -0
  34. 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.