@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.
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,55 @@
1
+ import { PayconiqAccount } from '@stamhoofd/structures';
2
+ import { TestUtils } from '@stamhoofd/test-utils';
3
+ import nock from 'nock';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ export class PayconiqMocker {
7
+ static infect() {
8
+ if (STAMHOOFD.environment !== 'test') {
9
+ throw new Error('PayconiqMocker can only be used in test environment');
10
+ }
11
+
12
+ TestUtils.addBeforeAll(async () => {
13
+ this.setup();
14
+ });
15
+
16
+ TestUtils.addAfterEach(() => {
17
+ this.reset();
18
+ });
19
+ }
20
+
21
+ static generateTestAccount() {
22
+ // Todo: store the api key somewhere so we can validate it in the API calls
23
+ return PayconiqAccount.create({
24
+ id: uuidv4(),
25
+ apiKey: 'testKey',
26
+ merchantId: 'test',
27
+ profileId: 'test',
28
+ name: 'test',
29
+ iban: 'BE56587127952688', // = random IBAN
30
+ callbackUrl: 'https://www.example.com',
31
+ });
32
+ }
33
+
34
+ static setup() {
35
+ nock('https://api.ext.payconiq.com')
36
+ .persist()
37
+ .post('/v3/payments')
38
+ .reply((uri, body) => {
39
+ // todo: do something smarter with the body
40
+
41
+ return [200, {
42
+ paymentId: uuidv4(),
43
+ _links: {
44
+ checkout: {
45
+ href: 'https://payconiq.com/pay/2/5bdb1685b93d1c000bde96f2?token=530ea8a4ec8ded7d87620c8637354022cd965b143f257f8f8cb118e7f4a22d8f&returnUrl=https%3A%2F%2Fummy.webshop%2Fcheckout%2Fsuccess',
46
+ },
47
+ },
48
+ }];
49
+ });
50
+ }
51
+
52
+ static reset() {
53
+ // todo: clean up any state if needed
54
+ }
55
+ }
@@ -0,0 +1,5 @@
1
+ export * from './initAdmin';
2
+ export * from './initPlatformAdmin';
3
+ export * from './initPayconiq';
4
+ export * from './initBundleDiscount';
5
+ export * from './initStripe';
@@ -0,0 +1,14 @@
1
+ import { Organization, Token, UserFactory } from '@stamhoofd/models';
2
+ import { PermissionLevel, Permissions } from '@stamhoofd/structures';
3
+
4
+ export async function initAdmin({ organization }: { organization: Organization }) {
5
+ const admin = await new UserFactory({
6
+ organization,
7
+ permissions: Permissions.create({
8
+ level: PermissionLevel.Full,
9
+ }),
10
+ }).create();
11
+
12
+ const adminToken = await Token.createToken(admin);
13
+ return { admin, adminToken };
14
+ }
@@ -0,0 +1,47 @@
1
+ import { Group, OrganizationRegistrationPeriod } from '@stamhoofd/models';
2
+ import { GroupPriceDiscountType, BundleDiscount, TranslatedString, GroupPriceDiscount, ReduceablePrice, GroupPrice, BundleDiscountGroupPriceSettings } from '@stamhoofd/structures';
3
+
4
+ function createBundleDiscount({
5
+ name = 'Bundle discount',
6
+ countWholeFamily = true,
7
+ countPerGroup = false,
8
+ discounts = [
9
+ { value: 10_00, type: GroupPriceDiscountType.Fixed },
10
+ { value: 15_00, type: GroupPriceDiscountType.Fixed },
11
+ ],
12
+ }: {
13
+ name?: string;
14
+ countWholeFamily?: boolean;
15
+ countPerGroup?: boolean;
16
+ discounts?: Array<{ value: number; type: GroupPriceDiscountType; reducedValue?: number }>;
17
+ } = {}) {
18
+ return BundleDiscount.create({
19
+ name: new TranslatedString(name),
20
+ countWholeFamily,
21
+ countPerGroup,
22
+ discounts: discounts.map(d => GroupPriceDiscount.create({
23
+ value: ReduceablePrice.create({
24
+ price: d.value,
25
+ reducedPrice: d.reducedValue ?? null,
26
+ }),
27
+ type: d.type,
28
+ })),
29
+ });
30
+ }
31
+
32
+ export async function enableDiscount({ group, groupPrice, bundleDiscount }: { group: Group; groupPrice: GroupPrice; bundleDiscount: BundleDiscount }) {
33
+ groupPrice.bundleDiscounts.set(bundleDiscount.id, BundleDiscountGroupPriceSettings.create({
34
+ name: bundleDiscount.name,
35
+ }));
36
+ await group.save();
37
+ }
38
+
39
+ /**
40
+ * Create a bundle discount
41
+ */
42
+ export async function initBundleDiscount({ organizationRegistrationPeriod, discount }: { organizationRegistrationPeriod: OrganizationRegistrationPeriod; discount: Parameters<typeof createBundleDiscount>[0] }) {
43
+ const bundleDiscount = createBundleDiscount(discount);
44
+ organizationRegistrationPeriod.settings.bundleDiscounts.push(bundleDiscount);
45
+ await organizationRegistrationPeriod.save();
46
+ return bundleDiscount;
47
+ }
@@ -0,0 +1,9 @@
1
+ import { PaymentMethod } from '@stamhoofd/structures';
2
+ import { PayconiqMocker } from '../helpers/PayconiqMocker';
3
+ import { Organization } from '@stamhoofd/models';
4
+
5
+ export async function initPayconiq({ organization }: { organization: Organization }) {
6
+ organization.meta.registrationPaymentConfiguration.paymentMethods.push(PaymentMethod.Payconiq);
7
+ organization.privateMeta.payconiqAccounts = [PayconiqMocker.generateTestAccount()];
8
+ await organization.save();
9
+ }
@@ -0,0 +1,13 @@
1
+ import { Token, UserFactory } from '@stamhoofd/models';
2
+ import { PermissionLevel, Permissions } from '@stamhoofd/structures';
3
+
4
+ export async function initPlatformAdmin() {
5
+ const admin = await new UserFactory({
6
+ globalPermissions: Permissions.create({
7
+ level: PermissionLevel.Full,
8
+ }),
9
+ }).create();
10
+
11
+ const adminToken = await Token.createToken(admin);
12
+ return { admin, adminToken };
13
+ }
@@ -0,0 +1,21 @@
1
+ import { Organization } from '@stamhoofd/models';
2
+ import { StripeMocker } from '../helpers/StripeMocker';
3
+ import { PaymentMethod } from '@stamhoofd/structures';
4
+ import { TestUtils } from '@stamhoofd/test-utils';
5
+
6
+ export async function initStripe({ organization }: { organization: Organization }) {
7
+ const stripeMocker = new StripeMocker();
8
+ const stripeAccount = await stripeMocker.createStripeAccount(organization.id);
9
+
10
+ stripeMocker.start();
11
+
12
+ TestUtils.scheduleAfterThisTest(() => {
13
+ stripeMocker.stop();
14
+ });
15
+
16
+ organization.meta.registrationPaymentConfiguration.paymentMethods.push(PaymentMethod.Bancontact, PaymentMethod.CreditCard, PaymentMethod.iDEAL);
17
+ organization.privateMeta.registrationPaymentConfiguration.stripeAccountId = stripeAccount.id;
18
+ await organization.save();
19
+
20
+ return { stripeMocker, stripeAccount };
21
+ }
@@ -11,6 +11,10 @@ import { GlobalHelper } from '../src/helpers/GlobalHelper';
11
11
  import * as jose from 'jose';
12
12
  import { TestUtils } from '@stamhoofd/test-utils';
13
13
  import './toMatchMap';
14
+ import { PayconiqMocker } from './helpers/PayconiqMocker';
15
+ import { BalanceItemService } from '../src/services/BalanceItemService';
16
+ import { QueueHandler } from '@stamhoofd/queues';
17
+ import { SimpleError } from '@simonbackx/simple-errors';
14
18
 
15
19
  // Set version of saved structures
16
20
  Column.setJSONVersion(Version);
@@ -75,6 +79,26 @@ beforeAll(async () => {
75
79
  });
76
80
 
77
81
  afterAll(async () => {
82
+ // Call twice to also wait on items that are scheduled withing scheduled tasks
83
+ await BalanceItemService.flushAll();
84
+ await BalanceItemService.flushAll();
85
+ QueueHandler.abortAll(
86
+ new SimpleError({
87
+ code: 'SHUTDOWN',
88
+ message: 'Shutting down',
89
+ statusCode: 503,
90
+ }),
91
+ );
92
+ await QueueHandler.awaitAll();
93
+ QueueHandler.abortAll(
94
+ new SimpleError({
95
+ code: 'SHUTDOWN',
96
+ message: 'Shutting down',
97
+ statusCode: 503,
98
+ }),
99
+ );
100
+ await QueueHandler.awaitAll();
101
+
78
102
  // Wait for email queue etc
79
103
  while (Email.currentQueue.length > 0) {
80
104
  console.info('Emails still in queue. Waiting...');
@@ -83,5 +107,10 @@ afterAll(async () => {
83
107
  await Database.end();
84
108
  });
85
109
 
110
+ afterEach(async () => {
111
+ await QueueHandler.awaitAll();
112
+ });
113
+
86
114
  TestUtils.setup();
87
115
  EmailMocker.infect();
116
+ PayconiqMocker.infect();
@@ -1,70 +0,0 @@
1
- import { Migration } from '@simonbackx/simple-database';
2
- import { logger } from '@simonbackx/simple-logging';
3
- import { BalanceItem, BalanceItemPayment, Organization, Payment } from '@stamhoofd/models';
4
- import { QueueHandler } from '@stamhoofd/queues';
5
- import { AuditLogSource, PaymentStatus } from '@stamhoofd/structures';
6
- import { AuditLogService } from '../services/AuditLogService';
7
- import { BalanceItemPaymentService } from '../services/BalanceItemPaymentService';
8
-
9
- export default new Migration(async () => {
10
- if (STAMHOOFD.environment == 'test') {
11
- console.log('skipped in tests');
12
- return;
13
- }
14
-
15
- process.stdout.write('\n');
16
- let c = 0;
17
-
18
- await logger.setContext({ tags: ['silent-seed', 'seed'] }, async () => {
19
- const q = Payment.select()
20
- .where('status', PaymentStatus.Succeeded)
21
- .where('createdAt', '>=', new Date('2024-12-12'))
22
- .limit(100);
23
- for await (const payment of q.all()) {
24
- await fix(payment);
25
-
26
- c += 1;
27
-
28
- if (c % 1000 === 0) {
29
- process.stdout.write('.');
30
- }
31
- }
32
- });
33
-
34
- console.log('Updated ' + c + ' payments');
35
-
36
- // Do something here
37
- return Promise.resolve();
38
- });
39
-
40
- async function fix(payment: Payment) {
41
- if (payment.status !== PaymentStatus.Succeeded) {
42
- return;
43
- }
44
-
45
- if (!payment.organizationId) {
46
- return;
47
- }
48
-
49
- const organization = await Organization.getByID(payment.organizationId);
50
-
51
- if (!organization) {
52
- return;
53
- }
54
-
55
- await AuditLogService.setContext({ fallbackUserId: payment.payingUserId, source: AuditLogSource.Payment, fallbackOrganizationId: payment.organizationId }, async () => {
56
- // Prevent concurrency issues
57
- await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
58
- const unloaded = (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment));
59
- const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
60
- unloaded,
61
- );
62
-
63
- for (const balanceItemPayment of balanceItemPayments) {
64
- await BalanceItemPaymentService.markPaidRepeated(balanceItemPayment, organization);
65
- }
66
-
67
- await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
68
- });
69
- });
70
- }