@stamhoofd/backend 2.107.2 → 2.108.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.107.2",
3
+ "version": "2.108.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -51,14 +51,14 @@
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.2",
55
- "@stamhoofd/backend-middleware": "2.107.2",
56
- "@stamhoofd/email": "2.107.2",
57
- "@stamhoofd/models": "2.107.2",
58
- "@stamhoofd/queues": "2.107.2",
59
- "@stamhoofd/sql": "2.107.2",
60
- "@stamhoofd/structures": "2.107.2",
61
- "@stamhoofd/utility": "2.107.2",
54
+ "@stamhoofd/backend-i18n": "2.108.0",
55
+ "@stamhoofd/backend-middleware": "2.108.0",
56
+ "@stamhoofd/email": "2.108.0",
57
+ "@stamhoofd/models": "2.108.0",
58
+ "@stamhoofd/queues": "2.108.0",
59
+ "@stamhoofd/sql": "2.108.0",
60
+ "@stamhoofd/structures": "2.108.0",
61
+ "@stamhoofd/utility": "2.108.0",
62
62
  "archiver": "^7.0.1",
63
63
  "axios": "^1.8.2",
64
64
  "cookie": "^0.7.0",
@@ -76,5 +76,5 @@
76
76
  "publishConfig": {
77
77
  "access": "public"
78
78
  },
79
- "gitHead": "33deb06a0bac15cc46fe141bc3dcf1dafb6e82a1"
79
+ "gitHead": "c8dd24207c94a4c077448545cdfcb94369a7ef47"
80
80
  }
@@ -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;
@@ -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;
@@ -8,13 +8,13 @@ import { CountFilteredRequest, GroupType, LimitedFilteredRequest, PaginatedRespo
8
8
  import { SQLResultNamespacedRow } from '@simonbackx/simple-database';
9
9
  import { RegistrationsBlob } from '@stamhoofd/structures/dist/src/members/RegistrationsBlob';
10
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';
11
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
12
+ import { Context } from '../../../helpers/Context.js';
13
+ import { LimitedFilteredRequestHelper } from '../../../helpers/LimitedFilteredRequestHelper.js';
14
+ import { groupJoin, registrationFilterCompilers } from '../../../sql-filters/registrations.js';
15
+ import { registrationSorters } from '../../../sql-sorters/registrations.js';
16
+ import { GetMembersEndpoint } from '../members/GetMembersEndpoint.js';
17
+ import { validateGroupFilter } from '../members/helpers/validateGroupFilter.js';
18
18
 
19
19
  type Params = Record<string, never>;
20
20
  type Query = LimitedFilteredRequest;
@@ -160,6 +160,8 @@ export class GetRegistrationsEndpoint extends Endpoint<Params, Query, Body, Resp
160
160
  .setMaxExecutionTime(15 * 1000)
161
161
  .where('registeredAt', '!=', null);
162
162
 
163
+ query;
164
+
163
165
  if (scopeFilter) {
164
166
  query.where(await compileToSQLFilter(scopeFilter, filterCompilers));
165
167
  }
@@ -340,7 +340,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformMember> = {
340
340
  },
341
341
  }
342
342
  : {},
343
- value: options.length === 1 && returnAmount ? options[0].amount : options.map(option => returnAmount ? option.amount : option).join(', '),
343
+ value: options.map(option => returnAmount ? option.amount : option.option.name).join(', '),
344
344
  };
345
345
  },
346
346
  },
@@ -1,12 +1,12 @@
1
1
  import { XlsxBuiltInNumberFormat, XlsxTransformerSheet } from '@stamhoofd/excel-writer';
2
2
  import { Platform } from '@stamhoofd/models';
3
3
  import { ExcelExportType, LimitedFilteredRequest, PlatformMember, PlatformRegistration, Platform as PlatformStruct, UnencodeablePaginatedResponse } from '@stamhoofd/structures';
4
- import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
5
- import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint';
6
- import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures';
7
- import { Context } from '../helpers/Context';
8
- import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper';
9
- import { baseMemberColumns } from './members';
4
+ import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint.js';
5
+ import { GetRegistrationsEndpoint } from '../endpoints/global/registration/GetRegistrationsEndpoint.js';
6
+ import { AuthenticatedStructures } from '../helpers/AuthenticatedStructures.js';
7
+ import { Context } from '../helpers/Context.js';
8
+ import { XlsxTransformerColumnHelper } from '../helpers/XlsxTransformerColumnHelper.js';
9
+ import { baseMemberColumns } from './members.js';
10
10
 
11
11
  // Assign to a typed variable to assure we have correct type checking in place
12
12
  const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
@@ -22,7 +22,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
22
22
  }),
23
23
  },
24
24
  {
25
- id: 'price',
25
+ id: 'priceName',
26
26
  name: $t(`dcc53f25-f0e9-4e3e-9f4f-e8cfa4e88755`),
27
27
  width: 30,
28
28
  getValue: (registration: PlatformRegistration) => {
@@ -31,6 +31,56 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
31
31
  };
32
32
  },
33
33
  },
34
+ {
35
+ id: 'price',
36
+ name: $t(`dcc53f25-f0e9-4e3e-9f4f-e8cfa4e88755`),
37
+ width: 30,
38
+ getValue: (registration: PlatformRegistration) => {
39
+ return {
40
+ value: registration.balances.reduce((sum, r) => sum + (r.amountOpen + r.amountPaid + r.amountPending), 0) / 1_0000,
41
+ style: {
42
+ numberFormat: {
43
+ id: XlsxBuiltInNumberFormat.Currency2DecimalWithRed,
44
+ },
45
+ },
46
+ };
47
+ },
48
+ },
49
+ {
50
+ id: 'registeredAt',
51
+ name: $t(`Inschrijvingsdatum`),
52
+ width: 20,
53
+ getValue: (registration: PlatformRegistration) => ({
54
+ value: registration.registeredAt,
55
+ style: {
56
+ numberFormat: {
57
+ id: XlsxBuiltInNumberFormat.DateSlash,
58
+ },
59
+ },
60
+ }),
61
+ },
62
+ {
63
+ id: 'organization',
64
+ name: $t('2f325358-6e2f-418c-9fea-31a14abbc17a'),
65
+ width: 40,
66
+ getValue: (registration: PlatformRegistration) => {
67
+ const organization = registration.member.family.getOrganization(registration.group.organizationId);
68
+ return ({
69
+ value: organization?.name ?? $t('836c2cd3-32a3-43f2-b09c-600170fcd9cb'),
70
+ });
71
+ },
72
+ },
73
+ {
74
+ id: 'uri',
75
+ name: $t('9d283cbb-7ba2-4a16-88ec-ff0c19f39674'),
76
+ width: 40,
77
+ getValue: (registration: PlatformRegistration) => {
78
+ const organization = registration.member.family.getOrganization(registration.group.organizationId);
79
+ return ({
80
+ value: organization?.uri ?? $t('836c2cd3-32a3-43f2-b09c-600170fcd9cb'),
81
+ });
82
+ },
83
+ },
34
84
  // option menu
35
85
  {
36
86
  match(id) {
@@ -71,7 +121,7 @@ const sheet: XlsxTransformerSheet<PlatformMember, PlatformRegistration> = {
71
121
  },
72
122
  }
73
123
  : {},
74
- value: options.length === 1 && returnAmount ? options[0].amount : options.map(option => returnAmount ? option.amount : option).join(', '),
124
+ value: options.map(option => returnAmount ? option.amount : option.option.name).join(', '),
75
125
  };
76
126
  },
77
127
  }];
@@ -3,17 +3,29 @@ import { BalanceItemType, MemberWithRegistrationsBlob } from '@stamhoofd/structu
3
3
 
4
4
  export class MemberCharger {
5
5
  static async chargeMany({ chargingOrganizationId, membersToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; membersToCharge: MemberWithRegistrationsBlob[]; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
6
- const balanceItems = membersToCharge.map(memberBeingCharged => MemberCharger.createBalanceItem({
6
+ await Promise.all(membersToCharge.map(memberToCharge => MemberCharger.charge({
7
7
  price,
8
8
  amount,
9
9
  description,
10
10
  chargingOrganizationId,
11
- memberBeingCharged,
11
+ memberToCharge,
12
12
  dueAt,
13
13
  createdAt,
14
- }));
14
+ })));
15
+ }
16
+
17
+ static async charge({ chargingOrganizationId, memberToCharge, price, amount, description, dueAt, createdAt }: { chargingOrganizationId: string; memberToCharge: MemberWithRegistrationsBlob; price: number; amount?: number; description: string; dueAt: Date | null; createdAt: Date | null }) {
18
+ const balanceItem = MemberCharger.createBalanceItem({
19
+ price,
20
+ amount,
21
+ description,
22
+ chargingOrganizationId,
23
+ memberBeingCharged: memberToCharge,
24
+ dueAt,
25
+ createdAt,
26
+ });
15
27
 
16
- await Promise.all(balanceItems.map(balanceItem => balanceItem.save()));
28
+ await balanceItem.save();
17
29
  }
18
30
 
19
31
  private static createBalanceItem({ price, amount, description, chargingOrganizationId, memberBeingCharged, dueAt, createdAt }: { price: number; amount?: number; description: string; chargingOrganizationId: string; memberBeingCharged: MemberWithRegistrationsBlob; dueAt: Date | null; createdAt: Date | null }): BalanceItem {
@@ -1,5 +1,5 @@
1
1
  import { IPaginatedResponse, LimitedFilteredRequest } from '@stamhoofd/structures';
2
- import { FileSignService } from '../services/FileSignService';
2
+ import { FileSignService } from '../services/FileSignService.js';
3
3
 
4
4
  export function fetchToAsyncIterator<T>(
5
5
  initialFilter: LimitedFilteredRequest,
@@ -63,7 +63,7 @@ export const registrationSorters: SQLSortDefinitions<RegistrationWithMemberBlob>
63
63
  },
64
64
  'member.birthDay': {
65
65
  getValue(a) {
66
- return a.member.details.birthDay === null ? null : Formatter.dateIso(a.member.details.birthDay as Date);
66
+ return a.member.details.birthDay === null ? null : Formatter.dateIso(a.member.details.birthDay);
67
67
  },
68
68
  toSQL: (direction: SQLOrderByDirection): SQLOrderBy => {
69
69
  return new SQLOrderBy({