@stamhoofd/backend 2.4.0 → 2.6.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 (23) hide show
  1. package/package.json +3 -3
  2. package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
  3. package/src/endpoints/global/events/PatchEventsEndpoint.ts +16 -9
  4. package/src/endpoints/global/members/GetMembersEndpoint.ts +18 -6
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -8
  6. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
  7. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
  8. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
  9. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +417 -178
  11. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
  12. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +88 -11
  13. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +10 -4
  14. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +4 -1
  15. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +6 -0
  16. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -2
  17. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -28
  18. package/src/helpers/AdminPermissionChecker.ts +9 -4
  19. package/src/helpers/AuthenticatedStructures.ts +89 -28
  20. package/src/helpers/MemberUserSyncer.ts +5 -5
  21. package/src/helpers/StripeHelper.ts +83 -40
  22. package/src/helpers/StripePayoutChecker.ts +7 -5
  23. package/src/seeds/1722344160-update-membership.ts +57 -0
@@ -78,7 +78,7 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
78
78
  }
79
79
 
80
80
  return new Response(GetWebshopFromDomainResult.create({
81
- organization: await organization.getStructure({emptyGroups: true}),
81
+ organization: organization.getBaseStructure(),
82
82
  webshop: WebshopStruct.create(webshop)
83
83
  }));
84
84
  }
@@ -113,14 +113,14 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
113
113
  if (!webshop) {
114
114
  // Return organization, so we know the locale + can do some custom logic
115
115
  return new Response(GetWebshopFromDomainResult.create({
116
- organization: await organization.getStructure({emptyGroups: true}),
116
+ organization: organization.getBaseStructure(),
117
117
  webshop: null,
118
118
  webshops: []
119
119
  }));
120
120
  }
121
121
 
122
122
  return new Response(GetWebshopFromDomainResult.create({
123
- organization: await organization.getStructure({emptyGroups: true}),
123
+ organization: organization.getBaseStructure(),
124
124
  webshop: WebshopStruct.create(webshop)
125
125
  }));
126
126
  }
@@ -156,7 +156,7 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
156
156
 
157
157
  // Return organization, and the known webshops on this domain
158
158
  return new Response(GetWebshopFromDomainResult.create({
159
- organization: await organization.getStructure({emptyGroups: true}),
159
+ organization: organization.getBaseStructure(),
160
160
  webshop: null,
161
161
  webshops: webshops.map(w => WebshopPreview.create(w)).filter(w => w.isClosed(0) === false).sort((a, b) => Sorter.byStringValue(a.meta.name, b.meta.name))
162
162
  }));
@@ -180,7 +180,7 @@ export class GetWebshopFromDomainEndpoint extends Endpoint<Params, Query, Body,
180
180
  }
181
181
 
182
182
  return new Response(GetWebshopFromDomainResult.create({
183
- organization: await organization.getStructure({emptyGroups: true}),
183
+ organization: organization.getBaseStructure(),
184
184
  webshop: WebshopStruct.create(webshop)
185
185
  }));
186
186
  }
@@ -1,10 +1,11 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
2
- import { Group as GroupStruct, GroupPrivateSettings, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from "@stamhoofd/structures";
2
+ import { Group as GroupStruct, GroupPrivateSettings, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version, GroupType } from "@stamhoofd/structures";
3
3
 
4
4
  import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, StringDecoder } from "@simonbackx/simple-encoding";
5
5
  import { Context } from "../../../../helpers/Context";
6
6
  import { Group, Member, OrganizationRegistrationPeriod, Platform, RegistrationPeriod } from "@stamhoofd/models";
7
7
  import { SimpleError } from "@simonbackx/simple-errors";
8
+ import { AuthenticatedStructures } from "../../../../helpers/AuthenticatedStructures";
8
9
 
9
10
  type Params = Record<string, never>;
10
11
  type Query = undefined;
@@ -33,13 +34,13 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
33
34
 
34
35
  async handle(request: DecodedRequest<Params, Query, Body>) {
35
36
  const organization = await Context.setOrganizationScope();
36
- const {user} = await Context.authenticate()
37
+ await Context.authenticate()
37
38
 
38
39
  if (!await Context.auth.hasFullAccess(organization.id)) {
39
40
  throw Context.auth.error()
40
41
  }
41
42
 
42
- const structs: OrganizationRegistrationPeriodStruct[] = [];
43
+ const periods: OrganizationRegistrationPeriod[] = [];
43
44
 
44
45
  for (const {put} of request.body.getPuts()) {
45
46
  if (!await Context.auth.hasFullAccess(organization.id)) {
@@ -70,7 +71,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
70
71
  // Delete unreachable categories first
71
72
  await organizationPeriod.cleanCategories(groups);
72
73
  await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
73
- structs.push(organizationPeriod.getPrivateStructure(period, groups));
74
+ periods.push(organizationPeriod);
74
75
  }
75
76
 
76
77
  for (const patch of request.body.getPatches()) {
@@ -131,28 +132,27 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
131
132
 
132
133
  for (const groupPut of patch.groups.getPuts()) {
133
134
  await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, organizationPeriod.periodId, {allowedIds})
135
+ deleteUnreachable = true
134
136
  }
135
137
 
136
138
  for (const struct of patch.groups.getPatches()) {
137
139
  await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(struct)
138
140
  }
139
141
 
140
- const period = await RegistrationPeriod.getByID(organizationPeriod.periodId);
141
- const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
142
142
 
143
143
  if (deleteUnreachable) {
144
+ const groups = await Group.getAll(organization.id, organizationPeriod.periodId)
145
+
144
146
  // Delete unreachable categories first
145
147
  await organizationPeriod.cleanCategories(groups);
146
148
  await Group.deleteUnreachable(organization.id, organizationPeriod, groups)
147
149
  }
148
150
 
149
- if (period) {
150
- structs.push(organizationPeriod.getPrivateStructure(period, groups));
151
- }
151
+ periods.push(organizationPeriod);
152
152
  }
153
153
 
154
154
  return new Response(
155
- structs
155
+ await AuthenticatedStructures.organizationRegistrationPeriods(periods),
156
156
  );
157
157
  }
158
158
 
@@ -223,10 +223,64 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
223
223
  if (struct.defaultAgeGroupId !== undefined) {
224
224
  model.defaultAgeGroupId = await this.validateDefaultGroupId(struct.defaultAgeGroupId)
225
225
  }
226
+
227
+ const patch = struct;
228
+ if (patch.waitingList !== undefined) {
229
+ if (patch.waitingList === null) {
230
+ // delete
231
+ if (model.waitingListId) {
232
+ // for now don't delete, as waiting lists can be shared between multiple groups
233
+ // await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
234
+ model.waitingListId = null;
235
+ }
236
+
237
+ } else if (patch.waitingList.isPatch()) {
238
+ if (!model.waitingListId) {
239
+ throw new SimpleError({
240
+ code: 'invalid_field',
241
+ field: 'waitingList',
242
+ message: 'Cannot patch waiting list before it is created'
243
+ })
244
+ }
245
+ patch.waitingList.id = model.waitingListId
246
+ patch.waitingList.type = GroupType.WaitingList
247
+ await PatchOrganizationRegistrationPeriodsEndpoint.patchGroup(patch.waitingList)
248
+ } else {
249
+ if (model.waitingListId) {
250
+ // for now don't delete, as waiting lists can be shared between multiple groups
251
+ // await PatchOrganizationRegistrationPeriodsEndpoint.deleteGroup(model.waitingListId)
252
+ model.waitingListId = null;
253
+ }
254
+ patch.waitingList.type = GroupType.WaitingList
255
+
256
+ const existing = await Group.getByID(patch.waitingList.id)
257
+ if (existing) {
258
+ if (existing.organizationId !== model.organizationId) {
259
+ throw new SimpleError({
260
+ code: 'invalid_field',
261
+ field: 'waitingList',
262
+ message: 'Waiting list group is already used in another organization'
263
+ })
264
+ }
265
+
266
+ model.waitingListId = existing.id
267
+ } else {
268
+ const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
269
+ patch.waitingList,
270
+ model.organizationId,
271
+ model.periodId
272
+ )
273
+ model.waitingListId = group.id
274
+ }
275
+ }
276
+ }
226
277
 
227
278
  await model.updateOccupancy()
228
279
  await model.save();
229
- Member.updateMembershipsForGroupId(model.id)
280
+
281
+ if (struct.deletedAt !== undefined || struct.defaultAgeGroupId !== undefined) {
282
+ Member.updateMembershipsForGroupId(model.id)
283
+ }
230
284
  }
231
285
 
232
286
 
@@ -278,6 +332,29 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
278
332
  })
279
333
  }
280
334
 
335
+ if (struct.waitingList) {
336
+ const existing = await Group.getByID(struct.waitingList.id)
337
+ if (existing) {
338
+ if (existing.organizationId !== model.organizationId) {
339
+ throw new SimpleError({
340
+ code: 'invalid_field',
341
+ field: 'waitingList',
342
+ message: 'Waiting list group is already used in another organization'
343
+ })
344
+ }
345
+
346
+ model.waitingListId = existing.id
347
+ } else {
348
+ struct.waitingList.type = GroupType.WaitingList
349
+ const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(
350
+ struct.waitingList,
351
+ model.organizationId,
352
+ model.periodId
353
+ )
354
+ model.waitingListId = group.id
355
+ }
356
+ }
357
+
281
358
  await model.updateOccupancy()
282
359
  await model.save();
283
360
  return model;
@@ -46,17 +46,22 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
46
46
  })
47
47
  }
48
48
 
49
- const type = 'express'
49
+ const type = STAMHOOFD.STRIPE_CONNECT_METHOD
50
50
 
51
- let expressData: Stripe.AccountCreateParams = {
52
- country: organization.address.country,
53
- // Problem: we cannot set company or business_type, because then it defaults the structure of the company to one that requires a company number
51
+ const sharedData: Stripe.AccountCreateParams = {
54
52
  capabilities: {
55
53
  card_payments: { requested: true },
56
54
  transfers: { requested: true },
57
55
  bancontact_payments: { requested: true },
58
56
  ideal_payments: { requested: true },
59
57
  },
58
+ }
59
+
60
+ let expressData: Stripe.AccountCreateParams = {
61
+ country: organization.address.country,
62
+ controller: {
63
+ requirement_collection: 'application',
64
+ },
60
65
  settings: {
61
66
  payouts: {
62
67
  schedule: {
@@ -76,6 +81,7 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
76
81
  const stripe = StripeHelper.getInstance()
77
82
  const account = await stripe.accounts.create({
78
83
  type,
84
+ ...sharedData,
79
85
  ...expressData
80
86
  });
81
87
 
@@ -68,7 +68,10 @@ export class GetStripeAccountLinkEndpoint extends Endpoint<Params, Query, Body,
68
68
  refresh_url: request.body.refreshUrl,
69
69
  return_url: request.body.returnUrl,
70
70
  type: 'account_onboarding',
71
- collect: model.meta.type === 'express' ? 'eventually_due' : undefined, // Collect all at the start
71
+ collection_options: {
72
+ fields: 'eventually_due',
73
+ future_requirements: 'include'
74
+ }
72
75
  });
73
76
 
74
77
  return new Response(ResponseBody.create({
@@ -59,6 +59,12 @@ export class GetStripeLoginLinkEndpoint extends Endpoint<Params, Query, Body, Re
59
59
  })
60
60
  }
61
61
 
62
+ if (model.meta.type === 'standard') {
63
+ return new Response(ResponseBody.create({
64
+ url: 'https://dashboard.stripe.com/'
65
+ }));
66
+ }
67
+
62
68
  const stripe = StripeHelper.getInstance()
63
69
  const accountLink = await stripe.accounts.createLoginLink(model.accountId);
64
70
 
@@ -243,13 +243,18 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
243
243
  // Update balance item prices for this order if price has changed
244
244
  if (previousToPay !== model.totalToPay) {
245
245
  const items = await BalanceItem.where({ orderId: model.id })
246
- if (items.length === 1) {
246
+ if (items.length >= 1) {
247
247
  model.markUpdated()
248
248
  items[0].price = model.totalToPay
249
249
  items[0].description = model.generateBalanceDescription(webshop)
250
250
  items[0].updateStatus();
251
251
  await items[0].save()
252
- } else if (items.length === 0 && model.totalToPay > 0) {
252
+
253
+ // Zero out the other items
254
+ const otherItems = items.slice(1)
255
+ await BalanceItem.deleteItems(otherItems)
256
+ } else if (items.length === 0
257
+ && model.totalToPay > 0) {
253
258
  model.markUpdated()
254
259
  const balanceItem = new BalanceItem();
255
260
  balanceItem.orderId = model.id;
@@ -2,11 +2,11 @@ import { createMollieClient } from '@mollie/api-client';
2
2
  import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
3
3
  import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
4
4
  import { SimpleError } from "@simonbackx/simple-errors";
5
- import { BalanceItem, BalanceItemPayment, Member, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Registration, STPendingInvoice } from '@stamhoofd/models';
5
+ import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, STPendingInvoice } from '@stamhoofd/models';
6
6
  import { QueueHandler } from '@stamhoofd/queues';
7
- import { Payment as PaymentStruct, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
8
- import { Formatter } from '@stamhoofd/utility';
7
+ import { PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
9
8
 
9
+ import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
10
10
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
11
11
  import { Context } from '../../../helpers/Context';
12
12
  import { StripeHelper } from '../../../helpers/StripeHelper';
@@ -27,7 +27,7 @@ class Query extends AutoEncoder {
27
27
  cancel = false
28
28
  }
29
29
  type Body = undefined
30
- type ResponseBody = PaymentStruct | undefined;
30
+ type ResponseBody = PaymentGeneral | undefined
31
31
 
32
32
  /**
33
33
  * One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
@@ -51,6 +51,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
51
51
 
52
52
  async handle(request: DecodedRequest<Params, Query, Body>) {
53
53
  const organization = await Context.setOrganizationScope()
54
+ if (!request.query.exchange) {
55
+ await Context.authenticate()
56
+ }
54
57
 
55
58
  // Not method on payment because circular references (not supprted in ts)
56
59
  const payment = await ExchangePaymentEndpoint.pollStatus(request.params.id, organization, request.query.cancel)
@@ -66,28 +69,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
66
69
  }
67
70
 
68
71
  return new Response(
69
- PaymentStruct.create({
70
- id: payment.id,
71
- method: payment.method,
72
- provider: payment.provider,
73
- status: payment.status,
74
- price: payment.price,
75
- transferDescription: payment.transferDescription,
76
- paidAt: payment.paidAt,
77
- createdAt: payment.createdAt,
78
- updatedAt: payment.updatedAt
79
- })
72
+ await AuthenticatedStructures.paymentGeneral(payment, true)
80
73
  );
81
74
  }
82
-
83
- static async updateOutstanding(items: BalanceItem[], organizationId: string) {
84
- // Update outstanding amount of related members and registrations
85
- const memberIds: string[] = Formatter.uniqueArray(items.map(p => p.memberId).filter(id => id !== null)) as any
86
- await Member.updateOutstandingBalance(memberIds)
87
-
88
- const registrationIds: string[] = Formatter.uniqueArray(items.map(p => p.registrationId).filter(id => id !== null)) as any
89
- await Registration.updateOutstandingBalance(registrationIds, organizationId)
90
- }
91
75
 
92
76
  static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
93
77
  if (payment.status === status) {
@@ -101,15 +85,16 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
101
85
 
102
86
  // Prevent concurrency issues
103
87
  await QueueHandler.schedule("balance-item-update/"+organization.id, async () => {
88
+ const unloaded = (await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
104
89
  const balanceItemPayments = await BalanceItemPayment.balanceItem.load(
105
- (await BalanceItemPayment.where({paymentId: payment.id})).map(r => r.setRelation(BalanceItemPayment.payment, payment))
90
+ unloaded
106
91
  );
107
92
 
108
93
  for (const balanceItemPayment of balanceItemPayments) {
109
94
  await balanceItemPayment.markPaid(organization);
110
95
  }
111
96
 
112
- await this.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
97
+ await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
113
98
  })
114
99
 
115
100
  if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
@@ -152,7 +137,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
152
137
  await balanceItemPayment.undoPaid(organization);
153
138
  }
154
139
 
155
- await this.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
140
+ await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
156
141
  })
157
142
  }
158
143
 
@@ -166,7 +151,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
166
151
  await balanceItemPayment.markFailed(organization);
167
152
  }
168
153
 
169
- await this.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
154
+ await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
170
155
  })
171
156
  }
172
157
 
@@ -224,6 +224,12 @@ export class AdminPermissionChecker {
224
224
  return true
225
225
  }
226
226
 
227
+ if (member.registrations.length === 0 && permissionLevel !== PermissionLevel.Full && (this.organization && await this.hasFullAccess(this.organization.id, PermissionLevel.Full))) {
228
+ // Everyone with at least full access to at least one organization can access this member
229
+ // This allows organizations to register new members themselves
230
+ return true;
231
+ }
232
+
227
233
  for (const registration of member.registrations) {
228
234
  if (await this.canAccessRegistration(registration, permissionLevel)) {
229
235
  return true;
@@ -360,8 +366,7 @@ export class AdminPermissionChecker {
360
366
  permissionLevel: PermissionLevel = PermissionLevel.Read,
361
367
  data?: {
362
368
  registrations: Registration[],
363
- orders: Order[],
364
- members: Member[]
369
+ orders: Order[]
365
370
  }
366
371
  ): Promise<boolean> {
367
372
  for (const balanceItem of balanceItems) {
@@ -397,7 +402,7 @@ export class AdminPermissionChecker {
397
402
  }
398
403
 
399
404
  // Slight optimization possible here
400
- const {registrations, orders, members} = data ?? (this.user.permissions || permissionLevel === PermissionLevel.Read) ? (await Payment.loadBalanceItemRelations(balanceItems)) : {registrations: [], members: [] as Member[], orders: []}
405
+ const {registrations, orders} = data ?? (this.user.permissions || permissionLevel === PermissionLevel.Read) ? (await Payment.loadBalanceItemRelations(balanceItems)) : {registrations: [], orders: []}
401
406
 
402
407
  if (this.user.permissions) {
403
408
  // We grant permission for a whole payment when the user has at least permission for a part of that payment.
@@ -425,7 +430,7 @@ export class AdminPermissionChecker {
425
430
  // Check members
426
431
  const userMembers = await Member.getMembersWithRegistrationForUser(this.user)
427
432
  for (const member of userMembers) {
428
- if (members.find(m => m.id === member.id)) {
433
+ if (balanceItems.find(m => m.memberId === member.id)) {
429
434
  return true;
430
435
  }
431
436
  }
@@ -1,6 +1,7 @@
1
1
  import { SimpleError } from "@simonbackx/simple-errors";
2
2
  import { Event, Group, Member, MemberPlatformMembership, MemberResponsibilityRecord, MemberWithRegistrations, Organization, OrganizationRegistrationPeriod, Payment, RegistrationPeriod, User, Webshop } from "@stamhoofd/models";
3
3
  import { Event as EventStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberResponsibilityRecord as MemberResponsibilityRecordStruct, MemberWithRegistrationsBlob, MembersBlob, Organization as OrganizationStruct, PaymentGeneral, PermissionLevel, PrivateWebshop, User as UserStruct, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
4
+ import { OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, GroupCategory, GroupPrivateSettings, GroupSettings, GroupStatus, Group as GroupStruct, GroupType } from '@stamhoofd/structures';
4
5
 
5
6
  import { Context } from "./Context";
6
7
  import { Formatter } from "@stamhoofd/utility";
@@ -25,11 +26,11 @@ export class AuthenticatedStructures {
25
26
  }
26
27
 
27
28
  const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
28
- const {registrations, orders, members, groups} = await Payment.loadBalanceItemRelations(balanceItems);
29
+ const {registrations, orders, groups} = await Payment.loadBalanceItemRelations(balanceItems);
29
30
 
30
31
  if (checkPermissions) {
31
32
  // Note: permission checking is moved here for performacne to avoid loading the data multiple times
32
- if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders, members}))) {
33
+ if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders}))) {
33
34
  throw new SimpleError({
34
35
  code: "not_found",
35
36
  message: "Payment not found",
@@ -40,22 +41,82 @@ export class AuthenticatedStructures {
40
41
 
41
42
  const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
42
43
 
43
- return Payment.getGeneralStructureFromRelations({
44
+ return await Payment.getGeneralStructureFromRelations({
44
45
  payments,
45
46
  balanceItemPayments,
46
47
  balanceItems,
47
48
  registrations,
48
49
  orders,
49
- members,
50
50
  groups
51
51
  }, includeSettlements)
52
52
  }
53
53
 
54
54
  static async group(group: Group) {
55
- if (!await Context.optionalAuth?.canAccessGroup(group)) {
56
- return group.getStructure()
55
+ return (await this.groups([group]))[0]
56
+ }
57
+
58
+ static async groups(groups: Group[]) {
59
+ const waitingListIds = Formatter.uniqueArray(groups.map(g => g.waitingListId).filter(id => id !== null) as string[])
60
+ const waitingLists = waitingListIds.length > 0 ? await Group.getByIDs(...waitingListIds) : []
61
+
62
+ const structs: GroupStruct[] = []
63
+ for (const group of groups) {
64
+ const waitingList = waitingLists.find(g => g.id == group.waitingListId) ?? null
65
+ const waitingListStruct = waitingList ? GroupStruct.create(waitingList) : null
66
+ if (waitingList && waitingListStruct && !await Context.optionalAuth?.canAccessGroup(waitingList)) {
67
+ waitingListStruct.privateSettings = null;
68
+ }
69
+
70
+ const struct = GroupStruct.create({
71
+ ...group,
72
+ waitingList: waitingListStruct
73
+ })
74
+
75
+ if (!await Context.optionalAuth?.canAccessGroup(group)) {
76
+ struct.privateSettings = null;
77
+ }
78
+
79
+ structs.push(struct)
80
+ }
81
+
82
+ return structs;
83
+ }
84
+
85
+ static async organizationRegistrationPeriods(organizationRegistrationPeriods: OrganizationRegistrationPeriod[]) {
86
+ if (organizationRegistrationPeriods.length === 0) {
87
+ return [];
88
+ }
89
+
90
+ const periodIds = Formatter.uniqueArray(organizationRegistrationPeriods.map(p => p.periodId))
91
+ const periods = await RegistrationPeriod.getByIDs(...periodIds)
92
+
93
+ const groupIds = Formatter.uniqueArray(organizationRegistrationPeriods.flatMap(p => p.settings.categories.flatMap(c => c.groupIds)))
94
+ const groups = groupIds.length ? await Group.getByIDs(...groupIds) : []
95
+
96
+ const groupStructs = await this.groups(groups)
97
+
98
+ const structs: OrganizationRegistrationPeriodStruct[] = []
99
+ for (const organizationPeriod of organizationRegistrationPeriods) {
100
+ const period = periods.find(p => p.id == organizationPeriod.periodId) ?? null
101
+ if (!period) {
102
+ continue
103
+ }
104
+ const groupIds = Formatter.uniqueArray(organizationPeriod.settings.categories.flatMap(c => c.groupIds))
105
+
106
+ structs.push(
107
+ OrganizationRegistrationPeriodStruct.create({
108
+ ...organizationPeriod,
109
+ period: period.getStructure(),
110
+ groups: groupStructs.filter(gg => groupIds.includes(gg.id)).sort(GroupStruct.defaultSort)
111
+ })
112
+ )
57
113
  }
58
- return group.getPrivateStructure()
114
+
115
+ return structs
116
+ }
117
+
118
+ static async organizationRegistrationPeriod(organizationRegistrationPeriod: OrganizationRegistrationPeriod) {
119
+ return (await this.organizationRegistrationPeriods([organizationRegistrationPeriod]))[0]
59
120
  }
60
121
 
61
122
  static async webshop(webshop: Webshop) {
@@ -66,6 +127,8 @@ export class AuthenticatedStructures {
66
127
  }
67
128
 
68
129
  static async organization(organization: Organization): Promise<OrganizationStruct> {
130
+ const organizationPeriod = await organization.getPeriod()
131
+
69
132
  if (await Context.optionalAuth?.canAccessPrivateOrganizationData(organization)) {
70
133
  const webshops = await Webshop.where({ organizationId: organization.id }, { select: Webshop.selectColumnsWithout(undefined, "products", "categories")})
71
134
  const webshopStructures: WebshopPreview[] = []
@@ -77,35 +140,29 @@ export class AuthenticatedStructures {
77
140
  webshopStructures.push(WebshopPreview.create(w))
78
141
  }
79
142
 
80
- const {groups, organizationPeriod, period} = await organization.getPeriod({emptyGroups: false})
81
-
82
143
  return OrganizationStruct.create({
83
- id: organization.id,
84
- name: organization.name,
85
- meta: organization.meta,
86
- address: organization.address,
87
- registerDomain: organization.registerDomain,
88
- uri: organization.uri,
89
- website: organization.website,
144
+ ...organization.getBaseStructure(),
90
145
  privateMeta: organization.privateMeta,
91
146
  webshops: webshopStructures,
92
- createdAt: organization.createdAt,
93
- period: organizationPeriod.getPrivateStructure(period, groups)
147
+ period: await this.organizationRegistrationPeriod(organizationPeriod)
94
148
  })
95
149
  }
96
150
 
97
- return await organization.getStructure()
151
+ return OrganizationStruct.create({
152
+ ...organization.getBaseStructure(),
153
+ period: await this.organizationRegistrationPeriod(organizationPeriod)
154
+ })
98
155
  }
99
156
 
100
157
  static async adminOrganizations(organizations: Organization[]): Promise<OrganizationStruct[]> {
101
158
  const structs: OrganizationStruct[] = [];
102
159
 
103
160
  for (const organization of organizations) {
104
- const base = await organization.getStructure({emptyGroups: true})
161
+ const base = organization.getBaseStructure()
105
162
  structs.push(base)
106
163
  }
107
164
 
108
- return structs
165
+ return Promise.resolve(structs)
109
166
  }
110
167
 
111
168
  static async userWithMembers(user: User): Promise<UserWithMembers> {
@@ -114,7 +171,9 @@ export class AuthenticatedStructures {
114
171
  return UserWithMembers.create({
115
172
  ...user,
116
173
  hasAccount: user.hasAccount(),
117
- members: await this.membersBlob(members, false, user)
174
+
175
+ // Always include the current context organization - because it is possible we switch organization and we don't want to refetch every time
176
+ members: await this.membersBlob(members, true, user)
118
177
  })
119
178
  }
120
179
 
@@ -207,17 +266,19 @@ export class AuthenticatedStructures {
207
266
  // Load groups
208
267
  const groupIds = events.map(e => e.groupId).filter(id => id !== null) as string[]
209
268
  const groups = groupIds.length > 0 ? await Group.getByIDs(...groupIds) : []
269
+ const groupStructs = await this.groups(groups)
210
270
 
211
271
  const result: EventStruct[] = []
212
272
 
213
273
  for (const event of events) {
214
- const group = groups.find(g => g.id == event.groupId) ?? null
274
+ const group = groupStructs.find(g => g.id == event.groupId) ?? null
215
275
 
216
- if (group && await Context.auth.canAccessGroup(group)) {
217
- result.push(event.getPrivateStructure(group))
218
- } else {
219
- result.push(event.getStructure(group))
220
- }
276
+ const struct = EventStruct.create({
277
+ ...event,
278
+ group
279
+ })
280
+
281
+ result.push(struct)
221
282
  }
222
283
 
223
284
  return result