@stamhoofd/backend 2.7.0 → 2.9.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/.env.template.json +3 -1
- package/package.json +3 -3
- package/src/crons.ts +3 -3
- package/src/decoders/StringArrayDecoder.ts +24 -0
- package/src/decoders/StringNullableDecoder.ts +18 -0
- package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +14 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
- package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +0 -31
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +34 -367
- package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -11
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +205 -110
- package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -2
- package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -40
- package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
- package/src/helpers/AdminPermissionChecker.ts +35 -24
- package/src/helpers/AuthenticatedStructures.ts +16 -7
- package/src/helpers/Context.ts +21 -0
- package/src/helpers/EmailResumer.ts +22 -2
- package/src/helpers/MemberUserSyncer.ts +42 -14
- package/src/seeds/1722344160-update-membership.ts +19 -22
- package/src/seeds/1722344161-sync-member-users.ts +60 -0
|
@@ -6,7 +6,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
6
6
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
7
7
|
import { Email } from '@stamhoofd/email';
|
|
8
8
|
import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
9
|
-
import { BalanceItemStatus, IDRegisterCheckout,
|
|
9
|
+
import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, IDRegisterCheckout, BalanceItemWithPayments, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version } from "@stamhoofd/structures";
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
|
|
12
12
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -98,11 +98,14 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
const deleteRegistrationIds = request.body.cart.deleteRegistrationIds
|
|
102
|
+
const deleteRegistrationModels = (deleteRegistrationIds.length ? (await Registration.getByIDs(...deleteRegistrationIds)) : []).filter(r => r.organizationId === organization.id)
|
|
103
|
+
|
|
101
104
|
const memberIds = Formatter.uniqueArray(
|
|
102
|
-
[...request.body.
|
|
105
|
+
[...request.body.memberIds, ...deleteRegistrationModels.map(i => i.memberId)]
|
|
103
106
|
)
|
|
104
107
|
const members = await Member.getBlobByIds(...memberIds)
|
|
105
|
-
const groupIds =
|
|
108
|
+
const groupIds = request.body.groupIds
|
|
106
109
|
const groups = await Group.getByIDs(...groupIds)
|
|
107
110
|
|
|
108
111
|
for (const group of groups) {
|
|
@@ -174,23 +177,21 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
174
177
|
|
|
175
178
|
// Validate balance items (can only happen serverside)
|
|
176
179
|
const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id)
|
|
177
|
-
let
|
|
178
|
-
let
|
|
180
|
+
let memberBalanceItemsStructs: BalanceItemWithPayments[] = []
|
|
181
|
+
let balanceItemsModels: BalanceItem[] = []
|
|
179
182
|
if (balanceItemIds.length > 0) {
|
|
180
|
-
|
|
181
|
-
if (
|
|
183
|
+
balanceItemsModels = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
|
|
184
|
+
if (balanceItemsModels.length != balanceItemIds.length) {
|
|
182
185
|
throw new SimpleError({
|
|
183
186
|
code: "invalid_data",
|
|
184
187
|
message: "Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw."
|
|
185
188
|
})
|
|
186
189
|
}
|
|
187
|
-
|
|
190
|
+
memberBalanceItemsStructs = await BalanceItem.getStructureWithPayments(balanceItemsModels)
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
|
|
191
|
-
|
|
192
193
|
// Validate the cart
|
|
193
|
-
checkout.validate({memberBalanceItems})
|
|
194
|
+
checkout.validate({memberBalanceItems: memberBalanceItemsStructs})
|
|
194
195
|
|
|
195
196
|
// Recalculate the price
|
|
196
197
|
checkout.updatePrices()
|
|
@@ -233,34 +234,37 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
233
234
|
|
|
234
235
|
// Check if this member is already registered in this group?
|
|
235
236
|
const existingRegistrations = await Registration.where({ memberId: member.id, groupId: item.groupId, cycle: group.cycle })
|
|
236
|
-
let registration: RegistrationWithMemberAndGroup | undefined = undefined;
|
|
237
237
|
|
|
238
238
|
for (const existingRegistration of existingRegistrations) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
if (item.replaceRegistrations.some(r => r.id === existingRegistration.id)) {
|
|
240
|
+
// Safe
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (checkout.cart.deleteRegistrations.some(r => r.id === existingRegistration.id)) {
|
|
245
|
+
// Safe
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
243
248
|
|
|
244
249
|
if (existingRegistration.registeredAt !== null && existingRegistration.deactivatedAt === null) {
|
|
245
250
|
throw new SimpleError({
|
|
246
251
|
code: "already_registered",
|
|
247
|
-
message:
|
|
252
|
+
message: `${member.firstName} is al ingeschreven voor ${group.settings.name}. Mogelijks heb je meerdere keren proberen in te schrijven en is het intussen wel gelukt. Herlaad de pagina best even om zeker te zijn.`
|
|
248
253
|
})
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
registration.periodId = group.periodId
|
|
258
|
-
}
|
|
257
|
+
const registration = new Registration()
|
|
258
|
+
.setRelation(registrationMemberRelation, member as Member)
|
|
259
|
+
.setRelation(Registration.group, group)
|
|
260
|
+
registration.organizationId = organization.id
|
|
261
|
+
registration.periodId = group.periodId
|
|
259
262
|
|
|
260
263
|
registration.memberId = member.id
|
|
261
264
|
registration.groupId = group.id
|
|
262
|
-
registration.
|
|
263
|
-
registration.
|
|
265
|
+
registration.price = 0 // will get filled by balance items themselves
|
|
266
|
+
registration.groupPrice = item.groupPrice;
|
|
267
|
+
registration.options = item.options
|
|
264
268
|
|
|
265
269
|
payRegistrations.push({
|
|
266
270
|
registration,
|
|
@@ -305,36 +309,71 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
305
309
|
|
|
306
310
|
console.log('Registering members using whoWillPayNow', whoWillPayNow, checkout.paymentMethod, totalPrice)
|
|
307
311
|
|
|
308
|
-
const
|
|
309
|
-
const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale
|
|
312
|
+
const createdBalanceItems: BalanceItem[] = []
|
|
313
|
+
const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale || checkout.paymentMethod === PaymentMethod.Unknown
|
|
314
|
+
|
|
315
|
+
// Create negative balance items
|
|
316
|
+
for (const registrationStruct of [...checkout.cart.deleteRegistrations, ...checkout.cart.items.flatMap(i => i.replaceRegistrations)]) {
|
|
317
|
+
if (whoWillPayNow !== 'nobody') {
|
|
318
|
+
// this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
|
|
319
|
+
throw new SimpleError({
|
|
320
|
+
code: "forbidden",
|
|
321
|
+
message: "Permission denied: you are not allowed to delete registrations",
|
|
322
|
+
human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
|
|
323
|
+
statusCode: 403
|
|
324
|
+
})
|
|
325
|
+
}
|
|
310
326
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
327
|
+
const existingRegistration = await Registration.getByID(registrationStruct.id)
|
|
328
|
+
if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
|
|
329
|
+
throw new SimpleError({
|
|
330
|
+
code: "invalid_data",
|
|
331
|
+
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
332
|
+
})
|
|
333
|
+
}
|
|
314
334
|
|
|
315
|
-
|
|
335
|
+
if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
|
|
336
|
+
throw new SimpleError({
|
|
337
|
+
code: "forbidden",
|
|
338
|
+
message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
|
|
339
|
+
statusCode: 403
|
|
340
|
+
})
|
|
341
|
+
}
|
|
316
342
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
343
|
+
if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
|
|
344
|
+
throw new SimpleError({
|
|
345
|
+
code: "invalid_data",
|
|
346
|
+
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
|
|
347
|
+
})
|
|
348
|
+
}
|
|
322
349
|
|
|
323
|
-
|
|
324
|
-
|
|
350
|
+
// We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
|
|
351
|
+
// Find all balance items of this registration and set them to zero
|
|
352
|
+
await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
|
|
353
|
+
|
|
354
|
+
// Clear the registration
|
|
355
|
+
await existingRegistration.deactivate()
|
|
356
|
+
|
|
357
|
+
const group = groups.find(g => g.id === existingRegistration.groupId)
|
|
358
|
+
if (!group) {
|
|
359
|
+
const g = await Group.getByID(existingRegistration.groupId)
|
|
360
|
+
if (g) {
|
|
361
|
+
groups.push(g)
|
|
325
362
|
}
|
|
326
|
-
await registration.save()
|
|
327
363
|
}
|
|
364
|
+
}
|
|
328
365
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
366
|
+
async function createBalanceItem({registration, amount, unitPrice, description, type, relations}: {amount?: number, registration: RegistrationWithMemberAndGroup, unitPrice: number, description: string, relations: Map<BalanceItemRelationType, BalanceItemRelation>, type: BalanceItemType}) {
|
|
367
|
+
// NOTE: We also need to save zero-price balance items because for online payments, we need to know which registrations to activate after payment
|
|
332
368
|
|
|
333
369
|
// Create balance item
|
|
334
370
|
const balanceItem = new BalanceItem();
|
|
335
371
|
balanceItem.registrationId = registration.id;
|
|
336
|
-
balanceItem.
|
|
337
|
-
balanceItem.
|
|
372
|
+
balanceItem.unitPrice = unitPrice
|
|
373
|
+
balanceItem.amount = amount ?? 1
|
|
374
|
+
balanceItem.description = description
|
|
375
|
+
balanceItem.relations = relations
|
|
376
|
+
balanceItem.type = type
|
|
338
377
|
|
|
339
378
|
// Who needs to receive this money?
|
|
340
379
|
balanceItem.organizationId = organization.id;
|
|
@@ -353,8 +392,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
353
392
|
// because otherwise the total price and pricePaid for the registration would be incorrect
|
|
354
393
|
//balanceItem2.registrationId = registration.id;
|
|
355
394
|
|
|
356
|
-
balanceItem2.
|
|
357
|
-
balanceItem2.
|
|
395
|
+
balanceItem2.unitPrice = unitPrice
|
|
396
|
+
balanceItem2.amount = amount ?? 1
|
|
397
|
+
balanceItem2.description = description
|
|
398
|
+
balanceItem2.relations = relations
|
|
399
|
+
balanceItem2.type = type
|
|
358
400
|
|
|
359
401
|
// Who needs to receive this money?
|
|
360
402
|
balanceItem2.organizationId = request.body.asOrganizationId;
|
|
@@ -366,7 +408,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
366
408
|
balanceItem2.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
367
409
|
await balanceItem2.save();
|
|
368
410
|
|
|
369
|
-
// do not add to
|
|
411
|
+
// do not add to createdBalanceItems array because we don't want to add this to the payment if we create a payment
|
|
370
412
|
} else {
|
|
371
413
|
balanceItem.memberId = registration.memberId;
|
|
372
414
|
balanceItem.userId = user.id
|
|
@@ -379,14 +421,108 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
379
421
|
balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null
|
|
380
422
|
|
|
381
423
|
await balanceItem.save();
|
|
382
|
-
|
|
424
|
+
createdBalanceItems.push(balanceItem)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Save registrations and add extra data if needed
|
|
428
|
+
for (const bundle of payRegistrations) {
|
|
429
|
+
const {item, registration} = bundle;
|
|
430
|
+
registration.reservedUntil = null
|
|
431
|
+
|
|
432
|
+
if (shouldMarkValid) {
|
|
433
|
+
await registration.markValid({skipEmail: bundle.item.replaceRegistrations.length > 0})
|
|
434
|
+
} else {
|
|
435
|
+
// Reserve registration for 30 minutes (if needed)
|
|
436
|
+
const group = groups.find(g => g.id === registration.groupId)
|
|
437
|
+
|
|
438
|
+
if (group && group.settings.maxMembers !== null) {
|
|
439
|
+
registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
|
|
440
|
+
}
|
|
441
|
+
await registration.save()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Note: we should always create the balance items: even when the price is zero
|
|
445
|
+
// Otherwise we don't know which registrations to activate after payment
|
|
446
|
+
|
|
447
|
+
if (shouldMarkValid && item.calculatedPrice === 0) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Create balance items
|
|
452
|
+
const sharedRelations: [BalanceItemRelationType, BalanceItemRelation][] = [
|
|
453
|
+
[
|
|
454
|
+
BalanceItemRelationType.Member,
|
|
455
|
+
BalanceItemRelation.create({
|
|
456
|
+
id: item.member.id,
|
|
457
|
+
name: item.member.patchedMember.name
|
|
458
|
+
})
|
|
459
|
+
],
|
|
460
|
+
[
|
|
461
|
+
BalanceItemRelationType.Group,
|
|
462
|
+
BalanceItemRelation.create({
|
|
463
|
+
id: item.group.id,
|
|
464
|
+
name: item.group.settings.name
|
|
465
|
+
})
|
|
466
|
+
]
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
if (item.group.settings.prices.length > 1) {
|
|
470
|
+
sharedRelations.push([
|
|
471
|
+
BalanceItemRelationType.GroupPrice,
|
|
472
|
+
BalanceItemRelation.create({
|
|
473
|
+
id: item.groupPrice.id,
|
|
474
|
+
name: item.groupPrice.name
|
|
475
|
+
})
|
|
476
|
+
])
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Base price
|
|
480
|
+
await createBalanceItem({
|
|
481
|
+
registration,
|
|
482
|
+
unitPrice: item.groupPrice.price.forMember(item.member),
|
|
483
|
+
type: BalanceItemType.Registration,
|
|
484
|
+
description: `${item.member.patchedMember.name} bij ${item.group.settings.name}`,
|
|
485
|
+
relations: new Map([
|
|
486
|
+
...sharedRelations
|
|
487
|
+
])
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
// Options
|
|
491
|
+
for (const option of item.options) {
|
|
492
|
+
await createBalanceItem({
|
|
493
|
+
registration,
|
|
494
|
+
amount: option.amount,
|
|
495
|
+
unitPrice: option.option.price.forMember(item.member),
|
|
496
|
+
type: BalanceItemType.Registration,
|
|
497
|
+
description: `${option.optionMenu.name}: ${option.option.name}`,
|
|
498
|
+
relations: new Map([
|
|
499
|
+
...sharedRelations,
|
|
500
|
+
[
|
|
501
|
+
BalanceItemRelationType.GroupOptionMenu,
|
|
502
|
+
BalanceItemRelation.create({
|
|
503
|
+
id: option.optionMenu.id,
|
|
504
|
+
name: option.optionMenu.name,
|
|
505
|
+
})
|
|
506
|
+
],
|
|
507
|
+
[
|
|
508
|
+
BalanceItemRelationType.GroupOption,
|
|
509
|
+
BalanceItemRelation.create({
|
|
510
|
+
id: option.option.id,
|
|
511
|
+
name: option.option.name,
|
|
512
|
+
})
|
|
513
|
+
]
|
|
514
|
+
])
|
|
515
|
+
})
|
|
516
|
+
}
|
|
517
|
+
|
|
383
518
|
}
|
|
384
519
|
|
|
385
520
|
const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
|
|
386
521
|
if (checkout.freeContribution && !request.body.asOrganizationId) {
|
|
387
522
|
// Create balance item
|
|
388
523
|
const balanceItem = new BalanceItem();
|
|
389
|
-
balanceItem.
|
|
524
|
+
balanceItem.type = BalanceItemType.FreeContribution
|
|
525
|
+
balanceItem.unitPrice = checkout.freeContribution
|
|
390
526
|
balanceItem.description = `Vrije bijdrage`
|
|
391
527
|
balanceItem.pricePaid = 0;
|
|
392
528
|
balanceItem.userId = user.id
|
|
@@ -399,13 +535,14 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
399
535
|
}
|
|
400
536
|
balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
401
537
|
await balanceItem.save();
|
|
402
|
-
|
|
538
|
+
createdBalanceItems.push(balanceItem)
|
|
403
539
|
}
|
|
404
540
|
|
|
405
541
|
if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
|
|
406
542
|
// Create balance item
|
|
407
543
|
const balanceItem = new BalanceItem();
|
|
408
|
-
balanceItem.
|
|
544
|
+
balanceItem.type = BalanceItemType.AdministrationFee
|
|
545
|
+
balanceItem.unitPrice = checkout.administrationFee
|
|
409
546
|
balanceItem.description = `Administratiekosten`
|
|
410
547
|
balanceItem.pricePaid = 0;
|
|
411
548
|
balanceItem.organizationId = organization.id;
|
|
@@ -423,62 +560,15 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
423
560
|
balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
424
561
|
await balanceItem.save();
|
|
425
562
|
|
|
426
|
-
|
|
563
|
+
createdBalanceItems.push(balanceItem);
|
|
427
564
|
}
|
|
428
565
|
|
|
429
566
|
if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
|
|
430
|
-
throw new
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (whoWillPayNow !== 'nobody') {
|
|
436
|
-
// this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
|
|
437
|
-
throw new SimpleError({
|
|
438
|
-
code: "forbidden",
|
|
439
|
-
message: "Permission denied: you are not allowed to delete registrations",
|
|
440
|
-
human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
|
|
441
|
-
statusCode: 403
|
|
442
|
-
})
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const existingRegistration = await Registration.getByID(registrationStruct.id)
|
|
446
|
-
if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
|
|
447
|
-
throw new SimpleError({
|
|
448
|
-
code: "invalid_data",
|
|
449
|
-
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
450
|
-
})
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
|
|
454
|
-
throw new SimpleError({
|
|
455
|
-
code: "forbidden",
|
|
456
|
-
message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
|
|
457
|
-
statusCode: 403
|
|
458
|
-
})
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
|
|
462
|
-
throw new SimpleError({
|
|
463
|
-
code: "invalid_data",
|
|
464
|
-
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
|
|
465
|
-
})
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
|
|
469
|
-
// Find all balance items of this registration and set them to zero
|
|
470
|
-
await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
|
|
471
|
-
|
|
472
|
-
// Clear the registration
|
|
473
|
-
await existingRegistration.deactivate()
|
|
474
|
-
|
|
475
|
-
const group = groups.find(g => g.id === existingRegistration.groupId)
|
|
476
|
-
if (!group) {
|
|
477
|
-
const g = await Group.getByID(existingRegistration.groupId)
|
|
478
|
-
if (g) {
|
|
479
|
-
groups.push(g)
|
|
480
|
-
}
|
|
481
|
-
}
|
|
567
|
+
throw new SimpleError({
|
|
568
|
+
code: 'invalid_data',
|
|
569
|
+
message: 'Not possible to pay balance items as the organization',
|
|
570
|
+
statusCode: 400
|
|
571
|
+
})
|
|
482
572
|
}
|
|
483
573
|
|
|
484
574
|
let paymentUrl: string | null = null
|
|
@@ -487,19 +577,21 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
487
577
|
if (whoWillPayNow !== 'nobody') {
|
|
488
578
|
const mappedBalanceItems = new Map<BalanceItem, number>()
|
|
489
579
|
|
|
490
|
-
for (const item of
|
|
580
|
+
for (const item of createdBalanceItems) {
|
|
491
581
|
mappedBalanceItems.set(item, item.price)
|
|
492
582
|
}
|
|
493
583
|
|
|
494
584
|
for (const item of checkout.cart.balanceItems) {
|
|
495
|
-
const balanceItem =
|
|
585
|
+
const balanceItem = balanceItemsModels.find(i => i.id === item.item.id)
|
|
496
586
|
if (!balanceItem) {
|
|
497
587
|
throw new Error('Balance item not found')
|
|
498
588
|
}
|
|
499
589
|
mappedBalanceItems.set(balanceItem, item.price)
|
|
500
|
-
|
|
590
|
+
createdBalanceItems.push(balanceItem)
|
|
501
591
|
}
|
|
502
592
|
|
|
593
|
+
// Make sure every price is accurate before creating a payment
|
|
594
|
+
await BalanceItem.updateOutstanding(createdBalanceItems, organization.id)
|
|
503
595
|
const response = await this.createPayment({
|
|
504
596
|
balanceItems: mappedBalanceItems,
|
|
505
597
|
organization,
|
|
@@ -512,9 +604,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
512
604
|
paymentUrl = response.paymentUrl
|
|
513
605
|
payment = response.payment
|
|
514
606
|
}
|
|
607
|
+
} else {
|
|
608
|
+
await BalanceItem.updateOutstanding(createdBalanceItems, organization.id)
|
|
515
609
|
}
|
|
516
610
|
|
|
517
|
-
await BalanceItem.updateOutstanding(items, organization.id)
|
|
518
611
|
|
|
519
612
|
// Update occupancy
|
|
520
613
|
for (const group of groups) {
|
|
@@ -524,9 +617,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
524
617
|
}
|
|
525
618
|
}
|
|
526
619
|
|
|
620
|
+
const updatedMembers = await Member.getBlobByIds(...memberIds)
|
|
621
|
+
|
|
527
622
|
return new Response(RegisterResponse.create({
|
|
528
623
|
payment: payment ? PaymentStruct.create(payment) : null,
|
|
529
|
-
members: await AuthenticatedStructures.membersBlob(
|
|
624
|
+
members: await AuthenticatedStructures.membersBlob(updatedMembers),
|
|
530
625
|
registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
|
|
531
626
|
paymentUrl
|
|
532
627
|
}));
|
|
@@ -2,10 +2,9 @@ import { ConvertArrayToPatchableArray, Decoder, PatchableArrayAutoEncoder, Patch
|
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { RegistrationPeriod as RegistrationPeriodStruct } from "@stamhoofd/structures";
|
|
4
4
|
|
|
5
|
-
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
6
|
-
import { Context } from '../../../helpers/Context';
|
|
7
|
-
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
8
5
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
+
import { Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
7
|
+
import { Context } from '../../../helpers/Context';
|
|
9
8
|
|
|
10
9
|
type Params = Record<string, never>;
|
|
11
10
|
type Query = undefined;
|
|
@@ -1,36 +1,24 @@
|
|
|
1
|
-
import { AutoEncoder, Data, Decoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
1
|
+
import { AutoEncoder, Data, Decoder, EnumDecoder, field, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
3
|
import { EmailTemplate } from '@stamhoofd/models';
|
|
4
4
|
import { EmailTemplate as EmailTemplateStruct, EmailTemplateType } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import { Context } from '../../../../helpers/Context';
|
|
7
|
+
import { StringNullableDecoder } from '../../../../decoders/StringNullableDecoder';
|
|
8
|
+
import { StringArrayDecoder } from '../../../../decoders/StringArrayDecoder';
|
|
7
9
|
|
|
8
10
|
type Params = Record<string, never>;
|
|
9
11
|
type Body = undefined;
|
|
10
12
|
|
|
11
|
-
export class StringNullableDecoder<T> implements Decoder<T | null> {
|
|
12
|
-
decoder: Decoder<T>;
|
|
13
|
-
|
|
14
|
-
constructor(decoder: Decoder<T>) {
|
|
15
|
-
this.decoder = decoder;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
decode(data: Data): T | null {
|
|
19
|
-
if (data.value === 'null') {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return data.decode(this.decoder);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
13
|
class Query extends AutoEncoder {
|
|
29
14
|
@field({ decoder: new StringNullableDecoder(StringDecoder), optional: true, nullable: true })
|
|
30
15
|
webshopId: string|null = null
|
|
31
16
|
|
|
32
|
-
@field({ decoder: new StringNullableDecoder(StringDecoder), optional: true, nullable: true})
|
|
33
|
-
|
|
17
|
+
@field({ decoder: new StringNullableDecoder(new StringArrayDecoder(StringDecoder)), optional: true, nullable: true})
|
|
18
|
+
groupIds: string[]|null = null
|
|
19
|
+
|
|
20
|
+
@field({ decoder: new StringNullableDecoder(new StringArrayDecoder(new EnumDecoder(EmailTemplateType))), optional: true, nullable: true})
|
|
21
|
+
types: EmailTemplateType[]|null = null
|
|
34
22
|
}
|
|
35
23
|
|
|
36
24
|
type ResponseBody = EmailTemplateStruct[];
|
|
@@ -65,15 +53,24 @@ export class GetEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
65
53
|
}
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
const types = [...Object.values(EmailTemplateType)].filter(type => {
|
|
56
|
+
const types = (request.query.types ?? [...Object.values(EmailTemplateType)]).filter(type => {
|
|
69
57
|
if (!organization) {
|
|
70
|
-
return
|
|
58
|
+
return EmailTemplateStruct.allowPlatformLevel(type)
|
|
71
59
|
}
|
|
72
60
|
return EmailTemplateStruct.allowOrganizationLevel(type)
|
|
73
61
|
})
|
|
74
62
|
|
|
75
63
|
|
|
76
|
-
const templates = organization ?
|
|
64
|
+
const templates = organization ?
|
|
65
|
+
(
|
|
66
|
+
await EmailTemplate.where({ organizationId: organization.id, webshopId: request.query.webshopId ?? null, groupId: request.query.groupIds ? {sign: 'IN', value: request.query.groupIds} : null, type: {sign: 'IN', value: types}})
|
|
67
|
+
)
|
|
68
|
+
: (
|
|
69
|
+
// Required for event emails when logged in as the platform admin
|
|
70
|
+
(request.query.webshopId || request.query.groupIds) ?
|
|
71
|
+
await EmailTemplate.where({ webshopId: request.query.webshopId ?? null, groupId: request.query.groupIds ? {sign: 'IN', value: request.query.groupIds} : null, type: {sign: 'IN', value: types}})
|
|
72
|
+
: []
|
|
73
|
+
);
|
|
77
74
|
const defaultTemplates = await EmailTemplate.where({ organizationId: null, type: {sign: 'IN', value: types} });
|
|
78
75
|
return new Response([...templates, ...defaultTemplates].map(template => EmailTemplateStruct.create(template)))
|
|
79
76
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
-
import { EmailTemplate } from '@stamhoofd/models';
|
|
3
|
+
import { EmailTemplate, Group, Webshop } from '@stamhoofd/models';
|
|
4
4
|
import { EmailTemplate as EmailTemplateStruct, PermissionLevel } from '@stamhoofd/structures';
|
|
5
5
|
|
|
6
6
|
import { Context } from '../../../../helpers/Context';
|
|
7
|
+
import { SimpleError } from '@simonbackx/simple-errors';
|
|
7
8
|
|
|
8
9
|
type Params = Record<string, never>;
|
|
9
10
|
type Body = PatchableArrayAutoEncoder<EmailTemplateStruct>;
|
|
@@ -67,12 +68,32 @@ export class PatchEmailTemplatesEndpoint extends Endpoint<Params, Query, Body, R
|
|
|
67
68
|
throw Context.auth.error();
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
if (!EmailTemplateStruct.allowPlatformLevel(struct.type) && !organization) {
|
|
72
|
+
throw Context.auth.error();
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
const template = new EmailTemplate()
|
|
71
76
|
template.id = struct.id
|
|
72
77
|
template.organizationId = organization?.id ?? null
|
|
73
78
|
template.webshopId = struct.webshopId
|
|
74
79
|
template.groupId = struct.groupId
|
|
75
80
|
|
|
81
|
+
if (struct.groupId) {
|
|
82
|
+
const group = await Group.getByID(struct.groupId)
|
|
83
|
+
if (!group || !await Context.auth.canAccessGroup(group, PermissionLevel.Full)) {
|
|
84
|
+
throw Context.auth.error();
|
|
85
|
+
}
|
|
86
|
+
template.organizationId = group.organizationId
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (struct.webshopId) {
|
|
90
|
+
const webshop = await Webshop.getByID(struct.webshopId)
|
|
91
|
+
if (!webshop || !await Context.auth.canAccessWebshop(webshop, PermissionLevel.Full)) {
|
|
92
|
+
throw Context.auth.error();
|
|
93
|
+
}
|
|
94
|
+
template.organizationId = webshop.organizationId
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
template.html = struct.html
|
|
77
98
|
template.subject = struct.subject
|
|
78
99
|
template.text = struct.text
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, ObjectData, 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, StripeAccount,
|
|
5
|
-
import { BuckarooSettings, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel
|
|
4
|
+
import { Organization, OrganizationRegistrationPeriod, PayconiqPayment, Platform, RegistrationPeriod, StripeAccount, Webshop } from '@stamhoofd/models';
|
|
5
|
+
import { BuckarooSettings, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel } from "@stamhoofd/structures";
|
|
6
6
|
import { Formatter } from '@stamhoofd/utility';
|
|
7
7
|
|
|
8
8
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
@@ -98,6 +98,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
|
|
|
98
98
|
|
|
99
99
|
if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
|
|
100
100
|
organization.privateMeta.emails = request.body.privateMeta.emails.applyTo(organization.privateMeta.emails)
|
|
101
|
+
organization.privateMeta.premises = patchObject(organization.privateMeta.premises, request.body.privateMeta.premises);
|
|
101
102
|
organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles)
|
|
102
103
|
organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities)
|
|
103
104
|
organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles)
|
|
@@ -6,6 +6,7 @@ import NodeRSA from 'node-rsa';
|
|
|
6
6
|
|
|
7
7
|
import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
|
|
8
8
|
import { Context } from '../../../../helpers/Context';
|
|
9
|
+
import { Formatter } from '@stamhoofd/utility';
|
|
9
10
|
|
|
10
11
|
type Params = Record<string, never>;
|
|
11
12
|
type Query = undefined;
|
|
@@ -98,7 +99,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
98
99
|
organization.privateMeta.dnsRecords = []
|
|
99
100
|
|
|
100
101
|
if (organization.privateMeta.pendingMailDomain !== null) {
|
|
101
|
-
const defaultFromDomain = "
|
|
102
|
+
const defaultFromDomain = Formatter.slug(STAMHOOFD.platformName) + "." + organization.privateMeta.pendingMailDomain;
|
|
102
103
|
if (organization.privateMeta.pendingRegisterDomain === null || !organization.privateMeta.pendingRegisterDomain.endsWith('.' + organization.privateMeta.pendingMailDomain)) {
|
|
103
104
|
// We set a custom domainname for webshops already
|
|
104
105
|
// This is not used at this moment
|
|
@@ -109,8 +110,7 @@ export class SetOrganizationDomainEndpoint extends Endpoint<Params, Query, Body,
|
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
if (organization.privateMeta.mailFromDomain !== organization.privateMeta.pendingRegisterDomain) {
|
|
112
|
-
|
|
113
|
-
organization.privateMeta.dnsRecords.push(DNSRecord.create({
|
|
113
|
+
organization.privateMeta.dnsRecords.push(DNSRecord.create({
|
|
114
114
|
type: DNSRecordType.CNAME,
|
|
115
115
|
name: organization.privateMeta.mailFromDomain + ".",
|
|
116
116
|
// Use shops for mail domain, to allow reuse
|