@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.
- package/index.ts +19 -4
- package/package.json +18 -14
- package/src/crons/amazon-ses.ts +26 -5
- package/src/crons/balance-emails.ts +18 -17
- package/src/email-recipient-loaders/registrations.ts +87 -0
- package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
- package/src/endpoints/global/files/UploadFile.ts +11 -16
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
- package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
- package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
- package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
- package/src/excel-loaders/members.ts +233 -232
- package/src/excel-loaders/payments.ts +1 -1
- package/src/excel-loaders/receivable-balances.ts +1 -1
- package/src/excel-loaders/registrations.ts +153 -0
- package/src/helpers/AdminPermissionChecker.ts +65 -37
- package/src/helpers/AuthenticatedStructures.ts +43 -3
- package/src/helpers/Context.ts +29 -1
- package/src/helpers/GlobalHelper.ts +3 -1
- package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
- package/src/helpers/GroupedThrottledQueue.ts +108 -0
- package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
- package/src/helpers/MemberCharger.ts +0 -5
- package/src/helpers/MembershipCharger.ts +3 -9
- package/src/helpers/OrganizationCharger.ts +0 -5
- package/src/helpers/ThrottledQueue.test.ts +194 -0
- package/src/helpers/ThrottledQueue.ts +145 -0
- package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
- package/src/middleware/ContextMiddleware.ts +1 -1
- package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
- package/src/services/BalanceItemPaymentService.ts +1 -33
- package/src/services/BalanceItemService.ts +167 -48
- package/src/services/FileSignService.ts +18 -13
- package/src/services/MemberRecordStore.ts +28 -19
- package/src/services/PaymentReallocationService.test.ts +25 -14
- package/src/services/PaymentReallocationService.ts +29 -10
- package/src/services/PaymentService.ts +4 -16
- package/src/services/PlatformMembershipService.ts +8 -4
- package/src/services/RegistrationService.ts +66 -2
- package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
- package/src/sql-filters/groups.ts +67 -0
- package/src/sql-filters/members.ts +33 -58
- package/src/sql-filters/organization-registration-periods.ts +8 -0
- package/src/sql-filters/registration-periods.ts +8 -0
- package/src/sql-filters/registrations.ts +11 -22
- package/src/sql-sorters/groups.ts +24 -0
- package/src/sql-sorters/organization-registration-periods.ts +24 -0
- package/src/sql-sorters/registration-periods.ts +47 -0
- package/src/sql-sorters/registrations.ts +77 -0
- package/tests/actions/patchOrganizationMember.ts +27 -0
- package/tests/actions/patchPaymentStatus.ts +45 -0
- package/tests/actions/patchUserMember.ts +27 -0
- package/tests/assertions/assertBalances.ts +49 -0
- package/tests/e2e/api-rate-limits.test.ts +5 -5
- package/tests/e2e/bundle-discounts.test.ts +4060 -0
- package/tests/e2e/charge-members.test.ts +27 -24
- package/tests/e2e/documents.test.ts +398 -0
- package/tests/e2e/register.test.ts +292 -312
- package/tests/helpers/PayconiqMocker.ts +55 -0
- package/tests/init/index.ts +5 -0
- package/tests/init/initAdmin.ts +14 -0
- package/tests/init/initBundleDiscount.ts +47 -0
- package/tests/init/initPayconiq.ts +9 -0
- package/tests/init/initPlatformAdmin.ts +13 -0
- package/tests/init/initStripe.ts +21 -0
- package/tests/jest.setup.ts +29 -0
- 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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
438
|
+
await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
|
|
439
439
|
});
|
|
440
440
|
});
|
|
441
441
|
});
|