@stamhoofd/backend 2.37.0 → 2.38.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/memberships/ChargeMembershipsEndpoint.ts +1 -2
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +8 -0
- package/src/endpoints/auth/CreateAdminEndpoint.ts +4 -3
- package/src/endpoints/auth/PatchUserEndpoint.ts +2 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +1 -1
- package/src/endpoints/global/files/ExportToExcelEndpoint.ts +9 -10
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +14 -3
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +11 -4
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +19 -2
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -2
- package/src/endpoints/organization/shared/GetPaymentEndpoint.ts +1 -1
- package/src/excel-loaders/members.ts +87 -24
- package/src/helpers/Context.ts +2 -2
- package/src/helpers/MembershipCharger.ts +84 -2
- package/src/helpers/fetchToAsyncIterator.ts +3 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.38.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"@simonbackx/simple-encoding": "2.15.1",
|
|
37
37
|
"@simonbackx/simple-endpoints": "1.14.0",
|
|
38
38
|
"@simonbackx/simple-logging": "^1.0.1",
|
|
39
|
-
"@stamhoofd/backend-i18n": "2.
|
|
40
|
-
"@stamhoofd/backend-middleware": "2.
|
|
41
|
-
"@stamhoofd/email": "2.
|
|
42
|
-
"@stamhoofd/models": "2.
|
|
43
|
-
"@stamhoofd/queues": "2.
|
|
44
|
-
"@stamhoofd/sql": "2.
|
|
45
|
-
"@stamhoofd/structures": "2.
|
|
46
|
-
"@stamhoofd/utility": "2.
|
|
39
|
+
"@stamhoofd/backend-i18n": "2.38.0",
|
|
40
|
+
"@stamhoofd/backend-middleware": "2.38.0",
|
|
41
|
+
"@stamhoofd/email": "2.38.0",
|
|
42
|
+
"@stamhoofd/models": "2.38.0",
|
|
43
|
+
"@stamhoofd/queues": "2.38.0",
|
|
44
|
+
"@stamhoofd/sql": "2.38.0",
|
|
45
|
+
"@stamhoofd/structures": "2.38.0",
|
|
46
|
+
"@stamhoofd/utility": "2.38.0",
|
|
47
47
|
"archiver": "^7.0.1",
|
|
48
48
|
"aws-sdk": "^2.885.0",
|
|
49
49
|
"axios": "1.6.8",
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"postmark": "^4.0.5",
|
|
61
61
|
"stripe": "^16.6.0"
|
|
62
62
|
},
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "4af6773c9729d690145d60993542452989d6219d"
|
|
64
64
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
3
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
3
|
-
import { sleep } from '@stamhoofd/utility';
|
|
4
4
|
import { Context } from '../../../helpers/Context';
|
|
5
5
|
import { MembershipCharger } from '../../../helpers/MembershipCharger';
|
|
6
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
type Params = Record<string, never>;
|
|
@@ -43,6 +43,10 @@ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
43
43
|
const platform = await Platform.getShared()
|
|
44
44
|
|
|
45
45
|
for (const id of request.body.getDeletes()) {
|
|
46
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
47
|
+
throw Context.auth.error('Enkel een platform hoofdbeheerder kan groepen verwijderen')
|
|
48
|
+
}
|
|
49
|
+
|
|
46
50
|
const organization = await Organization.getByID(id);
|
|
47
51
|
if (!organization) {
|
|
48
52
|
throw new SimpleError({ code: "not_found", message: "Organization not found", statusCode: 404 });
|
|
@@ -85,6 +89,10 @@ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
85
89
|
|
|
86
90
|
// Organization creation
|
|
87
91
|
for (const {put} of request.body.getPuts()) {
|
|
92
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
93
|
+
throw Context.auth.error('Enkel een platform hoofdbeheerder kan nieuwe groepen aanmaken')
|
|
94
|
+
}
|
|
95
|
+
|
|
88
96
|
if (put.name.length < 4) {
|
|
89
97
|
if (put.name.length == 0) {
|
|
90
98
|
throw new SimpleError({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Decoder } 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 { PasswordToken, sendEmailTemplate, User } from '@stamhoofd/models';
|
|
4
|
+
import { PasswordToken, Platform, sendEmailTemplate, User } from '@stamhoofd/models';
|
|
5
5
|
import { EmailTemplateType, Recipient, Replacement, UserPermissions, User as UserStruct, UserWithMembers } from "@stamhoofd/structures";
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
|
|
@@ -100,9 +100,10 @@ export class CreateAdminEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
100
100
|
|
|
101
101
|
const dateTime = Formatter.dateTime(validUntil)
|
|
102
102
|
const recoveryUrl = await PasswordToken.getPasswordRecoveryUrl(admin, organization, request.i18n, validUntil)
|
|
103
|
+
const platformName = ((await Platform.getSharedStruct()).config.name)
|
|
103
104
|
|
|
104
|
-
const name = organization?.name ??
|
|
105
|
-
const what = organization ? `de vereniging ${name} op ${
|
|
105
|
+
const name = organization?.name ?? platformName
|
|
106
|
+
const what = organization ? `de vereniging ${name} op ${platformName}` : platformName
|
|
106
107
|
|
|
107
108
|
const emailTo = admin.getEmailTo();
|
|
108
109
|
const email: string = typeof emailTo === 'string' ? emailTo : emailTo[0]?.email;
|
|
@@ -79,7 +79,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
79
79
|
if (organization) {
|
|
80
80
|
editUser.permissions = UserPermissions.limitedPatch(editUser.permissions, request.body.permissions, organization.id)
|
|
81
81
|
|
|
82
|
-
if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess())) {
|
|
82
|
+
if (editUser.id === user.id && (!editUser.permissions || !editUser.permissions.forOrganization(organization)?.hasFullAccess()) && STAMHOOFD.environment !== 'development') {
|
|
83
83
|
throw new SimpleError({
|
|
84
84
|
code: "permission_denied",
|
|
85
85
|
message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
|
|
@@ -96,7 +96,7 @@ export class PatchUserEndpoint extends Endpoint<Params, Query, Body, ResponseBod
|
|
|
96
96
|
editUser.permissions = null
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess()) {
|
|
99
|
+
if (editUser.id === user.id && !editUser.permissions?.platform?.hasFullAccess() && STAMHOOFD.environment !== 'development') {
|
|
100
100
|
throw new SimpleError({
|
|
101
101
|
code: "permission_denied",
|
|
102
102
|
message: "Je kan jezelf niet verwijderen als hoofdbeheerder"
|
|
@@ -60,7 +60,7 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
60
60
|
throw new SimpleError({
|
|
61
61
|
code: 'invalid_data',
|
|
62
62
|
message: 'Invalid organizationId',
|
|
63
|
-
human: 'Je kan
|
|
63
|
+
human: 'Je kan activiteiten aanmaken via het administratieportaal als je geen platform hoofdbeheerder bent',
|
|
64
64
|
})
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
|
-
import { Decoder
|
|
2
|
+
import { Decoder } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
-
import { Email } from '@stamhoofd/email';
|
|
6
5
|
import { ArchiverWriterAdapter, exportToExcel, XlsxTransformerSheet, XlsxWriter } from '@stamhoofd/excel-writer';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { Platform, RateLimiter, sendEmailTemplate } from '@stamhoofd/models';
|
|
7
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
|
+
import { EmailTemplateType, ExcelExportRequest, ExcelExportResponse, ExcelExportType, IPaginatedResponse, LimitedFilteredRequest, Replacement, Version } from '@stamhoofd/structures';
|
|
9
9
|
import { sleep } from "@stamhoofd/utility";
|
|
10
10
|
import { Context } from '../../../helpers/Context';
|
|
11
11
|
import { fetchToAsyncIterator } from '../../../helpers/fetchToAsyncIterator';
|
|
12
12
|
import { FileCache } from '../../../helpers/FileCache';
|
|
13
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
14
13
|
|
|
15
14
|
type Params = { type: string };
|
|
16
15
|
type Query = undefined;
|
|
17
16
|
type Body = ExcelExportRequest;
|
|
18
17
|
type ResponseBody = ExcelExportResponse;
|
|
19
18
|
|
|
20
|
-
type ExcelExporter<T
|
|
21
|
-
fetch(request: LimitedFilteredRequest): Promise<
|
|
19
|
+
type ExcelExporter<T> = {
|
|
20
|
+
fetch(request: LimitedFilteredRequest): Promise<IPaginatedResponse<T[], LimitedFilteredRequest>>
|
|
22
21
|
sheets: XlsxTransformerSheet<T, unknown>[]
|
|
23
22
|
}
|
|
24
23
|
|
|
@@ -36,7 +35,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
36
35
|
bodyDecoder = ExcelExportRequest as Decoder<ExcelExportRequest>
|
|
37
36
|
|
|
38
37
|
// Other endpoints can register exports here
|
|
39
|
-
static loaders: Map<ExcelExportType, ExcelExporter<
|
|
38
|
+
static loaders: Map<ExcelExportType, ExcelExporter<unknown>> = new Map()
|
|
40
39
|
|
|
41
40
|
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
42
41
|
if (request.method != "POST") {
|
|
@@ -52,7 +51,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
55
|
-
|
|
54
|
+
await Context.setOptionalOrganizationScope();
|
|
56
55
|
const {user} = await Context.authenticate()
|
|
57
56
|
|
|
58
57
|
if (user.isApiUser) {
|
|
@@ -138,7 +137,7 @@ export class ExportToExcelEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
138
137
|
}))
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
async job(loader: ExcelExporter<
|
|
140
|
+
async job(loader: ExcelExporter<unknown>, request: ExcelExportRequest, type: string): Promise<string> {
|
|
142
141
|
// Only run 1 export per user at the same time
|
|
143
142
|
return await QueueHandler.schedule('user-export-to-excel-' + Context.user!.id, async () => {
|
|
144
143
|
// Allow maximum 2 running Excel jobs at the same time for all users
|
|
@@ -10,6 +10,8 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
|
|
|
10
10
|
import { Context } from '../../../helpers/Context';
|
|
11
11
|
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
12
12
|
import { SetupStepUpdater } from '../../../helpers/SetupStepsUpdater';
|
|
13
|
+
import { MembershipCharger } from '../../../helpers/MembershipCharger';
|
|
14
|
+
import { QueueHandler } from '@stamhoofd/queues';
|
|
13
15
|
|
|
14
16
|
type Params = Record<string, never>;
|
|
15
17
|
type Query = undefined;
|
|
@@ -73,6 +75,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
const updateMembershipMemberIds = new Set<string>()
|
|
78
|
+
const updateMembershipsForOrganizations = new Set<string>()
|
|
76
79
|
|
|
77
80
|
// Loop all members one by one
|
|
78
81
|
for (const put of request.body.getPuts()) {
|
|
@@ -470,7 +473,7 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
470
473
|
})
|
|
471
474
|
}
|
|
472
475
|
|
|
473
|
-
if (!membership.canDelete()) {
|
|
476
|
+
if (!membership.canDelete() && !Context.auth.hasPlatformFullAccess()) {
|
|
474
477
|
throw new SimpleError({
|
|
475
478
|
code: "invalid_field",
|
|
476
479
|
message: "Invalid invoice",
|
|
@@ -478,8 +481,8 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
478
481
|
})
|
|
479
482
|
}
|
|
480
483
|
|
|
481
|
-
membership.
|
|
482
|
-
|
|
484
|
+
await membership.doDelete();
|
|
485
|
+
updateMembershipsForOrganizations.add(membership.organizationId) // can influence free memberships in other members of same organization
|
|
483
486
|
updateMembershipMemberIds.add(member.id)
|
|
484
487
|
}
|
|
485
488
|
|
|
@@ -496,6 +499,14 @@ export class PatchOrganizationMembersEndpoint extends Endpoint<Params, Query, Bo
|
|
|
496
499
|
}
|
|
497
500
|
}
|
|
498
501
|
|
|
502
|
+
if (updateMembershipsForOrganizations.size) {
|
|
503
|
+
QueueHandler.schedule('update-membership-prices', async () => {
|
|
504
|
+
for (const id of updateMembershipsForOrganizations) {
|
|
505
|
+
await MembershipCharger.updatePrices(id)
|
|
506
|
+
}
|
|
507
|
+
}).catch(console.error);
|
|
508
|
+
}
|
|
509
|
+
|
|
499
510
|
if(shouldUpdateSetupSteps && organization) {
|
|
500
511
|
SetupStepUpdater.updateForOrganization(organization).catch(console.error);
|
|
501
512
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { AutoEncoderPatchType, Decoder,
|
|
1
|
+
import { AutoEncoderPatchType, Decoder, isPatchableArray, patchObject } from "@simonbackx/simple-encoding";
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { Organization, Platform, RegistrationPeriod } from "@stamhoofd/models";
|
|
4
4
|
import { MemberResponsibility, PlatformConfig, PlatformPremiseType, Platform as PlatformStruct } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
6
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
7
|
+
import { QueueHandler } from "@stamhoofd/queues";
|
|
7
8
|
import { Context } from "../../../helpers/Context";
|
|
8
|
-
import {
|
|
9
|
-
import { PeriodHelper } from "../../../helpers/PeriodHelper";
|
|
9
|
+
import { MembershipCharger } from "../../../helpers/MembershipCharger";
|
|
10
10
|
import { MembershipHelper } from "../../../helpers/MembershipHelper";
|
|
11
|
+
import { PeriodHelper } from "../../../helpers/PeriodHelper";
|
|
12
|
+
import { SetupStepUpdater } from "../../../helpers/SetupStepsUpdater";
|
|
11
13
|
|
|
12
14
|
type Params = Record<string, never>;
|
|
13
15
|
type Query = undefined;
|
|
@@ -174,7 +176,12 @@ export class PatchPlatformEndpoint extends Endpoint<
|
|
|
174
176
|
await platform.save();
|
|
175
177
|
|
|
176
178
|
if (shouldUpdateMemberships) {
|
|
177
|
-
|
|
179
|
+
if (!QueueHandler.isRunning('update-membership-prices')) {
|
|
180
|
+
QueueHandler.schedule('update-membership-prices', async () => {
|
|
181
|
+
await MembershipCharger.updatePrices()
|
|
182
|
+
await MembershipHelper.updateAll()
|
|
183
|
+
}).catch(console.error);
|
|
184
|
+
}
|
|
178
185
|
}
|
|
179
186
|
|
|
180
187
|
if (shouldMoveToPeriod) {
|
|
@@ -37,12 +37,22 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
40
|
-
const organization = await Context.setOrganizationScope();
|
|
41
|
-
|
|
40
|
+
const organization = await Context.setOrganizationScope({allowInactive: true});
|
|
41
|
+
await Context.authenticate()
|
|
42
42
|
|
|
43
43
|
if (!await Context.auth.hasSomeAccess(organization.id)) {
|
|
44
44
|
throw Context.auth.error()
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
if (!organization.active && !Context.auth.hasPlatformFullAccess()) {
|
|
48
|
+
throw new SimpleError({
|
|
49
|
+
code: "permission_denied",
|
|
50
|
+
message: "You do not have permissions to edit an inactive organization",
|
|
51
|
+
human: 'Je hebt geen toegangsrechten om een inactieve groep te bewerken',
|
|
52
|
+
statusCode: 403
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
}
|
|
46
56
|
|
|
47
57
|
// check if organization ID matches
|
|
48
58
|
if (request.body.id !== organization.id) {
|
|
@@ -295,6 +305,13 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
295
305
|
}
|
|
296
306
|
}
|
|
297
307
|
|
|
308
|
+
if (request.body.active !== undefined) {
|
|
309
|
+
if (!Context.auth.hasPlatformFullAccess()) {
|
|
310
|
+
throw Context.auth.error('Enkel een platform hoofdbeheerder kan een groep (in)actief maken')
|
|
311
|
+
}
|
|
312
|
+
organization.active = request.body.active;
|
|
313
|
+
}
|
|
314
|
+
|
|
298
315
|
if (request.body.uri) {
|
|
299
316
|
if (!Context.auth.hasPlatformFullAccess()) {
|
|
300
317
|
throw Context.auth.error()
|
|
@@ -46,7 +46,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
49
|
-
const organization = await Context.
|
|
49
|
+
const organization = await Context.setOptionalOrganizationScope()
|
|
50
50
|
if (!request.query.exchange) {
|
|
51
51
|
await Context.authenticate()
|
|
52
52
|
}
|
|
@@ -152,7 +152,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
152
152
|
/**
|
|
153
153
|
* ID of payment is needed because of race conditions (need to fetch payment in a race condition save queue)
|
|
154
154
|
*/
|
|
155
|
-
static async pollStatus(paymentId: string,
|
|
155
|
+
static async pollStatus(paymentId: string, org: Organization|null, cancel = false): Promise<Payment | undefined> {
|
|
156
156
|
// Prevent polling the same payment multiple times at the same time: create a queue to prevent races
|
|
157
157
|
QueueHandler.cancel("payments/"+paymentId); // Prevent creating more than one queue item for the same payment
|
|
158
158
|
return await QueueHandler.schedule("payments/"+paymentId, async () => {
|
|
@@ -162,6 +162,17 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
162
162
|
return
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
if (!payment.organizationId) {
|
|
166
|
+
console.error('Payment without organization not supported', payment.id)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const organization = org ?? await Organization.getByID(payment.organizationId)
|
|
171
|
+
if (!organization) {
|
|
172
|
+
console.error('Organization not found for payment', payment.id)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
165
176
|
const testMode = organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production'
|
|
166
177
|
|
|
167
178
|
if (payment.status == PaymentStatus.Pending || payment.status == PaymentStatus.Created || (payment.provider === PaymentProvider.Buckaroo && payment.status == PaymentStatus.Failed)) {
|
|
@@ -26,7 +26,7 @@ export class GetPaymentEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
29
|
-
await Context.
|
|
29
|
+
await Context.setOptionalOrganizationScope()
|
|
30
30
|
await Context.authenticate()
|
|
31
31
|
|
|
32
32
|
const payment = await Payment.getByID(request.params.id);
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { XlsxBuiltInNumberFormat } from "@stamhoofd/excel-writer";
|
|
2
|
-
import {
|
|
2
|
+
import { Platform } from "@stamhoofd/models";
|
|
3
|
+
import { ExcelExportType, Gender, GroupType, LimitedFilteredRequest, MemberWithRegistrationsBlob, PlatformFamily, PlatformMember, UnencodeablePaginatedResponse, Platform as PlatformStruct } from "@stamhoofd/structures";
|
|
3
4
|
import { ExportToExcelEndpoint } from "../endpoints/global/files/ExportToExcelEndpoint";
|
|
4
5
|
import { GetMembersEndpoint } from "../endpoints/global/members/GetMembersEndpoint";
|
|
5
6
|
import { Context } from "../helpers/Context";
|
|
6
7
|
import { XlsxTransformerColumnHelper } from "../helpers/xlsxAddressTransformerColumnFactory";
|
|
8
|
+
import { Formatter } from "@stamhoofd/utility";
|
|
9
|
+
import { AuthenticatedStructures } from "../helpers/AuthenticatedStructures";
|
|
7
10
|
|
|
8
11
|
ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
9
12
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
10
13
|
const result = await GetMembersEndpoint.buildData(query)
|
|
11
14
|
|
|
12
|
-
return new
|
|
13
|
-
results: result.results
|
|
15
|
+
return new UnencodeablePaginatedResponse({
|
|
16
|
+
results: PlatformFamily.createSingles(result.results, {
|
|
17
|
+
contextOrganization: Context.organization ? (await AuthenticatedStructures.organization(Context.organization)) : null,
|
|
18
|
+
platform: await Platform.getSharedStruct()
|
|
19
|
+
}),
|
|
14
20
|
next: result.next
|
|
15
21
|
});
|
|
16
22
|
},
|
|
@@ -23,7 +29,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
23
29
|
id: 'id',
|
|
24
30
|
name: 'ID',
|
|
25
31
|
width: 20,
|
|
26
|
-
getValue: (object:
|
|
32
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
27
33
|
value: object.id
|
|
28
34
|
})
|
|
29
35
|
},
|
|
@@ -31,7 +37,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
31
37
|
id: 'memberNumber',
|
|
32
38
|
name: 'Nummer',
|
|
33
39
|
width: 20,
|
|
34
|
-
getValue: (object:
|
|
40
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
35
41
|
value: object.details.memberNumber
|
|
36
42
|
})
|
|
37
43
|
},
|
|
@@ -39,7 +45,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
39
45
|
id: 'firstName',
|
|
40
46
|
name: 'Voornaam',
|
|
41
47
|
width: 20,
|
|
42
|
-
getValue: (object:
|
|
48
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
43
49
|
value: object.details.firstName
|
|
44
50
|
})
|
|
45
51
|
},
|
|
@@ -47,7 +53,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
47
53
|
id: 'lastName',
|
|
48
54
|
name: 'Achternaam',
|
|
49
55
|
width: 20,
|
|
50
|
-
getValue: (object:
|
|
56
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
51
57
|
value: object.details.lastName
|
|
52
58
|
})
|
|
53
59
|
},
|
|
@@ -55,7 +61,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
55
61
|
id: 'birthDay',
|
|
56
62
|
name: 'Geboortedatum',
|
|
57
63
|
width: 20,
|
|
58
|
-
getValue: (object:
|
|
64
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
59
65
|
value: object.details.birthDay,
|
|
60
66
|
style: {
|
|
61
67
|
numberFormat: {
|
|
@@ -68,7 +74,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
68
74
|
id: 'age',
|
|
69
75
|
name: 'Leeftijd',
|
|
70
76
|
width: 20,
|
|
71
|
-
getValue: (object:
|
|
77
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
72
78
|
value: object.details.age,
|
|
73
79
|
})
|
|
74
80
|
},
|
|
@@ -76,7 +82,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
76
82
|
id: 'gender',
|
|
77
83
|
name: 'Geslacht',
|
|
78
84
|
width: 20,
|
|
79
|
-
getValue: (object:
|
|
85
|
+
getValue: ({patchedMember: object}: PlatformMember) => {
|
|
80
86
|
const gender = object.details.gender;
|
|
81
87
|
|
|
82
88
|
return ({
|
|
@@ -88,7 +94,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
88
94
|
id: 'phone',
|
|
89
95
|
name: 'Telefoonnummer',
|
|
90
96
|
width: 20,
|
|
91
|
-
getValue: (object:
|
|
97
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
92
98
|
value: object.details.phone,
|
|
93
99
|
})
|
|
94
100
|
},
|
|
@@ -96,14 +102,14 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
96
102
|
id: 'email',
|
|
97
103
|
name: 'E-mailadres',
|
|
98
104
|
width: 20,
|
|
99
|
-
getValue: (object:
|
|
105
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
100
106
|
value: object.details.email,
|
|
101
107
|
})
|
|
102
108
|
},
|
|
103
|
-
XlsxTransformerColumnHelper.createAddressColumns<
|
|
109
|
+
XlsxTransformerColumnHelper.createAddressColumns<PlatformMember>({
|
|
104
110
|
matchId: 'address',
|
|
105
111
|
identifier: 'Adres',
|
|
106
|
-
getAddress: (object) => {
|
|
112
|
+
getAddress: ({patchedMember: object}: PlatformMember) => {
|
|
107
113
|
// get member address if exists
|
|
108
114
|
const memberAddress = object.details.address;
|
|
109
115
|
if(memberAddress) {
|
|
@@ -124,7 +130,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
124
130
|
id: 'securityCode',
|
|
125
131
|
name: 'Beveiligingscode',
|
|
126
132
|
width: 20,
|
|
127
|
-
getValue: (object:
|
|
133
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
128
134
|
value: object.details.securityCode,
|
|
129
135
|
})
|
|
130
136
|
},
|
|
@@ -132,7 +138,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
132
138
|
id: 'uitpasNumber',
|
|
133
139
|
name: 'UiTPAS-nummer',
|
|
134
140
|
width: 20,
|
|
135
|
-
getValue: (object:
|
|
141
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
136
142
|
value: object.details.uitpasNumber,
|
|
137
143
|
})
|
|
138
144
|
},
|
|
@@ -141,7 +147,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
141
147
|
// todo: use correct term
|
|
142
148
|
name: 'Financiële ondersteuning',
|
|
143
149
|
width: 20,
|
|
144
|
-
getValue: (object:
|
|
150
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
145
151
|
value: XlsxTransformerColumnHelper.formatBoolean(object.details.requiresFinancialSupport?.value),
|
|
146
152
|
})
|
|
147
153
|
},
|
|
@@ -149,11 +155,69 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
149
155
|
id: 'notes',
|
|
150
156
|
name: 'Notities',
|
|
151
157
|
width: 20,
|
|
152
|
-
getValue: (object:
|
|
158
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
153
159
|
value: object.details.notes,
|
|
154
160
|
})
|
|
155
161
|
},
|
|
156
162
|
|
|
163
|
+
{
|
|
164
|
+
id: 'organization',
|
|
165
|
+
name: 'Groep',
|
|
166
|
+
width: 40,
|
|
167
|
+
getValue: (member: PlatformMember) => {
|
|
168
|
+
const organizations = member.filterOrganizations({currentPeriod: true, types: [GroupType.Membership]})
|
|
169
|
+
const str = Formatter.joinLast(organizations.map(o => o.name).sort(), ', ', ' en ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f')
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
value: str
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
{
|
|
178
|
+
id: 'uri',
|
|
179
|
+
name: 'Groepsnummer',
|
|
180
|
+
width: 30,
|
|
181
|
+
getValue: (member: PlatformMember) => {
|
|
182
|
+
const organizations = member.filterOrganizations({currentPeriod: true, types: [GroupType.Membership]})
|
|
183
|
+
const str = Formatter.joinLast(organizations.map(o => o.uri).sort(), ', ', ' en ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f')
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
value: str
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
id: 'group',
|
|
193
|
+
name: 'Leeftijdsgroep',
|
|
194
|
+
width: 40,
|
|
195
|
+
getValue: (member: PlatformMember) => {
|
|
196
|
+
const groups = member.filterRegistrations({currentPeriod: true, types: [GroupType.Membership], organizationId: Context.organization?.id})
|
|
197
|
+
const str = Formatter.joinLast(Formatter.uniqueArray(groups.map(o => o.group.settings.name)).sort(), ', ', ' en ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f')
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
value: str
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
{
|
|
206
|
+
id: 'defaultAgeGroup',
|
|
207
|
+
name: 'Standaard leeftijdsgroep',
|
|
208
|
+
width: 40,
|
|
209
|
+
getValue: (member: PlatformMember) => {
|
|
210
|
+
const groups = member.filterRegistrations({currentPeriod: true, types: [GroupType.Membership], organizationId: Context.organization?.id})
|
|
211
|
+
const defaultAgeGroupIds = Formatter.uniqueArray(groups.filter(o => o.group.defaultAgeGroupId))
|
|
212
|
+
const defaultAgeGroups = defaultAgeGroupIds.map(o => PlatformStruct.shared.config.defaultAgeGroups.find(g => g.id === o.group.defaultAgeGroupId)?.name ?? 'verwijderde leeftijdsgroep')
|
|
213
|
+
const str = Formatter.joinLast(Formatter.uniqueArray(defaultAgeGroups).sort(), ', ', ' en ') || Context.i18n.$t('1a16a32a-7ee4-455d-af3d-6073821efa8f')
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
value: str
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
157
221
|
...XlsxTransformerColumnHelper.creatColumnsForParents(),
|
|
158
222
|
|
|
159
223
|
// unverified data
|
|
@@ -161,7 +225,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
161
225
|
id: 'unverifiedPhones',
|
|
162
226
|
name: 'Niet-geverifieerde telefoonnummers',
|
|
163
227
|
width: 20,
|
|
164
|
-
getValue: (object:
|
|
228
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
165
229
|
value: object.details.unverifiedPhones.join(', '),
|
|
166
230
|
})
|
|
167
231
|
},
|
|
@@ -169,7 +233,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
169
233
|
id: 'unverifiedEmails',
|
|
170
234
|
name: 'Niet-geverifieerde e-mailadressen',
|
|
171
235
|
width: 20,
|
|
172
|
-
getValue: (object:
|
|
236
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
173
237
|
value: object.details.unverifiedEmails.join(', '),
|
|
174
238
|
})
|
|
175
239
|
},
|
|
@@ -183,7 +247,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
183
247
|
id: 'unverifiedAddresses',
|
|
184
248
|
name: 'Niet-geverifieerde adressen',
|
|
185
249
|
width: 20,
|
|
186
|
-
getValue: (object:
|
|
250
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
187
251
|
value: object.details.unverifiedAddresses.map(a => a.toString()).join('; '),
|
|
188
252
|
})
|
|
189
253
|
},
|
|
@@ -191,9 +255,8 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
191
255
|
// Dynamic records
|
|
192
256
|
{
|
|
193
257
|
match(id) {
|
|
194
|
-
console.log('match', id)
|
|
195
258
|
if (id.startsWith('recordAnswers.')) {
|
|
196
|
-
const platform =
|
|
259
|
+
const platform = PlatformStruct.shared
|
|
197
260
|
const organization = Context.organization
|
|
198
261
|
|
|
199
262
|
const recordSettings = [
|
|
@@ -219,7 +282,7 @@ ExportToExcelEndpoint.loaders.set(ExcelExportType.Members, {
|
|
|
219
282
|
id: `recordAnswers.${recordSettingId}.${index}`,
|
|
220
283
|
name: columnName,
|
|
221
284
|
width: 20,
|
|
222
|
-
getValue: (object:
|
|
285
|
+
getValue: ({patchedMember: object}: PlatformMember) => ({
|
|
223
286
|
value: object.details.recordAnswers.get(recordSettingId)?.excelValues[index]?.value ?? ''
|
|
224
287
|
})
|
|
225
288
|
}
|
package/src/helpers/Context.ts
CHANGED
|
@@ -133,8 +133,8 @@ export class ContextInstance {
|
|
|
133
133
|
return await this.setOrganizationScope()
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
async setOrganizationScope() {
|
|
137
|
-
const organization = await Organization.fromApiHost(this.request.host);
|
|
136
|
+
async setOrganizationScope(options?: {allowInactive?: boolean}) {
|
|
137
|
+
const organization = await Organization.fromApiHost(this.request.host, options);
|
|
138
138
|
|
|
139
139
|
this.organization = organization
|
|
140
140
|
this.i18n.switchToLocale({ country: organization.address.country })
|
|
@@ -27,6 +27,7 @@ export const MembershipCharger = {
|
|
|
27
27
|
|
|
28
28
|
let createdCount = 0;
|
|
29
29
|
let createdPrice = 0;
|
|
30
|
+
const chunkSize = 100;
|
|
30
31
|
|
|
31
32
|
// eslint-disable-next-line no-constant-condition
|
|
32
33
|
while (true) {
|
|
@@ -35,7 +36,7 @@ export const MembershipCharger = {
|
|
|
35
36
|
.where('balanceItemId', null)
|
|
36
37
|
.where('deletedAt', null)
|
|
37
38
|
.whereNot('organizationId', chargeVia)
|
|
38
|
-
.limit(
|
|
39
|
+
.limit(chunkSize)
|
|
39
40
|
.orderBy(
|
|
40
41
|
new SQLOrderBy({
|
|
41
42
|
column: SQL.column('id'),
|
|
@@ -57,6 +58,7 @@ export const MembershipCharger = {
|
|
|
57
58
|
if (membership.balanceItemId) {
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
61
|
+
|
|
60
62
|
const type = getType(membership.membershipTypeId);
|
|
61
63
|
if (!type) {
|
|
62
64
|
console.error('Unknown membership type id ', membership.membershipTypeId)
|
|
@@ -74,6 +76,14 @@ export const MembershipCharger = {
|
|
|
74
76
|
continue;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
// Force price update (required because could have changed - especially for free memberships in combination with deletes)
|
|
80
|
+
try {
|
|
81
|
+
await membership.calculatePrice(member)
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('Failed to update price for membership. Not charged.', membership.id, e)
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
const balanceItem = new BalanceItem();
|
|
78
88
|
balanceItem.unitPrice = membership.price
|
|
79
89
|
balanceItem.amount = 1
|
|
@@ -101,6 +111,7 @@ export const MembershipCharger = {
|
|
|
101
111
|
|
|
102
112
|
await balanceItem.save();
|
|
103
113
|
membership.balanceItemId = balanceItem.id;
|
|
114
|
+
membership.maximumFreeAmount = membership.freeAmount;
|
|
104
115
|
await membership.save()
|
|
105
116
|
|
|
106
117
|
createdBalanceItems.push(balanceItem)
|
|
@@ -111,7 +122,7 @@ export const MembershipCharger = {
|
|
|
111
122
|
|
|
112
123
|
await BalanceItem.updateOutstanding(createdBalanceItems)
|
|
113
124
|
|
|
114
|
-
if (memberships.length <
|
|
125
|
+
if (memberships.length < chunkSize) {
|
|
115
126
|
break;
|
|
116
127
|
}
|
|
117
128
|
|
|
@@ -124,5 +135,76 @@ export const MembershipCharger = {
|
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
console.log('Charged ' + Formatter.integer(createdCount) +' memberships, for a total value of ' + Formatter.price(createdPrice))
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async updatePrices(organizationId?: string) {
|
|
141
|
+
console.log('Update prices...')
|
|
142
|
+
|
|
143
|
+
// Loop all
|
|
144
|
+
let lastId = "";
|
|
145
|
+
let createdCount = 0;
|
|
146
|
+
const chunkSize = 100;
|
|
147
|
+
|
|
148
|
+
// eslint-disable-next-line no-constant-condition
|
|
149
|
+
while (true) {
|
|
150
|
+
const q = MemberPlatformMembership.select()
|
|
151
|
+
.where('id', SQLWhereSign.Greater, lastId)
|
|
152
|
+
.where('balanceItemId', null)
|
|
153
|
+
.where('deletedAt', null);
|
|
154
|
+
|
|
155
|
+
if (organizationId) {
|
|
156
|
+
q.where('organizationId', organizationId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const memberships = await q
|
|
160
|
+
.limit(chunkSize)
|
|
161
|
+
.orderBy(
|
|
162
|
+
new SQLOrderBy({
|
|
163
|
+
column: SQL.column('id'),
|
|
164
|
+
direction: 'ASC'
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
.fetch();
|
|
168
|
+
|
|
169
|
+
if (memberships.length === 0) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const memberIds = Formatter.uniqueArray(memberships.map(m => m.memberId))
|
|
174
|
+
const members = await Member.getByIDs(...memberIds)
|
|
175
|
+
|
|
176
|
+
for (const membership of memberships) {
|
|
177
|
+
const member = members.find(m => m.id === membership.memberId)
|
|
178
|
+
|
|
179
|
+
if (!member) {
|
|
180
|
+
console.error('Unexpected missing member id ', membership.memberId, 'for membership', membership.id)
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Force price update (required because could have changed - especially for free memberships in combination with deletes)
|
|
185
|
+
try {
|
|
186
|
+
await membership.calculatePrice(member)
|
|
187
|
+
await membership.save()
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error('Failed to update price for membership', membership.id, e)
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
console.log('Updated price for membership', membership.id, membership.price)
|
|
193
|
+
createdCount += 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (memberships.length < chunkSize) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const z = lastId;
|
|
201
|
+
lastId = memberships[memberships.length - 1].id;
|
|
202
|
+
|
|
203
|
+
if (lastId === z) {
|
|
204
|
+
throw new Error('Unexpected infinite loop found in MembershipCharger')
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log('Updated prices of ' + Formatter.integer(createdCount) +' memberships')
|
|
127
209
|
}
|
|
128
210
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { LimitedFilteredRequest, PaginatedResponse } from "@stamhoofd/structures";
|
|
1
|
+
import { IPaginatedResponse, LimitedFilteredRequest } from "@stamhoofd/structures";
|
|
3
2
|
|
|
4
|
-
export function fetchToAsyncIterator<T
|
|
3
|
+
export function fetchToAsyncIterator<T>(
|
|
5
4
|
initialFilter: LimitedFilteredRequest,
|
|
6
5
|
loader: {
|
|
7
|
-
fetch(request: LimitedFilteredRequest): Promise<
|
|
6
|
+
fetch(request: LimitedFilteredRequest): Promise<IPaginatedResponse<T, LimitedFilteredRequest>>
|
|
8
7
|
}
|
|
9
8
|
): AsyncIterable<T> {
|
|
10
9
|
return {
|