@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.
- package/package.json +3 -3
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +16 -9
- package/src/endpoints/global/members/GetMembersEndpoint.ts +18 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -8
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +417 -178
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +88 -11
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +10 -4
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +6 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -2
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -28
- package/src/helpers/AdminPermissionChecker.ts +9 -4
- package/src/helpers/AuthenticatedStructures.ts +89 -28
- package/src/helpers/MemberUserSyncer.ts +5 -5
- package/src/helpers/StripeHelper.ts +83 -40
- package/src/helpers/StripePayoutChecker.ts +7 -5
- 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,
|
|
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
|
|
92
|
-
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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:
|
|
252
|
-
message:
|
|
253
|
-
human:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
291
|
-
// Replaced with balance items
|
|
292
|
-
// registration.paymentId = payment.id
|
|
316
|
+
registration.reservedUntil = null
|
|
293
317
|
|
|
294
|
-
|
|
295
|
-
registration.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
372
|
-
balanceItem.userId = user.id
|
|
411
|
+
balanceItem.pricePaid = 0;
|
|
373
412
|
balanceItem.organizationId = organization.id;
|
|
374
413
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
balanceItem.
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
412
|
-
await
|
|
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
|
-
|
|
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:
|
|
453
|
-
lineItems:
|
|
690
|
+
i18n: Context.i18n,
|
|
691
|
+
lineItems: balanceItemPayments,
|
|
454
692
|
organization,
|
|
455
693
|
customer: {
|
|
456
|
-
name: user.name ??
|
|
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
|
-
|
|
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 =
|
|
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
|
|
522
|
-
payment
|
|
523
|
-
|
|
524
|
-
|
|
759
|
+
return {
|
|
760
|
+
payment,
|
|
761
|
+
balanceItemPayments,
|
|
762
|
+
provider,
|
|
763
|
+
stripeAccount,
|
|
525
764
|
paymentUrl
|
|
526
|
-
}
|
|
765
|
+
}
|
|
527
766
|
}
|
|
528
767
|
}
|