@stamhoofd/backend 2.107.3 → 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 +10 -10
- package/src/endpoints/admin/members/ChargeMembersEndpoint.ts +5 -5
- package/src/endpoints/admin/registrations/ChargeRegistrationsEndpoint.ts +87 -0
- package/src/endpoints/global/members/GetMembersCountEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetRegistrationsCountEndpoint.ts +2 -2
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +9 -7
- package/src/excel-loaders/members.ts +1 -1
- package/src/excel-loaders/registrations.ts +58 -8
- package/src/helpers/MemberCharger.ts +16 -4
- package/src/helpers/fetchToAsyncIterator.ts +1 -1
- package/src/sql-sorters/registrations.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "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.
|
|
55
|
-
"@stamhoofd/backend-middleware": "2.
|
|
56
|
-
"@stamhoofd/email": "2.
|
|
57
|
-
"@stamhoofd/models": "2.
|
|
58
|
-
"@stamhoofd/queues": "2.
|
|
59
|
-
"@stamhoofd/sql": "2.
|
|
60
|
-
"@stamhoofd/structures": "2.
|
|
61
|
-
"@stamhoofd/utility": "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": "
|
|
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
|
-
|
|
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.
|
|
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: '
|
|
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.
|
|
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
|
-
|
|
6
|
+
await Promise.all(membersToCharge.map(memberToCharge => MemberCharger.charge({
|
|
7
7
|
price,
|
|
8
8
|
amount,
|
|
9
9
|
description,
|
|
10
10
|
chargingOrganizationId,
|
|
11
|
-
|
|
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
|
|
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
|
|
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({
|