@stamhoofd/backend 2.105.0 → 2.106.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 (28) hide show
  1. package/package.json +10 -10
  2. package/src/crons.ts +39 -5
  3. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +953 -47
  4. package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -1
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +142 -0
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +1 -1
  7. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +163 -8
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -0
  9. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.test.ts +108 -0
  10. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +40 -0
  11. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
  12. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -1
  13. package/src/helpers/AdminPermissionChecker.ts +30 -6
  14. package/src/helpers/AuthenticatedStructures.ts +2 -2
  15. package/src/helpers/MemberUserSyncer.test.ts +400 -1
  16. package/src/helpers/MemberUserSyncer.ts +15 -10
  17. package/src/helpers/ServiceFeeHelper.ts +63 -0
  18. package/src/helpers/StripeHelper.ts +7 -4
  19. package/src/helpers/StripePayoutChecker.ts +1 -1
  20. package/src/seeds/0000000001-development-user.ts +2 -2
  21. package/src/seeds/0000000004-single-organization.ts +60 -0
  22. package/src/seeds/1754560914-groups-prices.test.ts +3023 -0
  23. package/src/seeds/1754560914-groups-prices.ts +408 -0
  24. package/src/seeds/{1722344162-sync-member-users.ts → 1761665607-sync-member-users.ts} +1 -1
  25. package/src/sql-filters/members.ts +1 -1
  26. package/tests/init/initAdmin.ts +19 -5
  27. package/tests/init/initPermissionRole.ts +14 -4
  28. package/tests/init/initPlatformRecordCategory.ts +8 -0
@@ -43,7 +43,7 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
43
43
  let scopeFilter: StamhoofdFilter | undefined = undefined;
44
44
 
45
45
  // First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
46
- if (!Context.auth.canAccessAllPlatformMembers() && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
46
+ if (!Context.auth.canAccessAllPlatformMembers(permissionLevel) && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: 'registrations' })) {
47
47
  if (!organization) {
48
48
  const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
49
49
  if (tags !== 'all' && tags.length === 0) {
@@ -269,6 +269,148 @@ describe('Endpoint.PatchOrganizationMembersEndpoint', () => {
269
269
  });
270
270
  });
271
271
 
272
+ test('A registration grants permission to a member', async () => {
273
+ // Member had a deactivated registration with a group that should give the admin acecss to the member, but since it is deactivated -> no access
274
+ const organization = await new OrganizationFactory({}).create();
275
+ const resources = new Map();
276
+
277
+ const group = await new GroupFactory({
278
+ organization,
279
+ }).create();
280
+
281
+ // Give read permission to the group
282
+ resources.set(
283
+ PermissionsResourceType.Groups, new Map([[
284
+ group.id,
285
+ ResourcePermissions.create({
286
+ level: PermissionLevel.Write,
287
+ }),
288
+ ]]),
289
+ );
290
+
291
+ const user = await new UserFactory({
292
+ permissions: Permissions.create({
293
+ level: PermissionLevel.None,
294
+ resources,
295
+ }),
296
+ organization, // since we are in platform mode, this will only set the permissions for this organization
297
+ }).create();
298
+
299
+ const member = await new MemberFactory({
300
+ firstName,
301
+ lastName,
302
+ birthDay,
303
+ generateData: false,
304
+ }).create();
305
+
306
+ // Register this member
307
+ await new RegistrationFactory({
308
+ member,
309
+ group,
310
+ }).create();
311
+
312
+ const token = await Token.createToken(user);
313
+
314
+ const arr: Body = new PatchableArray();
315
+ const patch = MemberWithRegistrationsBlob.patch({
316
+ id: member.id,
317
+ details: MemberDetails.patch({
318
+ firstName: 'Changed',
319
+ }),
320
+ });
321
+ arr.addPatch(patch);
322
+
323
+ // Try to request all members at organization
324
+ const request = Request.patch({
325
+ path: baseUrl,
326
+ host: organization.getApiHost(),
327
+ body: arr,
328
+ headers: {
329
+ authorization: 'Bearer ' + token.accessToken,
330
+ },
331
+ });
332
+ const response = await testServer.test(endpoint, request);
333
+
334
+ // Check returned
335
+ expect(response.status).toBe(200);
336
+ expect(response.body.members.length).toBe(1);
337
+ const memberStruct = response.body.members[0];
338
+ expect(memberStruct.details).toMatchObject({
339
+ firstName: 'Changed',
340
+ });
341
+ });
342
+
343
+ test('[REGRESSION] A deactivated registration does not grant permission to a member', async () => {
344
+ // Member had a deactivated registration with a group that should give the admin acecss to the member, but since it is deactivated -> no access
345
+ const organization = await new OrganizationFactory({}).create();
346
+ const resources = new Map();
347
+
348
+ const group = await new GroupFactory({
349
+ organization,
350
+ }).create();
351
+
352
+ // Give read permission to the group
353
+ resources.set(
354
+ PermissionsResourceType.Groups, new Map([[
355
+ group.id,
356
+ ResourcePermissions.create({
357
+ level: PermissionLevel.Write,
358
+ }),
359
+ ]]),
360
+ );
361
+
362
+ const user = await new UserFactory({
363
+ permissions: Permissions.create({
364
+ level: PermissionLevel.None,
365
+ resources,
366
+ }),
367
+ organization, // since we are in platform mode, this will only set the permissions for this organization
368
+ }).create();
369
+
370
+ const member = await new MemberFactory({
371
+ firstName,
372
+ lastName,
373
+ birthDay,
374
+ generateData: false,
375
+ }).create();
376
+
377
+ // Register this member
378
+ await new RegistrationFactory({
379
+ member,
380
+ group,
381
+ deactivatedAt: new Date(),
382
+ }).create();
383
+
384
+ const token = await Token.createToken(user);
385
+
386
+ const arr: Body = new PatchableArray();
387
+ const patch = MemberWithRegistrationsBlob.patch({
388
+ id: member.id,
389
+ details: MemberDetails.patch({
390
+ firstName: 'Changed',
391
+ }),
392
+ });
393
+ arr.addPatch(patch);
394
+
395
+ // Try to request all members at organization
396
+ const request = Request.patch({
397
+ path: baseUrl,
398
+ host: organization.getApiHost(),
399
+ body: arr,
400
+ headers: {
401
+ authorization: 'Bearer ' + token.accessToken,
402
+ },
403
+ });
404
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.simpleError({
405
+ code: 'not_found',
406
+ }));
407
+
408
+ await member.refresh();
409
+
410
+ // Not changed
411
+ expect(member.details.firstName).toEqual(firstName);
412
+ });
413
+
272
414
  test('A full platform admin can edit members without registrations', async () => {
273
415
  const organization = await new OrganizationFactory({}).create();
274
416
 
@@ -68,7 +68,7 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
68
68
  let scopeFilter: StamhoofdFilter | undefined = undefined;
69
69
 
70
70
  // First do a quick validation of the groups, so that prevents the backend from having to add a scope filter
71
- if (!Context.auth.canAccessAllPlatformMembers() && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: null })) {
71
+ if (!Context.auth.canAccessAllPlatformMembers(permissionLevel) && !await validateGroupFilter({ filter: q.filter, permissionLevel, key: null })) {
72
72
  if (!organization) {
73
73
  const tags = Context.auth.getPlatformAccessibleOrganizationTags(permissionLevel);
74
74
  if (tags !== 'all' && tags.length === 0) {
@@ -1,17 +1,16 @@
1
+ import { PatchMap } from '@simonbackx/simple-encoding';
1
2
  import { Request } from '@simonbackx/simple-endpoints';
2
3
  import { EmailMocker } from '@stamhoofd/email';
3
4
  import { BalanceItemFactory, Group, GroupFactory, MemberFactory, MemberWithRegistrations, Organization, OrganizationFactory, OrganizationRegistrationPeriodFactory, Registration, RegistrationFactory, RegistrationPeriod, RegistrationPeriodFactory, Token, UserFactory } from '@stamhoofd/models';
4
5
  import { AccessRight, BalanceItemCartItem, BalanceItemStatus, BalanceItemType, BooleanStatus, Company, GroupOption, GroupOptionMenu, IDRegisterCart, IDRegisterCheckout, IDRegisterItem, OrganizationPackages, PaymentCustomer, PaymentMethod, PermissionLevel, Permissions, PermissionsResourceType, ReduceablePrice, RegisterItemOption, ResourcePermissions, STPackageStatus, STPackageType, UserPermissions, Version } from '@stamhoofd/structures';
5
6
  import { STExpect, TestUtils } from '@stamhoofd/test-utils';
6
7
  import { v4 as uuidv4 } from 'uuid';
7
- import { testServer } from '../../../../tests/helpers/TestServer';
8
- import { initPayconiq } from '../../../../tests/init/initPayconiq';
9
- import { RegisterMembersEndpoint } from './RegisterMembersEndpoint';
10
8
  import { assertBalances } from '../../../../tests/assertions/assertBalances';
11
- import PersistentFile from 'formidable/PersistentFile';
12
- import { PatchMap } from '@simonbackx/simple-encoding';
9
+ import { testServer } from '../../../../tests/helpers/TestServer';
13
10
  import { initAdmin, initPermissionRole } from '../../../../tests/init';
11
+ import { initPayconiq } from '../../../../tests/init/initPayconiq';
14
12
  import { BalanceItemService } from '../../../services/BalanceItemService';
13
+ import { RegisterMembersEndpoint } from './RegisterMembersEndpoint';
15
14
 
16
15
  const baseUrl = `/v${Version}/members/register`;
17
16
 
@@ -19,6 +18,7 @@ describe('Endpoint.RegisterMembers', () => {
19
18
  // #region global
20
19
  const endpoint = new RegisterMembersEndpoint();
21
20
  let period: RegistrationPeriod;
21
+ let previousPeriod: RegistrationPeriod;
22
22
  let defaultPermissionLevel = PermissionLevel.None;
23
23
  let defaultLinkMembersToUser = true;
24
24
  const post = async (body: IDRegisterCheckout, organization: Organization, token: Token) => {
@@ -28,9 +28,10 @@ describe('Endpoint.RegisterMembers', () => {
28
28
  };
29
29
 
30
30
  beforeAll(async () => {
31
- const previousPeriod = await new RegistrationPeriodFactory({
31
+ previousPeriod = await new RegistrationPeriodFactory({
32
32
  startDate: new Date(2022, 0, 1),
33
33
  endDate: new Date(2022, 11, 31),
34
+ locked: true,
34
35
  }).create();
35
36
 
36
37
  period = await new RegistrationPeriodFactory({
@@ -54,12 +55,15 @@ describe('Endpoint.RegisterMembers', () => {
54
55
  .create();
55
56
 
56
57
  const organizationRegistrationPeriod = await new OrganizationRegistrationPeriodFactory({ organization, period: registrationPeriod }).create();
58
+ if (registrationPeriod !== previousPeriod) {
59
+ await new OrganizationRegistrationPeriodFactory({ organization, period: previousPeriod }).create();
60
+ }
57
61
 
58
62
  return { organization, organizationRegistrationPeriod };
59
63
  };
60
64
 
61
- async function initData({ otherMemberAmount = 0, groupPermissionLevel = PermissionLevel.None, memberPermissionLevel = PermissionLevel.None, permissionLevel = defaultPermissionLevel, linkMembersToUser = defaultLinkMembersToUser }: { otherMemberAmount?: number; memberPermissionLevel?: PermissionLevel; groupPermissionLevel?: PermissionLevel; permissionLevel?: PermissionLevel; linkMembersToUser?: boolean } = {}) {
62
- const { organization, organizationRegistrationPeriod } = await initOrganization(period);
65
+ async function initData({ registrationPeriod = period, otherMemberAmount = 0, groupPermissionLevel = PermissionLevel.None, memberPermissionLevel = PermissionLevel.None, permissionLevel = defaultPermissionLevel, linkMembersToUser = defaultLinkMembersToUser }: { registrationPeriod?: RegistrationPeriod; otherMemberAmount?: number; memberPermissionLevel?: PermissionLevel; groupPermissionLevel?: PermissionLevel; permissionLevel?: PermissionLevel; linkMembersToUser?: boolean } = {}) {
66
+ const { organization, organizationRegistrationPeriod } = await initOrganization(registrationPeriod);
63
67
 
64
68
  const user = await new UserFactory({
65
69
  organization,
@@ -794,6 +798,37 @@ describe('Endpoint.RegisterMembers', () => {
794
798
  expect(updatedGroup!.settings.reservedMembers).toBe(0);
795
799
  });
796
800
 
801
+ test('Cannot register in locked period', async () => {
802
+ const { member, group, groupPrice, organization, token } = await initData({
803
+ registrationPeriod: previousPeriod,
804
+ });
805
+
806
+ const body = IDRegisterCheckout.create({
807
+ cart: IDRegisterCart.create({
808
+ items: [
809
+ IDRegisterItem.create({
810
+ id: uuidv4(),
811
+ replaceRegistrationIds: [],
812
+ options: [],
813
+ groupPrice,
814
+ organizationId: organization.id,
815
+ groupId: group.id,
816
+ memberId: member.id,
817
+ }),
818
+ ],
819
+ balanceItems: [],
820
+ deleteRegistrationIds: [],
821
+ }),
822
+ administrationFee: 0,
823
+ freeContribution: 0,
824
+ paymentMethod: PaymentMethod.PointOfSale,
825
+ totalPrice: 25_00,
826
+ customer: null,
827
+ });
828
+
829
+ await expect(post(body, organization, token)).rejects.toThrow(STExpect.errorWithCode('locked_period'));
830
+ });
831
+
797
832
  test('Should set reserved members when using online payments', async () => {
798
833
  const { member, organization, token } = await initData();
799
834
  await initPayconiq({ organization });
@@ -1559,6 +1594,45 @@ describe('Endpoint.RegisterMembers', () => {
1559
1594
  expect(returnedRegistration.balances.length).not.toBe(0);
1560
1595
  });
1561
1596
 
1597
+ test('[REGRESSION] Cannot register a member in a locked period', async () => {
1598
+ const { member, organization, token } = await initData({});
1599
+ const group = await new GroupFactory({
1600
+ organization,
1601
+ price: 25_00,
1602
+ reducedPrice: 12_50,
1603
+ stock: 500,
1604
+ period: previousPeriod,
1605
+ })
1606
+ .create();
1607
+ const groupPrice = group.settings.prices[0];
1608
+
1609
+ const body = IDRegisterCheckout.create({
1610
+ cart: IDRegisterCart.create({
1611
+ items: [
1612
+ IDRegisterItem.create({
1613
+ id: uuidv4(),
1614
+ replaceRegistrationIds: [],
1615
+ options: [],
1616
+ groupPrice,
1617
+ organizationId: organization.id,
1618
+ groupId: group.id,
1619
+ memberId: member.id,
1620
+ }),
1621
+ ],
1622
+ balanceItems: [
1623
+ ],
1624
+ deleteRegistrationIds: [],
1625
+ }),
1626
+ administrationFee: 0,
1627
+ freeContribution: 0,
1628
+ paymentMethod: PaymentMethod.PointOfSale,
1629
+ totalPrice: 25_00,
1630
+ asOrganizationId: organization.id,
1631
+ });
1632
+
1633
+ await expect(post(body, organization, token)).rejects.toThrow(STExpect.errorWithCode('locked_period'));
1634
+ });
1635
+
1562
1636
  test('Can register a member as admin with write permission to new group only', async () => {
1563
1637
  // read permission to existing member, write permission to new group you want to register the member in
1564
1638
  const { member, groupPrice, group, organization, token } = await initData({
@@ -2059,6 +2133,51 @@ describe('Endpoint.RegisterMembers', () => {
2059
2133
  expect(updatedGroup1After!.settings.reservedMembers).toBe(0);
2060
2134
  });
2061
2135
 
2136
+ test('[REGRESSION] Cannot replace registrations of locked periods', async () => {
2137
+ const { member, group, groupPrice, organization, token } = await initData();
2138
+
2139
+ const group1 = await new GroupFactory({
2140
+ organization,
2141
+ price: 25_00,
2142
+ reducedPrice: 12_50,
2143
+ stock: 500,
2144
+ period: previousPeriod,
2145
+ })
2146
+ .create();
2147
+ const groupPrice1 = group1.settings.prices[0];
2148
+
2149
+ const registration = await new RegistrationFactory({
2150
+ member,
2151
+ group: group1,
2152
+ groupPrice: groupPrice1,
2153
+ }).create();
2154
+
2155
+ const body = IDRegisterCheckout.create({
2156
+ cart: IDRegisterCart.create({
2157
+ items: [
2158
+ IDRegisterItem.create({
2159
+ id: uuidv4(),
2160
+ replaceRegistrationIds: [registration.id],
2161
+ options: [],
2162
+ groupPrice,
2163
+ organizationId: organization.id,
2164
+ groupId: group.id,
2165
+ memberId: member.id,
2166
+ }),
2167
+ ],
2168
+ balanceItems: [],
2169
+ }),
2170
+ administrationFee: 0,
2171
+ freeContribution: 0,
2172
+ paymentMethod: PaymentMethod.PointOfSale,
2173
+ totalPrice: 30,
2174
+ asOrganizationId: organization.id,
2175
+ customer: null,
2176
+ });
2177
+
2178
+ await expect(post(body, organization, token)).rejects.toThrow(STExpect.errorWithCode('locked_period'));
2179
+ });
2180
+
2062
2181
  test('When replacing a registration, we should keep the original paying organization id', async () => {
2063
2182
  const { organization, group: group1, groupPrice: groupPrice1, token, member, user } = await initData();
2064
2183
 
@@ -2432,6 +2551,42 @@ describe('Endpoint.RegisterMembers', () => {
2432
2551
  expect(updatedRegistration!.deactivatedAt).not.toBe(null);
2433
2552
  });
2434
2553
 
2554
+ test('[REGRESSION] Cannot deactivate registrations of locked periods', async () => {
2555
+ const { member, organization, token } = await initData();
2556
+
2557
+ const group1 = await new GroupFactory({
2558
+ organization,
2559
+ price: 25_00,
2560
+ reducedPrice: 12_50,
2561
+ stock: 500,
2562
+ period: previousPeriod,
2563
+ })
2564
+ .create();
2565
+ const groupPrice1 = group1.settings.prices[0];
2566
+
2567
+ const registration = await new RegistrationFactory({
2568
+ member,
2569
+ group: group1,
2570
+ groupPrice: groupPrice1,
2571
+ }).create();
2572
+
2573
+ const body = IDRegisterCheckout.create({
2574
+ cart: IDRegisterCart.create({
2575
+ items: [],
2576
+ balanceItems: [],
2577
+ deleteRegistrationIds: [registration.id],
2578
+ }),
2579
+ administrationFee: 0,
2580
+ freeContribution: 0,
2581
+ paymentMethod: PaymentMethod.PointOfSale,
2582
+ totalPrice: 30,
2583
+ asOrganizationId: organization.id,
2584
+ customer: null,
2585
+ });
2586
+
2587
+ await expect(post(body, organization, token)).rejects.toThrow(STExpect.errorWithCode('locked_period'));
2588
+ });
2589
+
2435
2590
  test('Should fail if invalid cancelation fee', async () => {
2436
2591
  for (const cancellationFeePercentage of [10001, -1]) {
2437
2592
  const { member, group: group1, groupPrice: groupPrice1, organization, token } = await initData();
@@ -15,6 +15,7 @@ import { StripeHelper } from '../../../helpers/StripeHelper';
15
15
  import { BalanceItemService } from '../../../services/BalanceItemService';
16
16
  import { RegistrationService } from '../../../services/RegistrationService';
17
17
  import { PaymentService } from '../../../services/PaymentService';
18
+ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper';
18
19
  type Params = Record<string, never>;
19
20
  type Query = undefined;
20
21
  type Body = IDRegisterCheckout;
@@ -1062,6 +1063,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
1062
1063
  const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, organization.privateMeta.registrationPaymentConfiguration);
1063
1064
  payment.provider = provider;
1064
1065
  payment.stripeAccountId = stripeAccount?.id ?? null;
1066
+ ServiceFeeHelper.setServiceFee(payment, organization, 'members', [...balanceItems.entries()].map(([_, p]) => p));
1065
1067
 
1066
1068
  await payment.save();
1067
1069
 
@@ -0,0 +1,108 @@
1
+ import { Request } from '@simonbackx/simple-endpoints';
2
+ import { OrganizationFactory, STPackageFactory } from '@stamhoofd/models';
3
+ import { AccessRight, LimitedFilteredRequest, STPackageBundle, STPackageType } from '@stamhoofd/structures';
4
+ import { STExpect, TestUtils } from '@stamhoofd/test-utils';
5
+ import { testServer } from '../../../../../tests/helpers/TestServer';
6
+ import { initAdmin } from '../../../../../tests/init';
7
+ import { GetPackagesEndpoint } from './GetPackagesEndpoint';
8
+
9
+ const baseUrl = `/organization/packages`;
10
+ const endpoint = new GetPackagesEndpoint();
11
+
12
+ describe('Endpoint.GetPackagesEndpoint', () => {
13
+ beforeEach(async () => {
14
+ TestUtils.setEnvironment('userMode', 'organization');
15
+ });
16
+
17
+ test('Can get organization packages as finance director', async () => {
18
+ const organization = await new OrganizationFactory({}).create();
19
+
20
+ const package1 = await new STPackageFactory({
21
+ organization,
22
+ bundle: STPackageBundle.Members,
23
+ }).create();
24
+
25
+ // This old package is not returned
26
+ const oldPackage = await new STPackageFactory({
27
+ organization,
28
+ bundle: STPackageBundle.Webshops,
29
+ removeAt: new Date(Date.now() - 1000 * 60 * 60),
30
+ }).create();
31
+
32
+ // Never activated packages are not returned
33
+ const inactivePackage = await new STPackageFactory({
34
+ organization,
35
+ bundle: STPackageBundle.Webshops,
36
+ validAt: null,
37
+ }).create();
38
+
39
+ const package2 = await new STPackageFactory({
40
+ organization,
41
+ bundle: STPackageBundle.TrialWebshops,
42
+ }).create();
43
+
44
+ const { adminToken } = await initAdmin({
45
+ organization,
46
+ accessRights: [AccessRight.OrganizationFinanceDirector],
47
+ });
48
+
49
+ // Try to request all members at organization
50
+ const request = Request.get({
51
+ path: baseUrl,
52
+ host: organization.getApiHost(),
53
+ query: new LimitedFilteredRequest({
54
+ limit: 10,
55
+ }),
56
+ headers: {
57
+ authorization: 'Bearer ' + adminToken.accessToken,
58
+ },
59
+ });
60
+
61
+ const response = await testServer.test(endpoint, request);
62
+ expect(response.status).toBe(200);
63
+ expect(response.body.packages).toHaveLength(2);
64
+
65
+ expect(response.body.packages).toIncludeSameMembers([
66
+ expect.objectContaining({
67
+ id: package1.id,
68
+ meta: expect.objectContaining({
69
+ type: STPackageType.Members,
70
+ }),
71
+ }),
72
+ expect.objectContaining({
73
+ id: package2.id,
74
+ meta: expect.objectContaining({
75
+ type: STPackageType.TrialWebshops,
76
+ }),
77
+ }),
78
+ ]);
79
+ });
80
+
81
+ test('Cannot get organization packages without finance director right', async () => {
82
+ const organization = await new OrganizationFactory({}).create();
83
+
84
+ await new STPackageFactory({
85
+ organization,
86
+ bundle: STPackageBundle.Members,
87
+ }).create();
88
+
89
+ const { adminToken } = await initAdmin({
90
+ organization,
91
+ accessRights: [AccessRight.OrganizationCreateWebshops],
92
+ });
93
+
94
+ // Try to request all members at organization
95
+ const request = Request.get({
96
+ path: baseUrl,
97
+ host: organization.getApiHost(),
98
+ query: new LimitedFilteredRequest({
99
+ limit: 10,
100
+ }),
101
+ headers: {
102
+ authorization: 'Bearer ' + adminToken.accessToken,
103
+ },
104
+ });
105
+
106
+ await expect(testServer.test(endpoint, request)).rejects.toThrow(STExpect.errorWithCode('permission_denied'));
107
+ });
108
+ });
@@ -0,0 +1,40 @@
1
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
+ import { STPackage } from '@stamhoofd/models';
3
+ import { OrganizationPackagesStatus, STPackage as STPackageStruct } from '@stamhoofd/structures';
4
+ import { Context } from '../../../../helpers/Context';
5
+
6
+ type Params = Record<string, never>;
7
+ type Query = undefined;
8
+ type ResponseBody = OrganizationPackagesStatus;
9
+ type Body = undefined;
10
+
11
+ export class GetPackagesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
12
+ protected doesMatch(request: Request): [true, Params] | [false] {
13
+ if (request.method !== 'GET') {
14
+ return [false];
15
+ }
16
+
17
+ const params = Endpoint.parseParameters(request.url, '/organization/packages', {});
18
+
19
+ if (params) {
20
+ return [true, params as Params];
21
+ }
22
+ return [false];
23
+ }
24
+
25
+ async handle(_: DecodedRequest<Params, Query, Body>) {
26
+ const organization = await Context.setOrganizationScope();
27
+ await Context.authenticate();
28
+
29
+ // If the user has permission, we'll also search if he has access to the organization's key
30
+ if (!await Context.auth.canManageFinances(organization.id)) {
31
+ throw Context.auth.error();
32
+ }
33
+
34
+ const packages = await STPackage.getForOrganization(organization.id);
35
+
36
+ return new Response(OrganizationPackagesStatus.create({
37
+ packages: packages.map(p => STPackageStruct.create(p)),
38
+ }));
39
+ }
40
+ }
@@ -3,11 +3,12 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { BalanceItem, BalanceItemPayment, Order, Payment, Webshop, WebshopCounter } from '@stamhoofd/models';
5
5
  import { QueueHandler } from '@stamhoofd/queues';
6
- import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, TranslatedString, Webshop as WebshopStruct } from '@stamhoofd/structures';
6
+ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderStatus, PaymentMethod, PaymentStatus, PermissionLevel, PrivateOrder, TranslatedString, Webshop as WebshopStruct, WebshopTicketType } from '@stamhoofd/structures';
7
7
 
8
8
  import { Context } from '../../../../helpers/Context';
9
9
  import { AuditLogService } from '../../../../services/AuditLogService';
10
10
  import { shouldReserveUitpasNumbers, UitpasService } from '../../../../services/uitpas/UitpasService';
11
+ import { ServiceFeeHelper } from '../../../../helpers/ServiceFeeHelper';
11
12
 
12
13
  type Params = { id: string };
13
14
  type Query = undefined;
@@ -157,6 +158,12 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
157
158
 
158
159
  // Determine the payment provider (always null because no online payments here)
159
160
  payment.provider = null;
161
+ ServiceFeeHelper.setServiceFee(
162
+ payment,
163
+ organization,
164
+ webshop.meta.ticketType === WebshopTicketType.None ? 'webshop' : 'tickets',
165
+ order.data.cart.items.flatMap(i => i.calculatedPrices.map(p => p.discountedPrice)),
166
+ );
160
167
 
161
168
  await payment.save();
162
169
 
@@ -5,7 +5,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
5
5
  import { Email } from '@stamhoofd/email';
6
6
  import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
7
7
  import { QueueHandler } from '@stamhoofd/queues';
8
- import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct } from '@stamhoofd/structures';
8
+ import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct, WebshopTicketType } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
11
11
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
@@ -13,6 +13,7 @@ import { Context } from '../../../helpers/Context';
13
13
  import { StripeHelper } from '../../../helpers/StripeHelper';
14
14
  import { AuditLogService } from '../../../services/AuditLogService';
15
15
  import { UitpasService } from '../../../services/uitpas/UitpasService';
16
+ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper';
16
17
 
17
18
  type Params = { id: string };
18
19
  type Query = undefined;
@@ -182,6 +183,12 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
182
183
  const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, webshop.privateMeta.paymentConfiguration);
183
184
  payment.provider = provider;
184
185
  payment.stripeAccountId = stripeAccount?.id ?? null;
186
+ ServiceFeeHelper.setServiceFee(
187
+ payment,
188
+ organization,
189
+ webshop.meta.ticketType === WebshopTicketType.None ? 'webshop' : 'tickets',
190
+ order.data.cart.items.flatMap(i => i.calculatedPrices.map(p => p.discountedPrice)),
191
+ );
185
192
 
186
193
  await payment.save();
187
194