@stamhoofd/backend 2.3.1 → 2.5.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 (39) hide show
  1. package/index.ts +3 -0
  2. package/package.json +4 -4
  3. package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
  4. package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
  5. package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
  6. package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
  7. package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
  8. package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
  9. package/src/endpoints/auth/SignupEndpoint.ts +2 -2
  10. package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
  11. package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
  12. package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
  13. package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
  14. package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
  15. package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
  16. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +107 -117
  17. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
  18. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
  19. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
  20. package/src/endpoints/global/platform/GetPlatformAdminsEndpoint.ts +2 -1
  21. package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
  22. package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
  23. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +345 -176
  24. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
  25. package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
  26. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
  27. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
  28. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
  29. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -121
  30. package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
  31. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
  32. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -1
  33. package/src/helpers/AdminPermissionChecker.ts +60 -3
  34. package/src/helpers/AuthenticatedStructures.ts +164 -37
  35. package/src/helpers/Context.ts +4 -0
  36. package/src/helpers/EmailResumer.ts +17 -0
  37. package/src/helpers/MemberUserSyncer.ts +221 -0
  38. package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
  39. 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, MemberDetails, 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,53 @@ 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(request.body.cart.items.map(i => i.memberId))
103
+ const members = await Member.getBlobByIds(...memberIds)
104
+ const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
105
+ const groups = await Group.getByIDs(...groupIds)
106
+
107
+ for (const group of groups) {
108
+ if (group.organizationId !== organization.id) {
109
+ throw new SimpleError({
110
+ code: "invalid_data",
111
+ message: "Oeps, één of meerdere groepen waarin je probeert in te schrijven lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
112
+ })
113
+ }
114
+ }
115
+
116
+ for (const member of members) {
117
+ if (!await Context.auth.canAccessMember(member, PermissionLevel.Write)) {
118
+ throw new SimpleError({
119
+ code: "forbidden",
120
+ message: "No permission to register this member",
121
+ human: 'Je hebt niet de juiste toegangsrechten om dit lid in te schrijven.',
122
+ statusCode: 403
123
+ });
124
+ }
125
+ }
93
126
 
94
127
  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})
128
+ const platformMembers: PlatformMember[] = []
129
+
130
+ if (request.body.asOrganizationId) {
131
+ const _m = PlatformFamily.createSingles(blob, {
132
+ platform: await Platform.getSharedStruct(),
133
+ contextOrganization: await AuthenticatedStructures.organization(organization)
134
+ })
135
+ platformMembers.push(..._m)
136
+ } else {
137
+ const family = PlatformFamily.create(blob, {
138
+ platform: await Platform.getSharedStruct(),
139
+ contextOrganization: await AuthenticatedStructures.organization(organization)
140
+ })
141
+ platformMembers.push(...family.members)
142
+ }
143
+
144
+ const checkout = request.body.hydrate({
145
+ members: platformMembers,
146
+ groups: await AuthenticatedStructures.groups(groups),
147
+ organizations: [await AuthenticatedStructures.organization(organization)]
148
+ })
97
149
 
98
150
  const registrations: RegistrationWithMemberAndGroup[] = []
99
151
  const payRegistrations: {registration: RegistrationWithMemberAndGroup, item: RegisterItem}[] = []
@@ -108,9 +160,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
108
160
  // Update occupancies
109
161
  // TODO: might not be needed in the future (for performance)
110
162
  for (const group of groups) {
111
- if (request.body.cart.items.find(i => i.groupId == group.id)) {
112
- await group.updateOccupancy()
113
- }
163
+ await group.updateOccupancy()
114
164
  }
115
165
 
116
166
  // Validate balance items (can only happen serverside)
@@ -135,6 +185,13 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
135
185
  checkout.updatePrices()
136
186
 
137
187
  const totalPrice = checkout.totalPrice
188
+
189
+ if (totalPrice !== request.body.totalPrice) {
190
+ throw new SimpleError({
191
+ code: "changed_price",
192
+ 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."
193
+ })
194
+ }
138
195
 
139
196
  if (totalPrice < 0) {
140
197
  throw new SimpleError({
@@ -146,7 +203,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
146
203
  const registrationMemberRelation = new ManyToOneRelation(Member, "member")
147
204
  registrationMemberRelation.foreignKey = "memberId"
148
205
 
149
- mainLoop: for (const item of checkout.cart.items) {
206
+ for (const item of checkout.cart.items) {
150
207
  const member = members.find(m => m.id == item.memberId)
151
208
  if (!member) {
152
209
  throw new SimpleError({
@@ -171,18 +228,13 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
171
228
  registration = existingRegistration
172
229
  .setRelation(registrationMemberRelation, member as Member)
173
230
  .setRelation(Registration.group, group)
231
+
174
232
 
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;
233
+ if (existingRegistration.registeredAt !== null) {
234
+ throw new SimpleError({
235
+ code: "already_registered",
236
+ message: "Dit lid is reeds ingeschreven. Herlaad de pagina en probeer opnieuw."
237
+ })
186
238
  }
187
239
  }
188
240
 
@@ -197,31 +249,29 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
197
249
  registration.memberId = member.id
198
250
  registration.groupId = group.id
199
251
  registration.cycle = group.cycle
252
+ registration.price = item.calculatedPrice
253
+
254
+ payRegistrations.push({
255
+ registration,
256
+ item
257
+ });
200
258
 
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
259
  registrations.push(registration)
221
260
  }
222
261
 
262
+ // Who is going to pay?
263
+ let whoWillPay: '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
264
+
265
+ if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
266
+ // Will get added to the outstanding amount of the member
267
+ whoWillPay = 'nobody'
268
+ } else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
269
+ // The organization will pay to the organizing organization, and it will get added to the outstanding amount of the member towards the paying organization
270
+ whoWillPay = 'organization'
271
+ }
272
+
223
273
  // Validate payment method
224
- if (totalPrice > 0) {
274
+ if (totalPrice > 0 && whoWillPay !== 'nobody') {
225
275
  const allowedPaymentMethods = organization.meta.registrationPaymentConfiguration.paymentMethods
226
276
 
227
277
  if (!checkout.paymentMethod || !allowedPaymentMethods.includes(checkout.paymentMethod)) {
@@ -230,116 +280,104 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
230
280
  message: "Oeps, je hebt geen geldige betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw. Herlaad de pagina indien nodig."
231
281
  })
232
282
  }
233
- } else {
234
- checkout.paymentMethod = PaymentMethod.Unknown
235
- }
236
-
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
283
 
249
- if (!payment.transferSettings.iban) {
284
+ if ((checkout.paymentMethod !== PaymentMethod.Transfer && checkout.paymentMethod !== PaymentMethod.PointOfSale) && (!request.body.redirectUrl || !request.body.cancelUrl)) {
250
285
  throw new SimpleError({
251
- code: "no_iban",
252
- message: "No IBAN",
253
- human: "Er is geen rekeningnummer ingesteld voor overschrijvingen. Contacteer de beheerder."
286
+ code: 'missing_fields',
287
+ message: 'redirectUrl or cancelUrl is missing and is required for non-zero online payments',
288
+ human: 'Er is iets mis. Contacteer de webmaster.'
254
289
  })
255
290
  }
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()
291
+ } else {
292
+ checkout.paymentMethod = PaymentMethod.Unknown
274
293
  }
275
294
 
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
295
+ console.log('Registering members using whoWillPay', whoWillPay, checkout.paymentMethod, totalPrice)
281
296
 
282
- await payment.save()
283
297
  const items: BalanceItem[] = []
284
- const itemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = []
298
+ const shouldMarkValid = whoWillPay === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale
285
299
 
286
300
  // Save registrations and add extra data if needed
287
301
  for (const bundle of payRegistrations) {
288
302
  const registration = bundle.registration;
289
303
 
290
- if (!registration.waitingList) {
291
- // Replaced with balance items
292
- // registration.paymentId = payment.id
304
+ registration.reservedUntil = null
293
305
 
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)
306
+ if (shouldMarkValid) {
307
+ await registration.markValid()
308
+ } else {
309
+ // Reserve registration for 30 minutes (if needed)
310
+ const group = groups.find(g => g.id === registration.groupId)
302
311
 
303
- if (group && group.settings.maxMembers !== null) {
304
- registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
305
- }
306
- await registration.save()
312
+ if (group && group.settings.maxMembers !== null) {
313
+ registration.reservedUntil = new Date(new Date().getTime() + 1000*60*30)
307
314
  }
315
+ await registration.save()
316
+ }
317
+
318
+ if (bundle.item.calculatedPrice === 0) {
319
+ continue;
308
320
  }
309
-
310
- await registration.save()
311
321
 
312
322
  // Create balance item
313
323
  const balanceItem = new BalanceItem();
314
324
  balanceItem.registrationId = registration.id;
315
325
  balanceItem.price = bundle.item.calculatedPrice
316
326
  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
327
+
328
+ // Who needs to receive this money?
320
329
  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
330
 
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();
331
+ // Who is responsible for payment?
332
+ let balanceItem2: BalanceItem | null = null
333
+ if (whoWillPay === 'organization' && request.body.asOrganizationId) {
334
+ // Create a separate balance item for this meber to pay back the paying organization
335
+ // this is not yet associated with a payment but will be added to the outstanding balance of the member
336
+
337
+ balanceItem.payingOrganizationId = request.body.asOrganizationId
338
+
339
+ balanceItem2 = new BalanceItem();
340
+
341
+ // NOTE: we don't connect the registrationId here
342
+ // because otherwise the total price and pricePaid for the registration would be incorrect
343
+ //balanceItem2.registrationId = registration.id;
344
+
345
+ balanceItem2.price = bundle.item.calculatedPrice
346
+ balanceItem2.description = `Inschrijving ${registration.group.settings.name}`
347
+
348
+ // Who needs to receive this money?
349
+ balanceItem2.organizationId = request.body.asOrganizationId;
350
+
351
+ // Who is responsible for payment?
352
+ balanceItem2.memberId = registration.memberId;
331
353
 
354
+ // If the paying organization hasn't paid yet, this should be hidden and move to pending as soon as the paying organization has paid
355
+ balanceItem2.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
356
+ await balanceItem2.save();
357
+
358
+ // do not add to items array because we don't want to add this to the payment if we create a payment
359
+ } else {
360
+ balanceItem.memberId = registration.memberId;
361
+ balanceItem.userId = user.id
362
+ }
363
+
364
+ balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
365
+ balanceItem.pricePaid = 0
366
+
367
+ // 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
368
+ balanceItem.dependingBalanceItemId = balanceItem2?.id ?? null
369
+
370
+ await balanceItem.save();
332
371
  items.push(balanceItem)
333
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
334
372
  }
335
373
 
336
374
  const oldestMember = members.slice().sort((a, b) => b.details.defaultAge - a.details.defaultAge)[0]
337
- if (checkout.freeContribution) {
375
+ if (checkout.freeContribution && !request.body.asOrganizationId) {
338
376
  // Create balance item
339
377
  const balanceItem = new BalanceItem();
340
378
  balanceItem.price = checkout.freeContribution
341
379
  balanceItem.description = `Vrije bijdrage`
342
- balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? balanceItem.price : 0;
380
+ balanceItem.pricePaid = 0;
343
381
  balanceItem.userId = user.id
344
382
  balanceItem.organizationId = organization.id;
345
383
 
@@ -348,67 +386,72 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
348
386
  if (oldestMember) {
349
387
  balanceItem.memberId = oldestMember.id;
350
388
  }
351
- balanceItem.status = payment.status == PaymentStatus.Succeeded ? BalanceItemStatus.Paid : (payment.method == PaymentMethod.Transfer || payment.method == PaymentMethod.PointOfSale ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden);
389
+ balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
352
390
  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
391
  items.push(balanceItem)
363
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
364
392
  }
365
393
 
366
- if (checkout.administrationFee) {
394
+ if (checkout.administrationFee && whoWillPay !== 'nobody') {
367
395
  // Create balance item
368
396
  const balanceItem = new BalanceItem();
369
397
  balanceItem.price = checkout.administrationFee
370
398
  balanceItem.description = `Administratiekosten`
371
- balanceItem.pricePaid = payment.status == PaymentStatus.Succeeded ? balanceItem.price : 0;
372
- balanceItem.userId = user.id
399
+ balanceItem.pricePaid = 0;
373
400
  balanceItem.organizationId = organization.id;
374
401
 
375
- // Connect this to the oldest member
376
-
377
- if (oldestMember) {
378
- balanceItem.memberId = oldestMember.id;
402
+ if (request.body.asOrganizationId) {
403
+ balanceItem.payingOrganizationId = request.body.asOrganizationId
404
+ } else {
405
+ balanceItem.userId = user.id
406
+ // Connect this to the oldest member
407
+ if (oldestMember) {
408
+ balanceItem.memberId = oldestMember.id;
409
+ }
379
410
  }
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
411
 
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();
412
+ balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
413
+ await balanceItem.save();
390
414
 
391
415
  items.push(balanceItem)
392
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
393
416
  }
394
417
 
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();
418
+ if (checkout.cart.balanceItems.length && whoWillPay === 'nobody') {
419
+ throw new Error('Not possible to pay balance items when whoWillPay is nobody')
420
+ }
421
+
422
+ let paymentUrl: string | null = null
423
+ let payment: Payment | null = null
424
+
425
+ if (whoWillPay !== 'nobody') {
426
+ const mappedBalanceItems = new Map<BalanceItem, number>()
404
427
 
405
- const balanceItem = balanceItems.find(i => i.id === item.item.id)
406
- if (!balanceItem) {
407
- throw new Error('Balance item not found')
428
+ for (const item of items) {
429
+ mappedBalanceItems.set(item, item.price)
430
+ }
431
+
432
+ for (const item of checkout.cart.balanceItems) {
433
+ const balanceItem = balanceItems.find(i => i.id === item.item.id)
434
+ if (!balanceItem) {
435
+ throw new Error('Balance item not found')
436
+ }
437
+ mappedBalanceItems.set(balanceItem, item.price)
438
+ items.push(balanceItem)
439
+ }
440
+
441
+ const response = await this.createPayment({
442
+ balanceItems: mappedBalanceItems,
443
+ organization,
444
+ user,
445
+ checkout: request.body,
446
+ members
447
+ })
448
+
449
+ if (response) {
450
+ paymentUrl = response.paymentUrl
451
+ payment = response.payment
408
452
  }
409
- itemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
410
453
  }
411
- items.push(...balanceItems)
454
+
412
455
  await ExchangePaymentEndpoint.updateOutstanding(items, organization.id)
413
456
 
414
457
  // Update occupancy
@@ -419,8 +462,126 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
419
462
  }
420
463
  }
421
464
 
422
- // Update balance items
465
+ return new Response(RegisterResponse.create({
466
+ payment: payment ? PaymentStruct.create(payment) : null,
467
+ members: await AuthenticatedStructures.membersBlob(members),
468
+ registrations: registrations.map(r => Member.getRegistrationWithMemberStructure(r)),
469
+ paymentUrl
470
+ }));
471
+ }
472
+
473
+ async createPayment({balanceItems, organization, user, checkout, members}: {balanceItems: Map<BalanceItem, number>, organization: Organization, user: User, checkout: IDRegisterCheckout, members: MemberWithRegistrations[]}) {
474
+ // Calculate total price to pay
475
+ let totalPrice = 0
476
+ const payMembers: MemberWithRegistrations[] = []
477
+
478
+ for (const [balanceItem, price] of balanceItems) {
479
+ if (organization.id !== balanceItem.organizationId) {
480
+ throw new Error('Unexpected balance item from other organization')
481
+ }
482
+
483
+ if (price < 0 || (price > 0 && price > balanceItem.price - balanceItem.pricePaid)) {
484
+ throw new SimpleError({
485
+ code: "invalid_data",
486
+ message: "Oeps, het bedrag dat je probeert te betalen is ongeldig. Herlaad de pagina en probeer opnieuw."
487
+ })
488
+ }
489
+ totalPrice += price
490
+
491
+ if (price > 0 && balanceItem.memberId) {
492
+ const member = members.find(m => m.id === balanceItem.memberId)
493
+ if (!member) {
494
+ throw new SimpleError({
495
+ code: "invalid_data",
496
+ 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."
497
+ })
498
+ }
499
+ payMembers.push(member)
500
+ }
501
+ }
502
+
503
+ if (totalPrice < 0) {
504
+ throw new SimpleError({
505
+ code: "empty_data",
506
+ message: "Oeps! De totaalprijs is negatief."
507
+ })
508
+ }
509
+
510
+ if (totalPrice === 0) {
511
+ return;
512
+ }
513
+
514
+ if (!checkout.paymentMethod || checkout.paymentMethod === PaymentMethod.Unknown) {
515
+ throw new SimpleError({
516
+ code: "invalid_data",
517
+ message: "Oeps, je hebt geen betaalmethode geselecteerd. Selecteer een betaalmethode en probeer opnieuw."
518
+ })
519
+ }
520
+
521
+ const payment = new Payment()
522
+ payment.userId = user.id
523
+
524
+ // Who will receive this money?
525
+ payment.organizationId = organization.id
526
+
527
+ payment.method = checkout.paymentMethod
528
+ payment.status = PaymentStatus.Created
529
+ payment.price = totalPrice
530
+
423
531
  if (payment.method == PaymentMethod.Transfer) {
532
+ // remark: we cannot add the lastnames, these will get added in the frontend when it is decrypted
533
+ payment.transferSettings = organization.mappedTransferSettings
534
+
535
+ if (!payment.transferSettings.iban) {
536
+ throw new SimpleError({
537
+ code: "no_iban",
538
+ message: "No IBAN",
539
+ human: "Er is geen rekeningnummer ingesteld voor overschrijvingen. Contacteer de beheerder."
540
+ })
541
+ }
542
+
543
+ const m = payMembers.map(r => r.details)
544
+ payment.generateDescription(
545
+ organization,
546
+ Formatter.groupNamesByFamily(m),
547
+ {
548
+ name: Formatter.groupNamesByFamily(m),
549
+ naam: Formatter.groupNamesByFamily(m),
550
+ email: user.email
551
+ }
552
+ )
553
+ }
554
+ payment.paidAt = null
555
+
556
+ // Determine the payment provider
557
+ // Throws if invalid
558
+ const {provider, stripeAccount} = await organization.getPaymentProviderFor(payment.method, organization.privateMeta.registrationPaymentConfiguration)
559
+ payment.provider = provider
560
+ payment.stripeAccountId = stripeAccount?.id ?? null
561
+
562
+ await payment.save()
563
+
564
+ // Create balance item payments
565
+ const balanceItemPayments: (BalanceItemPayment & { balanceItem: BalanceItem })[] = []
566
+
567
+ for (const [balanceItem, price] of balanceItems) {
568
+ // Create one balance item payment to pay it in one payment
569
+ const balanceItemPayment = new BalanceItemPayment()
570
+ balanceItemPayment.balanceItemId = balanceItem.id;
571
+ balanceItemPayment.paymentId = payment.id;
572
+ balanceItemPayment.organizationId = organization.id;
573
+ balanceItemPayment.price = price;
574
+ await balanceItemPayment.save();
575
+
576
+ balanceItemPayments.push(balanceItemPayment.setRelation(BalanceItemPayment.balanceItem, balanceItem))
577
+ }
578
+
579
+ const description = 'Inschrijving '+organization.name
580
+
581
+ let paymentUrl: string | null = null
582
+
583
+ // Update balance items
584
+ if (payment.method === PaymentMethod.Transfer) {
424
585
  // Send a small reminder email
425
586
  try {
426
587
  await Registration.sendTransferEmail(user, organization, payment)
@@ -428,13 +589,20 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
428
589
  console.error("Failed to send transfer email")
429
590
  console.error(e)
430
591
  }
431
- }
592
+ } else if (payment.method !== PaymentMethod.PointOfSale) {
593
+ if (!checkout.redirectUrl || !checkout.cancelUrl) {
594
+ throw new Error('Should have been caught earlier')
595
+ }
596
+
597
+ const _redirectUrl = new URL(checkout.redirectUrl)
598
+ _redirectUrl.searchParams.set('id', payment.id);
599
+
600
+ const _cancelUrl = new URL(checkout.cancelUrl)
601
+ _cancelUrl.searchParams.set('id', payment.id);
602
+
603
+ const redirectUrl = _redirectUrl.href
604
+ const cancelUrl = _cancelUrl.href
432
605
 
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
606
  const webhookUrl = 'https://'+organization.getApiHost()+"/v"+Version+"/payments/"+encodeURIComponent(payment.id)+"?exchange=true"
439
607
 
440
608
  if (payment.provider === PaymentProvider.Stripe) {
@@ -449,11 +617,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
449
617
  user: user.id,
450
618
  payment: payment.id
451
619
  },
452
- i18n: request.i18n,
453
- lineItems: itemPayments,
620
+ i18n: Context.i18n,
621
+ lineItems: balanceItemPayments,
454
622
  organization,
455
623
  customer: {
456
- name: user.name ?? oldestMember?.details.name ?? 'Onbekend',
624
+ name: user.name ?? payMembers[0]?.details.name ?? 'Onbekend',
457
625
  email: user.email,
458
626
  }
459
627
  });
@@ -502,9 +670,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
502
670
  paymentUrl = await PayconiqPayment.createPayment(payment, organization, description, redirectUrl, webhookUrl)
503
671
  } else if (payment.provider == PaymentProvider.Buckaroo) {
504
672
  // Increase request timeout because buckaroo is super slow (in development)
505
- request.request.request?.setTimeout(60 * 1000)
673
+ Context.request.request?.setTimeout(60 * 1000)
506
674
  const buckaroo = new BuckarooHelper(organization.privateMeta?.buckarooSettings?.key ?? "", organization.privateMeta?.buckarooSettings?.secret ?? "", organization.privateMeta.useTestPayments ?? STAMHOOFD.environment != 'production')
507
- const ip = request.request.getIP()
675
+ const ip = Context.request.getIP()
508
676
  paymentUrl = await buckaroo.createPayment(payment, ip, description, redirectUrl, webhookUrl)
509
677
  await payment.save()
510
678
 
@@ -518,11 +686,12 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
518
686
  }
519
687
  }
520
688
 
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)),
689
+ return {
690
+ payment,
691
+ balanceItemPayments,
692
+ provider,
693
+ stripeAccount,
525
694
  paymentUrl
526
- }));
695
+ }
527
696
  }
528
697
  }