@stamhoofd/backend 2.83.5 → 2.84.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 (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
@@ -0,0 +1,234 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { Group, Member, Platform } from '@stamhoofd/models';
5
+ import { SQL, SQLSortDefinitions, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
6
+ import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort } from '@stamhoofd/structures';
7
+
8
+ import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
9
+ import { RegistrationsBlob } from '@stamhoofd/structures/dist/src/members/RegistrationsBlob';
10
+ import { RegistrationWithMemberBlob } from '@stamhoofd/structures/dist/src/members/RegistrationWithMemberBlob';
11
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
+ import { Context } from '../../../helpers/Context';
13
+ import { LimitedFilteredRequestHelper } from '../../../helpers/LimitedFilteredRequestHelper';
14
+ import { registrationFilterCompilers } from '../../../sql-filters/registrations';
15
+ import { registrationSorters } from '../../../sql-sorters/registrations';
16
+ import { GetMembersEndpoint } from '../members/GetMembersEndpoint';
17
+ import { validateGroupFilter } from '../members/helpers/validateGroupFilter';
18
+
19
+ type Params = Record<string, never>;
20
+ type Query = LimitedFilteredRequest;
21
+ type Body = undefined;
22
+ type ResponseBody = PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>;
23
+
24
+ const sorters: SQLSortDefinitions<RegistrationWithMemberBlob> = registrationSorters;
25
+ const filterCompilers = registrationFilterCompilers;
26
+
27
+ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
28
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
29
+
30
+ protected doesMatch(request: Request): [true, Params] | [false] {
31
+ if (request.method !== 'GET') {
32
+ return [false];
33
+ }
34
+
35
+ const params = Endpoint.parseParameters(request.url, '/registrations', {});
36
+
37
+ if (params) {
38
+ return [true, params as Params];
39
+ }
40
+ return [false];
41
+ }
42
+
43
+ static async buildQuery(q: CountFilteredRequest | LimitedFilteredRequest, permissionLevel: PermissionLevel = PermissionLevel.Read) {
44
+ const organization = Context.organization;
45
+ let scopeFilter: StamhoofdFilter | undefined = undefined;
46
+
47
+ // First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
48
+ if (!Context.auth.canAccessAllPlatformMembers() && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: null })) {
49
+ if (!organization) {
50
+ const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
51
+ if (tags !== 'all' && tags.length === 0) {
52
+ throw Context.auth.error();
53
+ }
54
+
55
+ if (tags !== 'all') {
56
+ const platform = await Platform.getShared();
57
+
58
+ // Add organization scope filter
59
+ scopeFilter = {
60
+ periodId: platform.periodId,
61
+ organization: {
62
+ $elemMatch: {
63
+ tags: {
64
+ $in: tags,
65
+ },
66
+ },
67
+
68
+ },
69
+ };
70
+ }
71
+ }
72
+
73
+ if (organization) {
74
+ // Add organization scope filter
75
+ if (await Context.auth.canAccessAllMembers(organization.id, permissionLevel)) {
76
+ if (await Context.auth.hasFullAccess(organization.id, permissionLevel)) {
77
+ // Can access full history for now
78
+ scopeFilter = {
79
+ organizationId: organization.id,
80
+ };
81
+ }
82
+ else {
83
+ // Can only access current period
84
+ scopeFilter = {
85
+ organizationId: organization.id,
86
+ periodId: organization.periodId,
87
+ };
88
+ }
89
+ }
90
+ else {
91
+ // Check which normal membership groups we have access to and filter on those
92
+ const groups = await Group.getAll(organization.id, organization.periodId, true, [GroupType.Membership, GroupType.WaitingList]);
93
+ Context.auth.cacheGroups(groups);
94
+ const groupIds: string[] = [];
95
+
96
+ for (const group of groups) {
97
+ if (await Context.auth.canAccessGroup(group, permissionLevel)) {
98
+ groupIds.push(group.id);
99
+ }
100
+ }
101
+
102
+ if (groupIds.length === 0) {
103
+ throw Context.auth.error({
104
+ message: 'You must filter on a group of the organization you are trying to access',
105
+ human: $t(`94e2d4ff-9b4b-4861-ba42-341ed67b32d8`),
106
+ });
107
+ }
108
+
109
+ scopeFilter = {
110
+ groupId: {
111
+ $in: groupIds,
112
+ },
113
+ };
114
+ }
115
+ }
116
+ }
117
+
118
+ const query = SQL
119
+ .select(
120
+ SQL.column('registrations', 'id'),
121
+ SQL.column('registrations', 'memberId'),
122
+ )
123
+ .setMaxExecutionTime(15 * 1000)
124
+ .from(
125
+ SQL.table('registrations'),
126
+ );
127
+
128
+ if (scopeFilter) {
129
+ query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
130
+ }
131
+
132
+ if (q.filter) {
133
+ query.where(await compileToSQLFilter(q.filter, filterCompilers));
134
+ }
135
+
136
+ const memberSearchFilter = GetMembersEndpoint.buildSearchFilter(q.search);
137
+
138
+ if (memberSearchFilter) {
139
+ const searchFilter: StamhoofdFilter = {
140
+ member: {
141
+ $elemMatch: memberSearchFilter,
142
+ },
143
+ };
144
+
145
+ query.where(await compileToSQLFilter(searchFilter, filterCompilers));
146
+ }
147
+
148
+ if (q instanceof LimitedFilteredRequest) {
149
+ if (q.pageFilter) {
150
+ query.where(await compileToSQLFilter(q.pageFilter, filterCompilers));
151
+ }
152
+
153
+ q.sort = assertSort(q.sort, [{ key: 'id' }]);
154
+ applySQLSorter(query, q.sort, sorters);
155
+ query.limit(q.limit);
156
+ }
157
+
158
+ return query;
159
+ }
160
+
161
+ static async buildData(requestQuery: LimitedFilteredRequest, permissionLevel = PermissionLevel.Read) {
162
+ const query = await GetRegistrationsEndpoint.buildQuery(requestQuery, permissionLevel);
163
+ let data: SQLResultNamespacedRow[];
164
+
165
+ try {
166
+ data = await query.fetch();
167
+ }
168
+ catch (error) {
169
+ if (error.message.includes('ER_QUERY_TIMEOUT')) {
170
+ throw new SimpleError({
171
+ code: 'timeout',
172
+ message: 'Query took too long',
173
+ human: $t(`dce51638-6129-448b-8a15-e6d778f3a76a`),
174
+ });
175
+ }
176
+ throw error;
177
+ }
178
+
179
+ const registrationData = data.map((r) => {
180
+ if (typeof r.registrations.memberId === 'string' && typeof r.registrations.id === 'string') {
181
+ return { memberId: r.registrations.memberId, id: r.registrations.id };
182
+ }
183
+ throw new Error('Expected string');
184
+ });
185
+
186
+ const members = await Member.getBlobByIds(...registrationData.map(r => r.memberId));
187
+
188
+ for (const member of members) {
189
+ if (!await Context.auth.canAccessMember(member, permissionLevel)) {
190
+ throw Context.auth.error();
191
+ }
192
+ }
193
+
194
+ const registrationsBlob = await AuthenticatedStructures.registrationsBlob(registrationData, members);
195
+
196
+ const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoop({
197
+ request: requestQuery,
198
+ results: registrationsBlob.registrations,
199
+ sorters,
200
+ });
201
+
202
+ return new PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>({
203
+ results: registrationsBlob,
204
+ next,
205
+ });
206
+ }
207
+
208
+ async handle(request: DecodedRequest<Params, Query, Body>) {
209
+ await Context.setOptionalOrganizationScope();
210
+ await Context.authenticate();
211
+
212
+ const maxLimit = Context.auth.hasSomePlatformAccess() ? 1000 : 100;
213
+
214
+ if (request.query.limit > maxLimit) {
215
+ throw new SimpleError({
216
+ code: 'invalid_field',
217
+ field: 'limit',
218
+ message: 'Limit can not be more than ' + maxLimit,
219
+ });
220
+ }
221
+
222
+ if (request.query.limit < 1) {
223
+ throw new SimpleError({
224
+ code: 'invalid_field',
225
+ field: 'limit',
226
+ message: 'Limit can not be less than 1',
227
+ });
228
+ }
229
+
230
+ return new Response(
231
+ await GetRegistrationsEndpoint.buildData(request.query),
232
+ );
233
+ }
234
+ }
@@ -5,7 +5,7 @@ import { MemberDetails, MemberWithRegistrationsBlob, OrganizationMetaData, Organ
5
5
  import { testServer } from '../../../../tests/helpers/TestServer';
6
6
  import { PatchUserMembersEndpoint } from './PatchUserMembersEndpoint';
7
7
  import { Database } from '@simonbackx/simple-database';
8
- import { SHExpect, TestUtils } from '@stamhoofd/test-utils';
8
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
9
9
 
10
10
  const baseUrl = `/members`;
11
11
  const endpoint = new PatchUserMembersEndpoint();
@@ -53,7 +53,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
53
53
  request.headers.authorization = 'Bearer ' + token.accessToken;
54
54
  await expect(testServer.test(endpoint, request))
55
55
  .rejects
56
- .toThrow(SHExpect.errorWithCode('known_member_missing_rights'));
56
+ .toThrow(STExpect.errorWithCode('known_member_missing_rights'));
57
57
  });
58
58
 
59
59
  test('The security code is not a requirement for members without additional data', async () => {
@@ -278,7 +278,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
278
278
 
279
279
  const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
280
280
  request.headers.authorization = 'Bearer ' + token.accessToken;
281
- await expect(testServer.test(endpoint, request)).rejects.toThrow(SHExpect.errorWithCode('permission_denied'));
281
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
282
282
  });
283
283
 
284
284
  test('A user can save answers of records of the platform', async () => {
@@ -389,7 +389,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
389
389
 
390
390
  const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
391
391
  request.headers.authorization = 'Bearer ' + token.accessToken;
392
- await expect(testServer.test(endpoint, request)).rejects.toThrow(SHExpect.errorWithCode('permission_denied'));
392
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
393
393
  });
394
394
 
395
395
  test('A user can not save anwers to inexisting records', async () => {
@@ -435,7 +435,7 @@ describe('Endpoint.PatchUserMembersEndpoint', () => {
435
435
 
436
436
  const request = Request.buildJson('PATCH', baseUrl, organization.getApiHost(), arr);
437
437
  request.headers.authorization = 'Bearer ' + token.accessToken;
438
- await expect(testServer.test(endpoint, request)).rejects.toThrow(SHExpect.errorWithCode('permission_denied'));
438
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
439
439
  });
440
440
  });
441
441
  });