@stamhoofd/backend 2.8.0 → 2.13.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 (35) hide show
  1. package/.env.template.json +3 -1
  2. package/package.json +11 -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 +20 -18
  7. package/src/endpoints/global/email/PatchEmailEndpoint.ts +1 -0
  8. package/src/endpoints/global/events/GetEventsEndpoint.ts +3 -9
  9. package/src/endpoints/global/events/PatchEventsEndpoint.ts +21 -1
  10. package/src/endpoints/global/groups/GetGroupsEndpoint.ts +79 -0
  11. package/src/endpoints/global/members/GetMembersEndpoint.ts +15 -62
  12. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +2 -2
  13. package/src/endpoints/global/registration/GetUserBalanceEndpoint.ts +3 -3
  14. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +165 -35
  15. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +20 -23
  16. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +22 -1
  17. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +56 -3
  18. package/src/endpoints/organization/dashboard/organization/SetOrganizationDomainEndpoint.ts +3 -3
  19. package/src/endpoints/organization/dashboard/payments/GetMemberBalanceEndpoint.ts +3 -3
  20. package/src/endpoints/organization/dashboard/payments/GetPaymentsCountEndpoint.ts +43 -0
  21. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.ts +292 -170
  22. package/src/endpoints/organization/dashboard/payments/PatchBalanceItemsEndpoint.ts +22 -37
  23. package/src/endpoints/organization/dashboard/payments/legacy/GetPaymentsEndpoint.ts +170 -0
  24. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
  25. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +14 -4
  26. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +12 -2
  27. package/src/helpers/AdminPermissionChecker.ts +95 -60
  28. package/src/helpers/AuthenticatedStructures.ts +16 -6
  29. package/src/helpers/Context.ts +21 -0
  30. package/src/helpers/EmailResumer.ts +22 -2
  31. package/src/helpers/MemberUserSyncer.ts +8 -2
  32. package/src/helpers/ViesHelper.ts +151 -0
  33. package/src/seeds/1722344160-update-membership.ts +19 -22
  34. package/src/seeds/1722344161-sync-member-users.ts +60 -0
  35. package/.env.json +0 -65
@@ -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, BalanceItemWithPayments, IDRegisterCheckout, PaymentCustomer, 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';
@@ -102,10 +102,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
102
102
  const deleteRegistrationModels = (deleteRegistrationIds.length ? (await Registration.getByIDs(...deleteRegistrationIds)) : []).filter(r => r.organizationId === organization.id)
103
103
 
104
104
  const memberIds = Formatter.uniqueArray(
105
- [...request.body.cart.items.map(i => i.memberId), ...deleteRegistrationModels.map(i => i.memberId)]
105
+ [...request.body.memberIds, ...deleteRegistrationModels.map(i => i.memberId)]
106
106
  )
107
107
  const members = await Member.getBlobByIds(...memberIds)
108
- const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
108
+ const groupIds = request.body.groupIds
109
109
  const groups = await Group.getByIDs(...groupIds)
110
110
 
111
111
  for (const group of groups) {
@@ -177,7 +177,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
177
177
 
178
178
  // Validate balance items (can only happen serverside)
179
179
  const balanceItemIds = request.body.cart.balanceItems.map(i => i.item.id)
180
- let memberBalanceItemsStructs: MemberBalanceItem[] = []
180
+ let memberBalanceItemsStructs: BalanceItemWithPayments[] = []
181
181
  let balanceItemsModels: BalanceItem[] = []
182
182
  if (balanceItemIds.length > 0) {
183
183
  balanceItemsModels = await BalanceItem.where({ id: { sign:'IN', value: balanceItemIds }, organizationId: organization.id })
@@ -187,11 +187,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
187
187
  message: "Oeps, één of meerdere openstaande bedragen in jouw winkelmandje zijn aangepast. Herlaad de pagina en probeer opnieuw."
188
188
  })
189
189
  }
190
- memberBalanceItemsStructs = await BalanceItem.getMemberStructure(balanceItemsModels)
190
+ memberBalanceItemsStructs = await BalanceItem.getStructureWithPayments(balanceItemsModels)
191
191
  }
192
192
 
193
- console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
194
-
195
193
  // Validate the cart
196
194
  checkout.validate({memberBalanceItems: memberBalanceItemsStructs})
197
195
 
@@ -365,33 +363,17 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
365
363
  }
366
364
  }
367
365
 
368
- // Save registrations and add extra data if needed
369
- for (const bundle of payRegistrations) {
370
- const registration = bundle.registration;
371
-
372
- registration.reservedUntil = null
373
-
374
- if (shouldMarkValid) {
375
- await registration.markValid()
376
- } else {
377
- // Reserve registration for 30 minutes (if needed)
378
- const group = groups.find(g => g.id === registration.groupId)
379
-
380
- if (group && group.settings.maxMembers !== null) {
381
- registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
382
- }
383
- await registration.save()
384
- }
385
-
386
- if (bundle.item.calculatedPrice === 0) {
387
- continue;
388
- }
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
389
368
 
390
369
  // Create balance item
391
370
  const balanceItem = new BalanceItem();
392
371
  balanceItem.registrationId = registration.id;
393
- balanceItem.price = bundle.item.calculatedPrice
394
- 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
395
377
 
396
378
  // Who needs to receive this money?
397
379
  balanceItem.organizationId = organization.id;
@@ -410,8 +392,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
410
392
  // because otherwise the total price and pricePaid for the registration would be incorrect
411
393
  //balanceItem2.registrationId = registration.id;
412
394
 
413
- balanceItem2.price = bundle.item.calculatedPrice
414
- 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
415
400
 
416
401
  // Who needs to receive this money?
417
402
  balanceItem2.organizationId = request.body.asOrganizationId;
@@ -438,12 +423,106 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
438
423
  await balanceItem.save();
439
424
  createdBalanceItems.push(balanceItem)
440
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
+
518
+ }
441
519
 
442
520
  const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
443
521
  if (checkout.freeContribution && !request.body.asOrganizationId) {
444
522
  // Create balance item
445
523
  const balanceItem = new BalanceItem();
446
- balanceItem.price = checkout.freeContribution
524
+ balanceItem.type = BalanceItemType.FreeContribution
525
+ balanceItem.unitPrice = checkout.freeContribution
447
526
  balanceItem.description = `Vrije bijdrage`
448
527
  balanceItem.pricePaid = 0;
449
528
  balanceItem.userId = user.id
@@ -462,7 +541,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
462
541
  if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
463
542
  // Create balance item
464
543
  const balanceItem = new BalanceItem();
465
- balanceItem.price = checkout.administrationFee
544
+ balanceItem.type = BalanceItemType.AdministrationFee
545
+ balanceItem.unitPrice = checkout.administrationFee
466
546
  balanceItem.description = `Administratiekosten`
467
547
  balanceItem.pricePaid = 0;
468
548
  balanceItem.organizationId = organization.id;
@@ -598,11 +678,61 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
598
678
  }
599
679
 
600
680
  const payment = new Payment()
601
- payment.userId = user.id
602
681
 
603
682
  // Who will receive this money?
604
683
  payment.organizationId = organization.id
605
684
 
685
+ // Who paid
686
+ payment.payingUserId = user.id
687
+ payment.payingOrganizationId = checkout.asOrganizationId ?? null
688
+
689
+ // Fill in customer:
690
+ payment.customer = PaymentCustomer.create({
691
+ firstName: user.firstName,
692
+ lastName: user.lastName,
693
+ email: user.email,
694
+ })
695
+
696
+ if (checkout.asOrganizationId) {
697
+ if (!checkout.customer) {
698
+ throw new SimpleError({
699
+ code: "missing_fields",
700
+ message: "customer is required when paying as an organization",
701
+ human: "Vul je facturatiegegevens in om verder te gaan."
702
+ })
703
+ }
704
+
705
+ if (!checkout.customer.company) {
706
+ throw new SimpleError({
707
+ code: "missing_fields",
708
+ message: "customer.company is required when paying as an organization",
709
+ human: "Als je een betaling uitvoert in naam van je vereniging, is het noodzakelijk om facturatiegegevens met bedrijfsgegevens in te vullen."
710
+ })
711
+ }
712
+
713
+ const payingOrganization = await Organization.getByID(checkout.asOrganizationId);
714
+ if (!payingOrganization) {
715
+ throw new SimpleError({
716
+ code: "invalid_data",
717
+ message: "Oeps, de organisatie waarvoor je probeert te betalen lijkt niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
718
+ })
719
+ }
720
+
721
+ // Search company id
722
+ // this avoids needing to check the VAT number every time
723
+ const id = checkout.customer.company.id
724
+ const foundCompany = payingOrganization.meta.companies.find(c => c.id === id)
725
+
726
+ if (!foundCompany) {
727
+ throw new SimpleError({
728
+ code: "invalid_data",
729
+ message: "Oeps, de facturatiegegevens die je probeerde te selecteren lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
730
+ })
731
+ }
732
+
733
+ payment.customer.company = foundCompany
734
+ }
735
+
606
736
  payment.method = checkout.paymentMethod
607
737
  payment.status = PaymentStatus.Created
608
738
  payment.price = totalPrice
@@ -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,13 +1,14 @@
1
- import { AutoEncoderPatchType, Decoder, ObjectData, patchObject } from '@simonbackx/simple-encoding';
1
+ import { AutoEncoderPatchType, 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, 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, Company, 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';
9
9
  import { BuckarooHelper } from '../../../../helpers/BuckarooHelper';
10
10
  import { Context } from '../../../../helpers/Context';
11
+ import { ViesHelper } from '../../../../helpers/ViesHelper';
11
12
 
12
13
  type Params = Record<string, never>;
13
14
  type Query = undefined;
@@ -98,6 +99,7 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
98
99
 
99
100
  if (request.body.privateMeta && request.body.privateMeta.isPatch()) {
100
101
  organization.privateMeta.emails = request.body.privateMeta.emails.applyTo(organization.privateMeta.emails)
102
+ organization.privateMeta.premises = patchObject(organization.privateMeta.premises, request.body.privateMeta.premises);
101
103
  organization.privateMeta.roles = request.body.privateMeta.roles.applyTo(organization.privateMeta.roles)
102
104
  organization.privateMeta.responsibilities = request.body.privateMeta.responsibilities.applyTo(organization.privateMeta.responsibilities)
103
105
  organization.privateMeta.inheritedResponsibilityRoles = request.body.privateMeta.inheritedResponsibilityRoles.applyTo(organization.privateMeta.inheritedResponsibilityRoles)
@@ -207,6 +209,10 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
207
209
  }
208
210
 
209
211
  if (request.body.meta) {
212
+ if (request.body.meta.companies) {
213
+ await this.validateCompanies(organization, request.body.meta.companies)
214
+ }
215
+
210
216
  const savedPackages = organization.meta.packages
211
217
  organization.meta.patchOrPut(request.body.meta)
212
218
  organization.meta.packages = savedPackages
@@ -365,5 +371,52 @@ export class PatchOrganizationEndpoint extends Endpoint<Params, Query, Body, Res
365
371
  errors.throwIfNotEmpty()
366
372
  return new Response(await AuthenticatedStructures.organization(organization));
367
373
  }
374
+
375
+ async validateCompanies(organization: Organization, companies: PatchableArrayAutoEncoder<Company>|Company[]) {
376
+ if (isPatchableArray(companies)) {
377
+ for (const patch of companies.getPatches()) {
378
+ // Changed VAT number
379
+ const original = organization.meta.companies.find(c => c.id === patch.id)
380
+
381
+ if (!original) {
382
+ throw new Error('Could not find company')
383
+ }
384
+
385
+ // Changed VAT number
386
+ const prepatched = original.patch(patch)
387
+ await ViesHelper.checkCompany(prepatched, patch)
388
+ }
389
+
390
+ let c = 0;
391
+ for (const {put} of companies.getPuts()) {
392
+ c++;
393
+
394
+ if ((organization.meta.companies.length + c) > 5) {
395
+ throw new SimpleError({
396
+ code: "invalid_field",
397
+ message: "Too many companies",
398
+ human: "Je kan maximaal 5 bedrijven toevoegen",
399
+ field: "companies"
400
+ })
401
+ }
402
+
403
+ await ViesHelper.checkCompany(put, put)
404
+ }
405
+
406
+ } else {
407
+ if (companies.length > 5) {
408
+ throw new SimpleError({
409
+ code: "invalid_field",
410
+ message: "Too many companies",
411
+ human: "Je kan maximaal 5 bedrijven toevoegen",
412
+ field: "companies"
413
+ })
414
+ }
415
+
416
+ for (const company of companies) {
417
+ await ViesHelper.checkCompany(company, company)
418
+ }
419
+ }
420
+ }
368
421
  }
369
422
 
@@ -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
@@ -1,14 +1,14 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
2
  import { SimpleError } from "@simonbackx/simple-errors";
3
3
  import { BalanceItem, Group, Member } from "@stamhoofd/models";
4
- import { MemberBalanceItem } from "@stamhoofd/structures";
4
+ import { BalanceItemWithPayments } from "@stamhoofd/structures";
5
5
 
6
6
  import { Context } from "../../../../helpers/Context";
7
7
 
8
8
  type Params = { id: string };
9
9
  type Query = undefined
10
10
  type Body = undefined
11
- type ResponseBody = MemberBalanceItem[]
11
+ type ResponseBody = BalanceItemWithPayments[]
12
12
 
13
13
  export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
14
  protected doesMatch(request: Request): [true, Params] | [false] {
@@ -42,7 +42,7 @@ export class GetMemberBalanceEndpoint extends Endpoint<Params, Query, Body, Resp
42
42
  const balanceItems = await BalanceItem.balanceItemsForUsersAndMembers(organization.id, member.users.map(u => u.id), [member.id])
43
43
 
44
44
  return new Response(
45
- await BalanceItem.getMemberStructure(balanceItems)
45
+ await BalanceItem.getStructureWithPayments(balanceItems)
46
46
  );
47
47
  }
48
48
  }
@@ -0,0 +1,43 @@
1
+ import { Decoder } from '@simonbackx/simple-encoding';
2
+ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
3
+ import { CountFilteredRequest, CountResponse } from '@stamhoofd/structures';
4
+
5
+ import { Context } from '../../../../helpers/Context';
6
+ import { GetPaymentsEndpoint } from './GetPaymentsEndpoint';
7
+
8
+ type Params = Record<string, never>;
9
+ type Query = CountFilteredRequest;
10
+ type Body = undefined;
11
+ type ResponseBody = CountResponse;
12
+
13
+ export class GetPaymentsCountEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
14
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>
15
+
16
+ protected doesMatch(request: Request): [true, Params] | [false] {
17
+ if (request.method != "GET") {
18
+ return [false];
19
+ }
20
+
21
+ const params = Endpoint.parseParameters(request.url, "/payments/count", {});
22
+
23
+ if (params) {
24
+ return [true, params as Params];
25
+ }
26
+ return [false];
27
+ }
28
+
29
+ async handle(request: DecodedRequest<Params, Query, Body>) {
30
+ await Context.setOrganizationScope();
31
+ await Context.authenticate()
32
+ const query = await GetPaymentsEndpoint.buildQuery(request.query)
33
+
34
+ const count = await query
35
+ .count();
36
+
37
+ return new Response(
38
+ CountResponse.create({
39
+ count
40
+ })
41
+ );
42
+ }
43
+ }