@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
@@ -5,8 +5,8 @@ import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-
5
5
  import { SimpleError } from '@simonbackx/simple-errors';
6
6
  import { I18n } from '@stamhoofd/backend-i18n';
7
7
  import { Email } from '@stamhoofd/email';
8
- import { BalanceItem, BalanceItemPayment, Group, Member, MolliePayment, MollieToken, PayconiqPayment, Payment, Platform, RateLimiter, Registration } from '@stamhoofd/models';
9
- import { BalanceItemStatus, IDRegisterCheckout, MemberBalanceItem, MemberDetails, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PlatformFamily, RegisterItem, RegisterResponse, Version } from "@stamhoofd/structures";
8
+ import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
9
+ import { BalanceItemStatus, IDRegisterCheckout, MemberBalanceItem, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, Payment as PaymentStruct, PermissionLevel, PlatformFamily, PlatformMember, RegisterItem, RegisterResponse, Version } from "@stamhoofd/structures";
10
10
  import { Formatter } from '@stamhoofd/utility';
11
11
 
12
12
  import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
@@ -66,6 +66,17 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
66
66
  const organization = await Context.setOrganizationScope();
67
67
  const {user} = await Context.authenticate()
68
68
 
69
+ if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
70
+ if (!await Context.auth.hasFullAccess(request.body.asOrganizationId)) {
71
+ throw new SimpleError({
72
+ code: "forbidden",
73
+ message: "No permission to register as this organization for a different organization",
74
+ human: 'Je hebt niet de juiste toegangsrechten om leden in te schrijven bij een andere organisatie.',
75
+ statusCode: 403
76
+ });
77
+ }
78
+ }
79
+
69
80
  // For non paid organizations, limit amount of tests
70
81
  if (!organization.meta.packages.isPaid) {
71
82
  const limiter = demoLimiter
@@ -88,12 +99,63 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
88
99
  }
89
100
  }
90
101
 
91
- const members = await Member.getMembersWithRegistrationForUser(user)
92
- const groups = await Group.getAll(organization.id, organization.periodId)
102
+ const memberIds = Formatter.uniqueArray(
103
+ [...request.body.cart.items.map(i => i.memberId), ...request.body.cart.deleteRegistrations.map(i => i.member.id)]
104
+ )
105
+ const members = await Member.getBlobByIds(...memberIds)
106
+ const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
107
+ const groups = await Group.getByIDs(...groupIds)
108
+
109
+ for (const group of groups) {
110
+ if (group.organizationId !== organization.id) {
111
+ throw new SimpleError({
112
+ code: "invalid_data",
113
+ message: "Oeps, één of meerdere groepen waarin je probeert in te schrijven lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
114
+ })
115
+ }
116
+ }
117
+
118
+ for (const member of members) {
119
+ if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
120
+ throw new SimpleError({
121
+ code: "forbidden",
122
+ message: "No permission to register this member",
123
+ human: 'Je hebt niet de juiste toegangsrechten om dit lid in te schrijven.',
124
+ statusCode: 403
125
+ });
126
+ }
127
+ }
93
128
 
94
129
  const blob = await AuthenticatedStructures.membersBlob(members, true)
95
- const family = PlatformFamily.create(blob, {platform: await Platform.getSharedStruct(), contextOrganization: await organization.getStructure()})
96
- const checkout = request.body.hydrate({family})
130
+ const platformMembers: PlatformMember[] = []
131
+
132
+ if (request.body.asOrganizationId) {
133
+ const _m = PlatformFamily.createSingles(blob, {
134
+ platform: await Platform.getSharedStruct(),
135
+ contextOrganization: await AuthenticatedStructures.organization(organization)
136
+ })
137
+ platformMembers.push(..._m)
138
+ } else {
139
+ const family = PlatformFamily.create(blob, {
140
+ platform: await Platform.getSharedStruct(),
141
+ contextOrganization: await AuthenticatedStructures.organization(organization)
142
+ })
143
+ platformMembers.push(...family.members)
144
+ }
145
+
146
+ const organizationStruct = await AuthenticatedStructures.organization(organization)
147
+ const checkout = request.body.hydrate({
148
+ members: platformMembers,
149
+ groups: await AuthenticatedStructures.groups(groups),
150
+ organizations: [organizationStruct]
151
+ })
152
+
153
+ // Set circular references
154
+ for (const member of platformMembers) {
155
+ member.family.checkout = checkout
156
+ }
157
+
158
+ checkout.setDefaultOrganization(organizationStruct)
97
159
 
98
160
  const registrations: RegistrationWithMemberAndGroup[] = []
99
161
  const payRegistrations: {registration: RegistrationWithMemberAndGroup, item: RegisterItem}[] = []
@@ -108,9 +170,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
108
170
  // Update occupancies
109
171
  // TODO: might not be needed in the future (for performance)
110
172
  for (const group of groups) {
111
- if (request.body.cart.items.find(i => i.groupId == group.id)) {
112
- await group.updateOccupancy()
113
- }
173
+ await group.updateOccupancy()
114
174
  }
115
175
 
116
176
  // Validate balance items (can only happen serverside)
@@ -128,6 +188,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
128
188
  memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
129
189
  }
130
190
 
191
+ console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
192
+
131
193
  // Validate the cart
132
194
  checkout.validate({memberBalanceItems})
133
195
 
@@ -135,6 +197,13 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
135
197
  checkout.updatePrices()
136
198
 
137
199
  const totalPrice = checkout.totalPrice
200
+
201
+ if (totalPrice !== request.body.totalPrice) {
202
+ throw new SimpleError({
203
+ code: "changed_price",
204
+ message: "Oeps! De prijs is gewijzigd terwijl je aan het afrekenen was (naar "+Formatter.price(totalPrice)+"). Herlaad de pagina even om ervoor te zorgen dat je alle aangepaste prijzen ziet. Contacteer de webmaster als je dit probleem blijft ondervinden na het te herladen."
205
+ })
206
+ }
138
207
 
139
208
  if (totalPrice < 0) {
140
209
  throw new SimpleError({
@@ -146,7 +215,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
146
215
  const registrationMemberRelation = new ManyToOneRelation(Member, "member")
147
216
  registrationMemberRelation.foreignKey = "memberId"
148
217
 
149
- mainLoop: for (const item of checkout.cart.items) {
218
+ for (const item of checkout.cart.items) {
150
219
  const member = members.find(m => m.id == item.memberId)
151
220
  if (!member) {
152
221
  throw new SimpleError({
@@ -171,18 +240,13 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
171
240
  registration = existingRegistration
172
241
  .setRelation(registrationMemberRelation, member as Member)
173
242
  .setRelation(Registration.group, group)
243
+
174
244
 
175
- if (existingRegistration.waitingList && item.waitingList) {
176
- // already on waiting list, no need to repeat it
177
- // skip without error
178
- registrations.push(registration)
179
- continue mainLoop;
180
- }
181
-
182
- if (!existingRegistration.waitingList && existingRegistration.registeredAt !== null) {
183
- // already registered, no need to put it on the waiting list or register (and pay) again
184
- registrations.push(registration)
185
- continue mainLoop;
245
+ if (existingRegistration.registeredAt !== null && existingRegistration.deactivatedAt === null) {
246
+ throw new SimpleError({
247
+ code: "already_registered",
248
+ message: "Dit lid is reeds ingeschreven. Herlaad de pagina en probeer opnieuw."
249
+ })
186
250
  }
187
251
  }
188
252
 
@@ -197,31 +261,29 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
197
261
  registration.memberId = member.id
198
262
  registration.groupId = group.id
199
263
  registration.cycle = group.cycle
264
+ registration.price = item.calculatedPrice
265
+
266
+ payRegistrations.push({
267
+ registration,
268
+ item
269
+ });
200
270
 
201
- if (item.waitingList) {
202
- registration.waitingList = true
203
- registration.canRegister = false
204
- registration.reservedUntil = null
205
- await registration.save()
206
- } else {
207
- if (registration.waitingList && registration.canRegister) {
208
- // Keep data: otherwise people cannot retry if the payment fails
209
- // We'll mark the registration as valid after the payment
210
- } else {
211
- registration.waitingList = false
212
- registration.canRegister = false
213
- }
214
- registration.price = item.calculatedPrice
215
- payRegistrations.push({
216
- registration,
217
- item
218
- });
219
- }
220
271
  registrations.push(registration)
221
272
  }
222
273
 
274
+ // Who is going to pay?
275
+ 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
276
+
277
+ if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
278
+ // Will get added to the outstanding amount of the member
279
+ whoWillPayNow = 'nobody'
280
+ } else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
281
+ // The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
282
+ whoWillPayNow = 'organization'
283
+ }
284
+
223
285
  // Validate payment method
224
- if (totalPrice > 0) {
286
+ if (totalPrice > 0 && whoWillPayNow !== 'nobody') {
225
287
  const allowedPaymentMethods = organization.meta.registrationPaymentConfiguration.paymentMethods
226
288
 
227
289
  if (!checkout.paymentMethod || !allowedPaymentMethods.includes(checkout.paymentMethod)) {
@@ -230,116 +292,104 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
230
292
  message: "Oeps, je hebt geen geldige betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw. Herlaad de pagina indien nodig."
231
293
  })
232
294
  }
233
- } else {
234
- checkout.paymentMethod = PaymentMethod.Unknown
235
- }
236
295
 
237
- const payment = new Payment()
238
- payment.userId = user.id
239
- payment.organizationId = organization.id
240
- payment.method = checkout.paymentMethod
241
- payment.status = PaymentStatus.Created
242
- payment.price = totalPrice
243
- payment.freeContribution = checkout.freeContribution
244
-
245
- if (payment.method == PaymentMethod.Transfer) {
246
- // remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
247
- payment.transferSettings = organization.mappedTransferSettings
248
-
249
- if (!payment.transferSettings.iban) {
296
+ if ((checkout.paymentMethod !== PaymentMethod.Transfer && checkout.paymentMethod !== PaymentMethod.PointOfSale) && (!request.body.redirectUrl || !request.body.cancelUrl)) {
250
297
  throw new SimpleError({
251
- code: "no_iban",
252
- message: "No IBAN",
253
- human: "Er is geen rekeningnummer ingesteld voor overschrijvingen. Contacteer de beheerder."
298
+ code: 'missing_fields',
299
+ message: 'redirectUrl or cancelUrl is missing and is required for non-zero online payments',
300
+ human: 'Er is iets mis. Contacteer de webmaster.'
254
301
  })
255
302
  }
256
-
257
- const m = [...payRegistrations.map(r => r.registration.member.details), ...memberBalanceItems.map(i => members.find(m => m.id === i.memberId)?.details).filter(n => n !== undefined)] as MemberDetails[]
258
- payment.generateDescription(
259
- organization,
260
- Formatter.groupNamesByFamily(m),
261
- {
262
- name: Formatter.groupNamesByFamily(m),
263
- naam: Formatter.groupNamesByFamily(m),
264
- email: user.email
265
- }
266
- )
267
- }
268
- payment.paidAt = null
269
-
270
- if (totalPrice == 0) {
271
- payment.status = PaymentStatus.Succeeded
272
- payment.method = PaymentMethod.Unknown
273
- payment.paidAt = new Date()
303
+ } else {
304
+ checkout.paymentMethod = PaymentMethod.Unknown
274
305
  }
275
306
 
276
- // Determine the payment provider
277
- // Throws if invalid
278
- const {provider, stripeAccount} = await organization.getPaymentProviderFor(payment.method, organization.privateMeta.registrationPaymentConfiguration)
279
- payment.provider = provider
280
- payment.stripeAccountId = stripeAccount?.id ?? null
307
+ console.log('Registering members using whoWillPayNow', whoWillPayNow, checkout.paymentMethod, totalPrice)
281
308
 
282
- await payment.save()
283
309
  const items: BalanceItem[] = []
284
- const itemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = []
310
+ const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale
285
311
 
286
312
  // Save registrations and add extra data if needed
287
313
  for (const bundle of payRegistrations) {
288
314
  const registration = bundle.registration;
289
315
 
290
- if (!registration.waitingList) {
291
- // Replaced with balance items
292
- // registration.paymentId = payment.id
316
+ registration.reservedUntil = null
293
317
 
294
- registration.reservedUntil = null
295
- registration.canRegister = false
296
-
297
- if (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale || payment.status == PaymentStatus.Succeeded) {
298
- await registration.markValid()
299
- } else {
300
- // Reserve registration for 30 minutes (if needed)
301
- const group = groups.find(g => g.id === registration.groupId)
318
+ if (shouldMarkValid) {
319
+ await registration.markValid()
320
+ } else {
321
+ // Reserve registration for 30 minutes (if needed)
322
+ const group = groups.find(g => g.id === registration.groupId)
302
323
 
303
- if (group && group.settings.maxMembers !== null) {
304
- registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
305
- }
306
- await registration.save()
324
+ if (group && group.settings.maxMembers !== null) {
325
+ registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
307
326
  }
327
+ await registration.save()
328
+ }
329
+
330
+ if (bundle.item.calculatedPrice === 0) {
331
+ continue;
308
332
  }
309
-
310
- await registration.save()
311
333
 
312
334
  // Create balance item
313
335
  const balanceItem = new BalanceItem();
314
336
  balanceItem.registrationId = registration.id;
315
337
  balanceItem.price = bundle.item.calculatedPrice
316
338
  balanceItem.description = `Inschrijving ${registration.group.settings.name}`
317
- balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? bundle.item.calculatedPrice : 0;
318
- balanceItem.memberId = registration.memberId;
319
- balanceItem.userId = user.id
339
+
340
+ // Who needs to receive this money?
320
341
  balanceItem.organizationId = organization.id;
321
- balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
322
- await balanceItem.save();
323
342
 
324
- // Create one balance item payment to pay it in one payment
325
- const balanceItemPayment = new BalanceItemPayment()
326
- balanceItemPayment.balanceItemId = balanceItem.id;
327
- balanceItemPayment.paymentId = payment.id;
328
- balanceItemPayment.organizationId = organization.id;
329
- balanceItemPayment.price = balanceItem.price;
330
- await balanceItemPayment.save();
343
+ // Who is responsible for payment?
344
+ let balanceItem2: BalanceItem | null = null
345
+ if (whoWillPayNow === 'organization' && request.body.asOrganizationId) {
346
+ // Create a separate balance item for this meber to pay back the paying organization
347
+ // this is not yet associated with a payment but will be added to the outstanding balance of the member
348
+
349
+ balanceItem.payingOrganizationId = request.body.asOrganizationId
350
+
351
+ balanceItem2 = new BalanceItem();
352
+
353
+ // NOTE: we don't connect the registrationId here
354
+ // because otherwise the total price and pricePaid for the registration would be incorrect
355
+ //balanceItem2.registrationId = registration.id;
331
356
 
357
+ balanceItem2.price = bundle.item.calculatedPrice
358
+ balanceItem2.description = `Inschrijving ${registration.group.settings.name}`
359
+
360
+ // Who needs to receive this money?
361
+ balanceItem2.organizationId = request.body.asOrganizationId;
362
+
363
+ // Who is responsible for payment?
364
+ balanceItem2.memberId = registration.memberId;
365
+
366
+ // If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
367
+ balanceItem2.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
368
+ await balanceItem2.save();
369
+
370
+ // do not add to items array because we don't want to add this to the payment if we create a payment
371
+ } else {
372
+ balanceItem.memberId = registration.memberId;
373
+ balanceItem.userId = user.id
374
+ }
375
+
376
+ balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
377
+ balanceItem.pricePaid = 0
378
+
379
+ // Connect the 'pay back' balance item to this balance item. As soon as this balance item is paid, we'll mark the other one as pending so the outstanding balance for the member increases
380
+ balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null
381
+
382
+ await balanceItem.save();
332
383
  items.push(balanceItem)
333
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
334
384
  }
335
385
 
336
386
  const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
337
- if (checkout.freeContribution) {
387
+ if (checkout.freeContribution && !request.body.asOrganizationId) {
338
388
  // Create balance item
339
389
  const balanceItem = new BalanceItem();
340
390
  balanceItem.price = checkout.freeContribution
341
391
  balanceItem.description = `Vrije bijdrage`
342
- balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? balanceItem.price : 0;
392
+ balanceItem.pricePaid = 0;
343
393
  balanceItem.userId = user.id
344
394
  balanceItem.organizationId = organization.id;
345
395
 
@@ -348,79 +398,257 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
348
398
  if (oldestMember) {
349
399
  balanceItem.memberId = oldestMember.id;
350
400
  }
351
- balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
401
+ balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
352
402
  await balanceItem.save();
353
-
354
- // Create one balance item payment to pay it in one payment
355
- const balanceItemPayment = new BalanceItemPayment()
356
- balanceItemPayment.balanceItemId = balanceItem.id;
357
- balanceItemPayment.paymentId = payment.id;
358
- balanceItemPayment.organizationId = organization.id;
359
- balanceItemPayment.price = balanceItem.price;
360
- await balanceItemPayment.save();
361
-
362
403
  items.push(balanceItem)
363
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
364
404
  }
365
405
 
366
- if (checkout.administrationFee) {
406
+ if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
367
407
  // Create balance item
368
408
  const balanceItem = new BalanceItem();
369
409
  balanceItem.price = checkout.administrationFee
370
410
  balanceItem.description = `Administratiekosten`
371
- balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? balanceItem.price : 0;
372
- balanceItem.userId = user.id
411
+ balanceItem.pricePaid = 0;
373
412
  balanceItem.organizationId = organization.id;
374
413
 
375
- // Connect this to the oldest member
376
-
377
- if (oldestMember) {
378
- balanceItem.memberId = oldestMember.id;
414
+ if (request.body.asOrganizationId) {
415
+ balanceItem.payingOrganizationId = request.body.asOrganizationId
416
+ } else {
417
+ balanceItem.userId = user.id
418
+ // Connect this to the oldest member
419
+ if (oldestMember) {
420
+ balanceItem.memberId = oldestMember.id;
421
+ }
379
422
  }
380
- balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
381
- await balanceItem.save();
382
423
 
383
- // Create one balance item payment to pay it in one payment
384
- const balanceItemPayment = new BalanceItemPayment()
385
- balanceItemPayment.balanceItemId = balanceItem.id;
386
- balanceItemPayment.paymentId = payment.id;
387
- balanceItemPayment.organizationId = organization.id;
388
- balanceItemPayment.price = balanceItem.price;
389
- await balanceItemPayment.save();
424
+ balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
425
+ await balanceItem.save();
390
426
 
391
427
  items.push(balanceItem)
392
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
393
428
  }
394
429
 
395
- // Create a payment pending balance item
396
- for (const item of checkout.cart.balanceItems) {
397
- // Create one balance item payment to pay it in one payment
398
- const balanceItemPayment = new BalanceItemPayment()
399
- balanceItemPayment.balanceItemId = item.item.id;
400
- balanceItemPayment.paymentId = payment.id;
401
- balanceItemPayment.organizationId = organization.id;
402
- balanceItemPayment.price = item.price;
403
- await balanceItemPayment.save();
430
+ if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
431
+ throw new Error('Not possible to pay balance items when whoWillPayNow is nobody')
432
+ }
433
+
434
+ // Create negative balance items
435
+ for (const registrationStruct of checkout.cart.deleteRegistrations) {
436
+ if (whoWillPayNow !== 'nobody') {
437
+ // this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
438
+ throw new SimpleError({
439
+ code: "forbidden",
440
+ message: "Permission denied: you are not allowed to delete registrations",
441
+ human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
442
+ statusCode: 403
443
+ })
444
+ }
445
+
446
+ const existingRegistration = await Registration.getByID(registrationStruct.id)
447
+ if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
448
+ throw new SimpleError({
449
+ code: "invalid_data",
450
+ message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
451
+ })
452
+ }
453
+
454
+ if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
455
+ throw new SimpleError({
456
+ code: "forbidden",
457
+ message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
458
+ statusCode: 403
459
+ })
460
+ }
461
+
462
+ if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
463
+ throw new SimpleError({
464
+ code: "invalid_data",
465
+ message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
466
+ })
467
+ }
468
+
469
+ // We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
470
+ // Find all balance items of this registration and set them to zero
471
+ await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
472
+
473
+ // Clear the registration
474
+ existingRegistration.deactivatedAt = new Date()
475
+ await existingRegistration.save()
476
+ existingRegistration.scheduleStockUpdate()
477
+
478
+ const group = groups.find(g => g.id === existingRegistration.groupId)
479
+ if (!group) {
480
+ const g = await Group.getByID(existingRegistration.groupId)
481
+ if (g) {
482
+ groups.push(g)
483
+ }
484
+ }
485
+ }
486
+
487
+ let paymentUrl: string | null = null
488
+ let payment: Payment | null = null
489
+
490
+ if (whoWillPayNow !== 'nobody') {
491
+ const mappedBalanceItems = new Map<BalanceItem, number>()
492
+
493
+ for (const item of items) {
494
+ mappedBalanceItems.set(item, item.price)
495
+ }
404
496
 
405
- const balanceItem = balanceItems.find(i => i.id === item.item.id)
406
- if (!balanceItem) {
407
- throw new Error('Balance item not found')
497
+ for (const item of checkout.cart.balanceItems) {
498
+ const balanceItem = balanceItems.find(i => i.id === item.item.id)
499
+ if (!balanceItem) {
500
+ throw new Error('Balance item not found')
501
+ }
502
+ mappedBalanceItems.set(balanceItem, item.price)
503
+ items.push(balanceItem)
504
+ }
505
+
506
+ const response = await this.createPayment({
507
+ balanceItems: mappedBalanceItems,
508
+ organization,
509
+ user,
510
+ checkout: request.body,
511
+ members
512
+ })
513
+
514
+ if (response) {
515
+ paymentUrl = response.paymentUrl
516
+ payment = response.payment
408
517
  }
409
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
410
518
  }
411
- items.push(...balanceItems)
412
- await ExchangePaymentEndpoint.updateOutstanding(items, organization.id)
519
+
520
+ await BalanceItem.updateOutstanding(items, organization.id)
413
521
 
414
522
  // Update occupancy
415
523
  for (const group of groups) {
416
- if (registrations.find(p => p.groupId === group.id)) {
524
+ if (registrations.find(p => p.groupId === group.id) || checkout.cart.deleteRegistrations.find(p => p.groupId === group.id)) {
417
525
  await group.updateOccupancy()
418
526
  await group.save()
419
527
  }
420
528
  }
421
529
 
422
- // Update balance items
530
+ return new Response(RegisterResponse.create({
531
+ payment: payment ? PaymentStruct.create(payment) : null,
532
+ members: await AuthenticatedStructures.membersBlob(members),
533
+ registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
534
+ paymentUrl
535
+ }));
536
+ }
537
+
538
+ async createPayment({balanceItems, organization, user, checkout, members}: {balanceItems: Map<BalanceItem, number>, organization: Organization, user: User, checkout: IDRegisterCheckout, members: MemberWithRegistrations[]}) {
539
+ // Calculate total price to pay
540
+ let totalPrice = 0
541
+ const payMembers: MemberWithRegistrations[] = []
542
+
543
+ for (const [balanceItem, price] of balanceItems) {
544
+ if (organization.id !== balanceItem.organizationId) {
545
+ throw new Error('Unexpected balance item from other organization')
546
+ }
547
+
548
+ if (price < 0 || (price > 0 && price > balanceItem.price - balanceItem.pricePaid)) {
549
+ throw new SimpleError({
550
+ code: "invalid_data",
551
+ message: "Oeps, het bedrag dat je probeert te betalen is ongeldig. Herlaad de pagina en probeer opnieuw."
552
+ })
553
+ }
554
+ totalPrice += price
555
+
556
+ if (price > 0 && balanceItem.memberId) {
557
+ const member = members.find(m => m.id === balanceItem.memberId)
558
+ if (!member) {
559
+ throw new SimpleError({
560
+ code: "invalid_data",
561
+ message: "Oeps, het lid dat je probeert in te schrijven konden we niet meer terugvinden. Je herlaadt best even de pagina om opnieuw te proberen."
562
+ })
563
+ }
564
+ payMembers.push(member)
565
+ }
566
+ }
567
+
568
+ if (totalPrice < 0) {
569
+ // No payment needed: the outstanding balance will be negative and can be used in the future
570
+ return;
571
+ // throw new SimpleError({
572
+ // code: "empty_data",
573
+ // message: "Oeps! De totaalprijs is negatief."
574
+ // })
575
+ }
576
+
577
+ if (totalPrice === 0) {
578
+ return;
579
+ }
580
+
581
+ if (!checkout.paymentMethod || checkout.paymentMethod === PaymentMethod.Unknown) {
582
+ throw new SimpleError({
583
+ code: "invalid_data",
584
+ message: "Oeps, je hebt geen betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw."
585
+ })
586
+ }
587
+
588
+ const payment = new Payment()
589
+ payment.userId = user.id
590
+
591
+ // Who will receive this money?
592
+ payment.organizationId = organization.id
593
+
594
+ payment.method = checkout.paymentMethod
595
+ payment.status = PaymentStatus.Created
596
+ payment.price = totalPrice
597
+
423
598
  if (payment.method == PaymentMethod.Transfer) {
599
+ // remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
600
+ payment.transferSettings = organization.mappedTransferSettings
601
+
602
+ if (!payment.transferSettings.iban) {
603
+ throw new SimpleError({
604
+ code: "no_iban",
605
+ message: "No IBAN",
606
+ human: "Er is geen rekeningnummer ingesteld voor overschrijvingen. Contacteer de beheerder."
607
+ })
608
+ }
609
+
610
+ const m = payMembers.map(r => r.details)
611
+ payment.generateDescription(
612
+ organization,
613
+ Formatter.groupNamesByFamily(m),
614
+ {
615
+ name: Formatter.groupNamesByFamily(m),
616
+ naam: Formatter.groupNamesByFamily(m),
617
+ email: user.email
618
+ }
619
+ )
620
+ }
621
+ payment.paidAt = null
622
+
623
+ // Determine the payment provider
624
+ // Throws if invalid
625
+ const {provider, stripeAccount} = await organization.getPaymentProviderFor(payment.method, organization.privateMeta.registrationPaymentConfiguration)
626
+ payment.provider = provider
627
+ payment.stripeAccountId = stripeAccount?.id ?? null
628
+
629
+ await payment.save()
630
+
631
+ // Create balance item payments
632
+ const balanceItemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = []
633
+
634
+ for (const [balanceItem, price] of balanceItems) {
635
+ // Create one balance item payment to pay it in one payment
636
+ const balanceItemPayment = new BalanceItemPayment()
637
+ balanceItemPayment.balanceItemId = balanceItem.id;
638
+ balanceItemPayment.paymentId = payment.id;
639
+ balanceItemPayment.organizationId = organization.id;
640
+ balanceItemPayment.price = price;
641
+ await balanceItemPayment.save();
642
+
643
+ balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
644
+ }
645
+
646
+ const description = 'Inschrijving '+organization.name
647
+
648
+ let paymentUrl: string | null = null
649
+
650
+ // Update balance items
651
+ if (payment.method === PaymentMethod.Transfer) {
424
652
  // Send a small reminder email
425
653
  try {
426
654
  await Registration.sendTransferEmail(user, organization, payment)
@@ -428,13 +656,23 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
428
656
  console.error("Failed to send transfer email")
429
657
  console.error(e)
430
658
  }
431
- }
659
+ } else if (payment.method !== PaymentMethod.PointOfSale) {
660
+ if (!checkout.redirectUrl || !checkout.cancelUrl) {
661
+ throw new Error('Should have been caught earlier')
662
+ }
663
+
664
+ const _redirectUrl = new URL(checkout.redirectUrl)
665
+ _redirectUrl.searchParams.set('paymentId', payment.id);
666
+ _redirectUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
667
+
668
+ const _cancelUrl = new URL(checkout.cancelUrl)
669
+ _cancelUrl.searchParams.set('paymentId', payment.id);
670
+ _cancelUrl.searchParams.set('cancel', 'true');
671
+ _cancelUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
672
+
673
+ const redirectUrl = _redirectUrl.href
674
+ const cancelUrl = _cancelUrl.href
432
675
 
433
- let paymentUrl: string | null = null
434
- const description = 'Inschrijving '+organization.name
435
- if (payment.status != PaymentStatus.Succeeded) {
436
- const redirectUrl = "https://"+organization.getHost()+'/payment?id='+encodeURIComponent(payment.id)
437
- const cancelUrl = "https://"+organization.getHost()+'/payment?id='+encodeURIComponent(payment.id) + '&cancel=true'
438
676
  const webhookUrl = 'https://'+organization.getApiHost()+"/v"+Version+"/payments/"+encodeURIComponent(payment.id)+"?exchange=true"
439
677
 
440
678
  if (payment.provider === PaymentProvider.Stripe) {
@@ -449,11 +687,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
449
687
  user: user.id,
450
688
  payment: payment.id
451
689
  },
452
- i18n: request.i18n,
453
- lineItems: itemPayments,
690
+ i18n: Context.i18n,
691
+ lineItems: balanceItemPayments,
454
692
  organization,
455
693
  customer: {
456
- name: user.name ?? oldestMember?.details.name ?? 'Onbekend',
694
+ name: user.name ?? payMembers[0]?.details.name ?? 'Onbekend',
457
695
  email: user.email,
458
696
  }
459
697
  });
@@ -502,9 +740,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
502
740
  paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl)
503
741
  } else if (payment.provider == PaymentProvider.Buckaroo) {
504
742
  // Increase request timeout because buckaroo is super slow (in development)
505
- request.request.request?.setTimeout(60 * 1000)
743
+ Context.request.request?.setTimeout(60 * 1000)
506
744
  const buckaroo = new BuckarooHelper(organization.privateMeta?.buckarooSettings?.key ?? "", organization.privateMeta?.buckarooSettings?.secret ?? "", organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production')
507
- const ip = request.request.getIP()
745
+ const ip = Context.request.getIP()
508
746
  paymentUrl = await buckaroo.createPayment(payment, ip, description, redirectUrl, webhookUrl)
509
747
  await payment.save()
510
748
 
@@ -518,11 +756,12 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
518
756
  }
519
757
  }
520
758
 
521
- return new Response(RegisterResponse.create({
522
- payment: PaymentStruct.create(payment),
523
- members: await AuthenticatedStructures.membersBlob(members),
524
- registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
759
+ return {
760
+ payment,
761
+ balanceItemPayments,
762
+ provider,
763
+ stripeAccount,
525
764
  paymentUrl
526
- }));
765
+ }
527
766
  }
528
767
  }