@stamhoofd/backend 2.106.0 → 2.107.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 (30) hide show
  1. package/index.ts +6 -1
  2. package/package.json +17 -11
  3. package/src/boot.ts +28 -22
  4. package/src/endpoints/frontend/FrontendEnvironmentEndpoint.ts +89 -0
  5. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +30 -0
  6. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +3 -2
  7. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -3
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +109 -109
  9. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +7 -0
  10. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +9 -7
  11. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +7 -0
  12. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +9 -2
  13. package/src/excel-loaders/payments.ts +5 -5
  14. package/src/excel-loaders/receivable-balances.ts +7 -7
  15. package/src/helpers/AdminPermissionChecker.ts +20 -0
  16. package/src/helpers/BuckarooHelper.ts +1 -1
  17. package/src/helpers/ServiceFeeHelper.ts +8 -4
  18. package/src/helpers/StripeHelper.ts +20 -35
  19. package/src/seeds/1752848561-groups-registration-periods.ts +35 -0
  20. package/src/services/BalanceItemService.ts +15 -2
  21. package/src/services/PaymentReallocationService.test.ts +298 -128
  22. package/src/services/PaymentReallocationService.ts +46 -16
  23. package/src/services/PaymentService.ts +49 -2
  24. package/src/services/uitpas/getSocialTariffForEvent.ts +2 -2
  25. package/src/services/uitpas/getSocialTariffForUitpasNumbers.ts +2 -2
  26. package/src/services/uitpas/registerTicketSales.ts +2 -2
  27. package/tests/e2e/bundle-discounts.test.ts +415 -391
  28. package/tests/e2e/documents.test.ts +21 -21
  29. package/tests/e2e/register.test.ts +93 -93
  30. package/tests/e2e/stock.test.ts +4 -4
package/index.ts CHANGED
@@ -8,7 +8,12 @@ backendEnv.load({ service: 'api' }).catch((error) => {
8
8
  const { run } = await import('./src/migrate');
9
9
  await run();
10
10
  }
11
- await import('./src/boot');
11
+ const { boot } = await import('./src/boot');
12
+
13
+ boot({ killProcess: true }).catch((error) => {
14
+ console.error('unhandledRejection', error);
15
+ process.exit(1);
16
+ });
12
17
  }).catch((error) => {
13
18
  console.error('Failed to start the API:', error);
14
19
  process.exit(1);
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.106.0",
3
+ "version": "2.107.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
7
7
  "require": "./dist/index.js"
8
+ },
9
+ "./src/migrate": {
10
+ "require": "./dist/src/migrate.js"
11
+ },
12
+ "./src/boot": {
13
+ "require": "./dist/src/boot.js"
8
14
  }
9
15
  },
10
16
  "license": "UNLICENCED",
@@ -41,18 +47,18 @@
41
47
  "@aws-sdk/s3-request-presigner": "3.823.0",
42
48
  "@bwip-js/node": "^4.5.1",
43
49
  "@mollie/api-client": "3.7.0",
44
- "@simonbackx/simple-database": "1.33.0",
50
+ "@simonbackx/simple-database": "1.34.0",
45
51
  "@simonbackx/simple-encoding": "2.22.0",
46
52
  "@simonbackx/simple-endpoints": "1.20.1",
47
53
  "@simonbackx/simple-logging": "^1.0.1",
48
- "@stamhoofd/backend-i18n": "2.106.0",
49
- "@stamhoofd/backend-middleware": "2.106.0",
50
- "@stamhoofd/email": "2.106.0",
51
- "@stamhoofd/models": "2.106.0",
52
- "@stamhoofd/queues": "2.106.0",
53
- "@stamhoofd/sql": "2.106.0",
54
- "@stamhoofd/structures": "2.106.0",
55
- "@stamhoofd/utility": "2.106.0",
54
+ "@stamhoofd/backend-i18n": "2.107.0",
55
+ "@stamhoofd/backend-middleware": "2.107.0",
56
+ "@stamhoofd/email": "2.107.0",
57
+ "@stamhoofd/models": "2.107.0",
58
+ "@stamhoofd/queues": "2.107.0",
59
+ "@stamhoofd/sql": "2.107.0",
60
+ "@stamhoofd/structures": "2.107.0",
61
+ "@stamhoofd/utility": "2.107.0",
56
62
  "archiver": "^7.0.1",
57
63
  "axios": "^1.8.2",
58
64
  "cookie": "^0.7.0",
@@ -70,5 +76,5 @@
70
76
  "publishConfig": {
71
77
  "access": "public"
72
78
  },
73
- "gitHead": "56fde0bd7b4d4ff0c3a4cd07924dcfe78a438215"
79
+ "gitHead": "54cbfe3b2fb090aa83c556be9eda97be712a9c71"
74
80
  }
package/src/boot.ts CHANGED
@@ -6,8 +6,10 @@ import { loadLogger } from '@stamhoofd/logging';
6
6
  import { Version } from '@stamhoofd/structures';
7
7
  import { sleep } from '@stamhoofd/utility';
8
8
 
9
+ import { SimpleError } from '@simonbackx/simple-errors';
9
10
  import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
10
11
  import { Platform } from '@stamhoofd/models';
12
+ import { QueueHandler } from '@stamhoofd/queues';
11
13
  import { resumeEmails } from './helpers/EmailResumer';
12
14
  import { GlobalHelper } from './helpers/GlobalHelper';
13
15
  import { SetupStepUpdater } from './helpers/SetupStepUpdater';
@@ -17,10 +19,8 @@ import { BalanceItemService } from './services/BalanceItemService';
17
19
  import { DocumentService } from './services/DocumentService';
18
20
  import { FileSignService } from './services/FileSignService';
19
21
  import { PlatformMembershipService } from './services/PlatformMembershipService';
20
- import { UniqueUserService } from './services/UniqueUserService';
21
- import { QueueHandler } from '@stamhoofd/queues';
22
- import { SimpleError } from '@simonbackx/simple-errors';
23
22
  import { UitpasService } from './services/uitpas/UitpasService';
23
+ import { UniqueUserService } from './services/UniqueUserService';
24
24
 
25
25
  process.on('unhandledRejection', (error: Error) => {
26
26
  console.error('unhandledRejection');
@@ -59,8 +59,11 @@ function productionLog(message: string) {
59
59
  }
60
60
  }
61
61
 
62
- const start = async () => {
62
+ export const boot = async (options: { killProcess: boolean }) => {
63
63
  productionLog('Running server at v' + Version);
64
+ productionLog('Running server at port ' + STAMHOOFD.PORT);
65
+ productionLog('Running server on DB ' + process.env.DB_DATABASE); // note, should always use process env here
66
+
64
67
  loadLogger();
65
68
 
66
69
  await GlobalHelper.load();
@@ -77,6 +80,8 @@ const start = async () => {
77
80
  // Note: we should load endpoints one by once to have a reliable order of url matching
78
81
  await router.loadAllEndpoints(__dirname + '/endpoints/global/*');
79
82
  await router.loadAllEndpoints(__dirname + '/endpoints/admin/*');
83
+ await router.loadAllEndpoints(__dirname + '/endpoints/frontend');
84
+
80
85
  await router.loadAllEndpoints(__dirname + '/endpoints/auth');
81
86
  await router.loadAllEndpoints(__dirname + '/endpoints/organization/dashboard/*');
82
87
  await router.loadAllEndpoints(__dirname + '/endpoints/organization/registration');
@@ -204,24 +209,28 @@ const start = async () => {
204
209
  }
205
210
 
206
211
  // Should not be needed, but added for security as sometimes a promise hangs somewhere
207
- process.exit(0);
212
+ if (options.killProcess) {
213
+ process.exit(0);
214
+ }
208
215
  };
209
216
 
210
- process.on('SIGTERM', () => {
211
- productionLog('SIGTERM signal received.');
212
- shutdown().catch((e) => {
213
- console.error(e);
214
- process.exit(1);
217
+ if (options.killProcess) {
218
+ process.on('SIGTERM', () => {
219
+ productionLog('SIGTERM signal received.');
220
+ shutdown().catch((e) => {
221
+ console.error(e);
222
+ process.exit(1);
223
+ });
215
224
  });
216
- });
217
225
 
218
- process.on('SIGINT', () => {
219
- productionLog('SIGINT signal received.');
220
- shutdown().catch((e) => {
221
- console.error(e);
222
- process.exit(1);
226
+ process.on('SIGINT', () => {
227
+ productionLog('SIGINT signal received.');
228
+ shutdown().catch((e) => {
229
+ console.error(e);
230
+ process.exit(1);
231
+ });
223
232
  });
224
- });
233
+ }
225
234
 
226
235
  // Register crons
227
236
  await import('./crons');
@@ -234,9 +243,6 @@ const start = async () => {
234
243
 
235
244
  startCrons();
236
245
  seeds().catch(console.error);
237
- };
238
246
 
239
- start().catch((error) => {
240
- console.error('unhandledRejection', error);
241
- process.exit(1);
242
- });
247
+ return { shutdown };
248
+ };
@@ -0,0 +1,89 @@
1
+ import {
2
+ DecodedRequest,
3
+ Endpoint,
4
+ Request,
5
+ Response,
6
+ } from '@simonbackx/simple-endpoints';
7
+ import { SimpleError } from '@simonbackx/simple-errors';
8
+ import { Version } from '@stamhoofd/structures';
9
+
10
+ type Params = Record<string, never>;
11
+ type Query = undefined;
12
+ type Body = undefined;
13
+ type ResponseBody = string;
14
+
15
+ export class FrontendEnvironmentEndpoint extends Endpoint<
16
+ Params,
17
+ Query,
18
+ Body,
19
+ ResponseBody
20
+ > {
21
+ protected doesMatch(request: Request): [true, Params] | [false] {
22
+ if (request.method !== 'GET') {
23
+ return [false];
24
+ }
25
+
26
+ if (STAMHOOFD.environment !== 'test') {
27
+ // Disallow exposing environments outside of tests (even in development!)
28
+ return [false];
29
+ }
30
+
31
+ const params = Endpoint.parseParameters(
32
+ request.url,
33
+ '/frontend-environment',
34
+ {},
35
+ );
36
+
37
+ if (params) {
38
+ return [true, params as Params];
39
+ }
40
+ return [false];
41
+ }
42
+
43
+ async handle(_request: DecodedRequest<Params, Query, Body>) {
44
+ const env: SharedEnvironment = {
45
+ environment: STAMHOOFD.environment,
46
+ domains: STAMHOOFD.domains,
47
+ locales: STAMHOOFD.locales,
48
+ userMode: STAMHOOFD.userMode,
49
+ translationNamespace: STAMHOOFD.translationNamespace,
50
+ platformName: STAMHOOFD.platformName,
51
+ fixedCountry: STAMHOOFD.fixedCountry,
52
+ singleOrganization: STAMHOOFD.singleOrganization,
53
+ };
54
+
55
+ const frontendEnv: FrontendSpecificEnvironment = {
56
+ VERSION: '0.0.0',
57
+ MOLLIE_CLIENT_ID: '',
58
+ };
59
+
60
+ // Load keys that could be defined in STAMHOOFD, but where typescript will complain about because the server
61
+ // normally does not have access to this. Playwright is the exception where it is possible.
62
+ const keys: (keyof FrontendSpecificEnvironment)[] = [
63
+ 'PORT',
64
+ 'VERSION',
65
+ 'NOLT_URL',
66
+ 'FEEDBACK_URL',
67
+ 'MOLLIE_CLIENT_ID',
68
+ 'APP_UPDATE_SERVER_URL',
69
+ 'APP_UPDATE_PRODUCTION_SERVER_URL',
70
+ 'APP_UPDATE_STAGING_SERVER_URL',
71
+ 'APP_UPDATE_DEVELOPMENT_SERVER_URL',
72
+ 'CHANGELOG_URL',
73
+ 'ILLUSTRATIONS_NAMESPACE',
74
+ 'ILLUSTRATIONS_COLORS',
75
+ 'PLAUSIBLE_DOMAIN',
76
+ 'REDIRECT_LOGIN_DOMAIN',
77
+ 'memberDocumentationPage',
78
+ ];
79
+ for (const key of keys) {
80
+ (frontendEnv as any)[key] = (STAMHOOFD as any)[key];
81
+ }
82
+
83
+ const code = `window.STAMHOOFD = ${JSON.stringify({ ...env, ...frontendEnv })};`;
84
+ const response = new Response(code);
85
+ response.status = 200;
86
+ response.headers['Content-Type'] = 'application/javascript';
87
+ return response;
88
+ }
89
+ }
@@ -0,0 +1,30 @@
1
+ /* import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request } from '@simonbackx/simple-endpoints';
3
+ import { IDRegisterCheckout, RegisterResponse } from '@stamhoofd/structures';
4
+
5
+ type Params = Record<string, never>;
6
+ type Query = undefined;
7
+ type Body = IDRegisterCheckout;
8
+ type ResponseBody = RegisterResponse;
9
+
10
+ export class ActivatePackagesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
11
+ bodyDecoder = Body as Decoder<Body>;
12
+
13
+ protected doesMatch(request: Request): [true, Params] | [false] {
14
+ if (request.method != 'POST') {
15
+ return [false];
16
+ }
17
+
18
+ const params = Endpoint.parseParameters(request.url, '/billing/activate-packages', {});
19
+
20
+ if (params) {
21
+ return [true, params as Params];
22
+ }
23
+ return [false];
24
+ }
25
+
26
+ async handle(request: DecodedRequest<Params, Query, Body>) {
27
+
28
+ }
29
+ }
30
+ */
@@ -89,11 +89,12 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
89
89
  if (organization && STAMHOOFD.userMode !== 'platform') {
90
90
  member.organizationId = organization.id;
91
91
  }
92
-
92
+ const securityCode = struct.details.securityCode; // will get cleared after the filter
93
+ Context.auth.filterMemberPut(member, struct, {asUserManager: false});
93
94
  struct.details.cleanData();
94
95
  member.details = struct.details;
95
96
 
96
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
97
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode, 'put');
97
98
  if (duplicate) {
98
99
  // Merge data
99
100
  member = duplicate;
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, Decoder, isEmptyPatch, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
- import { Document, Member, RateLimiter } from '@stamhoofd/models';
4
+ import { Document, Group, Member, RateLimiter, Registration } from '@stamhoofd/models';
5
5
  import { MemberDetails, MembersBlob, MemberWithRegistrationsBlob } from '@stamhoofd/structures';
6
6
 
7
7
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -9,6 +9,7 @@ import { Context } from '../../../helpers/Context';
9
9
  import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
10
10
  import { PatchOrganizationMembersEndpoint } from '../../global/members/PatchOrganizationMembersEndpoint';
11
11
  import { shouldCheckIfMemberIsDuplicateForPatch } from '../members/shouldCheckIfMemberIsDuplicate';
12
+ import { OneToManyRelation } from '@simonbackx/simple-database';
12
13
  type Params = Record<string, never>;
13
14
  type Query = undefined;
14
15
  type Body = PatchableArrayAutoEncoder<MemberWithRegistrationsBlob>;
@@ -52,16 +53,20 @@ export class PatchUserMembersEndpoint extends Endpoint<Params, Query, Body, Resp
52
53
  for (const put of request.body.getPuts()) {
53
54
  const struct = put.put;
54
55
 
55
- const member = new Member();
56
+ const member = new Member()
57
+ .setManyRelation(Member.registrations as any as OneToManyRelation<'registrations', Member, Registration & { group: Group }>, [])
58
+ .setManyRelation(Member.users, []);
56
59
  member.id = struct.id;
57
60
  member.organizationId = organization?.id ?? null;
58
61
 
62
+ const securityCode = struct.details.securityCode; // will get cleared after the filter
63
+ Context.auth.filterMemberPut(member, struct, {asUserManager: true});
59
64
  struct.details.cleanData();
60
65
  member.details = struct.details;
61
66
 
62
67
  this.throwIfInvalidDetails(member.details);
63
68
 
64
- const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, struct.details.securityCode, 'put');
69
+ const duplicate = await PatchOrganizationMembersEndpoint.checkDuplicate(member, securityCode, 'put');
65
70
  if (duplicate) {
66
71
  addedMembers.push(duplicate);
67
72
  continue;