@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.
Files changed (32) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +3 -3
  3. package/src/crons.ts +3 -3
  4. package/src/decoders/StringArrayDecoder.ts +24 -0
  5. package/src/decoders/StringNullableDecoder.ts +18 -0
  6. package/src/endpoints/admin/organizations/GetOrganizationsEndpoint.ts +14 -0
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  9. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  10. package/src/endpoints/global/members/GetMembersEndpoint.ts +0 -31
  11. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +34 -367
  12. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  13. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +8 -11
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +205 -110
  15. package/src/endpoints/global/registration-periods/PatchRegistrationPeriodsEndpoint.ts +2 -3
  16. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  17. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  18. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +3 -2
  19. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +3 -40
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  24. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  25. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  26. package/src/helpers/AdminPermissionChecker.ts +35 -24
  27. package/src/helpers/AuthenticatedStructures.ts +16 -7
  28. package/src/helpers/Context.ts +21 -0
  29. package/src/helpers/EmailResumer.ts +22 -2
  30. package/src/helpers/MemberUserSyncer.ts +42 -14
  31. package/src/seeds/1722344160-update-membership.ts +19 -22
  32. 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, MemberBalanceItem, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version } from "@stamhoofd/structures";
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.cart.items.map(i => i.memberId), ...request.body.cart.deleteRegistrations.map(i => i.member.id)]
105
+ [...request.body.memberIds, ...deleteRegistrationModels.map(i => i.memberId)]
103
106
  )
104
107
  const members = await Member.getBlobByIds(...memberIds)
105
- const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
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 memberBalanceItems: MemberBalanceItem[] = []
178
- let balanceItems: BalanceItem[] = []
180
+ let memberBalanceItemsStructs: BalanceItemWithPayments[] = []
181
+ let balanceItemsModels: BalanceItem[] = []
179
182
  if (balanceItemIds.length > 0) {
180
- balanceItems = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
181
- if (balanceItems.length != balanceItemIds.length) {
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
- memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
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
- registration = existingRegistration
240
- .setRelation(registrationMemberRelation, member as Member)
241
- .setRelation(Registration.group, group)
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: "Dit lid is reeds ingeschreven. Herlaad de pagina en probeer opnieuw."
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
- if (!registration) {
253
- registration = new Registration()
254
- .setRelation(registrationMemberRelation, member as Member)
255
- .setRelation(Registration.group, group)
256
- registration.organizationId = organization.id
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.cycle = group.cycle
263
- registration.price = item.calculatedPrice
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 items: BalanceItem[] = []
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
- // Save registrations and add extra data if needed
312
- for (const bundle of payRegistrations) {
313
- const registration = bundle.registration;
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
- registration.reservedUntil = null
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 (shouldMarkValid) {
318
- await registration.markValid()
319
- } else {
320
- // Reserve registration for 30 minutes (if needed)
321
- const group = groups.find(g => g.id === registration.groupId)
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
- if (group && group.settings.maxMembers !== null) {
324
- registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
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
- if (bundle.item.calculatedPrice === 0) {
330
- continue;
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.price = bundle.item.calculatedPrice
337
- balanceItem.description = `Inschrijving ${registration.group.settings.name}`
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.price = bundle.item.calculatedPrice
357
- balanceItem2.description = `Inschrijving ${registration.group.settings.name}`
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 items array because we don't want to add this to the payment if we create a payment
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
- items.push(balanceItem)
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.price = checkout.freeContribution
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
- items.push(balanceItem)
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.price = checkout.administrationFee
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
- items.push(balanceItem);
563
+ createdBalanceItems.push(balanceItem);
427
564
  }
428
565
 
429
566
  if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
430
- throw new Error('Not possible to pay balance items when whoWillPayNow is nobody')
431
- }
432
-
433
- // Create negative balance items
434
- for (const registrationStruct of checkout.cart.deleteRegistrations) {
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 items) {
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 = balanceItems.find(i => i.id === item.item.id)
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
- items.push(balanceItem)
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(members),
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
- groupId: string|null = null
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 true;
58
+ return EmailTemplateStruct.allowPlatformLevel(type)
71
59
  }
72
60
  return EmailTemplateStruct.allowOrganizationLevel(type)
73
61
  })
74
62
 
75
63
 
76
- const templates = organization ? (await EmailTemplate.where({ organizationId: organization.id, webshopId: request.query.webshopId ?? null, groupId: request.query.groupId ?? null, type: {sign: 'IN', value: types}})) : [];
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, User, Webshop } from '@stamhoofd/models';
5
- import { BuckarooSettings, OrganizationMetaData, OrganizationPatch, Organization as OrganizationStruct, PayconiqAccount, PaymentMethod, PaymentMethodHelper, PermissionLevel, UserPermissions } from "@stamhoofd/structures";
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 = "stamhoofd." + organization.privateMeta.pendingMailDomain;
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