@stamhoofd/backend 2.83.4 → 2.84.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.
Files changed (100) hide show
  1. package/index.ts +19 -4
  2. package/package.json +18 -14
  3. package/src/crons/amazon-ses.ts +26 -5
  4. package/src/crons/balance-emails.ts +18 -17
  5. package/src/email-recipient-loaders/registrations.ts +87 -0
  6. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
  7. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
  8. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
  10. package/src/endpoints/global/files/UploadFile.ts +11 -16
  11. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
  12. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
  13. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
  17. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
  18. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
  19. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
  20. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
  21. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
  22. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
  23. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
  25. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
  26. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
  27. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
  28. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
  29. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
  30. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
  31. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
  32. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
  33. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
  34. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
  35. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
  36. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
  37. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
  38. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
  39. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
  40. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
  41. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
  42. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
  43. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
  44. package/src/excel-loaders/members.ts +233 -232
  45. package/src/excel-loaders/payments.ts +1 -1
  46. package/src/excel-loaders/receivable-balances.ts +1 -1
  47. package/src/excel-loaders/registrations.ts +153 -0
  48. package/src/helpers/AdminPermissionChecker.ts +65 -37
  49. package/src/helpers/AuthenticatedStructures.ts +43 -3
  50. package/src/helpers/Context.ts +29 -1
  51. package/src/helpers/GlobalHelper.ts +3 -1
  52. package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
  53. package/src/helpers/GroupedThrottledQueue.ts +108 -0
  54. package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
  55. package/src/helpers/MemberCharger.ts +0 -5
  56. package/src/helpers/MembershipCharger.ts +3 -9
  57. package/src/helpers/OrganizationCharger.ts +0 -5
  58. package/src/helpers/ThrottledQueue.test.ts +194 -0
  59. package/src/helpers/ThrottledQueue.ts +145 -0
  60. package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
  61. package/src/middleware/ContextMiddleware.ts +1 -1
  62. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
  63. package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
  64. package/src/services/BalanceItemPaymentService.ts +1 -33
  65. package/src/services/BalanceItemService.ts +167 -48
  66. package/src/services/FileSignService.ts +18 -13
  67. package/src/services/MemberRecordStore.ts +28 -19
  68. package/src/services/PaymentReallocationService.test.ts +25 -14
  69. package/src/services/PaymentReallocationService.ts +29 -10
  70. package/src/services/PaymentService.ts +4 -16
  71. package/src/services/PlatformMembershipService.ts +8 -4
  72. package/src/services/RegistrationService.ts +66 -2
  73. package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
  74. package/src/sql-filters/groups.ts +67 -0
  75. package/src/sql-filters/members.ts +33 -58
  76. package/src/sql-filters/organization-registration-periods.ts +8 -0
  77. package/src/sql-filters/registration-periods.ts +8 -0
  78. package/src/sql-filters/registrations.ts +11 -22
  79. package/src/sql-sorters/groups.ts +24 -0
  80. package/src/sql-sorters/organization-registration-periods.ts +24 -0
  81. package/src/sql-sorters/registration-periods.ts +47 -0
  82. package/src/sql-sorters/registrations.ts +77 -0
  83. package/tests/actions/patchOrganizationMember.ts +27 -0
  84. package/tests/actions/patchPaymentStatus.ts +45 -0
  85. package/tests/actions/patchUserMember.ts +27 -0
  86. package/tests/assertions/assertBalances.ts +49 -0
  87. package/tests/e2e/api-rate-limits.test.ts +5 -5
  88. package/tests/e2e/bundle-discounts.test.ts +4060 -0
  89. package/tests/e2e/charge-members.test.ts +27 -24
  90. package/tests/e2e/documents.test.ts +398 -0
  91. package/tests/e2e/register.test.ts +292 -312
  92. package/tests/helpers/PayconiqMocker.ts +55 -0
  93. package/tests/init/index.ts +5 -0
  94. package/tests/init/initAdmin.ts +14 -0
  95. package/tests/init/initBundleDiscount.ts +47 -0
  96. package/tests/init/initPayconiq.ts +9 -0
  97. package/tests/init/initPlatformAdmin.ts +13 -0
  98. package/tests/init/initStripe.ts +21 -0
  99. package/tests/jest.setup.ts +29 -0
  100. package/src/seeds-temporary/1736266448-recall-balance-item-price-paid.ts +0 -70
@@ -49,11 +49,6 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
49
49
  throw Context.auth.error();
50
50
  }
51
51
  }
52
- else {
53
- if (!Context.auth.hasPlatformFullAccess()) {
54
- throw Context.auth.error();
55
- }
56
- }
57
52
 
58
53
  if (request.query.types?.length === 0) {
59
54
  throw new SimpleError({
@@ -120,6 +115,14 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
120
115
  defaultTemplates.unshift(...orgDefaults);
121
116
  }
122
117
 
123
- return new Response(templates.concat(defaultTemplates).map(template => EmailTemplateStruct.create(template)));
118
+ const allTemplates: EmailTemplate[] = [];
119
+
120
+ for (const template of templates.concat(defaultTemplates)) {
121
+ if (await Context.auth.canAccessEmailTemplate(template)) {
122
+ allTemplates.push(template);
123
+ }
124
+ }
125
+
126
+ return new Response(allTemplates.map(template => EmailTemplateStruct.create(template)));
124
127
  }
125
128
  }
@@ -0,0 +1,88 @@
1
+ import { PatchableArray, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
2
+ import { Request } from '@simonbackx/simple-endpoints';
3
+ import { EmailTemplate, GroupFactory, Organization, OrganizationFactory, Platform, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
4
+ import { EmailTemplate as EmailTemplateStruct, EmailTemplateType, PermissionLevel, PermissionRoleDetailed, Permissions, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
5
+ import { TestUtils } from '@stamhoofd/test-utils';
6
+ import { testServer } from '../../../../../tests/helpers/TestServer';
7
+ import { PatchEmailTemplatesEndpoint } from './PatchEmailTemplatesEndpoint';
8
+
9
+ const baseUrl = `/v${Version}/email-templates`;
10
+
11
+ describe('Endpoint.PatchEmailTemplatesEndpoint', () => {
12
+ const endpoint = new PatchEmailTemplatesEndpoint();
13
+ let period: RegistrationPeriod;
14
+
15
+ const patchEmailTemplates = async (body: PatchableArrayAutoEncoder<EmailTemplateStruct>, token: Token, organization?: Organization) => {
16
+ const request = Request.buildJson('PATCH', baseUrl, organization?.getApiHost(), body);
17
+ request.headers.authorization = 'Bearer ' + token.accessToken;
18
+ return await testServer.test(endpoint, request);
19
+ };
20
+
21
+ beforeEach(async () => {
22
+ TestUtils.setEnvironment('userMode', 'platform');
23
+ });
24
+
25
+ beforeAll(async () => {
26
+ period = await new RegistrationPeriodFactory({
27
+ startDate: new Date(2023, 0, 1),
28
+ endDate: new Date(2023, 11, 31),
29
+ }).create();
30
+ });
31
+
32
+ describe('User with platform role who has full permission for all organizations', () => {
33
+ test('should be allowed to patch templates for organizations', async () => {
34
+ const role = PermissionRoleDetailed.create({
35
+ name: 'Beroepsmedewerker',
36
+ resources: new Map([[PermissionsResourceType.OrganizationTags, new Map([[
37
+ '',
38
+ ResourcePermissions.create({
39
+ level: PermissionLevel.Full,
40
+ }),
41
+ ]])]]),
42
+ });
43
+
44
+ const globalPermissions = Permissions.create({
45
+ level: PermissionLevel.None,
46
+ roles: [
47
+ role,
48
+ ],
49
+ });
50
+
51
+ const platform = await Platform.getForEditing();
52
+ platform.privateConfig.roles.push(role);
53
+ await platform.save();
54
+
55
+ const user = await new UserFactory({
56
+ globalPermissions,
57
+ })
58
+ .create();
59
+
60
+ const organization = await new OrganizationFactory({ period }).create();
61
+ const group = await new GroupFactory({ organization }).create();
62
+
63
+ const token = await Token.createToken(user);
64
+
65
+ const template = new EmailTemplate();
66
+ template.subject = 'test template 1';
67
+ template.type = EmailTemplateType.RegistrationConfirmation;
68
+ template.json = {};
69
+ template.html = 'html test';
70
+ template.text = 'text test';
71
+ template.organizationId = organization.id;
72
+ template.groupId = group.id;
73
+ await template.save();
74
+
75
+ const body: PatchableArrayAutoEncoder<EmailTemplateStruct> = new PatchableArray();
76
+
77
+ body.addPatch(
78
+ EmailTemplateStruct.patch(({
79
+ id: template.id,
80
+ subject: 'new subject',
81
+ })),
82
+ );
83
+
84
+ const response = await patchEmailTemplates(body, token);
85
+ expect(response.body).toBeDefined();
86
+ });
87
+ });
88
+ });
@@ -4,7 +4,6 @@ import { EmailTemplate, Group, Webshop } from '@stamhoofd/models';
4
4
  import { EmailTemplate as EmailTemplateStruct, PermissionLevel } from '@stamhoofd/structures';
5
5
 
6
6
  import { Context } from '../../../../helpers/Context';
7
- import { SimpleError } from '@simonbackx/simple-errors';
8
7
 
9
8
  type Params = Record<string, never>;
10
9
  type Body = PatchableArrayAutoEncoder<EmailTemplateStruct>;
@@ -37,11 +36,6 @@ export class PatchEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, R
37
36
  throw Context.auth.error();
38
37
  }
39
38
  }
40
- else {
41
- if (!Context.auth.hasPlatformFullAccess()) {
42
- throw Context.auth.error();
43
- }
44
- }
45
39
 
46
40
  const templates: EmailTemplate[] = [];
47
41
 
@@ -2,7 +2,7 @@ import { AutoEncoderPatchType, cloneObject, Decoder, isPatchableArray, isPatchMa
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
4
4
  import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
5
- import { BuckarooSettings, Company, MemberResponsibility, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
5
+ import { BuckarooSettings, Company, MemberResponsibility, OrganizationMetaData, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PermissionRoleDetailed, PermissionRoleForResponsibility, PermissionsResourceType, ResourcePermissions } from '@stamhoofd/structures';
6
6
  import { Formatter } from '@stamhoofd/utility';
7
7
 
8
8
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
@@ -23,7 +23,7 @@ type ResponseBody = OrganizationStruct;
23
23
  */
24
24
 
25
25
  export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
26
- bodyDecoder = OrganizationPatch as Decoder<AutoEncoderPatchType<OrganizationStruct>>;
26
+ bodyDecoder = OrganizationStruct.patchType() as Decoder<AutoEncoderPatchType<OrganizationStruct>>;
27
27
 
28
28
  protected doesMatch(request: Request): [true, Params] | [false] {
29
29
  if (request.method !== 'PATCH') {
@@ -330,7 +330,8 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
330
330
  if (!organizationPeriod || organizationPeriod.organizationId !== organization.id) {
331
331
  throw new SimpleError({
332
332
  code: 'invalid_field',
333
- message: 'De periode die je wilt instellen bestaat niet (meer)',
333
+ message: 'The period you want to set does not exist (anymore)',
334
+ human: $t('a3795bf6-ed50-4aa6-9caf-33820292c159'),
334
335
  field: 'period',
335
336
  });
336
337
  }
@@ -339,7 +340,8 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
339
340
  if (!period || (period.organizationId && period.organizationId !== organization.id)) {
340
341
  throw new SimpleError({
341
342
  code: 'invalid_field',
342
- message: 'De periode die je wilt instellen bestaat niet (meer)',
343
+ message: 'The period you want to set does not exist (anymore)',
344
+ human: $t('a3795bf6-ed50-4aa6-9caf-33820292c159'),
343
345
  field: 'period',
344
346
  });
345
347
  }
@@ -347,7 +349,8 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
347
349
  if (period.locked) {
348
350
  throw new SimpleError({
349
351
  code: 'invalid_field',
350
- message: 'De periode die je wilt instellen is reeds afgesloten',
352
+ message: 'The period you want to set is already closed',
353
+ human: $t('b6bc2fef-71ac-43a1-b430-50945427a9e3'),
351
354
  field: 'period',
352
355
  });
353
356
  }
@@ -356,7 +359,8 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
356
359
  if (period.startDate > new Date(Date.now() + maximumStart)) {
357
360
  throw new SimpleError({
358
361
  code: 'invalid_field',
359
- message: 'Het werkjaar die je wilt instellen is nog niet gestart',
362
+ message: 'The period you want to set has not started yet',
363
+ human: $t('e0fff936-3f3c-46b8-adcf-c723c33907a2'),
360
364
  field: 'period',
361
365
  });
362
366
  }
@@ -1,15 +1,15 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { BalanceItem, Member } from '@stamhoofd/models';
3
- import { BalanceItemWithPayments } from '@stamhoofd/structures';
4
2
 
5
- import { Context } from '../../../../helpers/Context';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
6
4
 
7
5
  type Params = { id: string };
8
6
  type Query = undefined;
9
7
  type Body = undefined;
10
- type ResponseBody = BalanceItemWithPayments[];
8
+ type ResponseBody = undefined;
11
9
 
12
- // Rename to ReceiveableBalance
10
+ /**
11
+ * @deprecated
12
+ */
13
13
  export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
14
  protected doesMatch(request: Request): [true, Params] | [false] {
15
15
  if (request.method !== 'GET') {
@@ -24,25 +24,10 @@ export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, Resp
24
24
  return [false];
25
25
  }
26
26
 
27
- async handle(request: DecodedRequest<Params, Query, Body>) {
28
- const organization = await Context.setOrganizationScope();
29
- await Context.authenticate();
30
-
31
- if (!await Context.auth.hasSomeAccess(organization.id)) {
32
- throw Context.auth.error();
33
- }
34
-
35
- const member = (await Member.getWithRegistrations(request.params.id));
36
-
37
- if (!member || !await Context.auth.hasFinancialMemberAccess(member)) {
38
- throw Context.auth.notFoundOrNoAccess($t(`baf6fd8e-1937-4c8b-8510-fa811473c157`));
39
- }
40
-
41
- // Get all balance items for this member or users
42
- const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization.id, member.users.map(u => u.id), [member.id]);
43
-
44
- return new Response(
45
- await BalanceItem.getStructureWithPayments(balanceItems),
46
- );
27
+ async handle(_: DecodedRequest<Params, Query, Body>): Promise<Response<ResponseBody>> {
28
+ throw new SimpleError({
29
+ code: 'moved',
30
+ message: 'This endpoint has been moved to /receivable-balances/member/@id',
31
+ });
47
32
  }
48
33
  }
@@ -231,11 +231,6 @@ export class PatchBalanceItemsEndpoint extends Endpoint<Params, Query, Body, Res
231
231
  }
232
232
  });
233
233
 
234
- await BalanceItem.updateOutstanding(updateOutstandingBalance, additionalItems);
235
-
236
- // Reallocate
237
- await BalanceItemService.reallocate(updateOutstandingBalance, organization.id);
238
-
239
234
  // Reload returnedModels
240
235
  const returnedModelsReloaded = await BalanceItem.getByIDs(...returnedModels.map(m => m.id));
241
236
 
@@ -198,11 +198,6 @@ export class PatchPaymentsEndpoint extends Endpoint<Params, Query, Body, Respons
198
198
  for (const balanceItem of balanceItems) {
199
199
  await BalanceItemService.markUpdated(balanceItem, payment, organization);
200
200
  }
201
-
202
- await BalanceItem.updateOutstanding(balanceItems);
203
-
204
- // Reallocate
205
- await BalanceItemService.reallocate(balanceItems, organization.id);
206
201
  }
207
202
 
208
203
  changedPayments.push(payment);
@@ -5,6 +5,7 @@ import { BalanceItem, BalanceItemPayment, CachedBalance, Payment } from '@stamho
5
5
  import { Context } from '../../../../helpers/Context';
6
6
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
7
7
  import { SQL } from '@stamhoofd/sql';
8
+ import { BalanceItemService } from '../../../../services/BalanceItemService';
8
9
 
9
10
  type Params = { id: string; type: ReceivableBalanceType };
10
11
  type Query = undefined;
@@ -37,6 +38,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
37
38
  throw Context.auth.error();
38
39
  }
39
40
 
41
+ // Flush caches (this makes sure that we do a reload in the frontend after a registration or change, we get the newest balances)
42
+ await BalanceItemService.flushCaches(organization.id);
43
+
40
44
  const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type, true);
41
45
  let paymentModels: Payment[] = [];
42
46
 
@@ -9,6 +9,7 @@ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStruct
9
9
  import { Context } from '../../../../helpers/Context';
10
10
  import { receivableBalanceFilterCompilers } from '../../../../sql-filters/receivable-balances';
11
11
  import { receivableBalanceSorters } from '../../../../sql-sorters/receivable-balances';
12
+ import { BalanceItemService } from '../../../../services/BalanceItemService';
12
13
 
13
14
  type Params = Record<string, never>;
14
15
  type Query = LimitedFilteredRequest;
@@ -1,20 +1,28 @@
1
1
  import { Request } from '@simonbackx/simple-endpoints';
2
2
  import { GroupFactory, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
3
- import { GroupType, PermissionLevel, Permissions, Version } from '@stamhoofd/structures';
3
+ import { GroupType, LimitedFilteredRequest, PermissionLevel, Permissions } from '@stamhoofd/structures';
4
4
  import { testServer } from '../../../../../tests/helpers/TestServer';
5
- import { PatchRegistrationPeriodsEndpoint } from './GetOrganizationRegistrationPeriodsEndpoint';
5
+ import { GetOrganizationRegistrationPeriodsEndpoint } from './GetOrganizationRegistrationPeriodsEndpoint';
6
6
 
7
- const baseUrl = `/v${Version}/organization/registration-periods`;
7
+ const baseUrl = `/organization/registration-periods`;
8
8
 
9
9
  describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
10
- const endpoint = new PatchRegistrationPeriodsEndpoint();
10
+ const endpoint = new GetOrganizationRegistrationPeriodsEndpoint();
11
11
 
12
12
  let registrationPeriod1: RegistrationPeriod;
13
13
  let registrationPeriod2: RegistrationPeriod;
14
14
 
15
15
  const get = async (organization: Organization, token: Token) => {
16
- const request = Request.buildJson('GET', baseUrl, organization.getApiHost());
17
- request.headers.authorization = 'Bearer ' + token.accessToken;
16
+ const request = Request.get({
17
+ path: baseUrl,
18
+ host: organization.getApiHost(),
19
+ query: new LimitedFilteredRequest({
20
+ limit: 100,
21
+ }),
22
+ headers: {
23
+ authorization: 'Bearer ' + token.accessToken,
24
+ },
25
+ });
18
26
  return await testServer.test(endpoint, request);
19
27
  };
20
28
 
@@ -36,11 +44,11 @@ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
36
44
  .create();
37
45
  };
38
46
 
39
- describe('groups', () => {
47
+ describe('Groups', () => {
40
48
  test('Should contain waiting lists', async () => {
41
49
  // arrange
42
50
  const organization = await initOrganization(registrationPeriod1);
43
- await new OrganizationRegistrationPeriodFactory({
51
+ const organizationPeriod = await new OrganizationRegistrationPeriodFactory({
44
52
  organization,
45
53
  period: registrationPeriod1,
46
54
  }).create();
@@ -74,19 +82,21 @@ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
74
82
 
75
83
  const groupWithWaitingList = await new GroupFactory({
76
84
  organization,
85
+ waitingListId: waitingList1.id,
77
86
  }).create();
78
87
 
79
- groupWithWaitingList.waitingListId = waitingList1.id;
80
- await groupWithWaitingList.save();
88
+ // Make sure the group is in a category of the organization period
89
+ organizationPeriod.settings.rootCategory?.groupIds.push(groupWithWaitingList.id);
90
+ await organizationPeriod.save();
81
91
 
82
92
  // act
83
93
  const result = await get(organization, token);
84
94
 
85
95
  // assert
86
96
  expect(result.body).toBeDefined();
87
- expect(result.body.organizationPeriods.length).toBe(1);
88
- const organizationPeriod = result.body.organizationPeriods[0];
89
- const groups = organizationPeriod.groups;
97
+ expect(result.body.results.length).toBe(1);
98
+ const organizationPeriodStruct = result.body.results[0];
99
+ const groups = organizationPeriodStruct.groups;
90
100
  expect(groups.length).toBe(3);
91
101
  expect(groups).toEqual(expect.arrayContaining([
92
102
  expect.objectContaining({
@@ -99,13 +109,13 @@ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
99
109
  id: waitingList2.id,
100
110
  }),
101
111
  ]));
102
- expect(organizationPeriod.waitingLists.length).toBe(2);
112
+ expect(organizationPeriodStruct.waitingLists.length).toBe(2);
103
113
  });
104
114
 
105
- test('Should contain only groups of the organization registration period', async () => {
115
+ test('Should not contain groups of other organizations or periods', async () => {
106
116
  // arrange
107
117
  const organization = await initOrganization(registrationPeriod1);
108
- await new OrganizationRegistrationPeriodFactory({
118
+ const organizationPeriod = await new OrganizationRegistrationPeriodFactory({
109
119
  organization,
110
120
  period: registrationPeriod1,
111
121
  }).create();
@@ -130,13 +140,28 @@ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
130
140
  period: registrationPeriod1,
131
141
  }).create();
132
142
 
143
+ // Make sure the groups are in a category of the organization period
144
+ organizationPeriod.settings.rootCategory?.groupIds.push(group1.id);
145
+ organizationPeriod.settings.rootCategory?.groupIds.push(group2.id);
146
+
147
+ await organizationPeriod.save();
148
+
133
149
  // group list of other organization
134
150
  const otherOrganization = await initOrganization(registrationPeriod1);
151
+ const otherOrganizationPeriod = await new OrganizationRegistrationPeriodFactory({
152
+ organization: otherOrganization,
153
+ period: registrationPeriod1,
154
+ }).create();
135
155
 
136
- await new GroupFactory({
156
+ const otherGroup1 = await new GroupFactory({
137
157
  organization: otherOrganization,
158
+ period: registrationPeriod1,
138
159
  }).create();
139
160
 
161
+ // Add the group to the other organization's period
162
+ otherOrganizationPeriod.settings.rootCategory?.groupIds.push(otherGroup1.id);
163
+ await otherOrganizationPeriod.save();
164
+
140
165
  // group of other period
141
166
  await new GroupFactory({
142
167
  organization,
@@ -148,8 +173,8 @@ describe('Endpoint.GetOrganizationRegistrationPeriods', () => {
148
173
 
149
174
  // assert
150
175
  expect(result.body).toBeDefined();
151
- expect(result.body.organizationPeriods.length).toBe(1);
152
- const groups = result.body.organizationPeriods[0].groups;
176
+ expect(result.body.results.length).toBe(1);
177
+ const groups = result.body.results[0].groups;
153
178
  expect(groups.length).toBe(2);
154
179
  expect(groups).toEqual(expect.arrayContaining([
155
180
  expect.objectContaining({
@@ -1,20 +1,26 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { RegistrationPeriodList, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct } from '@stamhoofd/structures';
2
+ import { assertSort, CountFilteredRequest, getSortFilter, LimitedFilteredRequest, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PaginatedResponse, StamhoofdFilter } from '@stamhoofd/structures';
3
3
 
4
- import { Group, OrganizationRegistrationPeriod, RegistrationPeriod } from '@stamhoofd/models';
4
+ import { SimpleError } from '@simonbackx/simple-errors';
5
+ import { OrganizationRegistrationPeriod } from '@stamhoofd/models';
6
+ import { applySQLSorter, compileToSQLFilter, SQLFilterDefinitions, SQLSortDefinitions } from '@stamhoofd/sql';
7
+ import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
5
8
  import { Context } from '../../../../helpers/Context';
6
- import { Sorter } from '@stamhoofd/utility';
9
+ import { organizationRegistrationPeriodFilterCompilers } from '../../../../sql-filters/organization-registration-periods';
10
+ import { organizationRegistrationPeriodSorters } from '../../../../sql-sorters/organization-registration-periods';
11
+ import { Decoder } from '@simonbackx/simple-encoding';
7
12
 
8
13
  type Params = Record<string, never>;
9
- type Query = undefined;
14
+ type Query = LimitedFilteredRequest;
10
15
  type Body = undefined;
11
- type ResponseBody = RegistrationPeriodList;
16
+ type ResponseBody = PaginatedResponse<OrganizationRegistrationPeriodStruct[], LimitedFilteredRequest>;
12
17
 
13
- /**
14
- * One endpoint to create, patch and delete members and their registrations and payments
15
- */
18
+ const filterCompilers: SQLFilterDefinitions = organizationRegistrationPeriodFilterCompilers;
19
+ const sorters: SQLSortDefinitions<OrganizationRegistrationPeriod> = organizationRegistrationPeriodSorters;
20
+
21
+ export class GetOrganizationRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
16
23
 
17
- export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
18
24
  protected doesMatch(request: Request): [true, Params] | [false] {
19
25
  if (request.method !== 'GET') {
20
26
  return [false];
@@ -28,38 +34,147 @@ export class PatchRegistrationPeriodsEndpoint extends Endpoint<Params, Query, Bo
28
34
  return [false];
29
35
  }
30
36
 
37
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest) {
38
+ const organization = Context.organization;
39
+ let scopeFilter: StamhoofdFilter | undefined = undefined;
40
+
41
+ if (organization) {
42
+ scopeFilter = {
43
+ organizationId: organization.id,
44
+ };
45
+ }
46
+
47
+ const query = OrganizationRegistrationPeriod.select();
48
+
49
+ if (scopeFilter) {
50
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
51
+ }
52
+
53
+ if (q.filter) {
54
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
55
+ }
56
+
57
+ if (q.search) {
58
+ let searchFilter: StamhoofdFilter | null = null;
59
+
60
+ searchFilter = {
61
+ id: q.search,
62
+ };
63
+
64
+ if (searchFilter) {
65
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
66
+ }
67
+ }
68
+
69
+ if (q instanceof LimitedFilteredRequest) {
70
+ if (q.pageFilter) {
71
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
72
+ }
73
+
74
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
75
+ applySQLSorter(query, q.sort, sorters);
76
+ query.limit(q.limit);
77
+ }
78
+
79
+ return query;
80
+ }
81
+
82
+ static async buildData(requestQuery: LimitedFilteredRequest) {
83
+ const query = await GetOrganizationRegistrationPeriodsEndpoint.buildQuery(requestQuery);
84
+ const organizationRegistrationPeriods = await query.fetch();
85
+
86
+ let next: LimitedFilteredRequest | undefined;
87
+
88
+ if (organizationRegistrationPeriods.length >= requestQuery.limit) {
89
+ const lastObject = organizationRegistrationPeriods[organizationRegistrationPeriods.length - 1];
90
+ const nextFilter = getSortFilter(lastObject, sorters, requestQuery.sort);
91
+
92
+ next = new LimitedFilteredRequest({
93
+ filter: requestQuery.filter,
94
+ pageFilter: nextFilter,
95
+ sort: requestQuery.sort,
96
+ limit: requestQuery.limit,
97
+ search: requestQuery.search,
98
+ });
99
+
100
+ if (JSON.stringify(nextFilter) === JSON.stringify(requestQuery.pageFilter)) {
101
+ console.error('Found infinite loading loop for', requestQuery);
102
+ next = undefined;
103
+ }
104
+ }
105
+
106
+ return new PaginatedResponse<OrganizationRegistrationPeriodStruct[], LimitedFilteredRequest>({
107
+ results: await AuthenticatedStructures.organizationRegistrationPeriods(organizationRegistrationPeriods),
108
+ next,
109
+ });
110
+ }
111
+
31
112
  async handle(request: DecodedRequest<Params, Query, Body>) {
32
- const organization = await Context.setOrganizationScope();
113
+ if (request.request.getVersion() < 371) {
114
+ throw new SimpleError({
115
+ code: 'client_update_required',
116
+ statusCode: 400,
117
+ message: 'Er is een noodzakelijke update beschikbaar. Herlaad de pagina en wis indien nodig de cache van jouw browser.',
118
+ human: $t(`adb0e7c8-aed7-43f5-bfcc-a350f03aaabe`),
119
+ });
120
+ }
121
+
122
+ const organization = await Context.setOptionalOrganizationScope();
33
123
  await Context.authenticate();
34
124
 
35
- if (!await Context.auth.hasSomeAccess(organization.id)) {
36
- throw Context.auth.error();
125
+ if (organization) {
126
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
127
+ throw Context.auth.error();
128
+ }
129
+ }
130
+ else {
131
+ if (!Context.auth.hasPlatformFullAccess()) {
132
+ throw Context.auth.error();
133
+ }
37
134
  }
38
135
 
39
- const organizationPeriods = await OrganizationRegistrationPeriod.where({ organizationId: organization.id });
40
- const periods = await RegistrationPeriod.all();
41
- const groups = await Group.getAll(organization.id, null);
136
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
42
137
 
43
- const structs: OrganizationRegistrationPeriodStruct[] = [];
138
+ if (request.query.limit > maxLimit) {
139
+ throw new SimpleError({
140
+ code: 'invalid_field',
141
+ field: 'limit',
142
+ message: 'Limit can not be more than ' + maxLimit,
143
+ });
144
+ }
44
145
 
45
- for (const period of periods) {
46
- const organizationPeriod = organizationPeriods.find(p => p.periodId == period.id);
47
- if (!organizationPeriod) {
48
- continue;
49
- }
146
+ if (request.query.limit < 1) {
147
+ throw new SimpleError({
148
+ code: 'invalid_field',
149
+ field: 'limit',
150
+ message: 'Limit can not be less than 1',
151
+ });
152
+ }
50
153
 
51
- const gs = groups.filter(g => g.periodId === period.id);
52
- structs.push(organizationPeriod.getStructure(period, gs));
154
+ return new Response(
155
+ await GetOrganizationRegistrationPeriodsEndpoint.buildData(request.query),
156
+ );
157
+ }
158
+
159
+ /* async handle(request: DecodedRequest<Params, Query, Body>) {
160
+ const organization = await Context.setOrganizationScope();
161
+ await Context.authenticate();
162
+
163
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
164
+ throw Context.auth.error();
53
165
  }
54
166
 
167
+ const organizationPeriods = await OrganizationRegistrationPeriod.select().where('organizationId', organization.id).fetch();
168
+ const periods = await RegistrationPeriod.all();
169
+
55
170
  // Sort
56
171
  periods.sort((a, b) => Sorter.byDateValue(a.startDate, b.startDate));
57
172
 
58
173
  return new Response(
59
174
  RegistrationPeriodList.create({
60
- organizationPeriods: structs,
175
+ organizationPeriods: await AuthenticatedStructures.organizationRegistrationPeriods(organizationPeriods, periods),
61
176
  periods: periods.map(p => p.getStructure()),
62
177
  }),
63
178
  );
64
- }
179
+ } */
65
180
  }