@stamhoofd/backend 2.120.6 → 2.121.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 (61) hide show
  1. package/package.json +12 -12
  2. package/src/audit-logs/RegistrationInvitationLogger.ts +46 -0
  3. package/src/audit-logs/init.ts +2 -0
  4. package/src/crons/index.ts +2 -0
  5. package/src/crons/invoices.ts +166 -0
  6. package/src/crons/mollie-chargebacks.ts +87 -0
  7. package/src/crons.ts +47 -10
  8. package/src/email-recipient-loaders/payments.ts +84 -41
  9. package/src/endpoints/global/groups/GetGroupsCountEndpoint.ts +51 -0
  10. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +22 -3
  11. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +4 -0
  12. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsCountEndpoint.ts +45 -0
  13. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +495 -0
  14. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.ts +216 -0
  15. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +405 -0
  16. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +168 -0
  17. package/src/endpoints/organization/dashboard/balance-items/PatchBalanceItemsEndpoint.ts +15 -0
  18. package/src/endpoints/{global → organization/dashboard}/billing/DeactivatePackageEndpoint.ts +3 -4
  19. package/src/endpoints/organization/dashboard/billing/DeleteOrganizationMandateEndpoint.ts +62 -0
  20. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceCollectionEndpoint.ts +56 -0
  21. package/src/endpoints/organization/dashboard/billing/GetOrganizationDetailedPayableBalanceEndpoint.ts +42 -19
  22. package/src/endpoints/organization/dashboard/billing/GetOrganizationMandatesEndpoint.ts +64 -0
  23. package/src/endpoints/organization/dashboard/billing/GetPackagesEndpoint.ts +11 -3
  24. package/src/endpoints/organization/dashboard/billing/OrganizationCheckoutEndpoint.ts +308 -0
  25. package/src/endpoints/organization/dashboard/billing/PatchOrganizationMandatesEndpoint.ts +94 -0
  26. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.ts +7 -0
  27. package/src/endpoints/organization/dashboard/mollie/CheckMollieEndpoint.ts +5 -4
  28. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +7 -2
  29. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +17 -8
  30. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +3 -3
  31. package/src/endpoints/organization/dashboard/receivable-balances/ChargeReceivableBalancesEndpoint.ts +127 -0
  32. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +13 -4
  33. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.ts +7 -1
  34. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +1 -1
  35. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -11
  36. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +14 -19
  37. package/src/helpers/AdminPermissionChecker.ts +11 -3
  38. package/src/helpers/AuthenticatedStructures.ts +94 -6
  39. package/src/helpers/FinancialSupportHelper.ts +21 -0
  40. package/src/helpers/RecordAnswerHelper.test.ts +746 -0
  41. package/src/helpers/RecordAnswerHelper.ts +116 -0
  42. package/src/helpers/StripeHelper.ts +2 -3
  43. package/src/helpers/ViesHelper.ts +7 -3
  44. package/src/seeds/1750090030-records-configuration.ts +68 -3
  45. package/src/seeds/1752848561-groups-registration-periods.ts +26 -2
  46. package/src/seeds/1779121239-default-invoice-email-template.sql +3 -0
  47. package/src/services/BalanceItemService.ts +12 -16
  48. package/src/services/InvoiceService.ts +372 -72
  49. package/src/services/MollieService.ts +537 -0
  50. package/src/services/PaymentMandateService.ts +214 -0
  51. package/src/services/PaymentService.ts +578 -222
  52. package/src/services/PlatformMembershipService.ts +1 -1
  53. package/src/services/RegistrationService.ts +66 -5
  54. package/src/services/STPackageService.ts +0 -7
  55. package/src/services/data/invoice.hbs.html +686 -0
  56. package/src/sql-filters/groups.ts +11 -1
  57. package/src/sql-filters/payments.ts +5 -0
  58. package/src/sql-filters/registration-invitations.ts +90 -0
  59. package/src/sql-sorters/registration-invitations.ts +36 -0
  60. package/vitest.config.js +1 -0
  61. package/src/endpoints/global/billing/ActivatePackagesEndpoint.ts +0 -216
@@ -0,0 +1,127 @@
1
+ import type { Decoder } from '@simonbackx/simple-encoding';
2
+ import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
3
+ import { Endpoint, Response } from '@simonbackx/simple-endpoints';
4
+ import { CountFilteredRequest, CountResponse, PaymentCustomer, PaymentMethod, PermissionLevel, ReceivableBalanceType } from '@stamhoofd/structures';
5
+
6
+ import { Context } from '../../../../helpers/Context.js';
7
+ import { GetReceivableBalancesEndpoint } from './GetReceivableBalancesEndpoint.js';
8
+ import type { BalanceItem} from '@stamhoofd/models';
9
+ import { Organization, User} from '@stamhoofd/models';
10
+ import { CachedBalance } from '@stamhoofd/models';
11
+ import { PaymentService } from '../../../../services/PaymentService.js';
12
+ import { SimpleError } from '@simonbackx/simple-errors';
13
+ import { PaymentMandateService } from '../../../../services/PaymentMandateService.js';
14
+ import { BalanceItemService } from '../../../../services/BalanceItemService.js';
15
+
16
+ type Params = Record<string, never>;
17
+ type Query = CountFilteredRequest;
18
+ type Body = undefined;
19
+ type ResponseBody = undefined;
20
+
21
+ export class ChargeReceivableBalancesEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
22
+ queryDecoder = CountFilteredRequest as Decoder<CountFilteredRequest>;
23
+
24
+ protected doesMatch(request: Request): [true, Params] | [false] {
25
+ if (request.method !== 'POST') {
26
+ return [false];
27
+ }
28
+
29
+ const params = Endpoint.parseParameters(request.url, '/receivable-balances/charge', {});
30
+
31
+ if (params) {
32
+ return [true, params as Params];
33
+ }
34
+ return [false];
35
+ }
36
+
37
+ async handle(request: DecodedRequest<Params, Query, Body>) {
38
+ const sellingOrganization = await Context.setOrganizationScope();
39
+ await Context.authenticate();
40
+ const query = await GetReceivableBalancesEndpoint.buildQuery(request.query);
41
+
42
+ for await (const cachedBalance of query.all()) {
43
+ if (cachedBalance.organizationId !== sellingOrganization.id) {
44
+ throw new SimpleError({
45
+ code: 'wrong_organization',
46
+ message: 'Cannot charge a cached balance from a different organization'
47
+ })
48
+ }
49
+ // todo
50
+ const items = await CachedBalance.balanceForObjects(cachedBalance.organizationId, [cachedBalance.objectId], cachedBalance.objectType);
51
+
52
+ if (items.length === 0) {
53
+ console.log('Nothing to charge for', cachedBalance.id)
54
+ continue;
55
+ }
56
+
57
+ const map: Map<BalanceItem, number> = new Map();
58
+ for (const i of items) {
59
+ map.set(i, i.priceOpen)
60
+ }
61
+ let payingOrganization: Organization | null = null;
62
+ let user: User | null = null;
63
+ if (cachedBalance.objectType === ReceivableBalanceType.organization) {
64
+ const p = await Organization.getByID(cachedBalance.objectId);
65
+ if (!p || !(await Context.auth.hasFullAccess(p))) {
66
+ console.error('Unexpected missing paying organization id', cachedBalance)
67
+ continue
68
+ }
69
+ payingOrganization = p;
70
+ }
71
+
72
+ if (cachedBalance.objectType === ReceivableBalanceType.user || cachedBalance.objectType === ReceivableBalanceType.userWithoutMembers) {
73
+ const p = await User.getByID(cachedBalance.objectId);
74
+ if (!p || !(Context.auth.checkScope(p.organizationId))) {
75
+ console.error('Unexpected missing user id', cachedBalance)
76
+ continue
77
+ }
78
+ user = p
79
+ }
80
+
81
+ const mandates = await PaymentMandateService.getMandates({
82
+ sellingOrganization,
83
+ user,
84
+ payingOrganization
85
+ });
86
+
87
+ const mandate = mandates.find(m => m.isDefault)
88
+
89
+ if (!mandate) {
90
+ // Not possible
91
+ console.error('No mandates found for', cachedBalance.id)
92
+ continue;
93
+ }
94
+
95
+ const customerUser = user ?? (payingOrganization ? (await payingOrganization.getFullAdmins())[0] : null)
96
+ const customer = PaymentCustomer.create({
97
+ firstName: customerUser?.firstName,
98
+ lastName: customerUser?.lastName,
99
+ email: customerUser?.email ?? (payingOrganization ? (await payingOrganization.getReplyEmails())[0].email : null),
100
+ company: payingOrganization?.defaultCompanies[0]
101
+ })
102
+
103
+ await PaymentService.createPayment({
104
+ balanceItems: map,
105
+ checkout: {
106
+ paymentMethod: PaymentMethod.Unknown,
107
+ totalPrice: null,
108
+ customer,
109
+ cancelUrl: null,
110
+ redirectUrl: null
111
+ },
112
+ user,
113
+ organization: sellingOrganization,
114
+ payingOrganization,
115
+ serviceFeeType: 'system',
116
+ createMandate: null,
117
+ useMandate: mandate,
118
+ paymentConfiguration: sellingOrganization.meta.registrationPaymentConfiguration,
119
+ privatePaymentConfiguration: sellingOrganization.privateMeta.registrationPaymentConfiguration
120
+ })
121
+ }
122
+
123
+ // Make sure the user can refresh data and see the updated cached amounts
124
+ await BalanceItemService.flushCaches(sellingOrganization.id)
125
+ return new Response(undefined, 201);
126
+ }
127
+ }
@@ -1,15 +1,16 @@
1
- import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
1
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
2
2
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
3
3
  import { GroupPrivateSettings, Group as GroupStruct, GroupType, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, PermissionLevel, PermissionsResourceType, ResourcePermissions, Version } from '@stamhoofd/structures';
4
4
 
5
- import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder} from '@simonbackx/simple-encoding';
5
+ import type { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder } from '@simonbackx/simple-encoding';
6
6
  import { PatchableArrayDecoder, StringDecoder } from '@simonbackx/simple-encoding';
7
7
  import { SimpleError } from '@simonbackx/simple-errors';
8
- import type { Organization} from '@stamhoofd/models';
9
- import { Event, Group, OrganizationRegistrationPeriod, Platform, Registration, RegistrationPeriod } from '@stamhoofd/models';
8
+ import type { Organization } from '@stamhoofd/models';
9
+ import { Event, Group, OrganizationRegistrationPeriod, Platform, Registration, RegistrationInvitation, RegistrationPeriod } from '@stamhoofd/models';
10
10
  import { SQL } from '@stamhoofd/sql';
11
11
  import { AuthenticatedStructures } from '../../../../helpers/AuthenticatedStructures.js';
12
12
  import { Context } from '../../../../helpers/Context.js';
13
+ import { RecordAnswerHelper } from '../../../../helpers/RecordAnswerHelper.js';
13
14
  import { SetupStepUpdater } from '../../../../helpers/SetupStepUpdater.js';
14
15
 
15
16
  type Params = Record<string, never>;
@@ -377,6 +378,9 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
377
378
  event.groupId = null;
378
379
  await event.save();
379
380
  }
381
+
382
+ // delete invitations
383
+ await RegistrationInvitation.delete().where('groupId', id);
380
384
  }
381
385
 
382
386
  static async patchGroup(struct: AutoEncoderPatchType<GroupStruct>, period?: RegistrationPeriod | null, options: { allowPatchWaitingListPeriod?: boolean; isPatchingEvent?: boolean } = {}) {
@@ -390,6 +394,11 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
390
394
 
391
395
  if (struct.settings) {
392
396
  struct.settings.period = undefined; // Not allowed to patch manually
397
+
398
+ if (struct.settings.recordCategories) {
399
+ RecordAnswerHelper.throwIfPatchOrPutIsInvalid(model.settings.recordCategories, struct.settings.recordCategories);
400
+ }
401
+
393
402
  model.settings.patchOrPut(struct.settings);
394
403
  }
395
404
 
@@ -1,5 +1,5 @@
1
1
  import type { AutoEncoderPatchType, Decoder } from '@simonbackx/simple-encoding';
2
- import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
2
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
3
3
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
4
4
  import { SimpleError } from '@simonbackx/simple-errors';
5
5
  import { Webshop } from '@stamhoofd/models';
@@ -8,6 +8,7 @@ import { PermissionLevel, PrivateWebshop, WebshopPrivateMetaData } from '@stamho
8
8
  import { Formatter } from '@stamhoofd/utility';
9
9
 
10
10
  import { Context } from '../../../../helpers/Context.js';
11
+ import { RecordAnswerHelper } from '../../../../helpers/RecordAnswerHelper.js';
11
12
 
12
13
  type Params = { id: string };
13
14
  type Query = undefined;
@@ -56,6 +57,11 @@ export class PatchWebshopEndpoint extends Endpoint<Params, Query, Body, Response
56
57
  if (request.body.meta.customCode !== undefined && !await Context.auth.hasFullAccess(organization.id)) {
57
58
  throw Context.auth.error($t('%15n'));
58
59
  }
60
+
61
+ if (request.body.meta.recordCategories) {
62
+ RecordAnswerHelper.throwIfPatchOrPutIsInvalid(webshop.meta.recordCategories, request.body.meta.recordCategories);
63
+ }
64
+
59
65
  webshop.meta.patchOrPut(request.body.meta);
60
66
  }
61
67
 
@@ -157,7 +157,7 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
157
157
  payment.method = struct.data.paymentMethod;
158
158
  payment.status = PaymentStatus.Created;
159
159
  payment.price = totalPrice;
160
- PaymentService.round(payment);
160
+ PaymentService.roundPayment(payment);
161
161
  payment.paidAt = null;
162
162
 
163
163
  // Determine the payment provider (always null because no online payments here)
@@ -45,10 +45,8 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
45
45
  }
46
46
 
47
47
  async handle(request: DecodedRequest<Params, Query, Body>) {
48
- const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: false });
49
- if (!request.query.exchange) {
50
- await Context.optionalAuthenticate();
51
- }
48
+ const organization = await Context.setOptionalOrganizationScope({ willAuthenticate: true }); // will authentiate set to true because we allow exchanges for inactive organizations
49
+ await Context.optionalAuthenticate();
52
50
 
53
51
  // Not method on payment because circular references (not supprted in ts)
54
52
  const payment = await PaymentService.pollStatus(request.params.id, organization, request.query.cancel);
@@ -63,19 +61,23 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
63
61
  return new Response(undefined);
64
62
  }
65
63
 
66
- // #region skip check permissions if order and created less than hour ago
64
+ // Skip check permissions if order and created less than hour ago
67
65
  let checkPermissions = true;
68
66
  const hourAgo = new Date();
69
- hourAgo.setHours(-1);
67
+ hourAgo.setHours(-2);
70
68
 
71
69
  if (payment.createdAt > hourAgo) {
72
- const orders = await Order.where({ paymentId: payment.id }, { limit: 1 });
73
- const isOrder = orders[0] !== undefined;
74
- if (isOrder) {
75
- checkPermissions = false;
70
+ if (payment.payingOrganizationId) {
71
+ // B2B payments always required
72
+ checkPermissions = true;
73
+ } else {
74
+ const orders = await Order.where({ paymentId: payment.id }, { limit: 1 });
75
+ const isOrder = orders[0] !== undefined;
76
+ if (isOrder) {
77
+ checkPermissions = false;
78
+ }
76
79
  }
77
80
  }
78
- // #endregion
79
81
 
80
82
  return new Response(
81
83
  await AuthenticatedStructures.paymentGeneral(payment, checkPermissions),
@@ -1,21 +1,22 @@
1
- import { createMollieClient, PaymentMethod as molliePaymentMethod } from '@mollie/api-client';
1
+ import { PaymentMethod as molliePaymentMethod } from '@mollie/api-client';
2
2
  import type { Decoder } from '@simonbackx/simple-encoding';
3
- import type { DecodedRequest, Request} from '@simonbackx/simple-endpoints';
3
+ import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
4
4
  import { Endpoint, Response } from '@simonbackx/simple-endpoints';
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
6
  import { Email } from '@stamhoofd/email';
7
- import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
7
+ import { BalanceItem, BalanceItemPayment, MolliePayment, Order, PayconiqPayment, Payment, RateLimiter, Webshop, WebshopDiscountCode } from '@stamhoofd/models';
8
8
  import { QueueHandler } from '@stamhoofd/queues';
9
9
  import { AuditLogSource, BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, OrderData, OrderResponse, Order as OrderStruct, PaymentCustomer, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, TranslatedString, Version, WebshopAuthType, Webshop as WebshopStruct, WebshopTicketType } from '@stamhoofd/structures';
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
 
12
12
  import { BuckarooHelper } from '../../../helpers/BuckarooHelper.js';
13
13
  import { Context } from '../../../helpers/Context.js';
14
+ import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
14
15
  import { StripeHelper } from '../../../helpers/StripeHelper.js';
15
16
  import { AuditLogService } from '../../../services/AuditLogService.js';
16
- import { UitpasService } from '../../../services/uitpas/UitpasService.js';
17
- import { ServiceFeeHelper } from '../../../helpers/ServiceFeeHelper.js';
17
+ import { MollieService } from '../../../services/MollieService.js';
18
18
  import { PaymentService } from '../../../services/PaymentService.js';
19
+ import { UitpasService } from '../../../services/uitpas/UitpasService.js';
19
20
 
20
21
  type Params = { id: string };
21
22
  type Query = undefined;
@@ -180,7 +181,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
180
181
  payment.method = request.body.paymentMethod;
181
182
  payment.status = PaymentStatus.Created;
182
183
  payment.price = totalPrice;
183
- PaymentService.round(payment);
184
+ PaymentService.roundPayment(payment);
184
185
  totalPrice = payment.price;
185
186
  payment.paidAt = null;
186
187
  payment.customer = PaymentCustomer.create({
@@ -191,7 +192,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
191
192
 
192
193
  // Determine the payment provider
193
194
  // Throws if invalid
194
- const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, webshop.privateMeta.paymentConfiguration);
195
+ const { provider, stripeAccount } = await organization.getPaymentProviderFor(payment.method, null, webshop.privateMeta.paymentConfiguration);
195
196
  payment.provider = provider;
196
197
  payment.stripeAccountId = stripeAccount?.id ?? null;
197
198
  ServiceFeeHelper.setServiceFee(
@@ -210,8 +211,6 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
210
211
  // Save order to get the id
211
212
  await order.save();
212
213
 
213
- const balanceItemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = [];
214
-
215
214
  // Create balance item
216
215
  const balanceItem = new BalanceItem();
217
216
  balanceItem.type = BalanceItemType.Order;
@@ -239,7 +238,6 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
239
238
  balanceItemPayment.organizationId = organization.id;
240
239
  balanceItemPayment.price = balanceItem.price;
241
240
  await balanceItemPayment.save();
242
- balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem));
243
241
 
244
242
  let paymentUrl: string | null = null;
245
243
  let paymentQRCode: string | null = null;
@@ -287,7 +285,6 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
287
285
  payment: payment.id,
288
286
  },
289
287
  i18n: request.i18n,
290
- lineItems: balanceItemPayments,
291
288
  organization,
292
289
  customer: {
293
290
  name: order.data.customer.name,
@@ -298,28 +295,26 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
298
295
  }
299
296
  else if (payment.provider === PaymentProvider.Mollie) {
300
297
  // Mollie payment
301
- const token = await MollieToken.getTokenFor(webshop.organizationId);
302
- if (!token) {
298
+ const mollieService = await MollieService.create({sellingOrganization: organization});
299
+ if (!mollieService) {
303
300
  throw new SimpleError({
304
301
  code: '',
305
302
  message: $t(`%w3`, { method: PaymentMethodHelper.getName(payment.method) }),
306
303
  });
307
304
  }
308
- const profileId = organization.privateMeta.mollieProfile?.id ?? await token.getProfileId(webshop.getHost());
305
+ const profileId = await mollieService.getProfileId(webshop.getHost());
309
306
  if (!profileId) {
310
307
  throw new SimpleError({
311
308
  code: '',
312
309
  message: $t(`%w4`, { method: PaymentMethodHelper.getName(payment.method) }),
313
310
  });
314
311
  }
315
- const mollieClient = createMollieClient({ accessToken: await token.getAccessToken() });
316
- const locale = Context.i18n.locale.replace('-', '_');
317
- const molliePayment = await mollieClient.payments.create({
312
+ const molliePayment = await mollieService.client.payments.create({
318
313
  amount: {
319
314
  currency: 'EUR',
320
315
  value: (totalPrice / 10000).toFixed(2), // from 4 decimals to 0 decimals
321
316
  },
322
- method: payment.method == PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method == PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
317
+ method: payment.method === PaymentMethod.Bancontact ? molliePaymentMethod.bancontact : (payment.method === PaymentMethod.iDEAL ? molliePaymentMethod.ideal : molliePaymentMethod.creditcard),
323
318
  testmode: organization.privateMeta.useTestPayments ?? STAMHOOFD.environment !== 'production',
324
319
  profileId,
325
320
  description,
@@ -331,7 +326,7 @@ export class PlaceOrderEndpoint extends Endpoint<Params, Query, Body, ResponseBo
331
326
  webshop: webshop.id,
332
327
  payment: payment.id,
333
328
  },
334
- locale: ['en_US', 'en_GB', 'nl_NL', 'nl_BE', 'fr_FR', 'fr_BE', 'de_DE', 'de_AT', 'de_CH', 'es_ES', 'ca_ES', 'pt_PT', 'it_IT', 'nb_NO', 'sv_SE', 'fi_FI', 'da_DK', 'is_IS', 'hu_HU', 'pl_PL', 'lv_LV', 'lt_LT'].includes(locale) ? (locale as any) : null,
329
+ locale: mollieService.locale
335
330
  });
336
331
  console.log(molliePayment);
337
332
  paymentUrl = molliePayment.getCheckoutUrl();
@@ -8,6 +8,8 @@ import { AccessRight, EmailTemplate as EmailTemplateStruct, EventPermissionCheck
8
8
  import { Formatter } from '@stamhoofd/utility';
9
9
  import type { RecordCacheEntry } from '../services/MemberRecordStore.js';
10
10
  import { MemberRecordStore } from '../services/MemberRecordStore.js';
11
+ import { getFinancialSupportSettingsAsync } from './FinancialSupportHelper.js';
12
+ import { RecordAnswerHelper } from './RecordAnswerHelper.js';
11
13
  import { addTemporaryMemberAccess, hasTemporaryMemberAccess } from './TemporaryMemberAccess.js';
12
14
 
13
15
  /**
@@ -1117,7 +1119,7 @@ export class AdminPermissionChecker {
1117
1119
  return false;
1118
1120
  }
1119
1121
 
1120
- async hasFullAccess(organizationId: string, level = PermissionLevel.Full): Promise<boolean> {
1122
+ async hasFullAccess(organizationId: string | Organization, level = PermissionLevel.Full): Promise<boolean> {
1121
1123
  const organizationPermissions = await this.getOrganizationPermissions(organizationId);
1122
1124
 
1123
1125
  if (!organizationPermissions) {
@@ -1557,7 +1559,7 @@ export class AdminPermissionChecker {
1557
1559
  cloned.details.recordAnswers.delete(key);
1558
1560
  }
1559
1561
  else {
1560
- if (value) {
1562
+ if (value && RecordAnswerHelper.haveTypesSameClass(value.settings.type, record.type)) {
1561
1563
  // Force update
1562
1564
  value.settings = record;
1563
1565
  }
@@ -1731,7 +1733,13 @@ export class AdminPermissionChecker {
1731
1733
  // Has financial write access?
1732
1734
  if (!await this.hasFinancialMemberAccess(member, PermissionLevel.Write)) {
1733
1735
  if (isUserManager && isSetFinancialSupportTrue) {
1734
- const financialSupportSettings = this.platform.config.financialSupport;
1736
+ const financialSupportSettings = await getFinancialSupportSettingsAsync(this.platform, async () => {
1737
+ if (member.organizationId) {
1738
+ return await this.getOrganization(member.organizationId);
1739
+ }
1740
+ return null;
1741
+ });
1742
+
1735
1743
  const preventSelfAssignment = financialSupportSettings?.preventSelfAssignment === true;
1736
1744
 
1737
1745
  if (preventSelfAssignment) {
@@ -1,8 +1,8 @@
1
1
  import { SimpleError } from '@simonbackx/simple-errors';
2
- import type { AuditLog, Document, EventNotification, MemberWithUsersRegistrationsAndGroups, Order, Ticket} from '@stamhoofd/models';
3
- import { BalanceItem, CachedBalance, Event, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
4
- import type { PaymentGeneral} from '@stamhoofd/structures';
5
- import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentCustomer, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct, BalanceItem as BalanceItemStruct } from '@stamhoofd/structures';
2
+ import type { AuditLog, Document, EventNotification, MemberWithUsersRegistrationsAndGroups, Order, Ticket } from '@stamhoofd/models';
3
+ import { BalanceItem, CachedBalance, Event, Group, Invoice, Member, MemberPlatformMembership, MemberResponsibilityRecord, Organization, OrganizationRegistrationPeriod, Payment, Registration, RegistrationInvitation, RegistrationPeriod, User, Webshop } from '@stamhoofd/models';
4
+ import type { PaymentGeneral } from '@stamhoofd/structures';
5
+ import { AuditLogReplacement, AuditLogReplacementType, AuditLog as AuditLogStruct, BalanceItem as BalanceItemStruct, DetailedReceivableBalance, Document as DocumentStruct, EventNotification as EventNotificationStruct, Event as EventStruct, GenericBalance, Group as GroupStruct, GroupType, InvitationGroupData, InvitationMemberData, InvoicedBalanceItem, InvoiceStruct, MemberPlatformMembership as MemberPlatformMembershipStruct, MemberRegistrationInvitation, MembersBlob, MemberWithRegistrationsBlob, NamedObject, OrganizationRegistrationPeriod as OrganizationRegistrationPeriodStruct, Organization as OrganizationStruct, PaymentCustomer, PermissionLevel, Platform, PrivateOrder, PrivateWebshop, ReceivableBalanceObject, ReceivableBalanceObjectContact, ReceivableBalance as ReceivableBalanceStruct, ReceivableBalanceType, RegistrationInvitation as RegistrationInvitationStruct, RegistrationsBlob, RegistrationWithMemberBlob, TicketPrivate, UserWithMembers, WebshopPreview, Webshop as WebshopStruct } from '@stamhoofd/structures';
6
6
  import { Sorter } from '@stamhoofd/utility';
7
7
 
8
8
  import { SQL } from '@stamhoofd/sql';
@@ -34,7 +34,7 @@ export class AuthenticatedStructures {
34
34
  const { registrations, orders } = await Payment.loadBalanceItemRelations(balanceItems);
35
35
 
36
36
  // Note: permission checking is moved here for performacne to avoid loading the data multiple times
37
- if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, { registrations, orders }))) {
37
+ if (!(await Context.optionalAuth?.canAccessBalanceItems(balanceItems, PermissionLevel.Read, { registrations, orders }))) {
38
38
  throw new SimpleError({
39
39
  code: 'permission_denied',
40
40
  message: 'Permission denied',
@@ -43,7 +43,7 @@ export class AuthenticatedStructures {
43
43
  }
44
44
  }
45
45
 
46
- const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions;
46
+ const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions && payments.every(p => !!Context.optionalAuth?.checkScope(p.organizationId));
47
47
 
48
48
  const { payingOrganizations } = await Payment.loadPayingOrganizations(payments);
49
49
 
@@ -458,6 +458,12 @@ export class AuthenticatedStructures {
458
458
  .where('periodId', relevantPeriodIds)
459
459
  .fetch()
460
460
  : [];
461
+ const registrationInvitations = members.length > 0
462
+ ? await RegistrationInvitation.select()
463
+ .where('memberId', members.map(m => m.id))
464
+ .fetch() : [];
465
+
466
+ const memberRegistrationInvitations: Map<string, MemberRegistrationInvitation[]> = registrationInvitations.length > 0 ? await this.memberRegistrationInvitations(registrationInvitations) : new Map();
461
467
 
462
468
  // Load organizations
463
469
  const organizationIds = responsibilities.map(r => r.organizationId)
@@ -663,6 +669,7 @@ export class AuthenticatedStructures {
663
669
  return r.getStructure(group);
664
670
  });
665
671
  blob.platformMemberships = platformMemberships.filter(r => r.memberId == blob.id).map(r => MemberPlatformMembershipStruct.create(r));
672
+ blob.registrationInvitations = memberRegistrationInvitations.get(blob.id) ?? [];
666
673
  }
667
674
 
668
675
  return MembersBlob.create({
@@ -1220,4 +1227,85 @@ export class AuthenticatedStructures {
1220
1227
  static async balanceItemsWithPayments(balanceItems: BalanceItem[]) {
1221
1228
  return await BalanceItem.getStructureWithPayments(balanceItems);
1222
1229
  }
1230
+
1231
+ static async memberRegistrationInvitations(invitations: RegistrationInvitation[]): Promise<Map<string, MemberRegistrationInvitation[]>> {
1232
+ const results = new Map<string, MemberRegistrationInvitation[]>();
1233
+ const groups = await Group.getByIDs(...invitations.map(i => i.groupId));
1234
+
1235
+ for (const invitation of invitations) {
1236
+ const group = groups.find(g => g.id === invitation.groupId);
1237
+ if (!group) {
1238
+ throw new SimpleError({
1239
+ code: 'group_not_found',
1240
+ message: 'Group not found',
1241
+ human: $t(`%1SM`),
1242
+ })
1243
+ }
1244
+
1245
+ const result = MemberRegistrationInvitation.create({
1246
+ id: invitation.id,
1247
+ group: InvitationGroupData.create({
1248
+ id: invitation.groupId,
1249
+ name: group.settings.name,
1250
+ type: group.type,
1251
+ periodId: group.periodId,
1252
+ }),
1253
+ organizationId: invitation.organizationId,
1254
+ createdAt: invitation.createdAt
1255
+ })
1256
+
1257
+ let array = results.get(invitation.memberId);
1258
+ if (!array) {
1259
+ array = [result];
1260
+ results.set(invitation.memberId, array);
1261
+ } else {
1262
+ array.push(result);
1263
+ }
1264
+ }
1265
+
1266
+ return results;
1267
+ }
1268
+
1269
+ static async registrationInvitations(invitations: RegistrationInvitation[]): Promise<RegistrationInvitationStruct[]> {
1270
+ const members = await Member.getByIDs(...invitations.map(i => i.memberId));
1271
+ const groups = await Group.getByIDs(...invitations.map(i => i.groupId));
1272
+
1273
+ return invitations.map((invitation) => {
1274
+ const member = members.find(m => m.id === invitation.memberId);
1275
+ if (!member) {
1276
+ throw new SimpleError({
1277
+ code: 'member_not_found',
1278
+ message: 'Member not found',
1279
+ human: $t(`%EO`),
1280
+ })
1281
+ }
1282
+
1283
+ const group = groups.find(g => g.id === invitation.groupId);
1284
+ if (!group) {
1285
+ throw new SimpleError({
1286
+ code: 'group_not_found',
1287
+ message: 'Group not found',
1288
+ human: $t(`%1SM`),
1289
+ })
1290
+ }
1291
+
1292
+ return RegistrationInvitationStruct.create({
1293
+ id: invitation.id,
1294
+ group: InvitationGroupData.create({
1295
+ id: group.id,
1296
+ name: group.settings.name,
1297
+ type: group.type,
1298
+ periodId: group.periodId
1299
+ }),
1300
+ organizationId: invitation.organizationId,
1301
+ member: InvitationMemberData.create({
1302
+ id: member.id,
1303
+ firstName: member.firstName,
1304
+ lastName: member.lastName,
1305
+ birthDay: member.details.birthDay,
1306
+ }),
1307
+ createdAt: invitation.createdAt
1308
+ });
1309
+ });
1310
+ }
1223
1311
  }
@@ -0,0 +1,21 @@
1
+ import type { Organization, Platform } from '@stamhoofd/models';
2
+ import type { FinancialSupportSettings, Organization as OrganizationStruct, Platform as PlatformStruct } from '@stamhoofd/structures';
3
+
4
+ /**
5
+ * Get the financial support settings from the platform if userMode is platform, or from the organization otherwise.
6
+ * @param platform
7
+ * @param getOrganization callback to prevent fetching the organization if not needed
8
+ * @returns
9
+ */
10
+ export async function getFinancialSupportSettingsAsync(platform: Platform | PlatformStruct, getOrganization: () => Promise<Organization | OrganizationStruct | null>): Promise<FinancialSupportSettings | null> {
11
+ if (STAMHOOFD.userMode === 'platform') {
12
+ return platform.config.financialSupport;
13
+ }
14
+
15
+ const organization = await getOrganization();
16
+ if (organization) {
17
+ return organization.meta.financialSupport;
18
+ }
19
+
20
+ return null;
21
+ }