@stamhoofd/backend 2.55.2 → 2.57.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/index.ts +4 -0
- package/package.json +12 -11
- package/src/crons.ts +4 -3
- package/src/endpoints/global/audit-logs/GetAuditLogsEndpoint.ts +150 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +27 -3
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +27 -9
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +2 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +17 -2
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +17 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +12 -9
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +10 -1
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +5 -3
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +21 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +4 -306
- package/src/helpers/AdminPermissionChecker.ts +102 -1
- package/src/helpers/AuthenticatedStructures.ts +46 -2
- package/src/helpers/EmailResumer.ts +8 -3
- package/src/seeds/1732117645-move-rrn.ts +77 -0
- package/src/services/AuditLogService.ts +232 -0
- package/src/services/BalanceItemPaymentService.ts +45 -0
- package/src/services/BalanceItemService.ts +88 -0
- package/src/services/GroupService.ts +13 -0
- package/src/services/PaymentService.ts +308 -0
- package/src/services/RegistrationService.ts +78 -0
- package/src/services/explainPatch.ts +639 -0
- package/src/sql-filters/audit-logs.ts +10 -0
- package/src/sql-sorters/audit-logs.ts +35 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
2
|
-
import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
2
|
+
import { AuditLogType, GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
6
|
import { Group, Member, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod, SetupStepUpdater } from '@stamhoofd/models';
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { AuditLogService } from '../../../../services/AuditLogService';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -281,6 +282,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
281
282
|
model.deletedAt = new Date();
|
|
282
283
|
await model.save();
|
|
283
284
|
Member.updateMembershipsForGroupId(id);
|
|
285
|
+
|
|
286
|
+
await AuditLogService.log({
|
|
287
|
+
type: AuditLogType.GroupDeleted,
|
|
288
|
+
group: model,
|
|
289
|
+
});
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null) {
|
|
@@ -289,6 +295,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
289
295
|
if (!model || !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
|
|
290
296
|
throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te wijzigen');
|
|
291
297
|
}
|
|
298
|
+
const originalStruct = (await AuthenticatedStructures.group(model)).clone(); // Clone is required for deep changes
|
|
292
299
|
|
|
293
300
|
const previousProperties = {
|
|
294
301
|
deletedAt: model.deletedAt,
|
|
@@ -417,6 +424,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
417
424
|
if (struct.deletedAt !== undefined || struct.defaultAgeGroupId !== undefined) {
|
|
418
425
|
Member.updateMembershipsForGroupId(model.id);
|
|
419
426
|
}
|
|
427
|
+
|
|
428
|
+
await AuditLogService.log({
|
|
429
|
+
type: AuditLogType.GroupEdited,
|
|
430
|
+
group: model,
|
|
431
|
+
oldData: originalStruct,
|
|
432
|
+
patch,
|
|
433
|
+
});
|
|
420
434
|
}
|
|
421
435
|
|
|
422
436
|
static async createGroup(struct: GroupStruct, organizationId: string, period: RegistrationPeriod, options?: { allowedIds?: string[] }): Promise<Group> {
|
|
@@ -497,6 +511,12 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
497
511
|
|
|
498
512
|
await model.save();
|
|
499
513
|
await model.updateOccupancy({ isNew: true }); // Force update steps
|
|
514
|
+
|
|
515
|
+
await AuditLogService.log({
|
|
516
|
+
type: AuditLogType.GroupAdded,
|
|
517
|
+
group: model,
|
|
518
|
+
});
|
|
519
|
+
|
|
500
520
|
return model;
|
|
501
521
|
}
|
|
502
522
|
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import { createMollieClient, PaymentStatus as MolliePaymentStatus } from '@mollie/api-client';
|
|
2
1
|
import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
|
|
3
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
3
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { PaymentGeneral, PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
4
|
+
import { Order } from '@stamhoofd/models';
|
|
5
|
+
import { PaymentGeneral } from '@stamhoofd/structures';
|
|
8
6
|
|
|
9
7
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
10
|
-
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
11
8
|
import { Context } from '../../../helpers/Context';
|
|
12
|
-
import {
|
|
9
|
+
import { PaymentService } from '../../../services/PaymentService';
|
|
13
10
|
|
|
14
11
|
type Params = { id: string };
|
|
15
12
|
class Query extends AutoEncoder {
|
|
@@ -52,7 +49,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
// Not method on payment because circular references (not supprted in ts)
|
|
55
|
-
const payment = await
|
|
52
|
+
const payment = await PaymentService.pollStatus(request.params.id, organization, request.query.cancel);
|
|
56
53
|
if (!payment) {
|
|
57
54
|
throw new SimpleError({
|
|
58
55
|
code: '',
|
|
@@ -82,303 +79,4 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
82
79
|
await AuthenticatedStructures.paymentGeneral(payment, checkPermissions),
|
|
83
80
|
);
|
|
84
81
|
}
|
|
85
|
-
|
|
86
|
-
static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
87
|
-
if (payment.status === status) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (status === PaymentStatus.Succeeded) {
|
|
92
|
-
payment.status = PaymentStatus.Succeeded;
|
|
93
|
-
payment.paidAt = new Date();
|
|
94
|
-
await payment.save();
|
|
95
|
-
|
|
96
|
-
// Prevent concurrency issues
|
|
97
|
-
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
98
|
-
const unloaded = (await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment));
|
|
99
|
-
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
100
|
-
unloaded,
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
for (const balanceItemPayment of balanceItemPayments) {
|
|
104
|
-
await balanceItemPayment.markPaid(organization);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const oldStatus = payment.status;
|
|
113
|
-
|
|
114
|
-
// Save before updating balance items
|
|
115
|
-
payment.status = status;
|
|
116
|
-
payment.paidAt = null;
|
|
117
|
-
await payment.save();
|
|
118
|
-
|
|
119
|
-
// If OLD status was succeeded, we need to revert the actions
|
|
120
|
-
if (oldStatus === PaymentStatus.Succeeded) {
|
|
121
|
-
// No longer succeeded
|
|
122
|
-
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
123
|
-
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
124
|
-
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
for (const balanceItemPayment of balanceItemPayments) {
|
|
128
|
-
await balanceItemPayment.undoPaid(organization);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Moved to failed
|
|
136
|
-
if (status == PaymentStatus.Failed) {
|
|
137
|
-
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
138
|
-
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
139
|
-
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
for (const balanceItemPayment of balanceItemPayments) {
|
|
143
|
-
await balanceItemPayment.markFailed(organization);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// If OLD status was FAILED, we need to revert the actions
|
|
151
|
-
if (oldStatus === PaymentStatus.Failed) { // OLD FAILED!! -> NOW PENDING
|
|
152
|
-
await QueueHandler.schedule('balance-item-update/' + organization.id, async () => {
|
|
153
|
-
const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
|
|
154
|
-
(await BalanceItemPayment.where({ paymentId: payment.id })).map(r => r.setRelation(BalanceItemPayment.payment, payment)),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
for (const balanceItemPayment of balanceItemPayments) {
|
|
158
|
-
await balanceItemPayment.undoFailed(organization);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem));
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* ID of payment is needed because of race conditions (need to fetch payment in a race condition save queue)
|
|
168
|
-
*/
|
|
169
|
-
static async pollStatus(paymentId: string, org: Organization | null, cancel = false): Promise<Payment | undefined> {
|
|
170
|
-
// Prevent polling the same payment multiple times at the same time: create a queue to prevent races
|
|
171
|
-
return await QueueHandler.schedule('payments/' + paymentId, async () => {
|
|
172
|
-
// Get a new copy of the payment (is required to prevent concurreny bugs)
|
|
173
|
-
const payment = await Payment.getByID(paymentId);
|
|
174
|
-
if (!payment) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!payment.organizationId) {
|
|
179
|
-
console.error('Payment without organization not supported', payment.id);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const organization = org ?? await Organization.getByID(payment.organizationId);
|
|
184
|
-
if (!organization) {
|
|
185
|
-
console.error('Organization not found for payment', payment.id);
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const testMode = organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production';
|
|
190
|
-
|
|
191
|
-
if (payment.status == PaymentStatus.Pending || payment.status == PaymentStatus.Created || (payment.provider === PaymentProvider.Buckaroo && payment.status == PaymentStatus.Failed)) {
|
|
192
|
-
if (payment.provider === PaymentProvider.Stripe) {
|
|
193
|
-
try {
|
|
194
|
-
let status = await StripeHelper.getStatus(payment, cancel || this.shouldTryToCancel(payment.status, payment), testMode);
|
|
195
|
-
|
|
196
|
-
if (this.isManualExpired(status, payment)) {
|
|
197
|
-
console.error('Manually marking Stripe payment as expired', payment.id);
|
|
198
|
-
status = PaymentStatus.Failed;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
await this.handlePaymentStatusUpdate(payment, organization, status);
|
|
202
|
-
}
|
|
203
|
-
catch (e) {
|
|
204
|
-
console.error('Payment check failed Stripe', payment.id, e);
|
|
205
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
206
|
-
console.error('Manually marking Stripe payment as expired', payment.id);
|
|
207
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else if (payment.provider === PaymentProvider.Mollie) {
|
|
212
|
-
// check status via mollie
|
|
213
|
-
const molliePayments = await MolliePayment.where({ paymentId: payment.id }, { limit: 1 });
|
|
214
|
-
if (molliePayments.length == 1) {
|
|
215
|
-
const molliePayment = molliePayments[0];
|
|
216
|
-
// check status
|
|
217
|
-
const token = await MollieToken.getTokenFor(organization.id);
|
|
218
|
-
|
|
219
|
-
if (token) {
|
|
220
|
-
try {
|
|
221
|
-
const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
|
|
222
|
-
const mollieData = await mollieClient.payments.get(molliePayment.mollieId, {
|
|
223
|
-
testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
console.log(mollieData); // log to log files to check issues
|
|
227
|
-
|
|
228
|
-
const details = mollieData.details as any;
|
|
229
|
-
if (details?.consumerName) {
|
|
230
|
-
payment.ibanName = details.consumerName;
|
|
231
|
-
}
|
|
232
|
-
if (details?.consumerAccount) {
|
|
233
|
-
payment.iban = details.consumerAccount;
|
|
234
|
-
}
|
|
235
|
-
if (details?.cardHolder) {
|
|
236
|
-
payment.ibanName = details.cardHolder;
|
|
237
|
-
}
|
|
238
|
-
if (details?.cardNumber) {
|
|
239
|
-
payment.iban = 'xxxx xxxx xxxx ' + details.cardNumber;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (mollieData.status === MolliePaymentStatus.paid) {
|
|
243
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Succeeded);
|
|
244
|
-
}
|
|
245
|
-
else if (mollieData.status === MolliePaymentStatus.failed || mollieData.status === MolliePaymentStatus.expired || mollieData.status === MolliePaymentStatus.canceled) {
|
|
246
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
247
|
-
}
|
|
248
|
-
else if (this.isManualExpired(payment.status, payment)) {
|
|
249
|
-
// Mollie still returning pending after 1 day: mark as failed
|
|
250
|
-
console.error('Manually marking Mollie payment as expired', payment.id);
|
|
251
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
catch (e) {
|
|
255
|
-
console.error('Payment check failed Mollie', payment.id, e);
|
|
256
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
257
|
-
console.error('Manually marking Mollie payment as expired', payment.id);
|
|
258
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
console.warn('Mollie payment is missing for organization ' + organization.id + ' while checking payment status...');
|
|
264
|
-
|
|
265
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
266
|
-
console.error('Manually marking payment without mollie token as expired', payment.id);
|
|
267
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
else {
|
|
272
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
273
|
-
console.error('Manually marking payment without mollie payments as expired', payment.id);
|
|
274
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
else if (payment.provider == PaymentProvider.Buckaroo) {
|
|
279
|
-
const helper = new BuckarooHelper(organization.privateMeta.buckarooSettings?.key ?? '', organization.privateMeta.buckarooSettings?.secret ?? '', organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production');
|
|
280
|
-
try {
|
|
281
|
-
let status = await helper.getStatus(payment);
|
|
282
|
-
|
|
283
|
-
if (this.isManualExpired(status, payment)) {
|
|
284
|
-
console.error('Manually marking Buckaroo payment as expired', payment.id);
|
|
285
|
-
status = PaymentStatus.Failed;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
await this.handlePaymentStatusUpdate(payment, organization, status);
|
|
289
|
-
}
|
|
290
|
-
catch (e) {
|
|
291
|
-
console.error('Payment check failed Buckaroo', payment.id, e);
|
|
292
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
293
|
-
console.error('Manually marking Buckaroo payment as expired', payment.id);
|
|
294
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else if (payment.provider == PaymentProvider.Payconiq) {
|
|
299
|
-
// Check status
|
|
300
|
-
|
|
301
|
-
const payconiqPayments = await PayconiqPayment.where({ paymentId: payment.id }, { limit: 1 });
|
|
302
|
-
if (payconiqPayments.length == 1) {
|
|
303
|
-
const payconiqPayment = payconiqPayments[0];
|
|
304
|
-
|
|
305
|
-
if (cancel) {
|
|
306
|
-
console.error('Cancelling Payconiq payment on request', payment.id);
|
|
307
|
-
await payconiqPayment.cancel(organization);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
let status = await payconiqPayment.getStatus(organization);
|
|
311
|
-
|
|
312
|
-
if (!cancel && this.shouldTryToCancel(status, payment)) {
|
|
313
|
-
console.error('Manually cancelling Payconiq payment', payment.id);
|
|
314
|
-
if (await payconiqPayment.cancel(organization)) {
|
|
315
|
-
status = PaymentStatus.Failed;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (this.isManualExpired(status, payment)) {
|
|
320
|
-
console.error('Manually marking Payconiq payment as expired', payment.id);
|
|
321
|
-
status = PaymentStatus.Failed;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
await this.handlePaymentStatusUpdate(payment, organization, status);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
console.warn('Payconiq payment is missing for organization ' + organization.id + ' while checking payment status...');
|
|
328
|
-
|
|
329
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
330
|
-
console.error('Manually marking Payconiq payment as expired because not found', payment.id);
|
|
331
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
console.error('Invalid payment provider', payment.provider, 'for payment', payment.id);
|
|
337
|
-
if (this.isManualExpired(payment.status, payment)) {
|
|
338
|
-
console.error('Manually marking unknown payment as expired', payment.id);
|
|
339
|
-
await this.handlePaymentStatusUpdate(payment, organization, PaymentStatus.Failed);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
// Do a manual update if needed
|
|
345
|
-
if (payment.status === PaymentStatus.Succeeded) {
|
|
346
|
-
if (payment.provider === PaymentProvider.Stripe) {
|
|
347
|
-
// Update the status
|
|
348
|
-
await StripeHelper.getStatus(payment, false, testMode);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return payment;
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
static isManualExpired(status: PaymentStatus, payment: Payment) {
|
|
357
|
-
if ((status == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
358
|
-
// If payment is not succeeded after one day, mark as failed
|
|
359
|
-
if (payment.createdAt < new Date(new Date().getTime() - 60 * 1000 * 60 * 24)) {
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Try to cancel a payment that is still pending
|
|
368
|
-
*/
|
|
369
|
-
static shouldTryToCancel(status: PaymentStatus, payment: Payment) {
|
|
370
|
-
if ((status == PaymentStatus.Pending || status === PaymentStatus.Created) && payment.method !== PaymentMethod.DirectDebit) {
|
|
371
|
-
let timeout = STAMHOOFD.environment === 'development' ? 60 * 1000 * 2 : 60 * 1000 * 30;
|
|
372
|
-
|
|
373
|
-
// If payconiq and not yet 'identified' (scanned), cancel after 5 minutes
|
|
374
|
-
if (payment.provider === PaymentProvider.Payconiq && status === PaymentStatus.Created) {
|
|
375
|
-
timeout = STAMHOOFD.environment === 'development' ? 60 * 1000 * 1 : 60 * 1000 * 5;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (payment.createdAt < new Date(new Date().getTime() - timeout)) {
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
82
|
}
|
|
@@ -520,7 +520,18 @@ export class AdminPermissionChecker {
|
|
|
520
520
|
}
|
|
521
521
|
|
|
522
522
|
if (!user.organizationId) {
|
|
523
|
-
|
|
523
|
+
if (this.hasPlatformFullAccess()) {
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check if this user has permissions for the current scoped organization
|
|
528
|
+
if (this.organization && await this.hasFullAccess(this.organization.id)) {
|
|
529
|
+
if (user.permissions?.forOrganization(this.organization)) {
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return false;
|
|
524
535
|
}
|
|
525
536
|
|
|
526
537
|
return await this.canManageAdmins(user.organizationId);
|
|
@@ -932,6 +943,60 @@ export class AdminPermissionChecker {
|
|
|
932
943
|
return false;
|
|
933
944
|
}
|
|
934
945
|
|
|
946
|
+
/**
|
|
947
|
+
* Return a list of RecordSettings the current user can view or edit
|
|
948
|
+
*/
|
|
949
|
+
async hasNRNAccess(member: MemberWithRegistrations, level: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
|
|
950
|
+
const isUserManager = this.isUserManager(member);
|
|
951
|
+
|
|
952
|
+
if (isUserManager) {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (!await this.canAccessMember(member, level)) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// First list all organizations this member is part of
|
|
961
|
+
const organizations: Organization[] = [];
|
|
962
|
+
|
|
963
|
+
if (member.organizationId) {
|
|
964
|
+
if (this.checkScope(member.organizationId)) {
|
|
965
|
+
organizations.push(await this.getOrganization(member.organizationId));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
for (const registration of member.registrations) {
|
|
970
|
+
if (this.checkScope(registration.organizationId)) {
|
|
971
|
+
if (!organizations.find(o => o.id === registration.organizationId)) {
|
|
972
|
+
organizations.push(await this.getOrganization(registration.organizationId));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Loop all organizations.
|
|
978
|
+
for (const organization of organizations) {
|
|
979
|
+
const permissions = await this.getOrganizationPermissions(organization);
|
|
980
|
+
if (!permissions) {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (permissions.hasAccessRight(AccessRight.MemberManageNRN)) {
|
|
985
|
+
return true;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Platform data
|
|
990
|
+
const platformPermissions = this.platformPermissions;
|
|
991
|
+
if (platformPermissions) {
|
|
992
|
+
if (platformPermissions.hasAccessRight(AccessRight.MemberManageNRN)) {
|
|
993
|
+
return true;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
935
1000
|
/**
|
|
936
1001
|
* Return a list of RecordSettings the current user can view or edit
|
|
937
1002
|
*/
|
|
@@ -1006,6 +1071,14 @@ export class AdminPermissionChecker {
|
|
|
1006
1071
|
cloned.details.securityCode = null;
|
|
1007
1072
|
}
|
|
1008
1073
|
|
|
1074
|
+
if (!await this.hasNRNAccess(member, PermissionLevel.Read)) {
|
|
1075
|
+
cloned.details.nationalRegisterNumber = null;
|
|
1076
|
+
|
|
1077
|
+
for (const parent of cloned.details.parents) {
|
|
1078
|
+
parent.nationalRegisterNumber = null;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1009
1082
|
return cloned;
|
|
1010
1083
|
}
|
|
1011
1084
|
|
|
@@ -1021,6 +1094,14 @@ export class AdminPermissionChecker {
|
|
|
1021
1094
|
});
|
|
1022
1095
|
}
|
|
1023
1096
|
|
|
1097
|
+
if (Array.isArray(data.details.parents)) {
|
|
1098
|
+
throw new SimpleError({
|
|
1099
|
+
code: 'invalid_request',
|
|
1100
|
+
message: 'Cannot PUT a full member details parents',
|
|
1101
|
+
statusCode: 400,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1024
1105
|
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
1025
1106
|
const hasNotes = data.details.notes !== undefined;
|
|
1026
1107
|
const isSetFinancialSupportTrue = data.details.shouldApplyReducedPrice;
|
|
@@ -1155,6 +1236,26 @@ export class AdminPermissionChecker {
|
|
|
1155
1236
|
}
|
|
1156
1237
|
}
|
|
1157
1238
|
|
|
1239
|
+
if (!await this.hasNRNAccess(member, PermissionLevel.Write)) {
|
|
1240
|
+
if (data.details.nationalRegisterNumber) {
|
|
1241
|
+
throw new SimpleError({
|
|
1242
|
+
code: 'permission_denied',
|
|
1243
|
+
message: 'Je hebt geen toegangsrechten om het rijksregisternummer van dit lid aan te passen',
|
|
1244
|
+
statusCode: 400,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
for (const parent of data.details.parents.getPatches()) {
|
|
1249
|
+
if (parent.nationalRegisterNumber) {
|
|
1250
|
+
throw new SimpleError({
|
|
1251
|
+
code: 'permission_denied',
|
|
1252
|
+
message: 'Je hebt geen toegangsrechten om het rijksregisternummer van een ouder aan te passen',
|
|
1253
|
+
statusCode: 400,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1158
1259
|
return data;
|
|
1159
1260
|
}
|
|
1160
1261
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
|
|
3
|
-
import {
|
|
2
|
+
import { AuditLog, BalanceItem, CachedBalance, Document, Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, Ticket, User, Webshop } from '@stamhoofd/models';
|
|
3
|
+
import { AuditLog as AuditLogStruct, Document as DocumentStruct, Event as EventStruct, Group as GroupStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberWithRegistrationsBlob, MembersBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
|
|
4
4
|
|
|
5
5
|
import { Formatter } from '@stamhoofd/utility';
|
|
6
6
|
import { Context } from './Context';
|
|
@@ -639,4 +639,48 @@ export class AuthenticatedStructures {
|
|
|
639
639
|
|
|
640
640
|
return result;
|
|
641
641
|
}
|
|
642
|
+
|
|
643
|
+
static async auditLogs(logs: AuditLog[]): Promise<AuditLogStruct[]> {
|
|
644
|
+
const structs: AuditLogStruct[] = [];
|
|
645
|
+
|
|
646
|
+
const userIds = Formatter.uniqueArray(logs.map(l => l.userId).filter(id => id !== null));
|
|
647
|
+
const users = await User.getByIDs(...userIds);
|
|
648
|
+
|
|
649
|
+
for (const log of logs) {
|
|
650
|
+
const user = log.userId ? (users.find(u => u.id === log.userId) ?? null) : null;
|
|
651
|
+
let userStruct: NamedObject | null = null;
|
|
652
|
+
|
|
653
|
+
if (user) {
|
|
654
|
+
if (!await Context.auth.canAccessUser(user)) {
|
|
655
|
+
if (user.permissions?.platform !== null) {
|
|
656
|
+
userStruct = NamedObject.create({
|
|
657
|
+
id: '',
|
|
658
|
+
name: 'Beheerder van ' + Platform.shared.config.name,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
userStruct = NamedObject.create({
|
|
663
|
+
id: '',
|
|
664
|
+
name: 'Onbekend',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
userStruct = NamedObject.create({
|
|
670
|
+
id: user.id,
|
|
671
|
+
name: (user.firstName || user.lastName) ? (user.firstName + ' ' + user.lastName) : user.email,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
structs.push(
|
|
677
|
+
AuditLogStruct.create({
|
|
678
|
+
...log,
|
|
679
|
+
user: userStruct,
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return structs;
|
|
685
|
+
}
|
|
642
686
|
}
|
|
@@ -30,8 +30,13 @@ export async function resumeEmails() {
|
|
|
30
30
|
continue;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
await
|
|
35
|
-
|
|
33
|
+
try {
|
|
34
|
+
await ContextInstance.startForUser(user, organization, async () => {
|
|
35
|
+
await email.send();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
console.error('Error resuming email', email.id, e);
|
|
40
|
+
}
|
|
36
41
|
}
|
|
37
42
|
}
|