@stamhoofd/backend 2.83.5 → 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,77 @@
|
|
|
1
|
+
import { Member } from '@stamhoofd/models';
|
|
2
|
+
import { SQL, SQLOrderBy, SQLOrderByDirection, SQLSortDefinitions } from '@stamhoofd/sql';
|
|
3
|
+
import { RegistrationWithMemberBlob } from '@stamhoofd/structures';
|
|
4
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
5
|
+
import { memberJoin } from '../sql-filters/registrations';
|
|
6
|
+
|
|
7
|
+
export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob> = {
|
|
8
|
+
// WARNING! TEST NEW SORTERS THOROUGHLY!
|
|
9
|
+
// Try to avoid creating sorters on fields that er not 1:1 with the database, that often causes pagination issues if not thought through
|
|
10
|
+
// An example: sorting on 'name' is not a good idea, because it is a concatenation of two fields.
|
|
11
|
+
// You might be tempted to use ORDER BY firstName, lastName, but that will not work as expected and it needs to be ORDER BY CONCAT(firstName, ' ', lastName)
|
|
12
|
+
// Why? Because ORDER BY firstName, lastName produces a different order dan ORDER BY CONCAT(firstName, ' ', lastName) if there are multiple people with spaces in the first name
|
|
13
|
+
// And that again causes issues with pagination because the next query will append a filter of name > 'John Doe' - causing duplicate and/or skipped results
|
|
14
|
+
// What if you need mapping? simply map the sorters in the frontend: name -> firstname, lastname, age -> birthDay, etc.
|
|
15
|
+
|
|
16
|
+
'id': {
|
|
17
|
+
getValue(a) {
|
|
18
|
+
return a.id;
|
|
19
|
+
},
|
|
20
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
21
|
+
return new SQLOrderBy({
|
|
22
|
+
column: SQL.column('id'),
|
|
23
|
+
direction,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
'registeredAt': {
|
|
28
|
+
getValue(a) {
|
|
29
|
+
return a.registeredAt;
|
|
30
|
+
},
|
|
31
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
32
|
+
return new SQLOrderBy({
|
|
33
|
+
column: SQL.column('registeredAt'),
|
|
34
|
+
direction,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
'member.firstName': {
|
|
39
|
+
getValue(a) {
|
|
40
|
+
return a.member.firstName;
|
|
41
|
+
},
|
|
42
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
43
|
+
return new SQLOrderBy({
|
|
44
|
+
column: SQL.column(Member.table, 'firstName'),
|
|
45
|
+
direction,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
join: memberJoin,
|
|
49
|
+
select: [SQL.column(Member.table, 'firstName')],
|
|
50
|
+
},
|
|
51
|
+
'member.lastName': {
|
|
52
|
+
getValue(a) {
|
|
53
|
+
return a.member.lastName;
|
|
54
|
+
},
|
|
55
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
56
|
+
return new SQLOrderBy({
|
|
57
|
+
column: SQL.column(Member.table, 'lastName'),
|
|
58
|
+
direction,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
join: memberJoin,
|
|
62
|
+
select: [SQL.column(Member.table, 'lastName')],
|
|
63
|
+
},
|
|
64
|
+
'member.birthDay': {
|
|
65
|
+
getValue(a) {
|
|
66
|
+
return a.member.details.birthDay === null ? null : Formatter.dateIso(a.member.details.birthDay as Date);
|
|
67
|
+
},
|
|
68
|
+
toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
|
|
69
|
+
return new SQLOrderBy({
|
|
70
|
+
column: SQL.column(Member.table, 'birthDay'),
|
|
71
|
+
direction,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
join: memberJoin,
|
|
75
|
+
select: [SQL.column(Member.table, 'birthDay')],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AutoEncoderPatchType, PatchableArray } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { Organization } from '@stamhoofd/models';
|
|
4
|
+
import { MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
5
|
+
import { PatchOrganizationMembersEndpoint } from '../../src/endpoints/global/members/PatchOrganizationMembersEndpoint';
|
|
6
|
+
import { testServer } from '../helpers/TestServer';
|
|
7
|
+
import { initAdmin } from '../init/initAdmin';
|
|
8
|
+
|
|
9
|
+
export async function patchOrganizationMember({ patch, organization }: { patch: AutoEncoderPatchType<MemberWithRegistrationsBlob>; organization: Organization }) {
|
|
10
|
+
expect(patch.id).toBeString();
|
|
11
|
+
const { adminToken } = await initAdmin({ organization: organization });
|
|
12
|
+
|
|
13
|
+
const arr = new PatchableArray();
|
|
14
|
+
arr.addPatch(patch);
|
|
15
|
+
|
|
16
|
+
const request = Request.patch({
|
|
17
|
+
path: '/organization/members',
|
|
18
|
+
host: organization.getApiHost(),
|
|
19
|
+
body: arr,
|
|
20
|
+
headers: {
|
|
21
|
+
authorization: 'Bearer ' + adminToken.accessToken,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const response = await testServer.test(new PatchOrganizationMembersEndpoint(), request);
|
|
26
|
+
expect(response.status).toBe(200);
|
|
27
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PatchableArray } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { PaymentStatus, PaymentGeneral } from '@stamhoofd/structures';
|
|
3
|
+
import { PatchPaymentsEndpoint } from '../../src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint';
|
|
4
|
+
import { testServer } from '../helpers/TestServer';
|
|
5
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
6
|
+
import { Organization } from '@stamhoofd/models';
|
|
7
|
+
import { initAdmin } from '../init/initAdmin';
|
|
8
|
+
|
|
9
|
+
export async function changePaymentStatus({ payment, organization, status }: { payment: { id: string }; organization: Organization; status: PaymentStatus }) {
|
|
10
|
+
expect(payment.id).toBeString();
|
|
11
|
+
const { adminToken } = await initAdmin({ organization: organization });
|
|
12
|
+
|
|
13
|
+
const arr = new PatchableArray();
|
|
14
|
+
arr.addPatch(PaymentGeneral.patch({
|
|
15
|
+
id: payment.id,
|
|
16
|
+
status,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const request = Request.patch({
|
|
20
|
+
path: '/organization/payments',
|
|
21
|
+
host: organization.getApiHost(),
|
|
22
|
+
body: arr,
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: 'Bearer ' + adminToken.accessToken,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const response = await testServer.test(new PatchPaymentsEndpoint(), request);
|
|
29
|
+
expect(response.status).toBe(200);
|
|
30
|
+
}
|
|
31
|
+
export async function markPaid({ payment, organization }: { payment: { id: string }; organization: Organization }) {
|
|
32
|
+
await changePaymentStatus({
|
|
33
|
+
payment,
|
|
34
|
+
organization,
|
|
35
|
+
status: PaymentStatus.Succeeded,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function markNotPaid({ payment, organization }: { payment: { id: string }; organization: Organization }) {
|
|
40
|
+
await changePaymentStatus({
|
|
41
|
+
payment,
|
|
42
|
+
organization,
|
|
43
|
+
status: PaymentStatus.Created,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AutoEncoderPatchType, PatchableArray } from '@simonbackx/simple-encoding';
|
|
2
|
+
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
|
+
import { Organization, Token, User } from '@stamhoofd/models';
|
|
4
|
+
import { MemberWithRegistrationsBlob } from '@stamhoofd/structures';
|
|
5
|
+
import { PatchUserMembersEndpoint } from '../../src/endpoints/global/registration/PatchUserMembersEndpoint';
|
|
6
|
+
import { testServer } from '../helpers/TestServer';
|
|
7
|
+
|
|
8
|
+
export async function patchUserMember({ patch, organization, user }: { patch: AutoEncoderPatchType<MemberWithRegistrationsBlob>; organization: Organization; user: User }) {
|
|
9
|
+
expect(patch.id).toBeString();
|
|
10
|
+
|
|
11
|
+
const token = await Token.createToken(user);
|
|
12
|
+
|
|
13
|
+
const arr = new PatchableArray();
|
|
14
|
+
arr.addPatch(patch);
|
|
15
|
+
|
|
16
|
+
const request = Request.patch({
|
|
17
|
+
path: '/members',
|
|
18
|
+
host: organization.getApiHost(),
|
|
19
|
+
body: arr,
|
|
20
|
+
headers: {
|
|
21
|
+
authorization: 'Bearer ' + token.accessToken,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const response = await testServer.test(new PatchUserMembersEndpoint(), request);
|
|
26
|
+
expect(response.status).toBe(200);
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BalanceItem } from '@stamhoofd/models';
|
|
2
|
+
import { BalanceItemService } from '../../src/services/BalanceItemService';
|
|
3
|
+
|
|
4
|
+
export async function assertBalances(selector: { user: { id: string | null } } | { member: { id: string | null } }, balances: Partial<BalanceItem>[]) {
|
|
5
|
+
await BalanceItemService.flushAll();
|
|
6
|
+
|
|
7
|
+
// Fetch all user balances
|
|
8
|
+
const q = BalanceItem.select();
|
|
9
|
+
if ('user' in selector && selector.user.id) {
|
|
10
|
+
q.where('userId', selector.user.id);
|
|
11
|
+
}
|
|
12
|
+
else if ('member' in selector && selector.member.id) {
|
|
13
|
+
q.where('memberId', selector.member.id);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
throw new Error('Selector must contain either user or member with an id');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const userBalances = await q.fetch();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
expect(userBalances).toIncludeAllMembers(balances.map(b => expect.objectContaining(b)));
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
// List all the balances that were found and the ones that were missing
|
|
26
|
+
if (userBalances.length !== balances.length) {
|
|
27
|
+
console.error('Difference in number of balances found:', userBalances.length, 'expected:', balances.length);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const expectedBalance of balances) {
|
|
31
|
+
let found = false;
|
|
32
|
+
for (const userBalance of userBalances) {
|
|
33
|
+
try {
|
|
34
|
+
expect(userBalance).toEqual(expect.objectContaining(expectedBalance));
|
|
35
|
+
found = true;
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// ignore
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!found) {
|
|
43
|
+
console.error('Expected balance not found:', expectedBalance);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Organization, OrganizationFactory, Token, UserFactory } from '@stamhoof
|
|
|
4
4
|
|
|
5
5
|
import { PatchMap } from '@simonbackx/simple-encoding';
|
|
6
6
|
import { ApiUser, ApiUserRateLimits, PermissionLevel, Permissions, PermissionsResourceType, ResourcePermissions, UserMeta, UserPermissions } from '@stamhoofd/structures';
|
|
7
|
-
import {
|
|
7
|
+
import { STExpect, TestUtils } from '@stamhoofd/test-utils';
|
|
8
8
|
import { CreateApiUserEndpoint } from '../../src/endpoints/organization/dashboard/users/CreateApiUserEndpoint';
|
|
9
9
|
import { testServer } from '../helpers/TestServer';
|
|
10
10
|
import { GetUserEndpoint } from '../../src/endpoints/auth/GetUserEndpoint';
|
|
@@ -76,7 +76,7 @@ describe('E2E.APIRateLimits', () => {
|
|
|
76
76
|
}
|
|
77
77
|
else {
|
|
78
78
|
await expect(promise).rejects.toThrow(
|
|
79
|
-
|
|
79
|
+
STExpect.simpleError({
|
|
80
80
|
code: 'rate_limit',
|
|
81
81
|
}),
|
|
82
82
|
);
|
|
@@ -110,7 +110,7 @@ describe('E2E.APIRateLimits', () => {
|
|
|
110
110
|
}
|
|
111
111
|
else {
|
|
112
112
|
await expect(promise).rejects.toThrow(
|
|
113
|
-
|
|
113
|
+
STExpect.simpleError({
|
|
114
114
|
code: 'rate_limit',
|
|
115
115
|
}),
|
|
116
116
|
);
|
|
@@ -144,7 +144,7 @@ describe('E2E.APIRateLimits', () => {
|
|
|
144
144
|
}
|
|
145
145
|
else {
|
|
146
146
|
await expect(promise).rejects.toThrow(
|
|
147
|
-
|
|
147
|
+
STExpect.simpleError({
|
|
148
148
|
code: 'rate_limit',
|
|
149
149
|
}),
|
|
150
150
|
);
|
|
@@ -178,7 +178,7 @@ describe('E2E.APIRateLimits', () => {
|
|
|
178
178
|
}
|
|
179
179
|
else {
|
|
180
180
|
await expect(promise).rejects.toThrow(
|
|
181
|
-
|
|
181
|
+
STExpect.simpleError({
|
|
182
182
|
code: 'rate_limit',
|
|
183
183
|
}),
|
|
184
184
|
);
|