@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.
- package/package.json +10 -10
- package/src/crons.ts +39 -5
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +953 -47
- package/src/endpoints/global/members/GetMembersEndpoint.ts +1 -1
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +142 -0
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +1 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +163 -8
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +2 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.test.ts +108 -0
- package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +40 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +8 -1
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +8 -1
- package/src/helpers/AdminPermissionChecker.ts +30 -6
- package/src/helpers/AuthenticatedStructures.ts +2 -2
- package/src/helpers/MemberUserSyncer.test.ts +400 -1
- package/src/helpers/MemberUserSyncer.ts +15 -10
- package/src/helpers/ServiceFeeHelper.ts +63 -0
- package/src/helpers/StripeHelper.ts +7 -4
- package/src/helpers/StripePayoutChecker.ts +1 -1
- package/src/seeds/0000000001-development-user.ts +2 -2
- package/src/seeds/0000000004-single-organization.ts +60 -0
- package/src/seeds/1754560914-groups-prices.test.ts +3023 -0
- package/src/seeds/1754560914-groups-prices.ts +408 -0
- package/src/seeds/{1722344162-sync-member-users.ts → 1761665607-sync-member-users.ts} +1 -1
- package/src/sql-filters/members.ts +1 -1
- package/tests/init/initAdmin.ts +19 -5
- package/tests/init/initPermissionRole.ts +14 -4
- 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
|
|
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
|
-
|
|
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(
|
|
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
|
|