@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.
- package/index.ts +3 -0
- package/package.json +4 -4
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/admin/memberships/GetChargeMembershipsSummaryEndpoint.ts +63 -2
- package/src/endpoints/auth/CreateAdminEndpoint.ts +6 -3
- package/src/endpoints/auth/GetOtherUserEndpoint.ts +41 -0
- package/src/endpoints/auth/GetUserEndpoint.ts +6 -28
- package/src/endpoints/auth/PatchUserEndpoint.ts +25 -6
- package/src/endpoints/auth/SignupEndpoint.ts +2 -2
- package/src/endpoints/global/email/CreateEmailEndpoint.ts +120 -0
- package/src/endpoints/global/email/GetEmailEndpoint.ts +51 -0
- package/src/endpoints/global/email/PatchEmailEndpoint.ts +108 -0
- package/src/endpoints/global/events/GetEventsEndpoint.ts +223 -0
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +319 -0
- package/src/endpoints/global/members/GetMembersEndpoint.ts +124 -48
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +107 -117
- 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/platform/GetPlatformAdminsEndpoint.ts +2 -1
- package/src/endpoints/global/platform/PatchPlatformEnpoint.ts +9 -0
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.ts +3 -2
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +345 -176
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/email/EmailEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.ts +43 -25
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.ts +26 -7
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +23 -22
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +210 -121
- package/src/endpoints/organization/dashboard/stripe/DeleteStripeAccountEndpoint.ts +8 -8
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.ts +2 -1
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +2 -1
- package/src/helpers/AdminPermissionChecker.ts +60 -3
- package/src/helpers/AuthenticatedStructures.ts +164 -37
- package/src/helpers/Context.ts +4 -0
- package/src/helpers/EmailResumer.ts +17 -0
- package/src/helpers/MemberUserSyncer.ts +221 -0
- package/src/seeds/1722256498-group-update-occupancy.ts +52 -0
- 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
|
|
92
|
-
const
|
|
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
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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;
|
|
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 (!
|
|
284
|
+
if ((checkout.paymentMethod !== PaymentMethod.Transfer && checkout.paymentMethod !== PaymentMethod.PointOfSale) && (!request.body.redirectUrl || !request.body.cancelUrl)) {
|
|
250
285
|
throw new SimpleError({
|
|
251
|
-
code:
|
|
252
|
-
message:
|
|
253
|
-
human:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
291
|
-
// Replaced with balance items
|
|
292
|
-
// registration.paymentId = payment.id
|
|
304
|
+
registration.reservedUntil = null
|
|
293
305
|
|
|
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)
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
372
|
-
balanceItem.userId = user.id
|
|
399
|
+
balanceItem.pricePaid = 0;
|
|
373
400
|
balanceItem.organizationId = organization.id;
|
|
374
401
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
balanceItem.
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
453
|
-
lineItems:
|
|
620
|
+
i18n: Context.i18n,
|
|
621
|
+
lineItems: balanceItemPayments,
|
|
454
622
|
organization,
|
|
455
623
|
customer: {
|
|
456
|
-
name: user.name ??
|
|
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
|
-
|
|
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 =
|
|
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
|
|
522
|
-
payment
|
|
523
|
-
|
|
524
|
-
|
|
689
|
+
return {
|
|
690
|
+
payment,
|
|
691
|
+
balanceItemPayments,
|
|
692
|
+
provider,
|
|
693
|
+
stripeAccount,
|
|
525
694
|
paymentUrl
|
|
526
|
-
}
|
|
695
|
+
}
|
|
527
696
|
}
|
|
528
697
|
}
|