@stamhoofd/backend 2.79.7 → 2.80.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.
@@ -0,0 +1,429 @@
1
+ import { Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { GroupFactory, MemberFactory, Organization, OrganizationFactory, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
3
+ import { AccessRight, BalanceItemWithPayments, ChargeMembersRequest, LimitedFilteredRequest, PermissionLevel, PermissionRoleDetailed, Permissions, PermissionsResourceType, ResourcePermissions, StamhoofdFilter, Version } from '@stamhoofd/structures';
4
+ import { TestUtils } from '@stamhoofd/test-utils';
5
+ import { ChargeMembersEndpoint } from '../../src/endpoints/admin/members/ChargeMembersEndpoint';
6
+ import { GetMemberBalanceEndpoint } from '../../src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint';
7
+ import { testServer } from '../helpers/TestServer';
8
+
9
+ describe('E2E.ChargeMembers', () => {
10
+ const chargeMembersEndpoint = new ChargeMembersEndpoint();
11
+ const memberBalanceEndpoint = new GetMemberBalanceEndpoint();
12
+ let period: RegistrationPeriod;
13
+ let organization: Organization;
14
+ let otherOrganization: Organization;
15
+ let financialDirectorToken: Token;
16
+ let financialDirectorRole: PermissionRoleDetailed;
17
+ let financialDirectorRoleOfOtherOrganization: PermissionRoleDetailed;
18
+
19
+ const postCharge = async (filter: StamhoofdFilter, organization: Organization, body: ChargeMembersRequest, token: Token) => {
20
+ const request = Request.buildJson('POST', `/v${Version}/admin/charge-members`, organization.getApiHost(), body);
21
+ const filterRequest = new LimitedFilteredRequest({
22
+ filter,
23
+ limit: 100,
24
+ });
25
+ request.query = filterRequest.encode({ version: Version }) as any;
26
+ request.headers.authorization = 'Bearer ' + token.accessToken;
27
+ return await testServer.test(chargeMembersEndpoint, request);
28
+ };
29
+
30
+ const getBalance = async (memberId: string, organization: Organization, token: Token): Promise<Response<BalanceItemWithPayments[]>> => {
31
+ const request = Request.buildJson('GET', `/v${Version}/organization/members/${memberId}/balance`, organization.getApiHost());
32
+ request.headers.authorization = 'Bearer ' + token.accessToken;
33
+ return await testServer.test(memberBalanceEndpoint, request);
34
+ };
35
+
36
+ const createUserData = async (permissions: Permissions | null | undefined, roles: PermissionRoleDetailed[]) => {
37
+ const organization = await new OrganizationFactory({ period, roles })
38
+ .create();
39
+
40
+ const user = await new UserFactory({
41
+ organization,
42
+ permissions,
43
+ })
44
+ .create();
45
+
46
+ const token = await Token.createToken(user);
47
+
48
+ return { organization, user, token };
49
+ };
50
+
51
+ const createFinancialDirectorData = async () => {
52
+ const role = PermissionRoleDetailed.create({
53
+ name: 'financial director',
54
+ accessRights: [AccessRight.OrganizationFinanceDirector],
55
+ });
56
+
57
+ const { organization, user: financialDirector, token } = await createUserData(Permissions.create({
58
+ level: PermissionLevel.None,
59
+ roles: [
60
+ role,
61
+ ],
62
+ resources: new Map([[PermissionsResourceType.Groups, new Map([[
63
+ '',
64
+ ResourcePermissions.create({
65
+ level: PermissionLevel.Write,
66
+ }),
67
+ ]])]]),
68
+ }), [role]);
69
+
70
+ return { role, organization, financialDirector, token };
71
+ };
72
+
73
+ beforeEach(async () => {
74
+ TestUtils.setEnvironment('userMode', 'platform');
75
+ });
76
+
77
+ beforeAll(async () => {
78
+ period = await new RegistrationPeriodFactory({
79
+ startDate: new Date(2023, 0, 1),
80
+ endDate: new Date(2023, 11, 31),
81
+ }).create();
82
+
83
+ const financialDirectorData = await createFinancialDirectorData();
84
+ financialDirectorRole = financialDirectorData.role;
85
+ financialDirectorToken = financialDirectorData.token;
86
+ organization = financialDirectorData.organization;
87
+
88
+ const otherFinancialDirectorData = await createFinancialDirectorData();
89
+ otherOrganization = otherFinancialDirectorData.organization;
90
+ financialDirectorRoleOfOtherOrganization = otherFinancialDirectorData.role;
91
+ });
92
+
93
+ test('Should fail if user does can not manage the payments of the organization', async () => {
94
+ // arrange
95
+ const member1 = await new MemberFactory({ }).create();
96
+ const member2 = await new MemberFactory({ }).create();
97
+
98
+ await new RegistrationFactory({ member: member1, organization }).create();
99
+ await new RegistrationFactory({ member: member2, organization }).create();
100
+
101
+ const filter: StamhoofdFilter = {
102
+ id: {
103
+ $in: [member1.id, member2.id],
104
+ },
105
+ };
106
+
107
+ const body = ChargeMembersRequest.create({
108
+ description: 'test description',
109
+ price: 3,
110
+ amount: 4,
111
+ dueAt: new Date(2023, 0, 10),
112
+ createdAt: new Date(2023, 0, 4),
113
+ });
114
+
115
+ const testUserFactories: UserFactory[] = [
116
+ new UserFactory({
117
+ organization,
118
+ permissions: Permissions.create({
119
+ level: PermissionLevel.None,
120
+ roles: [
121
+ financialDirectorRole,
122
+ ],
123
+ resources: new Map([[PermissionsResourceType.Groups, new Map([[
124
+ '',
125
+ ResourcePermissions.create({
126
+ level: PermissionLevel.Read,
127
+ }),
128
+ ]])]]),
129
+ }),
130
+ }),
131
+ new UserFactory({
132
+ organization,
133
+ permissions: Permissions.create({
134
+ level: PermissionLevel.None,
135
+ resources: new Map([[PermissionsResourceType.Groups, new Map([[
136
+ '',
137
+ ResourcePermissions.create({
138
+ level: PermissionLevel.Write,
139
+ }),
140
+ ]])]]),
141
+ }),
142
+ }),
143
+ ];
144
+
145
+ for (const userFactory of testUserFactories) {
146
+ const user = await userFactory.create();
147
+
148
+ const token = await Token.createToken(user);
149
+
150
+ await expect(async () => await postCharge(filter, organization, body, token))
151
+ .rejects
152
+ .toThrow('You do not have permissions for this action');
153
+ }
154
+ });
155
+
156
+ test('Should create balance items for members', async () => {
157
+ // arrange
158
+ const member1 = await new MemberFactory({ }).create();
159
+
160
+ const member2 = await new MemberFactory({ }).create();
161
+
162
+ await new RegistrationFactory({ member: member1, organization }).create();
163
+ await new RegistrationFactory({ member: member2, organization }).create();
164
+
165
+ const filter: StamhoofdFilter = {
166
+ id: {
167
+ $in: [member1.id, member2.id],
168
+ },
169
+ };
170
+
171
+ const body = ChargeMembersRequest.create({
172
+ description: 'test description',
173
+ price: 3,
174
+ amount: 4,
175
+ dueAt: new Date(2023, 0, 10),
176
+ createdAt: new Date(2023, 0, 4),
177
+ });
178
+
179
+ const result = await postCharge(filter, organization, body, financialDirectorToken);
180
+ expect(result).toBeDefined();
181
+ expect(result.body).toBeUndefined();
182
+
183
+ // act and assert
184
+ const testBalanceResponse = (response: Response<BalanceItemWithPayments[]>) => {
185
+ expect(response).toBeDefined();
186
+ expect(response.body.length).toBe(1);
187
+ const balanceItem1 = response.body[0];
188
+ expect(balanceItem1.price).toEqual(12);
189
+ expect(balanceItem1.amount).toEqual(body.amount);
190
+ expect(balanceItem1.description).toEqual(body.description);
191
+ expect(balanceItem1.organizationId).toEqual(organization.id);
192
+ // const dueAt = balanceItem1.dueAt!;
193
+ expect(balanceItem1.dueAt).toEqual(body.dueAt);
194
+ expect(balanceItem1.createdAt).toEqual(body.createdAt);
195
+ };
196
+
197
+ testBalanceResponse(await getBalance(member1.id, organization, financialDirectorToken));
198
+ testBalanceResponse(await getBalance(member2.id, organization, financialDirectorToken));
199
+ });
200
+
201
+ test('Should not charge members of other organization', async () => {
202
+ // arrange
203
+ const member1 = await new MemberFactory({ }).create();
204
+
205
+ const member2 = await new MemberFactory({ }).create();
206
+
207
+ await new RegistrationFactory({ member: member1, organization: otherOrganization }).create();
208
+ await new RegistrationFactory({ member: member2, organization: otherOrganization }).create();
209
+
210
+ const filter: StamhoofdFilter = {
211
+ id: {
212
+ $in: [member1.id, member2.id],
213
+ },
214
+ };
215
+
216
+ const otherFinancialDirector = await new UserFactory({
217
+ organization: otherOrganization,
218
+ permissions: Permissions.create({
219
+ level: PermissionLevel.None,
220
+ roles: [
221
+ financialDirectorRoleOfOtherOrganization,
222
+ ],
223
+ resources: new Map([[PermissionsResourceType.Groups, new Map([[
224
+ '',
225
+ ResourcePermissions.create({
226
+ level: PermissionLevel.Write,
227
+ }),
228
+ ]])]]),
229
+ }),
230
+ })
231
+ .create();
232
+
233
+ const otherFinancialDirectorToken = await Token.createToken(otherFinancialDirector);
234
+
235
+ const body = ChargeMembersRequest.create({
236
+ description: 'test description',
237
+ price: 3,
238
+ amount: 4,
239
+ dueAt: new Date(2023, 0, 10),
240
+ createdAt: new Date(2023, 0, 4),
241
+ });
242
+
243
+ // act and assert
244
+ const result = await postCharge(filter, organization, body, financialDirectorToken);
245
+ expect(result).toBeDefined();
246
+ expect(result.body).toBeUndefined();
247
+
248
+ const testBalanceResponse = (response: Response<BalanceItemWithPayments[]>) => {
249
+ expect(response).toBeDefined();
250
+ expect(response.body.length).toBe(0);
251
+ };
252
+
253
+ testBalanceResponse(await getBalance(member1.id, otherOrganization, otherFinancialDirectorToken));
254
+ testBalanceResponse(await getBalance(member2.id, otherOrganization, otherFinancialDirectorToken));
255
+ });
256
+
257
+ test('Should fail if invalid request', async () => {
258
+ // arrange
259
+ const member1 = await new MemberFactory({ }).create();
260
+
261
+ const member2 = await new MemberFactory({ }).create();
262
+
263
+ await new RegistrationFactory({ member: member1, organization }).create();
264
+ await new RegistrationFactory({ member: member2, organization }).create();
265
+
266
+ const filter: StamhoofdFilter = {
267
+ id: {
268
+ $in: [member1.id, member2.id],
269
+ },
270
+ };
271
+
272
+ const testCases: [body: ChargeMembersRequest, expectedErrorMessage: string][] = [
273
+ // empty description
274
+ [ChargeMembersRequest.create({
275
+ description: ' ',
276
+ price: 3,
277
+ amount: 4,
278
+ dueAt: new Date(2023, 0, 10),
279
+ createdAt: new Date(2023, 0, 4),
280
+ }), 'Invalid description'],
281
+
282
+ // price 0
283
+ [ChargeMembersRequest.create({
284
+ description: 'test description',
285
+ price: 0,
286
+ amount: 4,
287
+ dueAt: new Date(2023, 0, 10),
288
+ createdAt: new Date(2023, 0, 4),
289
+ }), 'Invalid price'],
290
+
291
+ // amount 0
292
+ [ChargeMembersRequest.create({
293
+ description: 'test description',
294
+ price: 3,
295
+ amount: 0,
296
+ dueAt: new Date(2023, 0, 10),
297
+ createdAt: new Date(2023, 0, 4),
298
+ }), 'Invalid amount'],
299
+ ];
300
+
301
+ // act and assert
302
+ for (const [body, expectedErrorMessage] of testCases) {
303
+ await expect(async () => await postCharge(filter, organization, body, financialDirectorToken))
304
+ .rejects
305
+ .toThrow(expectedErrorMessage);
306
+ }
307
+ });
308
+
309
+ describe('Access for single group', () => {
310
+ test('Should create balance items for members if can manage payments of group', async () => {
311
+ // arrange
312
+ const role = PermissionRoleDetailed.create({
313
+ name: 'financial director',
314
+ accessRights: [AccessRight.OrganizationFinanceDirector],
315
+ });
316
+
317
+ const resources = new Map();
318
+
319
+ const { organization, token, user } = await createUserData(Permissions.create({
320
+ level: PermissionLevel.None,
321
+ roles: [
322
+ role,
323
+ ],
324
+ resources,
325
+ }), [role]);
326
+
327
+ const member1 = await new MemberFactory({ }).create();
328
+
329
+ const member2 = await new MemberFactory({ }).create();
330
+
331
+ const group = await new GroupFactory({ organization, period }).create();
332
+
333
+ resources.set(PermissionsResourceType.Groups, new Map([[group.id, ResourcePermissions.create({
334
+ level: PermissionLevel.Write,
335
+ })]]));
336
+
337
+ await user.save();
338
+
339
+ await new RegistrationFactory({ member: member1, group }).create();
340
+ await new RegistrationFactory({ member: member2, group }).create();
341
+
342
+ const filter: StamhoofdFilter = {
343
+ id: {
344
+ $in: [member1.id, member2.id],
345
+ },
346
+ };
347
+
348
+ const body = ChargeMembersRequest.create({
349
+ description: 'test description',
350
+ price: 3,
351
+ amount: 4,
352
+ dueAt: new Date(2023, 0, 10),
353
+ createdAt: new Date(2023, 0, 4),
354
+ });
355
+
356
+ const result = await postCharge(filter, organization, body, token);
357
+ expect(result).toBeDefined();
358
+ expect(result.body).toBeUndefined();
359
+
360
+ // act and assert
361
+ const testBalanceResponse = (response: Response<BalanceItemWithPayments[]>) => {
362
+ expect(response).toBeDefined();
363
+ expect(response.body.length).toBe(1);
364
+ const balanceItem1 = response.body[0];
365
+ expect(balanceItem1.price).toEqual(12);
366
+ expect(balanceItem1.amount).toEqual(body.amount);
367
+ expect(balanceItem1.description).toEqual(body.description);
368
+ expect(balanceItem1.organizationId).toEqual(organization.id);
369
+ // const dueAt = balanceItem1.dueAt!;
370
+ expect(balanceItem1.dueAt).toEqual(body.dueAt);
371
+ expect(balanceItem1.createdAt).toEqual(body.createdAt);
372
+ };
373
+
374
+ testBalanceResponse(await getBalance(member1.id, organization, token));
375
+ testBalanceResponse(await getBalance(member2.id, organization, token));
376
+ });
377
+
378
+ test('Should fail if no write access for group', async () => {
379
+ // arrange
380
+ const role = PermissionRoleDetailed.create({
381
+ name: 'financial director',
382
+ accessRights: [AccessRight.OrganizationFinanceDirector],
383
+ });
384
+
385
+ const resources = new Map();
386
+
387
+ const { organization, token, user } = await createUserData(Permissions.create({
388
+ level: PermissionLevel.None,
389
+ roles: [
390
+ role,
391
+ ],
392
+ resources,
393
+ }), [role]);
394
+
395
+ const member1 = await new MemberFactory({ }).create();
396
+
397
+ const member2 = await new MemberFactory({ }).create();
398
+
399
+ const group = await new GroupFactory({ organization, period }).create();
400
+
401
+ resources.set(PermissionsResourceType.Groups, new Map([[group.id, ResourcePermissions.create({
402
+ level: PermissionLevel.Read,
403
+ })]]));
404
+
405
+ await user.save();
406
+
407
+ await new RegistrationFactory({ member: member1, group }).create();
408
+ await new RegistrationFactory({ member: member2, group }).create();
409
+
410
+ const filter: StamhoofdFilter = {
411
+ id: {
412
+ $in: [member1.id, member2.id],
413
+ },
414
+ };
415
+
416
+ const body = ChargeMembersRequest.create({
417
+ description: 'test description',
418
+ price: 3,
419
+ amount: 4,
420
+ dueAt: new Date(2023, 0, 10),
421
+ createdAt: new Date(2023, 0, 4),
422
+ });
423
+
424
+ await expect(async () => await postCharge(filter, organization, body, token))
425
+ .rejects
426
+ .toThrow('You do not have permissions for this action');
427
+ });
428
+ });
429
+ });
@@ -3,13 +3,14 @@ backendEnv.load({ path: __dirname + '/../../.env.test.json' });
3
3
 
4
4
  import { Column, Database } from '@simonbackx/simple-database';
5
5
  import { Request } from '@simonbackx/simple-endpoints';
6
- import { Email } from '@stamhoofd/email';
6
+ import { Email, EmailMocker } from '@stamhoofd/email';
7
7
  import { Version } from '@stamhoofd/structures';
8
8
  import { sleep } from '@stamhoofd/utility';
9
9
  import nock from 'nock';
10
10
  import { GlobalHelper } from '../src/helpers/GlobalHelper';
11
11
  import * as jose from 'jose';
12
12
  import { TestUtils } from '@stamhoofd/test-utils';
13
+ import './toMatchMap';
13
14
 
14
15
  // Set version of saved structures
15
16
  Column.setJSONVersion(Version);
@@ -43,6 +44,7 @@ beforeAll(async () => {
43
44
  await Database.delete('DELETE FROM `provinces`');
44
45
  await Database.delete('DELETE FROM `email_recipients`');
45
46
  await Database.delete('DELETE FROM `emails`');
47
+ await Database.delete('DELETE FROM `email_templates`');
46
48
 
47
49
  await Database.delete('DELETE FROM `webshop_orders`');
48
50
  await Database.delete('DELETE FROM `webshops`');
@@ -67,6 +69,9 @@ beforeAll(async () => {
67
69
  TestUtils.setPermanentEnvironment('FILE_SIGNING_PRIVATE_KEY', exportedPrivateKey);
68
70
 
69
71
  await GlobalHelper.load();
72
+
73
+ // Override default $t handlers
74
+ TestUtils.loadEnvironment();
70
75
  });
71
76
 
72
77
  afterAll(async () => {
@@ -79,3 +84,4 @@ afterAll(async () => {
79
84
  });
80
85
 
81
86
  TestUtils.setup();
87
+ EmailMocker.infect();
@@ -0,0 +1,68 @@
1
+ import { expect } from '@jest/globals';
2
+ import type { MatcherFunction } from 'expect';
3
+ import 'jest';
4
+
5
+ const toMatchMap: MatcherFunction<[map: unknown]> = function (actual, map: Map<any, any>) {
6
+ if (
7
+ !(map instanceof Map)
8
+ ) {
9
+ throw new TypeError('You need to pass a Map to toMatchMap');
10
+ }
11
+
12
+ if (
13
+ !(actual instanceof Map)
14
+ ) {
15
+ return {
16
+ message: () =>
17
+ `expected ${this.utils.printReceived(
18
+ actual,
19
+ )} to be a Map`,
20
+ pass: false,
21
+ };
22
+ }
23
+
24
+ for (const key of map.keys()) {
25
+ // Check key exists
26
+ if (!actual.has(key)) {
27
+ return {
28
+ message: () =>
29
+ `expected ${this.utils.printReceived(
30
+ actual,
31
+ )} to have key ${this.utils.printExpected(key)}`,
32
+ pass: false,
33
+ };
34
+ }
35
+
36
+ // Compare values
37
+ const expectedValue = map.get(key);
38
+ const actualValue = actual.get(key);
39
+
40
+ if (!this.equals(expectedValue, actualValue)) {
41
+ return {
42
+ message: () =>
43
+ `expected ${this.utils.diff(actualValue, expectedValue)} at key ${this.utils.printExpected(key)}`,
44
+ pass: false,
45
+ };
46
+ }
47
+ }
48
+
49
+ // Check for extra keys in actual
50
+ for (const key of actual.keys()) {
51
+ if (!map.has(key)) {
52
+ return {
53
+ message: () =>
54
+ `unexpected key ${this.utils.printExpected(key)} in Map`,
55
+ pass: false,
56
+ };
57
+ }
58
+ }
59
+
60
+ return {
61
+ message: () => `ok`,
62
+ pass: true,
63
+ };
64
+ };
65
+
66
+ expect.extend({
67
+ toMatchMap,
68
+ });