@stamhoofd/backend 2.107.3 → 2.109.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 (44) hide show
  1. package/index.ts +2 -2
  2. package/package.json +20 -20
  3. package/src/crons/disable-auto-update-documents.test.ts +164 -0
  4. package/src/crons/disable-auto-update-documents.ts +82 -0
  5. package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +5 -5
  6. package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +87 -0
  7. package/src/endpoints/global/members/GetMembersCountEndpoint.ts +2 -2
  8. package/src/endpoints/global/members/GetMembersEndpoint.ts +5 -5
  9. package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +2 -2
  10. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +14 -11
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +9 -8
  12. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesCountEndpoint.ts +48 -0
  13. package/src/endpoints/organization/dashboard/documents/GetDocumentTemplatesEndpoint.ts +95 -19
  14. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +282 -0
  15. package/src/endpoints/organization/dashboard/documents/{PatchDocumentTemplateEndpoint.ts → PatchDocumentTemplatesEndpoint.ts} +56 -3
  16. package/src/excel-loaders/members.ts +62 -8
  17. package/src/excel-loaders/registrations.ts +180 -9
  18. package/src/helpers/LimitedFilteredRequestHelper.ts +24 -0
  19. package/src/helpers/MemberCharger.ts +16 -4
  20. package/src/helpers/SQLTranslatedString.ts +14 -0
  21. package/src/helpers/TagHelper.test.ts +9 -9
  22. package/src/helpers/fetchToAsyncIterator.ts +1 -1
  23. package/src/helpers/outstandingBalanceJoin.ts +49 -0
  24. package/src/seeds/1765896674-document-update-year.test.ts +179 -0
  25. package/src/seeds/1765896674-document-update-year.ts +75 -0
  26. package/src/seeds/1766150402-document-published-at.test.ts +46 -0
  27. package/src/seeds/1766150402-document-published-at.ts +20 -0
  28. package/src/services/PaymentService.ts +14 -32
  29. package/src/sql-filters/base-registration-filter-compilers.ts +51 -4
  30. package/src/sql-filters/document-templates.ts +45 -0
  31. package/src/sql-filters/documents.ts +1 -1
  32. package/src/sql-filters/events.ts +6 -6
  33. package/src/sql-filters/groups.ts +7 -6
  34. package/src/sql-filters/members.ts +31 -26
  35. package/src/sql-filters/orders.ts +16 -16
  36. package/src/sql-filters/organizations.ts +11 -11
  37. package/src/sql-filters/payments.ts +10 -10
  38. package/src/sql-filters/registrations.ts +14 -6
  39. package/src/sql-sorters/document-templates.ts +79 -0
  40. package/src/sql-sorters/documents.ts +1 -1
  41. package/src/sql-sorters/members.ts +22 -0
  42. package/src/sql-sorters/orders.ts +5 -5
  43. package/src/sql-sorters/organizations.ts +3 -3
  44. package/src/sql-sorters/registrations.ts +186 -15
package/index.ts CHANGED
@@ -5,10 +5,10 @@ backendEnv.load({ service: 'api' }).catch((error) => {
5
5
  process.exit(1);
6
6
  }).then(async () => {
7
7
  if (STAMHOOFD.environment === 'development') {
8
- const { run } = await import('./src/migrate');
8
+ const { run } = await import('./src/migrate.js');
9
9
  await run();
10
10
  }
11
- const { boot } = await import('./src/boot');
11
+ const { boot } = await import('./src/boot.js');
12
12
 
13
13
  boot({ killProcess: true }).catch((error) => {
14
14
  console.error('unhandledRejection', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.107.3",
3
+ "version": "2.109.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -32,7 +32,7 @@
32
32
  "devDependencies": {
33
33
  "@types/cookie": "^0.6.0",
34
34
  "@types/luxon": "3.4.2",
35
- "@types/mailparser": "3.4.4",
35
+ "@types/mailparser": "^3",
36
36
  "@types/mysql": "^2.15.20",
37
37
  "@types/node": "^22",
38
38
  "nock": "^13.5.1",
@@ -40,33 +40,33 @@
40
40
  "sinon": "^18.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@aws-sdk/client-s3": "3.823.0",
44
- "@aws-sdk/client-ses": "3.823.0",
45
- "@aws-sdk/client-sesv2": "3.823.0",
46
- "@aws-sdk/client-sqs": "3.823.0",
47
- "@aws-sdk/s3-request-presigner": "3.823.0",
43
+ "@aws-sdk/client-s3": "^3.839.0",
44
+ "@aws-sdk/client-ses": "^3.839.0",
45
+ "@aws-sdk/client-sesv2": "^3.839.0",
46
+ "@aws-sdk/client-sqs": "^3.839.0",
47
+ "@aws-sdk/s3-request-presigner": "^3.839.0",
48
48
  "@bwip-js/node": "^4.5.1",
49
- "@mollie/api-client": "3.7.0",
49
+ "@mollie/api-client": "4.3.3",
50
50
  "@simonbackx/simple-database": "1.34.0",
51
51
  "@simonbackx/simple-encoding": "2.22.0",
52
52
  "@simonbackx/simple-endpoints": "1.20.1",
53
53
  "@simonbackx/simple-logging": "^1.0.1",
54
- "@stamhoofd/backend-i18n": "2.107.3",
55
- "@stamhoofd/backend-middleware": "2.107.3",
56
- "@stamhoofd/email": "2.107.3",
57
- "@stamhoofd/models": "2.107.3",
58
- "@stamhoofd/queues": "2.107.3",
59
- "@stamhoofd/sql": "2.107.3",
60
- "@stamhoofd/structures": "2.107.3",
61
- "@stamhoofd/utility": "2.107.3",
54
+ "@stamhoofd/backend-i18n": "2.109.0",
55
+ "@stamhoofd/backend-middleware": "2.109.0",
56
+ "@stamhoofd/email": "2.109.0",
57
+ "@stamhoofd/models": "2.109.0",
58
+ "@stamhoofd/queues": "2.109.0",
59
+ "@stamhoofd/sql": "2.109.0",
60
+ "@stamhoofd/structures": "2.109.0",
61
+ "@stamhoofd/utility": "2.109.0",
62
62
  "archiver": "^7.0.1",
63
- "axios": "^1.8.2",
63
+ "axios": "^1.13.2",
64
64
  "cookie": "^0.7.0",
65
65
  "formidable": "3.5.4",
66
66
  "handlebars": "^4.7.7",
67
- "jsonwebtoken": "9.0.0",
67
+ "jsonwebtoken": "9.0.3",
68
68
  "luxon": "3.4.4",
69
- "mailparser": "3.7.0",
69
+ "mailparser": "^3.9.1",
70
70
  "mockdate": "^3.0.2",
71
71
  "mysql2": "^3.14.1",
72
72
  "node-rsa": "1.1.1",
@@ -76,5 +76,5 @@
76
76
  "publishConfig": {
77
77
  "access": "public"
78
78
  },
79
- "gitHead": "2827f3e8dbc9046fe6f71b42c608cda407da3a2d"
79
+ "gitHead": "9a987df4a751f24aabd56e1480dfb9239fb8e019"
80
80
  }
@@ -0,0 +1,164 @@
1
+ import { DocumentTemplate, DocumentTemplateFactory } from '@stamhoofd/models';
2
+ import { DocumentStatus } from '@stamhoofd/structures';
3
+ import { disableAutoUpdateForFiscalDocuments, disableAutoUpdateForOtherDocuments } from './disable-auto-update-documents.js';
4
+
5
+ describe('cron.disable-auto-update-documents', () => {
6
+ describe('disableAutoUpdateForFiscalDocuments', () => {
7
+ it('should only run on the 1st of March', async () => {
8
+ // arrange
9
+ const firstOfMarch2025 = new Date(2025, 2, 1);
10
+
11
+ const cases: { now: Date; expected: boolean }[] = [
12
+ { now: new Date(firstOfMarch2025.getTime() - 1), expected: false },
13
+ { now: new Date(firstOfMarch2025), expected: true },
14
+ { now: new Date(2025, 2, 2), expected: false },
15
+ { now: new Date(new Date(2026, 2, 1)), expected: true },
16
+ ];
17
+
18
+ for (const { now, expected } of cases) {
19
+ // act
20
+ const result = await disableAutoUpdateForFiscalDocuments(now);
21
+ // assert
22
+ expect(result).toBe(expected);
23
+ }
24
+ });
25
+
26
+ it('should disable auto-update for fiscal documents of the previous year', async () => {
27
+ // // arrange
28
+ const firstOfMarch2025 = new Date(2025, 2, 1);
29
+
30
+ const document1 = await createDocument({
31
+ status: DocumentStatus.Published,
32
+ publishedAt: new Date(2025, 0, 1),
33
+ createdAt: new Date(2024, 11, 15),
34
+ year: 2024,
35
+ type: 'fiscal',
36
+ updatesEnabled: true,
37
+ });
38
+
39
+ // old year
40
+ const document2 = await createDocument({
41
+ status: DocumentStatus.Published,
42
+ publishedAt: new Date(2025, 0, 1),
43
+ createdAt: new Date(2024, 11, 15),
44
+ year: 2023,
45
+ type: 'fiscal',
46
+ updatesEnabled: true,
47
+ });
48
+
49
+ // current year
50
+ const document3 = await createDocument({
51
+ status: DocumentStatus.Published,
52
+ publishedAt: new Date(2025, 0, 1),
53
+ createdAt: new Date(2024, 11, 15),
54
+ year: 2025,
55
+ type: 'fiscal',
56
+ updatesEnabled: true,
57
+ });
58
+
59
+ // type participation
60
+ const document4 = await createDocument({
61
+ status: DocumentStatus.Published,
62
+ publishedAt: new Date(2025, 0, 1),
63
+ createdAt: new Date(2024, 11, 15),
64
+ year: 2024,
65
+ type: 'participation',
66
+ updatesEnabled: true,
67
+ });
68
+
69
+ // act
70
+ const didRun = await disableAutoUpdateForFiscalDocuments(firstOfMarch2025);
71
+
72
+ // assert
73
+ const updatedDocument1 = (await DocumentTemplate.getByID(document1.id))!;
74
+ const updatedDocument2 = (await DocumentTemplate.getByID(document2.id))!;
75
+ const updatedDocument3 = (await DocumentTemplate.getByID(document3.id))!;
76
+ const updatedDocument4 = (await DocumentTemplate.getByID(document4.id))!;
77
+
78
+ expect(didRun).toBe(true);
79
+
80
+ // should update because previous year, type is fiscal and updates enabled
81
+ expect(updatedDocument1.updatesEnabled).toBe(false);
82
+ // should not update because not previous year
83
+ expect(updatedDocument2.updatesEnabled).toBe(true);
84
+ // should not update because not previous year
85
+ expect(updatedDocument3.updatesEnabled).toBe(true);
86
+ // should not update because type is not fiscal
87
+ expect(updatedDocument4.updatesEnabled).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('disableAutoUpdateForOtherDocuments', () => {
92
+ it('should disable auto-update for documents, other than fiscal documents, that have been published 90 days ago', async () => {
93
+ // // arrange
94
+ const today = new Date(2025, 5, 15);
95
+ const ninetyDaysAgo = new Date(2025, 2, 17);
96
+
97
+ const cases: { document: DocumentTemplate; expected: boolean }[] = [
98
+ // should update because 90 days ago and type is not fiscal
99
+ {
100
+ document: await createDocument({
101
+ status: DocumentStatus.Published,
102
+ publishedAt: new Date(ninetyDaysAgo),
103
+ createdAt: new Date(2024, 11, 15),
104
+ year: 2024,
105
+ type: 'participation',
106
+ updatesEnabled: true,
107
+ }),
108
+ expected: false,
109
+ },
110
+ // day after
111
+ {
112
+ document: await createDocument({
113
+ status: DocumentStatus.Published,
114
+ publishedAt: new Date(2025, 2, 18),
115
+ createdAt: new Date(2024, 11, 15),
116
+ year: 2024,
117
+ type: 'participation',
118
+ updatesEnabled: true,
119
+ }),
120
+ expected: true,
121
+ },
122
+ // day before
123
+ {
124
+ document: await createDocument({
125
+ status: DocumentStatus.Published,
126
+ publishedAt: new Date(ninetyDaysAgo.getTime() - 1),
127
+ createdAt: new Date(2024, 11, 15),
128
+ year: 2024,
129
+ type: 'participation',
130
+ updatesEnabled: true,
131
+ }),
132
+ expected: true,
133
+ },
134
+ // type fiscal
135
+ {
136
+ document: await createDocument({
137
+ status: DocumentStatus.Published,
138
+ publishedAt: new Date(ninetyDaysAgo),
139
+ createdAt: new Date(2024, 11, 15),
140
+ year: 2024,
141
+ type: 'fiscal',
142
+ updatesEnabled: true,
143
+ }),
144
+ expected: true,
145
+ },
146
+ ];
147
+
148
+ // act
149
+ await disableAutoUpdateForOtherDocuments(today);
150
+
151
+ // assert
152
+ for (const { document, expected } of cases) {
153
+ const updatedDocument = (await DocumentTemplate.getByID(document.id))!;
154
+ expect(updatedDocument.updatesEnabled).toBe(expected);
155
+ }
156
+ });
157
+ });
158
+ });
159
+
160
+ async function createDocument({ year, status, createdAt, updatesEnabled, type, publishedAt }: { year: number; status: DocumentStatus; createdAt: Date; updatesEnabled: boolean; type: 'fiscal' | 'participation'; publishedAt: Date | null }) {
161
+ const document = await new DocumentTemplateFactory({ year, groups: [], status, updatesEnabled, publishedAt, createdAt, type }).create();
162
+
163
+ return document;
164
+ };
@@ -0,0 +1,82 @@
1
+ import { registerCron } from '@stamhoofd/crons';
2
+ import { DocumentTemplate } from '@stamhoofd/models';
3
+ import { SQL } from '@stamhoofd/sql';
4
+
5
+ let lastRunDate: number | null = null;
6
+
7
+ registerCron('disableAutoUpdateDocuments', disableAutoUpdateDocuments);
8
+
9
+ function shouldRun() {
10
+ const now = new Date();
11
+
12
+ if (now.getDate() === lastRunDate) {
13
+ return false;
14
+ }
15
+
16
+ const hour = now.getHours();
17
+
18
+ // between 5 and 6 AM
19
+ if (hour !== 5 && STAMHOOFD.environment !== 'development') {
20
+ return false;
21
+ }
22
+
23
+ return true;
24
+ }
25
+
26
+ async function disableAutoUpdateDocuments() {
27
+ if (!shouldRun()) {
28
+ return;
29
+ }
30
+
31
+ const now = new Date();
32
+ lastRunDate = now.getDate();
33
+
34
+ await disableAutoUpdateForFiscalDocuments(now);
35
+ await disableAutoUpdateForOtherDocuments(now);
36
+ }
37
+
38
+ /**
39
+ * Disable auto-update for fiscal documents on the 1st of March.
40
+ * @returns if query was run
41
+ */
42
+ export async function disableAutoUpdateForFiscalDocuments(now: Date): Promise<boolean> {
43
+ // only run on 1st of march
44
+ const isFirstOfMarch = now.getMonth() === 2 && now.getDate() === 1;
45
+ if (!isFirstOfMarch) {
46
+ return false;
47
+ }
48
+
49
+ // set updates enabled to false
50
+ await SQL.update(DocumentTemplate.table).set('updatesEnabled', false)
51
+ // where previous year
52
+ .where('year', now.getFullYear() - 1)
53
+ // where updates enabled
54
+ .andWhere('updatesEnabled', true)
55
+ // where type is fiscal
56
+ .andWhere(SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'), 'fiscal')
57
+ .update();
58
+
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * Disable auto-update for documents, other then fiscal, that have been published 90 days ago.
64
+ */
65
+ export async function disableAutoUpdateForOtherDocuments(now: Date) {
66
+ const min = new Date(now);
67
+ min.setDate(min.getDate() - 90);
68
+
69
+ const max = new Date(now);
70
+ max.setDate(max.getDate() - 89);
71
+
72
+ // set updates enabled to false
73
+ await SQL.update(DocumentTemplate.table).set('updatesEnabled', false)
74
+ // where updates enabled
75
+ .where('updatesEnabled', true)
76
+ // where published 90 days ago
77
+ .andWhere('publishedAt', '>=', min)
78
+ .andWhere('publishedAt', '<', max)
79
+ // where type is not fiscal
80
+ .whereNot(SQL.jsonExtract(SQL.column('privateSettings'), '$.value.templateDefinition.type'), 'fiscal')
81
+ .update();
82
+ }
@@ -4,10 +4,10 @@ import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { ChargeMembersRequest, LimitedFilteredRequest, PermissionLevel } from '@stamhoofd/structures';
5
5
 
6
6
  import { QueueHandler } from '@stamhoofd/queues';
7
- import { Context } from '../../../helpers/Context';
8
- import { fetchToAsyncIterator } from '../../../helpers/fetchToAsyncIterator';
9
- import { MemberCharger } from '../../../helpers/MemberCharger';
10
- import { GetMembersEndpoint } from '../../global/members/GetMembersEndpoint';
7
+ import { Context } from '../../../helpers/Context.js';
8
+ import { fetchToAsyncIterator } from '../../../helpers/fetchToAsyncIterator.js';
9
+ import { MemberCharger } from '../../../helpers/MemberCharger.js';
10
+ import { GetMembersEndpoint } from '../../global/members/GetMembersEndpoint.js';
11
11
 
12
12
  type Params = Record<string, never>;
13
13
  type Query = LimitedFilteredRequest;
@@ -31,7 +31,7 @@ export class ChargeMembersEndpoint extends Endpoint<Params, Query, Body, Respons
31
31
  return [false];
32
32
  }
33
33
 
34
- private static throwIfInvalidBody(body: Body) {
34
+ static throwIfInvalidBody(body: Body) {
35
35
  if (!body.description?.trim()?.length) {
36
36
  throw new SimpleError({
37
37
  code: 'invalid_field',
@@ -0,0 +1,87 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { SimpleError } from '@simonbackx/simple-errors';
4
+ import { ChargeMembersRequest, LimitedFilteredRequest, PermissionLevel } from '@stamhoofd/structures';
5
+
6
+ import { QueueHandler } from '@stamhoofd/queues';
7
+ import { Context } from '../../../helpers/Context.js';
8
+ import { fetchToAsyncIterator } from '../../../helpers/fetchToAsyncIterator.js';
9
+ import { MemberCharger } from '../../../helpers/MemberCharger.js';
10
+ import { GetRegistrationsEndpoint } from '../../global/registration/GetRegistrationsEndpoint.js';
11
+ import { ChargeMembersEndpoint } from '../members/ChargeMembersEndpoint.js';
12
+
13
+ type Params = Record<string, never>;
14
+ type Query = LimitedFilteredRequest;
15
+ type Body = ChargeMembersRequest;
16
+ type ResponseBody = undefined;
17
+
18
+ export class ChargeRegistrationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
19
+ queryDecoder = LimitedFilteredRequest as Decoder<LimitedFilteredRequest>;
20
+ bodyDecoder = ChargeMembersRequest as Decoder<ChargeMembersRequest>;
21
+
22
+ protected doesMatch(request: Request): [true, Params] | [false] {
23
+ if (request.method !== 'POST') {
24
+ return [false];
25
+ }
26
+
27
+ const params = Endpoint.parseParameters(request.url, '/admin/charge-registrations', {});
28
+
29
+ if (params) {
30
+ return [true, params as Params];
31
+ }
32
+ return [false];
33
+ }
34
+
35
+ async handle(request: DecodedRequest<Params, Query, Body>) {
36
+ const organization = await Context.setOrganizationScope();
37
+ const body = request.body;
38
+
39
+ await Context.authenticate();
40
+
41
+ if (!await Context.auth.canManagePayments(organization.id)) {
42
+ throw Context.auth.error();
43
+ }
44
+
45
+ ChargeMembersEndpoint.throwIfInvalidBody(body);
46
+
47
+ const queueId = 'charge-registrations';
48
+
49
+ if (QueueHandler.isRunning(queueId)) {
50
+ throw new SimpleError({
51
+ code: 'charge_pending',
52
+ message: 'Charge registrations already pending',
53
+ human: $t(`d2b84fdd-035b-4307-a897-000081aa814f`),
54
+ });
55
+ }
56
+
57
+ await QueueHandler.schedule(queueId, async () => {
58
+ const dataGenerator = fetchToAsyncIterator(request.query, {
59
+ fetch: request => GetRegistrationsEndpoint.buildData(request, PermissionLevel.Write),
60
+ });
61
+
62
+ const chargedMemberIds = new Set<string>();
63
+
64
+ for await (const data of dataGenerator) {
65
+ for (const registration of data.registrations) {
66
+ const memberId = registration.member.id;
67
+
68
+ // only charge members once
69
+ if (!chargedMemberIds.has(memberId)) {
70
+ chargedMemberIds.add(memberId);
71
+ await MemberCharger.charge({
72
+ chargingOrganizationId: organization.id,
73
+ memberToCharge: registration.member,
74
+ price: body.price,
75
+ amount: body.amount ?? 1,
76
+ description: body.description,
77
+ dueAt: body.dueAt,
78
+ createdAt: body.createdAt,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ });
84
+
85
+ return new Response(undefined);
86
+ }
87
+ }
@@ -2,8 +2,8 @@ import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
4
 
5
- import { Context } from '../../../helpers/Context';
6
- import { GetMembersEndpoint } from './GetMembersEndpoint';
5
+ import { Context } from '../../../helpers/Context.js';
6
+ import { GetMembersEndpoint } from './GetMembersEndpoint.js';
7
7
 
8
8
  type Params = Record<string, never>;
9
9
  type Query = CountFilteredRequest;
@@ -8,11 +8,11 @@ import { DataValidator } from '@stamhoofd/utility';
8
8
 
9
9
  import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
10
10
  import parsePhoneNumber from 'libphonenumber-js/max';
11
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
- import { Context } from '../../../helpers/Context';
13
- import { memberFilterCompilers } from '../../../sql-filters/members';
14
- import { memberSorters } from '../../../sql-sorters/members';
15
- import { validateGroupFilter } from './helpers/validateGroupFilter';
11
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
+ import { Context } from '../../../helpers/Context.js';
13
+ import { memberFilterCompilers } from '../../../sql-filters/members.js';
14
+ import { memberSorters } from '../../../sql-sorters/members.js';
15
+ import { validateGroupFilter } from './helpers/validateGroupFilter.js';
16
16
 
17
17
  type Params = Record<string, never>;
18
18
  type Query = LimitedFilteredRequest;
@@ -2,8 +2,8 @@ import { Decoder } from '@simonbackx/simple-encoding';
2
2
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
3
  import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
4
 
5
- import { Context } from '../../../helpers/Context';
6
- import { GetRegistrationsEndpoint } from './GetRegistrationsEndpoint';
5
+ import { Context } from '../../../helpers/Context.js';
6
+ import { GetRegistrationsEndpoint } from './GetRegistrationsEndpoint.js';
7
7
 
8
8
  type Params = Record<string, never>;
9
9
  type Query = CountFilteredRequest;
@@ -3,25 +3,24 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
3
3
  import { SimpleError } from '@simonbackx/simple-errors';
4
4
  import { Group, Member, Platform, Registration } from '@stamhoofd/models';
5
5
  import { SQL, SQLExpression, SQLSelect, SQLSortDefinitions, applySQLSorter, compileToSQLFilter } from '@stamhoofd/sql';
6
- import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, StamhoofdFilter, assertSort } from '@stamhoofd/structures';
6
+ import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedResponse, PermissionLevel, RegistrationWithMemberBlob, StamhoofdFilter, assertSort } from '@stamhoofd/structures';
7
7
 
8
8
  import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
9
9
  import { RegistrationsBlob } from '@stamhoofd/structures/dist/src/members/RegistrationsBlob';
10
- import { RegistrationWithMemberBlob } from '@stamhoofd/structures/dist/src/members/RegistrationWithMemberBlob';
11
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
- import { Context } from '../../../helpers/Context';
13
- import { LimitedFilteredRequestHelper } from '../../../helpers/LimitedFilteredRequestHelper';
14
- import { groupJoin, registrationFilterCompilers } from '../../../sql-filters/registrations';
15
- import { registrationSorters } from '../../../sql-sorters/registrations';
16
- import { GetMembersEndpoint } from '../members/GetMembersEndpoint';
17
- import { validateGroupFilter } from '../members/helpers/validateGroupFilter';
10
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
11
+ import { Context } from '../../../helpers/Context.js';
12
+ import { LimitedFilteredRequestHelper } from '../../../helpers/LimitedFilteredRequestHelper.js';
13
+ import { groupJoin, registrationFilterCompilers } from '../../../sql-filters/registrations.js';
14
+ import { RegistrationSortData, registrationSorters } from '../../../sql-sorters/registrations.js';
15
+ import { GetMembersEndpoint } from '../members/GetMembersEndpoint.js';
16
+ import { validateGroupFilter } from '../members/helpers/validateGroupFilter.js';
18
17
 
19
18
  type Params = Record<string, never>;
20
19
  type Query = LimitedFilteredRequest;
21
20
  type Body = undefined;
22
21
  type ResponseBody = PaginatedResponse<RegistrationsBlob, LimitedFilteredRequest>;
23
22
 
24
- const sorters: SQLSortDefinitions<RegistrationWithMemberBlob> = registrationSorters;
23
+ const sorters: SQLSortDefinitions<RegistrationSortData> = registrationSorters;
25
24
  const filterCompilers = registrationFilterCompilers;
26
25
 
27
26
  export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
@@ -234,9 +233,13 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
234
233
 
235
234
  const registrationsBlob = await AuthenticatedStructures.registrationsBlob(data, members);
236
235
 
237
- const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoop({
236
+ const next = LimitedFilteredRequestHelper.fixInfiniteLoadingLoopWithTransform<RegistrationWithMemberBlob, RegistrationSortData>({
238
237
  request: requestQuery,
239
238
  results: registrationsBlob.registrations,
239
+ transformer: registration => new RegistrationSortData({
240
+ registration,
241
+ organizations: registrationsBlob.organizations,
242
+ }),
240
243
  sorters,
241
244
  });
242
245
 
@@ -8,14 +8,14 @@ import { BalanceItem, BalanceItemPayment, CachedBalance, Group, Member, MemberWi
8
8
  import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, ReceivableBalanceType, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
11
- import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
12
- import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
13
- import { Context } from '../../../helpers/Context';
14
- import { StripeHelper } from '../../../helpers/StripeHelper';
15
- import { BalanceItemService } from '../../../services/BalanceItemService';
16
- import { RegistrationService } from '../../../services/RegistrationService';
17
- import { PaymentService } from '../../../services/PaymentService';
18
- import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper';
11
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
+ import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
13
+ import { Context } from '../../../helpers/Context.js';
14
+ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
15
+ import { StripeHelper } from '../../../helpers/StripeHelper.js';
16
+ import { BalanceItemService } from '../../../services/BalanceItemService.js';
17
+ import { PaymentService } from '../../../services/PaymentService.js';
18
+ import { RegistrationService } from '../../../services/RegistrationService.js';
19
19
  type Params = Record<string, never>;
20
20
  type Query = undefined;
21
21
  type Body = IDRegisterCheckout;
@@ -381,6 +381,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
381
381
  registration.options = item.options;
382
382
  registration.recordAnswers = item.recordAnswers;
383
383
  registration.startDate = startDate;
384
+ registration.endDate = item.calculatedEndDate;
384
385
 
385
386
  // Clear if we are reusing an existing registration
386
387
  registration.trialUntil = null;
@@ -0,0 +1,48 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../../helpers/Context.js';
6
+ import { GetDocumentTemplatesEndpoint } from './GetDocumentTemplatesEndpoint.js';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetDocumentTemplatesCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method !== 'GET') {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, '/organization/document-templates/count', {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ const organization = await Context.setOrganizationScope();
31
+ await Context.authenticate();
32
+
33
+ if (!await Context.auth.canManageDocuments(organization.id)) {
34
+ throw Context.auth.error();
35
+ }
36
+
37
+ const query = await GetDocumentTemplatesEndpoint.buildQuery(request.query);
38
+
39
+ const count = await query
40
+ .count();
41
+
42
+ return new Response(
43
+ CountResponse.create({
44
+ count,
45
+ }),
46
+ );
47
+ }
48
+ }