@stamhoofd/backend 2.59.0 → 2.61.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 +10 -10
- package/src/audit-logs/DocumentTemplateLogger.ts +22 -0
- package/src/audit-logs/EmailAddressLogger.ts +45 -0
- package/src/audit-logs/EmailLogger.ts +67 -0
- package/src/audit-logs/EmailTemplateLogger.ts +33 -0
- package/src/audit-logs/MemberPlatformMembershipLogger.ts +6 -3
- package/src/audit-logs/ModelLogger.ts +23 -6
- package/src/audit-logs/OrderLogger.ts +2 -2
- package/src/audit-logs/OrganizationLogger.ts +1 -11
- package/src/audit-logs/UserLogger.ts +45 -0
- package/src/crons/amazon-ses.ts +324 -0
- package/src/crons/clearExcelCache.ts +3 -0
- package/src/crons/endFunctionsOfUsersWithoutRegistration.ts +3 -0
- package/src/crons/index.ts +4 -0
- package/src/crons/postmark.ts +223 -0
- package/src/crons.ts +3 -315
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +3 -3
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +2 -1
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +38 -25
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +3 -11
- package/src/helpers/MemberUserSyncer.ts +11 -7
- package/src/helpers/PeriodHelper.ts +2 -1
- package/src/helpers/SetupStepUpdater.ts +503 -0
- package/src/seeds/1726847064-setup-steps.ts +1 -1
- package/src/seeds/1733319079-fill-paying-organization-ids.ts +68 -0
- package/src/services/AuditLogService.ts +19 -14
- package/src/services/DocumentService.ts +43 -0
- package/src/services/RegistrationService.ts +2 -0
- package/src/services/diff.ts +514 -0
- package/src/sql-filters/events.ts +13 -1
- package/src/crons/updateSetupSteps.ts +0 -9
- package/src/services/explainPatch.ts +0 -851
package/src/crons.ts
CHANGED
|
@@ -1,25 +1,13 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
2
1
|
import { Database } from '@simonbackx/simple-database';
|
|
3
|
-
import { Email, EmailAddress } from '@stamhoofd/email';
|
|
4
2
|
import { Group, Organization, Payment, Registration, STPackage, Webshop } from '@stamhoofd/models';
|
|
5
3
|
import { PaymentMethod, PaymentProvider, PaymentStatus } from '@stamhoofd/structures';
|
|
6
4
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
-
import AWS from 'aws-sdk';
|
|
8
|
-
import { DateTime } from 'luxon';
|
|
9
5
|
|
|
10
6
|
import { registerCron } from '@stamhoofd/crons';
|
|
11
|
-
import { clearExcelCache } from './crons/clearExcelCache';
|
|
12
|
-
import { endFunctionsOfUsersWithoutRegistration } from './crons/endFunctionsOfUsersWithoutRegistration';
|
|
13
|
-
import { ExchangePaymentEndpoint } from './endpoints/organization/shared/ExchangePaymentEndpoint';
|
|
14
7
|
import { checkSettlements } from './helpers/CheckSettlements';
|
|
15
|
-
import { ForwardHandler } from './helpers/ForwardHandler';
|
|
16
8
|
import { PaymentService } from './services/PaymentService';
|
|
17
9
|
import { RegistrationService } from './services/RegistrationService';
|
|
18
10
|
|
|
19
|
-
// Importing postmark returns undefined (this is a bug, so we need to use require)
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
21
|
-
const postmark = require('postmark');
|
|
22
|
-
|
|
23
11
|
let lastDNSCheck: Date | null = null;
|
|
24
12
|
let lastDNSId = '';
|
|
25
13
|
async function checkDNS() {
|
|
@@ -133,303 +121,6 @@ async function checkWebshopDNS() {
|
|
|
133
121
|
lastWebshopDNSId = webshops[webshops.length - 1].id;
|
|
134
122
|
}
|
|
135
123
|
|
|
136
|
-
async function checkReplies() {
|
|
137
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
console.log('Checking replies from AWS SQS');
|
|
142
|
-
const sqs = new AWS.SQS();
|
|
143
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding', MaxNumberOfMessages: 10 }).promise();
|
|
144
|
-
if (messages.Messages) {
|
|
145
|
-
for (const message of messages.Messages) {
|
|
146
|
-
console.log('Received message from forwarding queue');
|
|
147
|
-
|
|
148
|
-
if (message.ReceiptHandle) {
|
|
149
|
-
if (STAMHOOFD.environment === 'production') {
|
|
150
|
-
await sqs.deleteMessage({
|
|
151
|
-
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-email-forwarding',
|
|
152
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
153
|
-
}).promise();
|
|
154
|
-
console.log('Deleted from queue');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
if (message.Body) {
|
|
160
|
-
// decode the JSON value
|
|
161
|
-
const bounce = JSON.parse(message.Body);
|
|
162
|
-
|
|
163
|
-
if (bounce.Message) {
|
|
164
|
-
const message = JSON.parse(bounce.Message);
|
|
165
|
-
|
|
166
|
-
// Read message content
|
|
167
|
-
if (message.mail && message.content && message.receipt) {
|
|
168
|
-
const content = message.content;
|
|
169
|
-
const receipt = message.receipt as {
|
|
170
|
-
recipients: string[];
|
|
171
|
-
spamVerdict: { status: 'PASS' | string };
|
|
172
|
-
virusVerdict: { status: 'PASS' | string };
|
|
173
|
-
spfVerdict: { status: 'PASS' | string };
|
|
174
|
-
dkimVerdict: { status: 'PASS' | string };
|
|
175
|
-
dmarcVerdict: { status: 'PASS' | string };
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const options = await ForwardHandler.handle(content, receipt);
|
|
179
|
-
if (options) {
|
|
180
|
-
if (STAMHOOFD.environment === 'production') {
|
|
181
|
-
Email.send(options);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
catch (e) {
|
|
189
|
-
console.error(e);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
let lastPostmarkCheck: Date | null = null;
|
|
196
|
-
let lastPostmarkId: string | null = null;
|
|
197
|
-
async function checkPostmarkBounces() {
|
|
198
|
-
if (STAMHOOFD.environment !== 'production') {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const token = STAMHOOFD.POSTMARK_SERVER_TOKEN;
|
|
203
|
-
if (!token) {
|
|
204
|
-
console.log('[POSTMARK BOUNCES] No postmark token, skipping postmark bounces');
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const fromDate = (lastPostmarkCheck ?? new Date(new Date().getTime() - 24 * 60 * 60 * 1000 * 2));
|
|
208
|
-
const ET = DateTime.fromJSDate(fromDate).setZone('EST').toISO({ includeOffset: false });
|
|
209
|
-
console.log('[POSTMARK BOUNCES] Checking bounces from Postmark since', fromDate, ET);
|
|
210
|
-
const client = new postmark.ServerClient(token);
|
|
211
|
-
|
|
212
|
-
const bounces = await client.getBounces({
|
|
213
|
-
fromdate: ET,
|
|
214
|
-
todate: DateTime.now().setZone('EST').toISO({ includeOffset: false }),
|
|
215
|
-
count: 500,
|
|
216
|
-
offset: 0,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
if (bounces.TotalCount == 0) {
|
|
220
|
-
console.log('[POSTMARK BOUNCES] No Postmark bounces at this time');
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let lastId: string | null = null;
|
|
225
|
-
|
|
226
|
-
for (const bounce of bounces.Bounces) {
|
|
227
|
-
// Try to get the organization, if possible, else default to global blocking: "null", which is not visible for an organization, but it is applied
|
|
228
|
-
const source = bounce.From;
|
|
229
|
-
const organization = source ? await Organization.getByEmail(source) : undefined;
|
|
230
|
-
|
|
231
|
-
if (bounce.Type === 'HardBounce' || bounce.Type === 'BadEmailAddress' || bounce.Type === 'Blocked') {
|
|
232
|
-
// Block for everyone, but not visible
|
|
233
|
-
console.log('[POSTMARK BOUNCES] Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
234
|
-
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
235
|
-
emailAddress.hardBounce = true;
|
|
236
|
-
await emailAddress.save();
|
|
237
|
-
}
|
|
238
|
-
else if (bounce.Type === 'SpamComplaint' || bounce.Type === 'SpamNotification' || bounce.Type === 'VirusNotification') {
|
|
239
|
-
console.log('[POSTMARK BOUNCES] Postmark ' + bounce.Type + ' for: ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
240
|
-
const emailAddress = await EmailAddress.getOrCreate(bounce.Email, organization?.id ?? null);
|
|
241
|
-
emailAddress.markedAsSpam = true;
|
|
242
|
-
await emailAddress.save();
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
console.log('[POSTMARK BOUNCES] Unhandled Postmark ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
246
|
-
console.error('[POSTMARK BOUNCES] Unhandled Postmark ' + bounce.Type + ': ', bounce.Email, 'from', source, 'organization', organization?.name);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const bouncedAt = new Date(bounce.BouncedAt);
|
|
250
|
-
lastPostmarkCheck = lastPostmarkCheck ? new Date(Math.max(bouncedAt.getTime(), lastPostmarkCheck.getTime())) : bouncedAt;
|
|
251
|
-
|
|
252
|
-
lastId = bounce.ID;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (lastId && lastPostmarkId) {
|
|
256
|
-
if (lastId === lastPostmarkId) {
|
|
257
|
-
console.log('[POSTMARK BOUNCES] Postmark has no new bounces');
|
|
258
|
-
// Increase timestamp by one second to avoid refetching it every time
|
|
259
|
-
if (lastPostmarkCheck) {
|
|
260
|
-
lastPostmarkCheck = new Date(lastPostmarkCheck.getTime() + 1000);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
lastPostmarkId = lastId;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function checkBounces() {
|
|
268
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
console.log('[AWS BOUNCES] Checking bounces from AWS SQS');
|
|
273
|
-
const sqs = new AWS.SQS();
|
|
274
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue', MaxNumberOfMessages: 10 }).promise();
|
|
275
|
-
if (messages.Messages) {
|
|
276
|
-
for (const message of messages.Messages) {
|
|
277
|
-
console.log('[AWS BOUNCES] Received bounce message');
|
|
278
|
-
console.log('[AWS BOUNCES]', message);
|
|
279
|
-
|
|
280
|
-
if (message.ReceiptHandle) {
|
|
281
|
-
if (STAMHOOFD.environment === 'production') {
|
|
282
|
-
await sqs.deleteMessage({
|
|
283
|
-
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-bounces-queue',
|
|
284
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
285
|
-
}).promise();
|
|
286
|
-
console.log('[AWS BOUNCES] Deleted from queue');
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
try {
|
|
291
|
-
if (message.Body) {
|
|
292
|
-
// decode the JSON value
|
|
293
|
-
const bounce = JSON.parse(message.Body);
|
|
294
|
-
|
|
295
|
-
if (bounce.Message) {
|
|
296
|
-
const message = JSON.parse(bounce.Message);
|
|
297
|
-
|
|
298
|
-
if (message.bounce) {
|
|
299
|
-
const b = message.bounce;
|
|
300
|
-
// Block all receivers that generate a permanent bounce
|
|
301
|
-
const type = b.bounceType;
|
|
302
|
-
|
|
303
|
-
const source = message.mail.source;
|
|
304
|
-
|
|
305
|
-
// try to find organization that is responsible for this e-mail address
|
|
306
|
-
|
|
307
|
-
for (const recipient of b.bouncedRecipients) {
|
|
308
|
-
const email = recipient.emailAddress;
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
type === 'Permanent'
|
|
312
|
-
|| (
|
|
313
|
-
recipient.diagnosticCode && (
|
|
314
|
-
(recipient.diagnosticCode as string).toLowerCase().includes('invalid domain')
|
|
315
|
-
|| (recipient.diagnosticCode as string).toLowerCase().includes('unable to lookup dns')
|
|
316
|
-
)
|
|
317
|
-
)
|
|
318
|
-
) {
|
|
319
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
320
|
-
if (organization) {
|
|
321
|
-
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
322
|
-
emailAddress.hardBounce = true;
|
|
323
|
-
await emailAddress.save();
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
console.error('[AWS BOUNCES] Unknown organization for email address ' + source);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
console.log('[AWS BOUNCES] For domain ' + source);
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
console.log("[AWS BOUNCES] 'bounce' field missing in bounce message");
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
console.log("[AWS BOUNCES] 'Message' field missing in bounce message");
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
console.log('[AWS BOUNCES] Message Body missing in bounce');
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch (e) {
|
|
345
|
-
console.log('[AWS BOUNCES] Bounce message processing failed:');
|
|
346
|
-
console.error('[AWS BOUNCES] Bounce message processing failed:');
|
|
347
|
-
console.error('[AWS BOUNCES]', e);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function checkComplaints() {
|
|
354
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.AWS_ACCESS_KEY_ID) {
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
console.log('[AWS COMPLAINTS] Checking complaints from AWS SQS');
|
|
359
|
-
const sqs = new AWS.SQS();
|
|
360
|
-
const messages = await sqs.receiveMessage({ QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue', MaxNumberOfMessages: 10 }).promise();
|
|
361
|
-
if (messages.Messages) {
|
|
362
|
-
for (const message of messages.Messages) {
|
|
363
|
-
console.log('[AWS COMPLAINTS] Received complaint message');
|
|
364
|
-
console.log('[AWS COMPLAINTS]', message);
|
|
365
|
-
|
|
366
|
-
if (message.ReceiptHandle) {
|
|
367
|
-
if (STAMHOOFD.environment === 'production') {
|
|
368
|
-
await sqs.deleteMessage({
|
|
369
|
-
QueueUrl: 'https://sqs.eu-west-1.amazonaws.com/118244293157/stamhoofd-complaints-queue',
|
|
370
|
-
ReceiptHandle: message.ReceiptHandle,
|
|
371
|
-
}).promise();
|
|
372
|
-
console.log('[AWS COMPLAINTS] Deleted from queue');
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
if (message.Body) {
|
|
378
|
-
// decode the JSON value
|
|
379
|
-
const complaint = JSON.parse(message.Body);
|
|
380
|
-
console.log('[AWS COMPLAINTS]', complaint);
|
|
381
|
-
|
|
382
|
-
if (complaint.Message) {
|
|
383
|
-
const message = JSON.parse(complaint.Message);
|
|
384
|
-
|
|
385
|
-
if (message.complaint) {
|
|
386
|
-
const b = message.complaint;
|
|
387
|
-
const source = message.mail.source;
|
|
388
|
-
const organization: Organization | undefined = source ? await Organization.getByEmail(source) : undefined;
|
|
389
|
-
|
|
390
|
-
const type: 'abuse' | 'auth-failure' | 'fraud' | 'not-spam' | 'other' | 'virus' = b.complaintFeedbackType;
|
|
391
|
-
|
|
392
|
-
if (organization) {
|
|
393
|
-
for (const recipient of b.complainedRecipients) {
|
|
394
|
-
const email = recipient.emailAddress;
|
|
395
|
-
const emailAddress = await EmailAddress.getOrCreate(email, organization.id);
|
|
396
|
-
emailAddress.markedAsSpam = type !== 'not-spam';
|
|
397
|
-
await emailAddress.save();
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
console.error('[AWS COMPLAINTS] Unknown organization for email address ' + source);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (type == 'virus' || type == 'fraud') {
|
|
405
|
-
console.error('[AWS COMPLAINTS] Received virus / fraud complaint!');
|
|
406
|
-
console.error('[AWS COMPLAINTS]', complaint);
|
|
407
|
-
if (STAMHOOFD.environment === 'production') {
|
|
408
|
-
Email.sendWebmaster({
|
|
409
|
-
subject: 'Received a ' + type + ' email notification',
|
|
410
|
-
text: 'We received a ' + type + ' notification for an e-mail from the organization: ' + organization?.name + '. Please check and adjust if needed.\n',
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
console.log('[AWS COMPLAINTS] Missing complaint field');
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
console.log('[AWS COMPLAINTS] Missing message field in complaint');
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
catch (e) {
|
|
425
|
-
console.log('[AWS COMPLAINTS] Complain message processing failed:');
|
|
426
|
-
console.error('[AWS COMPLAINTS] Complain message processing failed:');
|
|
427
|
-
console.error('[AWS COMPLAINTS]', e);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
124
|
// Keep checking pending paymetns for 3 days
|
|
434
125
|
async function checkPayments() {
|
|
435
126
|
if (STAMHOOFD.environment === 'development') {
|
|
@@ -658,14 +349,11 @@ async function checkDrips() {
|
|
|
658
349
|
|
|
659
350
|
registerCron('checkSettlements', checkSettlements);
|
|
660
351
|
registerCron('checkExpirationEmails', checkExpirationEmails);
|
|
661
|
-
registerCron('checkPostmarkBounces', checkPostmarkBounces);
|
|
662
352
|
registerCron('checkReservedUntil', checkReservedUntil);
|
|
663
|
-
registerCron('checkComplaints', checkComplaints);
|
|
664
|
-
registerCron('checkReplies', checkReplies);
|
|
665
|
-
registerCron('checkBounces', checkBounces);
|
|
666
353
|
registerCron('checkDNS', checkDNS);
|
|
667
354
|
registerCron('checkWebshopDNS', checkWebshopDNS);
|
|
668
355
|
registerCron('checkPayments', checkPayments);
|
|
669
356
|
registerCron('checkDrips', checkDrips);
|
|
670
|
-
|
|
671
|
-
|
|
357
|
+
|
|
358
|
+
// Register other crons
|
|
359
|
+
import './crons/index.js';
|
|
@@ -2,8 +2,8 @@ import { OneToManyRelation } from '@simonbackx/simple-database';
|
|
|
2
2
|
import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } 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 { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration,
|
|
6
|
-
import {
|
|
5
|
+
import { BalanceItem, Document, Group, Member, MemberFactory, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, mergeTwoMembers, Organization, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
6
|
+
import { GroupType, MembersBlob, MemberWithRegistrationsBlob, PermissionLevel } from '@stamhoofd/structures';
|
|
7
7
|
import { Formatter } from '@stamhoofd/utility';
|
|
8
8
|
|
|
9
9
|
import { Email } from '@stamhoofd/email';
|
|
@@ -13,9 +13,9 @@ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructure
|
|
|
13
13
|
import { Context } from '../../../helpers/Context';
|
|
14
14
|
import { MembershipCharger } from '../../../helpers/MembershipCharger';
|
|
15
15
|
import { MemberUserSyncer } from '../../../helpers/MemberUserSyncer';
|
|
16
|
-
import { AuditLogService } from '../../../services/AuditLogService';
|
|
17
16
|
import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
|
|
18
17
|
import { RegistrationService } from '../../../services/RegistrationService';
|
|
18
|
+
import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
|
|
19
19
|
|
|
20
20
|
type Params = Record<string, never>;
|
|
21
21
|
type Query = undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, isPatchableArray, patchObject } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
|
-
import { Organization, Platform, RegistrationPeriod
|
|
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';
|
|
@@ -10,6 +10,7 @@ import { MembershipCharger } from '../../../helpers/MembershipCharger';
|
|
|
10
10
|
import { PeriodHelper } from '../../../helpers/PeriodHelper';
|
|
11
11
|
import { TagHelper } from '../../../helpers/TagHelper';
|
|
12
12
|
import { PlatformMembershipService } from '../../../services/PlatformMembershipService';
|
|
13
|
+
import { SetupStepUpdater } from '../../../helpers/SetupStepUpdater';
|
|
13
14
|
|
|
14
15
|
type Params = Record<string, never>;
|
|
15
16
|
type Query = undefined;
|
|
@@ -209,6 +209,18 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
// Who is going to pay?
|
|
213
|
+
let whoWillPayNow: 'member' | 'organization' | 'nobody' = 'member'; // if this is set to 'organization', there will also be created separate balance items so the member can pay back the paying organization
|
|
214
|
+
|
|
215
|
+
if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
|
|
216
|
+
// Will get added to the outstanding amount of the member / already paying organization
|
|
217
|
+
whoWillPayNow = 'nobody';
|
|
218
|
+
}
|
|
219
|
+
else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
|
|
220
|
+
// The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
|
|
221
|
+
whoWillPayNow = 'organization';
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
|
|
213
225
|
registrationMemberRelation.foreignKey = 'memberId';
|
|
214
226
|
|
|
@@ -264,6 +276,19 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
264
276
|
registration.options = item.options;
|
|
265
277
|
registration.recordAnswers = item.recordAnswers;
|
|
266
278
|
|
|
279
|
+
if (whoWillPayNow === 'organization' && request.body.asOrganizationId) {
|
|
280
|
+
registration.payingOrganizationId = request.body.asOrganizationId;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (whoWillPayNow === 'nobody' && item.replaceRegistrations.length > 0) {
|
|
284
|
+
// If the replace registration was paid by an organization
|
|
285
|
+
// Make sure this updated registration will also be paid by the organization, not the member
|
|
286
|
+
const paidAsOrganization = item.replaceRegistrations[0].payingOrganizationId;
|
|
287
|
+
if (paidAsOrganization) {
|
|
288
|
+
registration.payingOrganizationId = paidAsOrganization;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
267
292
|
payRegistrations.push({
|
|
268
293
|
registration,
|
|
269
294
|
item,
|
|
@@ -272,18 +297,6 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
272
297
|
registrations.push(registration);
|
|
273
298
|
}
|
|
274
299
|
|
|
275
|
-
// Who is going to pay?
|
|
276
|
-
let whoWillPayNow: 'member' | 'organization' | 'nobody' = 'member'; // if this is set to 'organization', there will also be created separate balance items so the member can pay back the paying organization
|
|
277
|
-
|
|
278
|
-
if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
|
|
279
|
-
// Will get added to the outstanding amount of the member
|
|
280
|
-
whoWillPayNow = 'nobody';
|
|
281
|
-
}
|
|
282
|
-
else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
|
|
283
|
-
// The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
|
|
284
|
-
whoWillPayNow = 'organization';
|
|
285
|
-
}
|
|
286
|
-
|
|
287
300
|
// Validate payment method
|
|
288
301
|
if (totalPrice > 0 && whoWillPayNow !== 'nobody') {
|
|
289
302
|
const allowedPaymentMethods = organization.meta.registrationPaymentConfiguration.getAvailablePaymentMethods({
|
|
@@ -369,8 +382,13 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
369
382
|
deactivatedRegistrationGroupIds.push(existingRegistration.groupId);
|
|
370
383
|
}
|
|
371
384
|
|
|
372
|
-
async function createBalanceItem({ registration, amount, unitPrice, description, type, relations }: { amount?: number; registration: RegistrationWithMemberAndGroup; unitPrice: number; description: string; relations: Map<BalanceItemRelationType, BalanceItemRelation>; type: BalanceItemType }) {
|
|
385
|
+
async function createBalanceItem({ registration, skipZero, amount, unitPrice, description, type, relations }: { amount?: number; skipZero?: boolean; registration: RegistrationWithMemberAndGroup; unitPrice: number; description: string; relations: Map<BalanceItemRelationType, BalanceItemRelation>; type: BalanceItemType }) {
|
|
373
386
|
// NOTE: We also need to save zero-price balance items because for online payments, we need to know which registrations to activate after payment
|
|
387
|
+
if (skipZero === true) {
|
|
388
|
+
if (unitPrice === 0 || amount === 0) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
374
392
|
|
|
375
393
|
// Create balance item
|
|
376
394
|
const balanceItem = new BalanceItem();
|
|
@@ -386,11 +404,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
386
404
|
|
|
387
405
|
// Who is responsible for payment?
|
|
388
406
|
let balanceItem2: BalanceItem | null = null;
|
|
389
|
-
if (
|
|
407
|
+
if (registration.payingOrganizationId) {
|
|
390
408
|
// Create a separate balance item for this meber to pay back the paying organization
|
|
391
409
|
// this is not yet associated with a payment but will be added to the outstanding balance of the member
|
|
392
410
|
|
|
393
|
-
balanceItem.payingOrganizationId =
|
|
411
|
+
balanceItem.payingOrganizationId = registration.payingOrganizationId;
|
|
394
412
|
|
|
395
413
|
balanceItem2 = new BalanceItem();
|
|
396
414
|
|
|
@@ -405,7 +423,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
405
423
|
balanceItem2.type = type;
|
|
406
424
|
|
|
407
425
|
// Who needs to receive this money?
|
|
408
|
-
balanceItem2.organizationId =
|
|
426
|
+
balanceItem2.organizationId = registration.payingOrganizationId;
|
|
409
427
|
|
|
410
428
|
// Who is responsible for payment?
|
|
411
429
|
balanceItem2.memberId = registration.memberId;
|
|
@@ -437,26 +455,19 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
437
455
|
const { item, registration } = bundle;
|
|
438
456
|
registration.reservedUntil = null;
|
|
439
457
|
|
|
440
|
-
/* if (shouldMarkValid) {
|
|
441
|
-
await registration.markValid({ skipEmail: bundle.item.replaceRegistrations.length > 0 });
|
|
442
|
-
}
|
|
443
|
-
else { */
|
|
444
458
|
// Reserve registration for 30 minutes (if needed)
|
|
445
459
|
const group = groups.find(g => g.id === registration.groupId);
|
|
446
460
|
|
|
447
461
|
if (group && group.settings.maxMembers !== null) {
|
|
448
462
|
registration.reservedUntil = new Date(new Date().getTime() + 1000 * 60 * 30);
|
|
449
463
|
}
|
|
464
|
+
|
|
465
|
+
// Only now save the registration
|
|
450
466
|
await registration.save();
|
|
451
|
-
// }
|
|
452
467
|
|
|
453
468
|
// Note: we should always create the balance items: even when the price is zero
|
|
454
469
|
// Otherwise we don't know which registrations to activate after payment
|
|
455
470
|
|
|
456
|
-
if (shouldMarkValid && item.calculatedPrice === 0) {
|
|
457
|
-
// continue;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
471
|
// Create balance items
|
|
461
472
|
const sharedRelations: [BalanceItemRelationType, BalanceItemRelation][] = [
|
|
462
473
|
[
|
|
@@ -490,6 +501,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
490
501
|
registration,
|
|
491
502
|
unitPrice: item.groupPrice.price.forMember(item.member),
|
|
492
503
|
type: BalanceItemType.Registration,
|
|
504
|
+
skipZero: false, // Always create at least one balance item for each registration - even when the price is zero
|
|
493
505
|
description: `${item.member.patchedMember.name} bij ${item.group.settings.name}`,
|
|
494
506
|
relations: new Map([
|
|
495
507
|
...sharedRelations,
|
|
@@ -502,6 +514,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
502
514
|
registration,
|
|
503
515
|
amount: option.amount,
|
|
504
516
|
unitPrice: option.option.price.forMember(item.member),
|
|
517
|
+
skipZero: true, // Do not create for zero option prices
|
|
505
518
|
type: BalanceItemType.Registration,
|
|
506
519
|
description: `${option.optionMenu.name}: ${option.option.name}`,
|
|
507
520
|
relations: new Map([
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, cloneObject, Decoder, isPatchableArray, ObjectData, PatchableArrayAutoEncoder, patchObject } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { SimpleError, SimpleErrors } from '@simonbackx/simple-errors';
|
|
4
|
-
import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod,
|
|
4
|
+
import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
|
|
5
5
|
import { BuckarooSettings, Company, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, PlatformConfig } from '@stamhoofd/structures';
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
|
|
@@ -10,6 +10,7 @@ import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
|
|
|
10
10
|
import { Context } from '../../../../helpers/Context';
|
|
11
11
|
import { TagHelper } from '../../../../helpers/TagHelper';
|
|
12
12
|
import { ViesHelper } from '../../../../helpers/ViesHelper';
|
|
13
|
+
import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
|
|
13
14
|
|
|
14
15
|
type Params = Record<string, never>;
|
|
15
16
|
type Query = undefined;
|
|
@@ -3,9 +3,10 @@ import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegi
|
|
|
3
3
|
|
|
4
4
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
5
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
-
import { Group, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod
|
|
6
|
+
import { Group, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -303,11 +304,6 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
303
304
|
throw Context.auth.error('Je hebt geen toegangsrechten om deze groep te wijzigen');
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
const previousProperties = {
|
|
307
|
-
deletedAt: model.deletedAt,
|
|
308
|
-
defaultAgeGroupId: model.defaultAgeGroupId,
|
|
309
|
-
};
|
|
310
|
-
|
|
311
307
|
if (struct.settings) {
|
|
312
308
|
struct.settings.period = undefined; // Not allowed to patch manually
|
|
313
309
|
model.settings.patchOrPut(struct.settings);
|
|
@@ -422,9 +418,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
422
418
|
}
|
|
423
419
|
}
|
|
424
420
|
|
|
425
|
-
await model.updateOccupancy(
|
|
426
|
-
previousProperties,
|
|
427
|
-
});
|
|
421
|
+
await model.updateOccupancy();
|
|
428
422
|
await model.save();
|
|
429
423
|
}
|
|
430
424
|
|
|
@@ -505,8 +499,6 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
505
499
|
}
|
|
506
500
|
|
|
507
501
|
await model.save();
|
|
508
|
-
await model.updateOccupancy({ isNew: true }); // Force update steps
|
|
509
|
-
|
|
510
502
|
return model;
|
|
511
503
|
}
|
|
512
504
|
}
|
|
@@ -187,7 +187,6 @@ export class MemberUserSyncerStatic {
|
|
|
187
187
|
|
|
188
188
|
if (user.memberId === member.id) {
|
|
189
189
|
user.memberId = null;
|
|
190
|
-
await user.save();
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
// Update model relation to correct response
|
|
@@ -239,11 +238,7 @@ export class MemberUserSyncerStatic {
|
|
|
239
238
|
await this.updateInheritedPermissions(user);
|
|
240
239
|
}
|
|
241
240
|
else {
|
|
242
|
-
|
|
243
|
-
// Unlink: parents are never 'equal' to the member
|
|
244
|
-
user.memberId = null;
|
|
245
|
-
await this.updateInheritedPermissions(user);
|
|
246
|
-
}
|
|
241
|
+
let shouldSave = false;
|
|
247
242
|
|
|
248
243
|
if (!user.firstName && !user.lastName) {
|
|
249
244
|
const parents = member.details.parents.filter(p => p.email === email);
|
|
@@ -252,13 +247,22 @@ export class MemberUserSyncerStatic {
|
|
|
252
247
|
user.firstName = parents[0].firstName;
|
|
253
248
|
user.lastName = parents[0].lastName;
|
|
254
249
|
}
|
|
255
|
-
|
|
250
|
+
shouldSave = true;
|
|
256
251
|
}
|
|
257
252
|
}
|
|
258
253
|
|
|
259
254
|
if (user.firstName === member.details.firstName && user.lastName === member.details.lastName) {
|
|
260
255
|
user.firstName = null;
|
|
261
256
|
user.lastName = null;
|
|
257
|
+
shouldSave = true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (user.memberId === member.id) {
|
|
261
|
+
// Unlink: parents are never 'equal' to the member
|
|
262
|
+
user.memberId = null;
|
|
263
|
+
await this.updateInheritedPermissions(user);
|
|
264
|
+
}
|
|
265
|
+
if (shouldSave) {
|
|
262
266
|
await user.save();
|
|
263
267
|
}
|
|
264
268
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
|
-
import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod
|
|
2
|
+
import { Group, Member, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
3
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
4
4
|
import { AuditLogSource, Group as GroupStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
5
5
|
import { PatchOrganizationRegistrationPeriodsEndpoint } from '../endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint';
|
|
6
6
|
import { AuthenticatedStructures } from './AuthenticatedStructures';
|
|
7
7
|
import { MemberUserSyncer } from './MemberUserSyncer';
|
|
8
8
|
import { AuditLogService } from '../services/AuditLogService';
|
|
9
|
+
import { SetupStepUpdater } from './SetupStepUpdater';
|
|
9
10
|
|
|
10
11
|
export class PeriodHelper {
|
|
11
12
|
static async moveOrganizationToPeriod(organization: Organization, period: RegistrationPeriod) {
|