@stamhoofd/backend 2.101.0 → 2.103.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 (29) hide show
  1. package/package.json +10 -10
  2. package/src/crons.ts +21 -26
  3. package/src/email-recipient-loaders/receivable-balances.ts +18 -11
  4. package/src/endpoints/global/events/PatchEventsEndpoint.ts +1 -0
  5. package/src/endpoints/global/members/helpers/validateGroupFilter.ts +12 -8
  6. package/src/endpoints/global/registration/GetRegistrationsEndpoint.ts +30 -7
  7. package/src/endpoints/global/registration/GetUserPayableBalanceEndpoint.ts +4 -4
  8. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +382 -31
  9. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +47 -23
  10. package/src/endpoints/organization/dashboard/billing/GetOrganizationPayableBalanceEndpoint.ts +2 -2
  11. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.ts +60 -8
  12. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint.ts +6 -1
  13. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +12 -9
  14. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +4 -2
  15. package/src/excel-loaders/receivable-balances.ts +2 -2
  16. package/src/helpers/AdminPermissionChecker.ts +68 -10
  17. package/src/helpers/AuthenticatedStructures.ts +18 -10
  18. package/src/helpers/MemberUserSyncer.ts +2 -2
  19. package/src/seeds/{1735577912-update-cached-outstanding-balance-from-items.ts → 1760702454-update-cached-outstanding-balance-from-items.ts} +2 -0
  20. package/src/services/BalanceItemService.ts +12 -0
  21. package/src/services/PaymentService.ts +3 -0
  22. package/src/sql-filters/base-registration-filter-compilers.ts +59 -0
  23. package/src/sql-filters/orders.ts +97 -1
  24. package/src/sql-filters/receivable-balances.ts +7 -1
  25. package/tests/e2e/bundle-discounts.test.ts +327 -1
  26. package/tests/helpers/PayconiqMocker.ts +22 -0
  27. package/tests/init/index.ts +1 -0
  28. package/tests/init/initAdmin.ts +2 -2
  29. package/tests/init/initPermissionRole.ts +12 -0
@@ -5,7 +5,7 @@ import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
6
  import { Email } from '@stamhoofd/email';
7
7
  import { BalanceItem, BalanceItemPayment, CachedBalance, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
8
- import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
8
+ import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItem as BalanceItemStruct, BalanceItemType, IDRegisterCheckout, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PaymentType, PermissionLevel, PlatformFamily, PlatformMember, ReceivableBalanceType, RegisterItem, RegisterResponse, TranslatedString, Version } from '@stamhoofd/structures';
9
9
  import { Formatter } from '@stamhoofd/utility';
10
10
 
11
11
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -67,8 +67,25 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
67
67
  const organization = await Context.setOrganizationScope();
68
68
  const { user } = await Context.authenticate();
69
69
 
70
- if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
71
- if (!await Context.auth.canManageFinances(request.body.asOrganizationId)) {
70
+ // Who is going to pay?
71
+ let whoWillPayNow: 'member' | 'organization' | 'nobody' = 'member'; // if this is set to 'organization', there will also be created separate balance items so the member can pay back the paying organization
72
+
73
+ if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
74
+ // Fast fail
75
+ if (!await Context.auth.hasSomeAccess(request.body.asOrganizationId)) {
76
+ throw new SimpleError({
77
+ code: 'forbidden',
78
+ message: 'No permission to register members manually',
79
+ human: $t(`62fe6e39-f6b0-4836-b0f7-dc84d22a81e3`),
80
+ statusCode: 403,
81
+ });
82
+ }
83
+
84
+ // We won't create a payment. The balance will get added to the outstanding amount of the member / already paying organization
85
+ whoWillPayNow = 'nobody';
86
+ }
87
+ else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
88
+ if (!await Context.auth.hasFullAccess(request.body.asOrganizationId)) {
72
89
  throw new SimpleError({
73
90
  code: 'forbidden',
74
91
  message: 'No permission to register as this organization for a different organization',
@@ -76,6 +93,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
76
93
  statusCode: 403,
77
94
  });
78
95
  }
96
+
97
+ // The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
98
+ whoWillPayNow = 'organization';
79
99
  }
80
100
 
81
101
  // For non paid organizations, limit amount of tests
@@ -145,7 +165,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
145
165
  }
146
166
 
147
167
  for (const member of members) {
148
- if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
168
+ if (!await Context.auth.canAccessMember(
169
+ member,
170
+ request.body.asOrganizationId ? PermissionLevel.Read : PermissionLevel.Write,
171
+ )) {
149
172
  throw new SimpleError({
150
173
  code: 'forbidden',
151
174
  message: 'No permission to register this member',
@@ -170,7 +193,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
170
193
  for (const memberId of memberIds) {
171
194
  const familyMembers = (await Member.getFamilyWithRegistrations(memberId));
172
195
  members.push(...familyMembers);
173
- const blob = await AuthenticatedStructures.membersBlob(familyMembers, true);
196
+ const blob = await AuthenticatedStructures.membersBlob(familyMembers, true, undefined, { forAdminCartCalculation: true });
174
197
  const family = PlatformFamily.create(blob, {
175
198
  platform: platformStruct,
176
199
  contextOrganization,
@@ -225,22 +248,18 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
225
248
  const totalPrice = checkout.totalPrice;
226
249
 
227
250
  if (totalPrice !== request.body.totalPrice) {
228
- throw new SimpleError({
229
- code: 'changed_price',
230
- message: $t(`e424d549-2bb8-4103-9a14-ac4063d7d454`, { total: Formatter.price(totalPrice) }),
231
- });
232
- }
233
-
234
- // Who is going to pay?
235
- let whoWillPayNow: 'member' | 'organization' | 'nobody' = 'member'; // if this is set to 'organization', there will also be created separate balance items so the member can pay back the paying organization
236
-
237
- if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
238
- // Will get added to the outstanding amount of the member / already paying organization
239
- whoWillPayNow = 'nobody';
240
- }
241
- else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
242
- // The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
243
- whoWillPayNow = 'organization';
251
+ if (whoWillPayNow === 'nobody') {
252
+ // Safe and important to ignore: we are only updating the outstanding amounts
253
+ // If we would throw here, that could leak personal data (e.g. that the user uses financial support)
254
+ }
255
+ else {
256
+ // when whoWillPay = organization/member, we should throw or the payment amount could be different / incorrect.
257
+ // This never leaks information because in this case the user already has full access to the organization (asOrganizationId) or the member
258
+ throw new SimpleError({
259
+ code: 'changed_price',
260
+ message: $t(`e424d549-2bb8-4103-9a14-ac4063d7d454`, { total: Formatter.price(totalPrice) }),
261
+ });
262
+ }
244
263
  }
245
264
 
246
265
  const registrationMemberRelation = new ManyToOneRelation(Member, 'member');
@@ -315,7 +334,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
315
334
  );
316
335
 
317
336
  // Never reuse a registration that has a balance - that means they had a cancellation fee and not all balance items were canceled (we don't want to merge in that state)
318
- const balances = await CachedBalance.getForObjects(possibleReuseRegistrations.map(r => r.id), null);
337
+ const balances = await CachedBalance.getForObjects(possibleReuseRegistrations.map(r => r.id), null, ReceivableBalanceType.registration);
319
338
 
320
339
  reuseRegistration = possibleReuseRegistrations.find((r) => {
321
340
  const balance = balances.filter(b => b.objectId === r.id).reduce((a, b) => a + b.amountOpen + b.amountPaid + b.amountPending, 0);
@@ -763,6 +782,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
763
782
  }
764
783
 
765
784
  let paymentUrl: string | null = null;
785
+ let paymentQRCode: string | null = null;
766
786
  let payment: Payment | null = null;
767
787
 
768
788
  // Delay marking as valid as late as possible so any errors will prevent creating valid balance items
@@ -814,6 +834,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
814
834
 
815
835
  if (response) {
816
836
  paymentUrl = response.paymentUrl;
837
+ paymentQRCode = response.paymentQRCode;
817
838
  payment = response.payment;
818
839
  }
819
840
  }
@@ -841,6 +862,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
841
862
  members: await AuthenticatedStructures.membersBlob(updatedMembers),
842
863
  registrations: registrations.map(r => Member.getRegistrationWithTinyMemberStructure(r)),
843
864
  paymentUrl,
865
+ paymentQRCode,
844
866
  }));
845
867
  }
846
868
 
@@ -1047,6 +1069,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
1047
1069
  const description = $t(`33a926ea-9bc7-444e-becc-c0f2f70e1f0e`) + ' ' + organization.name;
1048
1070
 
1049
1071
  let paymentUrl: string | null = null;
1072
+ let paymentQRCode: string | null = null;
1050
1073
 
1051
1074
  try {
1052
1075
  // Update balance items
@@ -1144,7 +1167,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
1144
1167
  await dbPayment.save();
1145
1168
  }
1146
1169
  else if (payment.provider === PaymentProvider.Payconiq) {
1147
- paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl);
1170
+ ({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl));
1148
1171
  }
1149
1172
  else if (payment.provider == PaymentProvider.Buckaroo) {
1150
1173
  // Increase request timeout because buckaroo is super slow (in development)
@@ -1175,6 +1198,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
1175
1198
  provider,
1176
1199
  stripeAccount,
1177
1200
  paymentUrl,
1201
+ paymentQRCode,
1178
1202
  };
1179
1203
  }
1180
1204
  }
@@ -1,5 +1,5 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
- import { PayableBalanceCollection } from '@stamhoofd/structures';
2
+ import { PayableBalanceCollection, ReceivableBalanceType } from '@stamhoofd/structures';
3
3
 
4
4
  import { Context } from '../../../../helpers/Context';
5
5
  import { GetUserPayableBalanceEndpoint } from '../../../global/registration/GetUserPayableBalanceEndpoint';
@@ -34,6 +34,6 @@ export class GetOrganizationPayableBalanceEndpoint extends Endpoint<Params, Quer
34
34
  throw Context.auth.error();
35
35
  }
36
36
 
37
- return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([organization.id], null));
37
+ return new Response(await GetUserPayableBalanceEndpoint.getBillingStatusForObjects([organization.id], null, ReceivableBalanceType.organization));
38
38
  }
39
39
  }
@@ -1,11 +1,12 @@
1
1
  import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
2
2
  import { DetailedReceivableBalance, PaymentStatus, ReceivableBalanceType } from '@stamhoofd/structures';
3
3
 
4
- import { BalanceItem, BalanceItemPayment, CachedBalance, Payment } from '@stamhoofd/models';
4
+ import { BalanceItem, BalanceItemPayment, CachedBalance, MemberUser, Payment } from '@stamhoofd/models';
5
5
  import { Context } from '../../../../helpers/Context';
6
6
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures';
7
7
  import { SQL } from '@stamhoofd/sql';
8
8
  import { BalanceItemService } from '../../../../services/BalanceItemService';
9
+ import { Formatter } from '@stamhoofd/utility';
9
10
 
10
11
  type Params = { id: string; type: ReceivableBalanceType };
11
12
  type Query = undefined;
@@ -38,14 +39,13 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
38
39
  throw Context.auth.error();
39
40
  }
40
41
 
41
- // Flush caches (this makes sure that we do a reload in the frontend after a registration or change, we get the newest balances)
42
- await BalanceItemService.flushCaches(organization.id);
43
-
44
- const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type, true);
45
42
  let paymentModels: Payment[] = [];
46
43
 
47
44
  switch (request.params.type) {
48
45
  case ReceivableBalanceType.organization: {
46
+ // Force cache updates, because sometimes the cache could be out of date
47
+ BalanceItemService.scheduleOrganizationUpdate(organization.id, request.params.id);
48
+
49
49
  paymentModels = await Payment.select()
50
50
  .where('organizationId', organization.id)
51
51
  .andWhere(
@@ -66,6 +66,9 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
66
66
  }
67
67
 
68
68
  case ReceivableBalanceType.member: {
69
+ // Force cache updates, because sometimes the cache could be out of date
70
+ BalanceItemService.scheduleMemberUpdate(organization.id, request.params.id);
71
+
69
72
  paymentModels = await Payment.select()
70
73
  .where('organizationId', organization.id)
71
74
  .join(
@@ -86,7 +89,17 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
86
89
  }
87
90
 
88
91
  case ReceivableBalanceType.user: {
89
- paymentModels = await Payment.select()
92
+ const memberUsers = await MemberUser.select().where('usersId', request.params.id).fetch();
93
+ const memberIds = Formatter.uniqueArray(memberUsers.map(mu => mu.membersId));
94
+
95
+ // Force cache updates, because sometimes the cache could be out of date
96
+ BalanceItemService.scheduleUserUpdate(organization.id, request.params.id);
97
+
98
+ for (const memberId of memberIds) {
99
+ BalanceItemService.scheduleMemberUpdate(organization.id, memberId);
100
+ }
101
+
102
+ const q = Payment.select()
90
103
  .where('organizationId', organization.id)
91
104
  .join(
92
105
  SQL.join(BalanceItemPayment.table)
@@ -95,8 +108,44 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
95
108
  .join(
96
109
  SQL.join(BalanceItem.table)
97
110
  .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
111
+ );
112
+
113
+ if (memberIds.length === 0) {
114
+ q.where(SQL.column(BalanceItem.table, 'userId'), request.params.id);
115
+ }
116
+ else {
117
+ q.where(
118
+ SQL.where(SQL.column(BalanceItem.table, 'userId'), request.params.id)
119
+ .or(SQL.column(BalanceItem.table, 'memberId'), memberIds),
120
+ );
121
+ }
122
+
123
+ paymentModels = await q
124
+ .andWhere(
125
+ SQL.whereNot('status', PaymentStatus.Failed),
98
126
  )
99
- .where(SQL.column(BalanceItem.table, 'userId'), request.params.id)
127
+ .groupBy(SQL.column(Payment.table, 'id'))
128
+ .fetch();
129
+ break;
130
+ }
131
+
132
+ case ReceivableBalanceType.userWithoutMembers: {
133
+ // Force cache updates, because sometimes the cache could be out of date
134
+ BalanceItemService.scheduleUserUpdate(organization.id, request.params.id);
135
+
136
+ const q = Payment.select()
137
+ .where('organizationId', organization.id)
138
+ .join(
139
+ SQL.join(BalanceItemPayment.table)
140
+ .where(SQL.column(BalanceItemPayment.table, 'paymentId'), SQL.column(Payment.table, 'id')),
141
+ )
142
+ .join(
143
+ SQL.join(BalanceItem.table)
144
+ .where(SQL.column(BalanceItemPayment.table, 'balanceItemId'), SQL.column(BalanceItem.table, 'id')),
145
+ )
146
+ .where(SQL.column(BalanceItem.table, 'userId'), request.params.id);
147
+
148
+ paymentModels = await q
100
149
  .andWhere(
101
150
  SQL.whereNot('status', PaymentStatus.Failed),
102
151
  )
@@ -126,10 +175,13 @@ export class GetReceivableBalanceEndpoint extends Endpoint<Params, Query, Body,
126
175
  }
127
176
  }
128
177
 
178
+ // Flush caches (this makes sure that we do a reload in the frontend after a registration or change, we get the newest balances)
179
+ await BalanceItemService.flushCaches(organization.id);
180
+ const balanceItemModels = await CachedBalance.balanceForObjects(organization.id, [request.params.id], request.params.type);
129
181
  const balanceItems = await BalanceItem.getStructureWithPayments(balanceItemModels);
130
182
  const payments = await AuthenticatedStructures.paymentsGeneral(paymentModels, false);
131
183
 
132
- const balances = await CachedBalance.getForObjects([request.params.id], organization.id);
184
+ const balances = await CachedBalance.getForObjects([request.params.id], organization.id, request.params.type);
133
185
 
134
186
  const created = new CachedBalance();
135
187
  created.amountOpen = 0;
@@ -83,7 +83,12 @@ export class GetReceivableBalancesEndpoint extends Endpoint<Params, Query, Body,
83
83
  },
84
84
  {
85
85
  users: {
86
- $elemMatch: { name: { $contains: q.search } },
86
+ $elemMatch: {
87
+ $or: {
88
+ name: { $contains: q.search },
89
+ email: { $contains: q.search },
90
+ },
91
+ },
87
92
  },
88
93
  },
89
94
  ],
@@ -37,7 +37,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
37
37
  const organization = await Context.setOrganizationScope();
38
38
  await Context.authenticate();
39
39
 
40
- if (!await Context.auth.hasFullAccess(organization.id)) {
40
+ if (!await Context.auth.hasSomeAccess(organization.id)) {
41
41
  throw Context.auth.error();
42
42
  }
43
43
 
@@ -214,6 +214,14 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
214
214
  for (const groupPut of patch.groups.getPuts()) {
215
215
  shouldUpdateSetupSteps = true;
216
216
  groupPut.put.settings.throwIfInvalidPrices();
217
+ if (groupPut.put.type === GroupType.EventRegistration) {
218
+ throw new SimpleError({
219
+ code: 'invalid_group_type',
220
+ message: 'Cannot create groups for events via this endpoint',
221
+ human: $t(`40dde58e-47fb-4adb-971a-537b16c479d5`),
222
+ });
223
+ }
224
+
217
225
  const group = await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, period, { allowedIds });
218
226
  deleteUnreachable = true;
219
227
  forceGroupIds.push(group.id);
@@ -528,14 +536,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
528
536
  static async createGroup(struct: GroupStruct, organizationId: string, period: RegistrationPeriod, options?: { allowedIds?: string[] }): Promise<Group> {
529
537
  const allowedIds = options?.allowedIds ?? [];
530
538
 
531
- if (struct.type === GroupType.Membership || struct.type === GroupType.WaitingList) {
539
+ if (struct.type !== GroupType.EventRegistration && !allowedIds.includes(struct.id)) {
532
540
  if (!await Context.auth.hasFullAccess(organizationId)) {
533
- if (allowedIds.includes(struct.id)) {
534
- // Ok
535
- }
536
- else {
537
- throw Context.auth.error($t(`153a7443-e2d9-4126-8e10-089b54964fb8`));
538
- }
541
+ throw Context.auth.error($t(`153a7443-e2d9-4126-8e10-089b54964fb8`));
539
542
  }
540
543
  }
541
544
  else {
@@ -587,7 +590,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
587
590
  model.settings.registeredMembers = 0;
588
591
  model.settings.reservedMembers = 0;
589
592
 
590
- if (!await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
593
+ if (struct.type !== GroupType.EventRegistration && !await Context.auth.canAccessGroup(model, PermissionLevel.Full)) {
591
594
  // Create a temporary permission role for this user
592
595
  const organizationPermissions = user.permissions?.organizationPermissions?.get(organizationId);
593
596
  if (!organizationPermissions) {
@@ -224,7 +224,8 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
224
224
  balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem));
225
225
 
226
226
  let paymentUrl: string | null = null;
227
- const description = webshop.meta.name + ' - ' + payment.id;
227
+ let paymentQRCode: string | null = null;
228
+ const description = webshop.meta.name + ' - ' + organization.name;
228
229
 
229
230
  if (payment.method === PaymentMethod.Transfer) {
230
231
  await order.markValid(payment, []);
@@ -324,7 +325,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
324
325
  await dbPayment.save();
325
326
  }
326
327
  else if (payment.provider == PaymentProvider.Payconiq) {
327
- paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, exchangeUrl);
328
+ ({ paymentUrl, paymentQRCode } = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, exchangeUrl));
328
329
  }
329
330
  else if (payment.provider == PaymentProvider.Buckaroo) {
330
331
  // Increase request timeout because buckaroo is super slow
@@ -351,6 +352,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
351
352
 
352
353
  return new Response(OrderResponse.create({
353
354
  paymentUrl: paymentUrl,
355
+ paymentQRCode,
354
356
  order: OrderStruct.create({ ...order, payment: PaymentStruct.create(payment) }),
355
357
  }));
356
358
  }
@@ -1,5 +1,5 @@
1
1
  import { XlsxBuiltInNumberFormat, XlsxTransformerColumn, XlsxTransformerConcreteColumn } from '@stamhoofd/excel-writer';
2
- import { BalanceItemRelationType, BalanceItemWithPayments, DetailedReceivableBalance, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemStatusName, getBalanceItemTypeName, getReceivableBalanceTypeNameNotTranslated, PaginatedResponse, ReceivableBalance } from '@stamhoofd/structures';
2
+ import { BalanceItemRelationType, BalanceItemWithPayments, DetailedReceivableBalance, ExcelExportType, getBalanceItemRelationTypeName, getBalanceItemStatusName, getBalanceItemTypeName, getReceivableBalanceTypeName, PaginatedResponse, ReceivableBalance } from '@stamhoofd/structures';
3
3
  import { Formatter } from '@stamhoofd/utility';
4
4
  import { ExportToExcelEndpoint } from '../endpoints/global/files/ExportToExcelEndpoint';
5
5
  import { GetReceivableBalancesEndpoint } from '../endpoints/organization/dashboard/receivable-balances/GetReceivableBalancesEndpoint';
@@ -287,7 +287,7 @@ function getGeneralColumns(): XlsxTransformerConcreteColumn<ReceivableBalance>[]
287
287
  name: $t(`a0dfe596-0670-48bc-a5f3-2c9308c70a17`),
288
288
  width: 10,
289
289
  getValue: (object: ReceivableBalance) => ({
290
- value: getReceivableBalanceTypeNameNotTranslated(object.objectType),
290
+ value: getReceivableBalanceTypeName(object.objectType),
291
291
  }),
292
292
  },
293
293
  ];
@@ -1,7 +1,7 @@
1
1
  import { AutoEncoderPatchType, PatchMap } from '@simonbackx/simple-encoding';
2
2
  import { isSimpleError, isSimpleErrors, SimpleError } from '@simonbackx/simple-errors';
3
3
  import { BalanceItem, CachedBalance, Document, Email, EmailTemplate, Event, EventNotification, Group, Member, MemberPlatformMembership, MemberWithRegistrations, Order, Organization, OrganizationRegistrationPeriod, Payment, Registration, User, Webshop } from '@stamhoofd/models';
4
- import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
4
+ import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionChecker, FinancialSupportSettings, GroupCategory, GroupStatus, GroupType, MemberWithRegistrationsBlob, PermissionLevel, PermissionsResourceType, Platform as PlatformStruct, ReceivableBalanceType, RecordSettings, ResourcePermissions } from '@stamhoofd/structures';
5
5
  import { Formatter } from '@stamhoofd/utility';
6
6
  import { MemberRecordStore } from '../services/MemberRecordStore';
7
7
  import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess';
@@ -70,6 +70,7 @@ export class AdminPermissionChecker {
70
70
  return await cache;
71
71
  }
72
72
 
73
+ console.log('Get group', groupId);
73
74
  const promise = Group.select()
74
75
  .where('id', groupId)
75
76
  .first(false);
@@ -80,6 +81,46 @@ export class AdminPermissionChecker {
80
81
  return group;
81
82
  }
82
83
 
84
+ async getGroups(groupIds: string[]): Promise<Group[]> {
85
+ const cached: Group[] = [];
86
+ const remainingIds: string[] = [];
87
+
88
+ for (const groupId of groupIds) {
89
+ const cache = this.groupsCache.get(groupId);
90
+ if (cache !== undefined) {
91
+ const resolved = await cache;
92
+ if (resolved) {
93
+ cached.push(resolved);
94
+ }
95
+ else {
96
+ // Not found, no need to readd
97
+ }
98
+ }
99
+ else {
100
+ remainingIds.push(groupId);
101
+ }
102
+ }
103
+
104
+ if (remainingIds.length > 0) {
105
+ console.log('Get groups', remainingIds);
106
+ const promise = Group.select()
107
+ .where('id', remainingIds)
108
+ .fetch();
109
+
110
+ for (const groupId of remainingIds) {
111
+ this.groupsCache.set(groupId, promise.then(list => list.find(l => l.id === groupId) ?? null));
112
+ }
113
+ const groups = await promise;
114
+ cached.push(...groups);
115
+
116
+ for (const groupId of remainingIds) {
117
+ this.groupsCache.set(groupId, groups.find(l => l.id === groupId) ?? null);
118
+ }
119
+ }
120
+
121
+ return cached;
122
+ }
123
+
83
124
  cacheGroup(group: Group) {
84
125
  this.groupsCache.set(group.id, group);
85
126
  }
@@ -178,7 +219,7 @@ export class AdminPermissionChecker {
178
219
  async canAccessGroup(group: Group, permissionLevel: PermissionLevel = PermissionLevel.Read): Promise<boolean> {
179
220
  // Check permissions aren't scoped to a specific organization, and they mismatch
180
221
  if (!this.checkScope(group.organizationId)) {
181
- return false;
222
+ // return false;
182
223
  }
183
224
  const organization = await this.getOrganization(group.organizationId);
184
225
 
@@ -246,8 +287,13 @@ export class AdminPermissionChecker {
246
287
  return true;
247
288
  }
248
289
  if (asOrganizationId) {
249
- if (group.settings.allowRegistrationsByOrganization) {
250
- return await this.hasFullAccess(asOrganizationId);
290
+ if (group.settings.allowRegistrationsByOrganization && !group.getStructure().closed) {
291
+ if (group.organizationId !== asOrganizationId) {
292
+ return await this.hasFullAccess(asOrganizationId);
293
+ }
294
+ else {
295
+ return await this.hasSomeAccess(asOrganizationId);
296
+ }
251
297
  }
252
298
  }
253
299
  return false;
@@ -337,7 +383,7 @@ export class AdminPermissionChecker {
337
383
  }
338
384
 
339
385
  for (const registration of member.registrations) {
340
- if (await this.canAccessRegistration(registration, permissionLevel)) {
386
+ if (await this.canAccessRegistration(registration, permissionLevel, false)) {
341
387
  return true;
342
388
  }
343
389
  }
@@ -359,7 +405,7 @@ export class AdminPermissionChecker {
359
405
  */
360
406
  async canDeleteMember(member: MemberWithRegistrations) {
361
407
  if (member.registrations.length === 0 && this.isUserManager(member)) {
362
- const cachedBalance = await CachedBalance.getForObjects([member.id]);
408
+ const cachedBalance = await CachedBalance.getForObjects([member.id], null, ReceivableBalanceType.member);
363
409
  if (cachedBalance.length === 0 || (cachedBalance[0].amountOpen === 0 && cachedBalance[0].amountPending === 0)) {
364
410
  const platformMemberships = await MemberPlatformMembership.where({ memberId: member.id });
365
411
  if (platformMemberships.length === 0) {
@@ -378,7 +424,7 @@ export class AdminPermissionChecker {
378
424
  /**
379
425
  * Note: only checks admin permissions. Users that 'own' this member can also access it but that does not use the AdminPermissionChecker
380
426
  */
381
- async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read) {
427
+ async canAccessRegistration(registration: Registration, permissionLevel: PermissionLevel = PermissionLevel.Read, checkMember: boolean | MemberWithRegistrations = true) {
382
428
  if (registration.deactivatedAt || !registration.registeredAt) {
383
429
  // No full access: cannot access deactivated registrations
384
430
  return false;
@@ -404,7 +450,7 @@ export class AdminPermissionChecker {
404
450
  }
405
451
  }
406
452
 
407
- const group = await this.getGroup(registration.groupId);
453
+ const group = Registration.group.isLoaded(registration) ? ((registration as any).group as Group) : await this.getGroup(registration.groupId);
408
454
  if (!group || group.deletedAt) {
409
455
  return false;
410
456
  }
@@ -413,6 +459,17 @@ export class AdminPermissionChecker {
413
459
  return true;
414
460
  }
415
461
 
462
+ if (permissionLevel === PermissionLevel.Read && checkMember && group.settings.implicitlyAllowViewRegistrations) {
463
+ // We can also view this registration if we have access to the member
464
+ const members = checkMember === true ? await Member.getBlobByIds(registration.memberId) : [checkMember];
465
+
466
+ if (members.length === 1) {
467
+ if (await this.canAccessMember(members[0], permissionLevel)) {
468
+ return true;
469
+ }
470
+ }
471
+ }
472
+
416
473
  return false;
417
474
  }
418
475
 
@@ -1298,6 +1355,7 @@ export class AdminPermissionChecker {
1298
1355
  }
1299
1356
 
1300
1357
  if (hasTemporaryMemberAccess(this.user.id, member.id, PermissionLevel.Full)) {
1358
+ // You created this member, so temporary can read all records in order to set the member up correctly
1301
1359
  return {
1302
1360
  canAccess: true,
1303
1361
  record: record.record,
@@ -1358,7 +1416,7 @@ export class AdminPermissionChecker {
1358
1416
  /**
1359
1417
  * Changes data inline
1360
1418
  */
1361
- async filterMemberData(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob): Promise<MemberWithRegistrationsBlob> {
1419
+ async filterMemberData(member: MemberWithRegistrations, data: MemberWithRegistrationsBlob, options?: { forAdminCartCalculation?: boolean }): Promise<MemberWithRegistrationsBlob> {
1362
1420
  const cloned = data.clone();
1363
1421
 
1364
1422
  for (const [key, value] of cloned.details.recordAnswers.entries()) {
@@ -1386,7 +1444,7 @@ export class AdminPermissionChecker {
1386
1444
  }
1387
1445
 
1388
1446
  // Has financial read access?
1389
- if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
1447
+ if (!options?.forAdminCartCalculation && !await this.hasFinancialMemberAccess(member, PermissionLevel.Read)) {
1390
1448
  cloned.details.requiresFinancialSupport = null;
1391
1449
  cloned.details.uitpasNumber = null;
1392
1450
  cloned.outstandingBalance = 0;