@stamhoofd/backend 2.83.4 → 2.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/index.ts +19 -4
  2. package/package.json +18 -14
  3. package/src/crons/amazon-ses.ts +26 -5
  4. package/src/crons/balance-emails.ts +18 -17
  5. package/src/email-recipient-loaders/registrations.ts +87 -0
  6. package/src/endpoints/global/addresses/SearchRegionsEndpoint.ts +5 -2
  7. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +40 -40
  8. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +28 -22
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +81 -49
  10. package/src/endpoints/global/files/UploadFile.ts +11 -16
  11. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +234 -0
  12. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +117 -43
  13. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +1054 -0
  14. package/src/endpoints/global/members/GetMembersEndpoint.ts +163 -141
  15. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +6 -6
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +0 -16
  17. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +73 -0
  18. package/src/endpoints/global/registration/GetPaymentRegistrations.ts +1 -2
  19. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +43 -0
  20. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +1016 -0
  21. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +234 -0
  22. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +5 -5
  23. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +474 -554
  24. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +191 -52
  25. package/src/endpoints/global/registration-periods/GetRegistrationPeriodsEndpoint.ts +107 -9
  26. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +89 -0
  27. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +9 -6
  28. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +88 -0
  29. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +0 -6
  30. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -6
  31. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +10 -25
  32. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +0 -5
  33. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +0 -5
  34. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +4 -0
  35. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +1 -0
  36. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +44 -19
  37. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.ts +140 -25
  38. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +40 -10
  39. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +2 -2
  40. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +2 -2
  41. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +4 -1
  42. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +2 -2
  43. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +2 -2
  44. package/src/excel-loaders/members.ts +233 -232
  45. package/src/excel-loaders/payments.ts +1 -1
  46. package/src/excel-loaders/receivable-balances.ts +1 -1
  47. package/src/excel-loaders/registrations.ts +153 -0
  48. package/src/helpers/AdminPermissionChecker.ts +65 -37
  49. package/src/helpers/AuthenticatedStructures.ts +43 -3
  50. package/src/helpers/Context.ts +29 -1
  51. package/src/helpers/GlobalHelper.ts +3 -1
  52. package/src/helpers/GroupedThrottledQueue.test.ts +219 -0
  53. package/src/helpers/GroupedThrottledQueue.ts +108 -0
  54. package/src/helpers/LimitedFilteredRequestHelper.ts +26 -1
  55. package/src/helpers/MemberCharger.ts +0 -5
  56. package/src/helpers/MembershipCharger.ts +3 -9
  57. package/src/helpers/OrganizationCharger.ts +0 -5
  58. package/src/helpers/ThrottledQueue.test.ts +194 -0
  59. package/src/helpers/ThrottledQueue.ts +145 -0
  60. package/src/helpers/XlsxTransformerColumnHelper.ts +44 -1
  61. package/src/middleware/ContextMiddleware.ts +1 -1
  62. package/src/seeds/1728928974-update-cached-outstanding-balance-from-items.ts +2 -1
  63. package/src/seeds/1735577912-update-cached-outstanding-balance-from-items.ts +2 -1
  64. package/src/services/BalanceItemPaymentService.ts +1 -33
  65. package/src/services/BalanceItemService.ts +167 -48
  66. package/src/services/FileSignService.ts +18 -13
  67. package/src/services/MemberRecordStore.ts +28 -19
  68. package/src/services/PaymentReallocationService.test.ts +25 -14
  69. package/src/services/PaymentReallocationService.ts +29 -10
  70. package/src/services/PaymentService.ts +4 -16
  71. package/src/services/PlatformMembershipService.ts +8 -4
  72. package/src/services/RegistrationService.ts +66 -2
  73. package/src/sql-filters/base-registration-filter-compilers.ts +43 -0
  74. package/src/sql-filters/groups.ts +67 -0
  75. package/src/sql-filters/members.ts +33 -58
  76. package/src/sql-filters/organization-registration-periods.ts +8 -0
  77. package/src/sql-filters/registration-periods.ts +8 -0
  78. package/src/sql-filters/registrations.ts +11 -22
  79. package/src/sql-sorters/groups.ts +24 -0
  80. package/src/sql-sorters/organization-registration-periods.ts +24 -0
  81. package/src/sql-sorters/registration-periods.ts +47 -0
  82. package/src/sql-sorters/registrations.ts +77 -0
  83. package/tests/actions/patchOrganizationMember.ts +27 -0
  84. package/tests/actions/patchPaymentStatus.ts +45 -0
  85. package/tests/actions/patchUserMember.ts +27 -0
  86. package/tests/assertions/assertBalances.ts +49 -0
  87. package/tests/e2e/api-rate-limits.test.ts +5 -5
  88. package/tests/e2e/bundle-discounts.test.ts +4060 -0
  89. package/tests/e2e/charge-members.test.ts +27 -24
  90. package/tests/e2e/documents.test.ts +398 -0
  91. package/tests/e2e/register.test.ts +292 -312
  92. package/tests/helpers/PayconiqMocker.ts +55 -0
  93. package/tests/init/index.ts +5 -0
  94. package/tests/init/initAdmin.ts +14 -0
  95. package/tests/init/initBundleDiscount.ts +47 -0
  96. package/tests/init/initPayconiq.ts +9 -0
  97. package/tests/init/initPlatformAdmin.ts +13 -0
  98. package/tests/init/initStripe.ts +21 -0
  99. package/tests/jest.setup.ts +29 -0
  100. 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 { SHExpect, TestUtils } from '@stamhoofd/test-utils';
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
- SHExpect.simpleError({
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
- SHExpect.simpleError({
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
- SHExpect.simpleError({
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
- SHExpect.simpleError({
181
+ STExpect.simpleError({
182
182
  code: 'rate_limit',
183
183
  }),
184
184
  );