@stamhoofd/backend 2.5.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/global/events/PatchEventsEndpoint.ts +16 -9
- package/src/endpoints/global/members/GetMembersEndpoint.ts +18 -6
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +95 -25
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +1 -0
- 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 +11 -27
- package/src/helpers/AdminPermissionChecker.ts +3 -4
- package/src/helpers/AuthenticatedStructures.ts +3 -4
- package/src/helpers/MemberUserSyncer.ts +5 -5
- package/src/helpers/StripeHelper.ts +83 -40
- package/src/helpers/StripePayoutChecker.ts +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"node-rsa": "1.1.1",
|
|
49
49
|
"openid-client": "^5.4.0",
|
|
50
50
|
"postmark": "4.0.2",
|
|
51
|
-
"stripe": "^
|
|
51
|
+
"stripe": "^16.6.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "7a3f9f6c08058dc8b671befbfad73184afdc6d7c"
|
|
54
54
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AutoEncoderPatchType, Decoder, PatchableArrayAutoEncoder, PatchableArrayDecoder, patchObject, StringDecoder } from '@simonbackx/simple-encoding';
|
|
2
2
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
3
|
-
import { Event, Organization, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
3
|
+
import { Event, Group, Organization, Platform, RegistrationPeriod } from '@stamhoofd/models';
|
|
4
4
|
import { Event as EventStruct, GroupType, PermissionLevel } from "@stamhoofd/structures";
|
|
5
5
|
|
|
6
6
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
@@ -80,6 +80,12 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
80
80
|
event.startDate = put.startDate
|
|
81
81
|
event.endDate = put.endDate
|
|
82
82
|
event.meta = put.meta
|
|
83
|
+
event.typeId = await PatchEventsEndpoint.validateEventType(put.typeId)
|
|
84
|
+
await PatchEventsEndpoint.checkEventLimits(event)
|
|
85
|
+
|
|
86
|
+
if (!(await Context.auth.canAccessEvent(event, PermissionLevel.Full))) {
|
|
87
|
+
throw Context.auth.error()
|
|
88
|
+
}
|
|
83
89
|
|
|
84
90
|
if (put.group) {
|
|
85
91
|
const period = await RegistrationPeriod.getByDate(event.startDate)
|
|
@@ -99,14 +105,8 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
99
105
|
put.group.organizationId,
|
|
100
106
|
period.id
|
|
101
107
|
)
|
|
108
|
+
await event.syncGroupRequirements(group)
|
|
102
109
|
event.groupId = group.id
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
event.typeId = await PatchEventsEndpoint.validateEventType(put.typeId)
|
|
106
|
-
await PatchEventsEndpoint.checkEventLimits(event)
|
|
107
|
-
|
|
108
|
-
if (!(await Context.auth.canAccessEvent(event, PermissionLevel.Full))) {
|
|
109
|
-
throw Context.auth.error()
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
await event.save()
|
|
@@ -130,7 +130,6 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
130
130
|
event.endDate = patch.endDate ?? event.endDate
|
|
131
131
|
event.meta = patchObject(event.meta, patch.meta)
|
|
132
132
|
|
|
133
|
-
|
|
134
133
|
if (patch.organizationId !== undefined) {
|
|
135
134
|
if (organization?.id && patch.organizationId !== organization.id) {
|
|
136
135
|
throw new SimpleError({
|
|
@@ -210,6 +209,14 @@ export class PatchEventsEndpoint extends Endpoint<Params, Query, Body, ResponseB
|
|
|
210
209
|
}
|
|
211
210
|
|
|
212
211
|
await event.save()
|
|
212
|
+
|
|
213
|
+
if (event.groupId) {
|
|
214
|
+
const group = await Group.getByID(event.groupId)
|
|
215
|
+
if (group) {
|
|
216
|
+
await event.syncGroupRequirements(group)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
213
220
|
events.push(event)
|
|
214
221
|
}
|
|
215
222
|
|
|
@@ -157,6 +157,15 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
157
157
|
.where(
|
|
158
158
|
SQL.column('memberId'),
|
|
159
159
|
SQL.column('members', 'id'),
|
|
160
|
+
).whereNot(
|
|
161
|
+
SQL.column('registeredAt'),
|
|
162
|
+
null,
|
|
163
|
+
).where(
|
|
164
|
+
SQL.column('deactivatedAt'),
|
|
165
|
+
null,
|
|
166
|
+
).where(
|
|
167
|
+
SQL.column('groups', 'deletedAt'),
|
|
168
|
+
null
|
|
160
169
|
),
|
|
161
170
|
{
|
|
162
171
|
...registrationFilterCompilers,
|
|
@@ -245,9 +254,12 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
245
254
|
).whereNot(
|
|
246
255
|
SQL.column('registeredAt'),
|
|
247
256
|
null,
|
|
248
|
-
).
|
|
249
|
-
SQL.column('
|
|
250
|
-
|
|
257
|
+
).where(
|
|
258
|
+
SQL.column('deactivatedAt'),
|
|
259
|
+
null,
|
|
260
|
+
).where(
|
|
261
|
+
SQL.column('groups', 'deletedAt'),
|
|
262
|
+
null
|
|
251
263
|
),
|
|
252
264
|
registrationFilterCompilers
|
|
253
265
|
),
|
|
@@ -276,9 +288,9 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
276
288
|
).whereNot(
|
|
277
289
|
SQL.column('registeredAt'),
|
|
278
290
|
null,
|
|
279
|
-
).
|
|
280
|
-
SQL.column('groups', '
|
|
281
|
-
|
|
291
|
+
).where(
|
|
292
|
+
SQL.column('groups', 'deletedAt'),
|
|
293
|
+
null
|
|
282
294
|
),
|
|
283
295
|
organizationFilterCompilers
|
|
284
296
|
),
|
|
@@ -18,6 +18,12 @@ class Body extends AutoEncoder {
|
|
|
18
18
|
@field({ decoder: StringDecoder })
|
|
19
19
|
id: string
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Events for direct charges
|
|
23
|
+
*/
|
|
24
|
+
@field({ decoder: StringDecoder, nullable: true, optional: true })
|
|
25
|
+
account: string|null = null
|
|
26
|
+
|
|
21
27
|
@field({ decoder: AnyDecoder })
|
|
22
28
|
data: any
|
|
23
29
|
}
|
|
@@ -6,7 +6,7 @@ import { SimpleError } from '@simonbackx/simple-errors';
|
|
|
6
6
|
import { I18n } from '@stamhoofd/backend-i18n';
|
|
7
7
|
import { Email } from '@stamhoofd/email';
|
|
8
8
|
import { BalanceItem, BalanceItemPayment, Group, Member, MemberWithRegistrations, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, Platform, RateLimiter, Registration, User } from '@stamhoofd/models';
|
|
9
|
-
import { BalanceItemStatus, IDRegisterCheckout, MemberBalanceItem,
|
|
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';
|
|
@@ -99,7 +99,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const memberIds = Formatter.uniqueArray(
|
|
102
|
+
const memberIds = Formatter.uniqueArray(
|
|
103
|
+
[...request.body.cart.items.map(i => i.memberId), ...request.body.cart.deleteRegistrations.map(i => i.member.id)]
|
|
104
|
+
)
|
|
103
105
|
const members = await Member.getBlobByIds(...memberIds)
|
|
104
106
|
const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
|
|
105
107
|
const groups = await Group.getByIDs(...groupIds)
|
|
@@ -141,11 +143,19 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
141
143
|
platformMembers.push(...family.members)
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
const organizationStruct = await AuthenticatedStructures.organization(organization)
|
|
144
147
|
const checkout = request.body.hydrate({
|
|
145
148
|
members: platformMembers,
|
|
146
149
|
groups: await AuthenticatedStructures.groups(groups),
|
|
147
|
-
organizations: [
|
|
150
|
+
organizations: [organizationStruct]
|
|
148
151
|
})
|
|
152
|
+
|
|
153
|
+
// Set circular references
|
|
154
|
+
for (const member of platformMembers) {
|
|
155
|
+
member.family.checkout = checkout
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
checkout.setDefaultOrganization(organizationStruct)
|
|
149
159
|
|
|
150
160
|
const registrations: RegistrationWithMemberAndGroup[] = []
|
|
151
161
|
const payRegistrations: {registration: RegistrationWithMemberAndGroup, item: RegisterItem}[] = []
|
|
@@ -178,6 +188,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
178
188
|
memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
|
|
179
189
|
}
|
|
180
190
|
|
|
191
|
+
console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
|
|
192
|
+
|
|
181
193
|
// Validate the cart
|
|
182
194
|
checkout.validate({memberBalanceItems})
|
|
183
195
|
|
|
@@ -230,7 +242,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
230
242
|
.setRelation(Registration.group, group)
|
|
231
243
|
|
|
232
244
|
|
|
233
|
-
if (existingRegistration.registeredAt !== null) {
|
|
245
|
+
if (existingRegistration.registeredAt !== null && existingRegistration.deactivatedAt === null) {
|
|
234
246
|
throw new SimpleError({
|
|
235
247
|
code: "already_registered",
|
|
236
248
|
message: "Dit lid is reeds ingeschreven. Herlaad de pagina en probeer opnieuw."
|
|
@@ -260,18 +272,18 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
// Who is going to pay?
|
|
263
|
-
let
|
|
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
|
|
264
276
|
|
|
265
277
|
if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
|
|
266
278
|
// Will get added to the outstanding amount of the member
|
|
267
|
-
|
|
279
|
+
whoWillPayNow = 'nobody'
|
|
268
280
|
} else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
|
|
269
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
|
|
270
|
-
|
|
282
|
+
whoWillPayNow = 'organization'
|
|
271
283
|
}
|
|
272
284
|
|
|
273
285
|
// Validate payment method
|
|
274
|
-
if (totalPrice > 0 &&
|
|
286
|
+
if (totalPrice > 0 && whoWillPayNow !== 'nobody') {
|
|
275
287
|
const allowedPaymentMethods = organization.meta.registrationPaymentConfiguration.paymentMethods
|
|
276
288
|
|
|
277
289
|
if (!checkout.paymentMethod || !allowedPaymentMethods.includes(checkout.paymentMethod)) {
|
|
@@ -292,10 +304,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
292
304
|
checkout.paymentMethod = PaymentMethod.Unknown
|
|
293
305
|
}
|
|
294
306
|
|
|
295
|
-
console.log('Registering members using
|
|
307
|
+
console.log('Registering members using whoWillPayNow', whoWillPayNow, checkout.paymentMethod, totalPrice)
|
|
296
308
|
|
|
297
309
|
const items: BalanceItem[] = []
|
|
298
|
-
const shouldMarkValid =
|
|
310
|
+
const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale
|
|
299
311
|
|
|
300
312
|
// Save registrations and add extra data if needed
|
|
301
313
|
for (const bundle of payRegistrations) {
|
|
@@ -330,7 +342,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
330
342
|
|
|
331
343
|
// Who is responsible for payment?
|
|
332
344
|
let balanceItem2: BalanceItem | null = null
|
|
333
|
-
if (
|
|
345
|
+
if (whoWillPayNow === 'organization' && request.body.asOrganizationId) {
|
|
334
346
|
// Create a separate balance item for this meber to pay back the paying organization
|
|
335
347
|
// this is not yet associated with a payment but will be added to the outstanding balance of the member
|
|
336
348
|
|
|
@@ -391,7 +403,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
391
403
|
items.push(balanceItem)
|
|
392
404
|
}
|
|
393
405
|
|
|
394
|
-
if (checkout.administrationFee &&
|
|
406
|
+
if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
|
|
395
407
|
// Create balance item
|
|
396
408
|
const balanceItem = new BalanceItem();
|
|
397
409
|
balanceItem.price = checkout.administrationFee
|
|
@@ -415,14 +427,67 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
415
427
|
items.push(balanceItem)
|
|
416
428
|
}
|
|
417
429
|
|
|
418
|
-
if (checkout.cart.balanceItems.length &&
|
|
419
|
-
throw new Error('Not possible to pay balance items when
|
|
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
|
+
}
|
|
420
485
|
}
|
|
421
486
|
|
|
422
487
|
let paymentUrl: string | null = null
|
|
423
488
|
let payment: Payment | null = null
|
|
424
489
|
|
|
425
|
-
if (
|
|
490
|
+
if (whoWillPayNow !== 'nobody') {
|
|
426
491
|
const mappedBalanceItems = new Map<BalanceItem, number>()
|
|
427
492
|
|
|
428
493
|
for (const item of items) {
|
|
@@ -452,11 +517,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
452
517
|
}
|
|
453
518
|
}
|
|
454
519
|
|
|
455
|
-
await
|
|
520
|
+
await BalanceItem.updateOutstanding(items, organization.id)
|
|
456
521
|
|
|
457
522
|
// Update occupancy
|
|
458
523
|
for (const group of groups) {
|
|
459
|
-
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)) {
|
|
460
525
|
await group.updateOccupancy()
|
|
461
526
|
await group.save()
|
|
462
527
|
}
|
|
@@ -501,10 +566,12 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
501
566
|
}
|
|
502
567
|
|
|
503
568
|
if (totalPrice < 0) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
// })
|
|
508
575
|
}
|
|
509
576
|
|
|
510
577
|
if (totalPrice === 0) {
|
|
@@ -595,11 +662,14 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
595
662
|
}
|
|
596
663
|
|
|
597
664
|
const _redirectUrl = new URL(checkout.redirectUrl)
|
|
598
|
-
_redirectUrl.searchParams.set('
|
|
599
|
-
|
|
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
|
+
|
|
600
668
|
const _cancelUrl = new URL(checkout.cancelUrl)
|
|
601
|
-
_cancelUrl.searchParams.set('
|
|
602
|
-
|
|
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
|
+
|
|
603
673
|
const redirectUrl = _redirectUrl.href
|
|
604
674
|
const cancelUrl = _cancelUrl.href
|
|
605
675
|
|
|
@@ -132,6 +132,7 @@ export class PatchOrganizationRegistrationPeriodsEndpoint extends Endpoint<Param
|
|
|
132
132
|
|
|
133
133
|
for (const groupPut of patch.groups.getPuts()) {
|
|
134
134
|
await PatchOrganizationRegistrationPeriodsEndpoint.createGroup(groupPut.put, organization.id, organizationPeriod.periodId, {allowedIds})
|
|
135
|
+
deleteUnreachable = true
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
for (const struct of patch.groups.getPatches()) {
|
|
@@ -46,17 +46,22 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
46
46
|
})
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const type =
|
|
49
|
+
const type = STAMHOOFD.STRIPE_CONNECT_METHOD
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
country: organization.address.country,
|
|
53
|
-
// Problem: we cannot set company or business_type, because then it defaults the structure of the company to one that requires a company number
|
|
51
|
+
const sharedData: Stripe.AccountCreateParams = {
|
|
54
52
|
capabilities: {
|
|
55
53
|
card_payments: { requested: true },
|
|
56
54
|
transfers: { requested: true },
|
|
57
55
|
bancontact_payments: { requested: true },
|
|
58
56
|
ideal_payments: { requested: true },
|
|
59
57
|
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let expressData: Stripe.AccountCreateParams = {
|
|
61
|
+
country: organization.address.country,
|
|
62
|
+
controller: {
|
|
63
|
+
requirement_collection: 'application',
|
|
64
|
+
},
|
|
60
65
|
settings: {
|
|
61
66
|
payouts: {
|
|
62
67
|
schedule: {
|
|
@@ -76,6 +81,7 @@ export class ConnectMollieEndpoint extends Endpoint<Params, Query, Body, Respons
|
|
|
76
81
|
const stripe = StripeHelper.getInstance()
|
|
77
82
|
const account = await stripe.accounts.create({
|
|
78
83
|
type,
|
|
84
|
+
...sharedData,
|
|
79
85
|
...expressData
|
|
80
86
|
});
|
|
81
87
|
|
|
@@ -68,7 +68,10 @@ export class GetStripeAccountLinkEndpoint extends Endpoint<Params, Query, Body,
|
|
|
68
68
|
refresh_url: request.body.refreshUrl,
|
|
69
69
|
return_url: request.body.returnUrl,
|
|
70
70
|
type: 'account_onboarding',
|
|
71
|
-
|
|
71
|
+
collection_options: {
|
|
72
|
+
fields: 'eventually_due',
|
|
73
|
+
future_requirements: 'include'
|
|
74
|
+
}
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
return new Response(ResponseBody.create({
|
|
@@ -59,6 +59,12 @@ export class GetStripeLoginLinkEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
59
59
|
})
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
if (model.meta.type === 'standard') {
|
|
63
|
+
return new Response(ResponseBody.create({
|
|
64
|
+
url: 'https://dashboard.stripe.com/'
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
const stripe = StripeHelper.getInstance()
|
|
63
69
|
const accountLink = await stripe.accounts.createLoginLink(model.accountId);
|
|
64
70
|
|
|
@@ -243,13 +243,18 @@ export class PatchWebshopOrdersEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
243
243
|
// Update balance item prices for this order if price has changed
|
|
244
244
|
if (previousToPay !== model.totalToPay) {
|
|
245
245
|
const items = await BalanceItem.where({ orderId: model.id })
|
|
246
|
-
if (items.length
|
|
246
|
+
if (items.length >= 1) {
|
|
247
247
|
model.markUpdated()
|
|
248
248
|
items[0].price = model.totalToPay
|
|
249
249
|
items[0].description = model.generateBalanceDescription(webshop)
|
|
250
250
|
items[0].updateStatus();
|
|
251
251
|
await items[0].save()
|
|
252
|
-
|
|
252
|
+
|
|
253
|
+
// Zero out the other items
|
|
254
|
+
const otherItems = items.slice(1)
|
|
255
|
+
await BalanceItem.deleteItems(otherItems)
|
|
256
|
+
} else if (items.length === 0
|
|
257
|
+
&& model.totalToPay > 0) {
|
|
253
258
|
model.markUpdated()
|
|
254
259
|
const balanceItem = new BalanceItem();
|
|
255
260
|
balanceItem.orderId = model.id;
|
|
@@ -2,11 +2,11 @@ import { createMollieClient } from '@mollie/api-client';
|
|
|
2
2
|
import { AutoEncoder, BooleanDecoder, Decoder, field } from '@simonbackx/simple-encoding';
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from "@simonbackx/simple-endpoints";
|
|
4
4
|
import { SimpleError } from "@simonbackx/simple-errors";
|
|
5
|
-
import { BalanceItem, BalanceItemPayment,
|
|
5
|
+
import { BalanceItem, BalanceItemPayment, MolliePayment, MollieToken, Organization, PayconiqPayment, Payment, STPendingInvoice } from '@stamhoofd/models';
|
|
6
6
|
import { QueueHandler } from '@stamhoofd/queues';
|
|
7
|
-
import {
|
|
8
|
-
import { Formatter } from '@stamhoofd/utility';
|
|
7
|
+
import { PaymentGeneral, PaymentMethod, PaymentMethodHelper, PaymentProvider, PaymentStatus, STInvoiceItem } from "@stamhoofd/structures";
|
|
9
8
|
|
|
9
|
+
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
10
10
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
11
11
|
import { Context } from '../../../helpers/Context';
|
|
12
12
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
@@ -27,7 +27,7 @@ class Query extends AutoEncoder {
|
|
|
27
27
|
cancel = false
|
|
28
28
|
}
|
|
29
29
|
type Body = undefined
|
|
30
|
-
type ResponseBody =
|
|
30
|
+
type ResponseBody = PaymentGeneral | undefined
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* One endpoint to create, patch and delete groups. Usefull because on organization setup, we need to create multiple groups at once. Also, sometimes we need to link values and update multiple groups at once
|
|
@@ -51,6 +51,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
51
51
|
|
|
52
52
|
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
53
53
|
const organization = await Context.setOrganizationScope()
|
|
54
|
+
if (!request.query.exchange) {
|
|
55
|
+
await Context.authenticate()
|
|
56
|
+
}
|
|
54
57
|
|
|
55
58
|
// Not method on payment because circular references (not supprted in ts)
|
|
56
59
|
const payment = await ExchangePaymentEndpoint.pollStatus(request.params.id, organization, request.query.cancel)
|
|
@@ -66,28 +69,9 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
return new Response(
|
|
69
|
-
|
|
70
|
-
id: payment.id,
|
|
71
|
-
method: payment.method,
|
|
72
|
-
provider: payment.provider,
|
|
73
|
-
status: payment.status,
|
|
74
|
-
price: payment.price,
|
|
75
|
-
transferDescription: payment.transferDescription,
|
|
76
|
-
paidAt: payment.paidAt,
|
|
77
|
-
createdAt: payment.createdAt,
|
|
78
|
-
updatedAt: payment.updatedAt
|
|
79
|
-
})
|
|
72
|
+
await AuthenticatedStructures.paymentGeneral(payment, true)
|
|
80
73
|
);
|
|
81
74
|
}
|
|
82
|
-
|
|
83
|
-
static async updateOutstanding(items: BalanceItem[], organizationId: string) {
|
|
84
|
-
// Update outstanding amount of related members and registrations
|
|
85
|
-
const memberIds: string[] = Formatter.uniqueArray(items.map(p => p.memberId).filter(id => id !== null)) as any
|
|
86
|
-
await Member.updateOutstandingBalance(memberIds)
|
|
87
|
-
|
|
88
|
-
const registrationIds: string[] = Formatter.uniqueArray(items.map(p => p.registrationId).filter(id => id !== null)) as any
|
|
89
|
-
await Registration.updateOutstandingBalance(registrationIds, organizationId)
|
|
90
|
-
}
|
|
91
75
|
|
|
92
76
|
static async handlePaymentStatusUpdate(payment: Payment, organization: Organization, status: PaymentStatus) {
|
|
93
77
|
if (payment.status === status) {
|
|
@@ -110,7 +94,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
110
94
|
await balanceItemPayment.markPaid(organization);
|
|
111
95
|
}
|
|
112
96
|
|
|
113
|
-
await
|
|
97
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
114
98
|
})
|
|
115
99
|
|
|
116
100
|
if (!wasPaid && payment.provider === PaymentProvider.Buckaroo && payment.method) {
|
|
@@ -153,7 +137,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
153
137
|
await balanceItemPayment.undoPaid(organization);
|
|
154
138
|
}
|
|
155
139
|
|
|
156
|
-
await
|
|
140
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
157
141
|
})
|
|
158
142
|
}
|
|
159
143
|
|
|
@@ -167,7 +151,7 @@ export class ExchangePaymentEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
167
151
|
await balanceItemPayment.markFailed(organization);
|
|
168
152
|
}
|
|
169
153
|
|
|
170
|
-
await
|
|
154
|
+
await BalanceItem.updateOutstanding(balanceItemPayments.map(p => p.balanceItem), organization.id)
|
|
171
155
|
})
|
|
172
156
|
}
|
|
173
157
|
|
|
@@ -366,8 +366,7 @@ export class AdminPermissionChecker {
|
|
|
366
366
|
permissionLevel: PermissionLevel = PermissionLevel.Read,
|
|
367
367
|
data?: {
|
|
368
368
|
registrations: Registration[],
|
|
369
|
-
orders: Order[]
|
|
370
|
-
members: Member[]
|
|
369
|
+
orders: Order[]
|
|
371
370
|
}
|
|
372
371
|
): Promise<boolean> {
|
|
373
372
|
for (const balanceItem of balanceItems) {
|
|
@@ -403,7 +402,7 @@ export class AdminPermissionChecker {
|
|
|
403
402
|
}
|
|
404
403
|
|
|
405
404
|
// Slight optimization possible here
|
|
406
|
-
const {registrations, orders
|
|
405
|
+
const {registrations, orders} = data ?? (this.user.permissions || permissionLevel === PermissionLevel.Read) ? (await Payment.loadBalanceItemRelations(balanceItems)) : {registrations: [], orders: []}
|
|
407
406
|
|
|
408
407
|
if (this.user.permissions) {
|
|
409
408
|
// We grant permission for a whole payment when the user has at least permission for a part of that payment.
|
|
@@ -431,7 +430,7 @@ export class AdminPermissionChecker {
|
|
|
431
430
|
// Check members
|
|
432
431
|
const userMembers = await Member.getMembersWithRegistrationForUser(this.user)
|
|
433
432
|
for (const member of userMembers) {
|
|
434
|
-
if (
|
|
433
|
+
if (balanceItems.find(m => m.memberId === member.id)) {
|
|
435
434
|
return true;
|
|
436
435
|
}
|
|
437
436
|
}
|
|
@@ -26,11 +26,11 @@ export class AuthenticatedStructures {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const {balanceItemPayments, balanceItems} = await Payment.loadBalanceItems(payments)
|
|
29
|
-
const {registrations, orders,
|
|
29
|
+
const {registrations, orders, groups} = await Payment.loadBalanceItemRelations(balanceItems);
|
|
30
30
|
|
|
31
31
|
if (checkPermissions) {
|
|
32
32
|
// Note: permission checking is moved here for performacne to avoid loading the data multiple times
|
|
33
|
-
if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders
|
|
33
|
+
if (!(await Context.auth.canAccessBalanceItems(balanceItems, PermissionLevel.Read, {registrations, orders}))) {
|
|
34
34
|
throw new SimpleError({
|
|
35
35
|
code: "not_found",
|
|
36
36
|
message: "Payment not found",
|
|
@@ -41,13 +41,12 @@ export class AuthenticatedStructures {
|
|
|
41
41
|
|
|
42
42
|
const includeSettlements = checkPermissions && !!Context.user && !!Context.user.permissions
|
|
43
43
|
|
|
44
|
-
return Payment.getGeneralStructureFromRelations({
|
|
44
|
+
return await Payment.getGeneralStructureFromRelations({
|
|
45
45
|
payments,
|
|
46
46
|
balanceItemPayments,
|
|
47
47
|
balanceItems,
|
|
48
48
|
registrations,
|
|
49
49
|
orders,
|
|
50
|
-
members,
|
|
51
50
|
groups
|
|
52
51
|
}, includeSettlements)
|
|
53
52
|
}
|
|
@@ -15,7 +15,8 @@ export class MemberUserSyncerStatic {
|
|
|
15
15
|
userEmails.push(member.details.email)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const uncategorizedEmails: string[] = member.details.uncategorizedEmails;
|
|
19
|
+
const parentAndUncategorizedEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(uncategorizedEmails) : []
|
|
19
20
|
|
|
20
21
|
// Make sure all these users have access to the member
|
|
21
22
|
for (const email of userEmails) {
|
|
@@ -23,18 +24,17 @@ export class MemberUserSyncerStatic {
|
|
|
23
24
|
await this.linkUser(email, member, false)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
for (const email of
|
|
27
|
-
// Link parents
|
|
27
|
+
for (const email of parentAndUncategorizedEmails) {
|
|
28
|
+
// Link parents and uncategorized emails
|
|
28
29
|
await this.linkUser(email, member, true)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// Remove access of users that are not in this list
|
|
32
33
|
for (const user of member.users) {
|
|
33
|
-
if (!userEmails.includes(user.email) && !
|
|
34
|
+
if (!userEmails.includes(user.email) && !parentAndUncategorizedEmails.includes(user.email)) {
|
|
34
35
|
await this.unlinkUser(user, member)
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async onDeleteMember(member: MemberWithRegistrations) {
|
|
@@ -6,8 +6,46 @@ import { Formatter } from '@stamhoofd/utility';
|
|
|
6
6
|
import Stripe from 'stripe';
|
|
7
7
|
|
|
8
8
|
export class StripeHelper {
|
|
9
|
-
static getInstance() {
|
|
10
|
-
return new Stripe(STAMHOOFD.STRIPE_SECRET_KEY, {apiVersion: '
|
|
9
|
+
static getInstance(accountId: string|null = null) {
|
|
10
|
+
return new Stripe(STAMHOOFD.STRIPE_SECRET_KEY, {apiVersion: '2024-06-20', typescript: true, maxNetworkRetries: 0, timeout: 10000, stripeAccount: accountId ?? undefined});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static async saveChargeInfo(model: StripePaymentIntent|StripeCheckoutSession, charge: Stripe.Charge, payment: Payment) {
|
|
14
|
+
try {
|
|
15
|
+
if (model.accountId) {
|
|
16
|
+
// This is a direct charge
|
|
17
|
+
|
|
18
|
+
if (charge.balance_transaction !== null && typeof charge.balance_transaction !== 'string') {
|
|
19
|
+
const fees = charge.balance_transaction.fee;
|
|
20
|
+
payment.transferFee = fees;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (charge.billing_details.name) {
|
|
25
|
+
payment.ibanName = charge.billing_details.name
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (charge.payment_method_details?.bancontact) {
|
|
29
|
+
if (charge.payment_method_details.bancontact.iban_last4) {
|
|
30
|
+
payment.iban = "xxxx " + charge.payment_method_details.bancontact.iban_last4
|
|
31
|
+
}
|
|
32
|
+
payment.ibanName = charge.payment_method_details.bancontact.verified_name
|
|
33
|
+
}
|
|
34
|
+
if (charge.payment_method_details?.ideal) {
|
|
35
|
+
if (charge.payment_method_details.ideal.iban_last4) {
|
|
36
|
+
payment.iban = "xxxx " + charge.payment_method_details.ideal.iban_last4
|
|
37
|
+
}
|
|
38
|
+
payment.ibanName = charge.payment_method_details.ideal.verified_name
|
|
39
|
+
}
|
|
40
|
+
if (charge.payment_method_details?.card) {
|
|
41
|
+
if (charge.payment_method_details.card.last4) {
|
|
42
|
+
payment.iban = "xxxx " + charge.payment_method_details.card.last4
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
await payment.save()
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.error('Failed processing charge', e)
|
|
48
|
+
}
|
|
11
49
|
}
|
|
12
50
|
|
|
13
51
|
static async getStatus(payment: Payment, cancel = false, testMode = false): Promise<PaymentStatus> {
|
|
@@ -22,37 +60,15 @@ export class StripeHelper {
|
|
|
22
60
|
return await this.getStatusFromCheckoutSession(payment, cancel)
|
|
23
61
|
}
|
|
24
62
|
|
|
25
|
-
const stripe = this.getInstance()
|
|
63
|
+
const stripe = this.getInstance(model.accountId)
|
|
26
64
|
|
|
27
|
-
let intent = await stripe.paymentIntents.retrieve(model.stripeIntentId
|
|
65
|
+
let intent = await stripe.paymentIntents.retrieve(model.stripeIntentId, {
|
|
66
|
+
expand: ['latest_charge.balance_transaction']
|
|
67
|
+
})
|
|
28
68
|
console.log(intent);
|
|
29
69
|
if (intent.status === "succeeded") {
|
|
30
|
-
if (intent.latest_charge) {
|
|
31
|
-
|
|
32
|
-
const charge = await stripe.charges.retrieve(typeof intent.latest_charge === 'string' ? intent.latest_charge : intent.latest_charge.id)
|
|
33
|
-
if (charge.payment_method_details?.bancontact) {
|
|
34
|
-
if (charge.payment_method_details.bancontact.iban_last4) {
|
|
35
|
-
payment.iban = "xxxx " + charge.payment_method_details.bancontact.iban_last4
|
|
36
|
-
}
|
|
37
|
-
payment.ibanName = charge.payment_method_details.bancontact.verified_name
|
|
38
|
-
await payment.save()
|
|
39
|
-
}
|
|
40
|
-
if (charge.payment_method_details?.ideal) {
|
|
41
|
-
if (charge.payment_method_details.ideal.iban_last4) {
|
|
42
|
-
payment.iban = "xxxx " + charge.payment_method_details.ideal.iban_last4
|
|
43
|
-
}
|
|
44
|
-
payment.ibanName = charge.payment_method_details.ideal.verified_name
|
|
45
|
-
await payment.save()
|
|
46
|
-
}
|
|
47
|
-
if (charge.payment_method_details?.card) {
|
|
48
|
-
if (charge.payment_method_details.card.last4) {
|
|
49
|
-
payment.iban = "xxxx " + charge.payment_method_details.card.last4
|
|
50
|
-
}
|
|
51
|
-
await payment.save()
|
|
52
|
-
}
|
|
53
|
-
} catch (e) {
|
|
54
|
-
console.error('Failed fatching charge', e)
|
|
55
|
-
}
|
|
70
|
+
if (intent.latest_charge !== null && typeof intent.latest_charge !== 'string') {
|
|
71
|
+
await this.saveChargeInfo(model, intent.latest_charge, payment)
|
|
56
72
|
}
|
|
57
73
|
return PaymentStatus.Succeeded
|
|
58
74
|
}
|
|
@@ -93,10 +109,22 @@ export class StripeHelper {
|
|
|
93
109
|
return PaymentStatus.Failed
|
|
94
110
|
}
|
|
95
111
|
|
|
96
|
-
const stripe = this.getInstance()
|
|
97
|
-
const session = await stripe.checkout.sessions.retrieve(model.stripeSessionId
|
|
112
|
+
const stripe = this.getInstance(model.accountId)
|
|
113
|
+
const session = await stripe.checkout.sessions.retrieve(model.stripeSessionId, {
|
|
114
|
+
expand: ['payment_intent.latest_charge.balance_transaction']
|
|
115
|
+
})
|
|
116
|
+
|
|
98
117
|
console.log("session", session);
|
|
118
|
+
|
|
99
119
|
if (session.status === "complete") {
|
|
120
|
+
// This is a direct charge
|
|
121
|
+
const payment_intent = session.payment_intent
|
|
122
|
+
if (payment_intent !== null && typeof payment_intent !== 'string') {
|
|
123
|
+
const charge = payment_intent.latest_charge
|
|
124
|
+
if (charge !== null && typeof charge !== 'string') {
|
|
125
|
+
await this.saveChargeInfo(model, charge, payment)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
100
128
|
return PaymentStatus.Succeeded
|
|
101
129
|
}
|
|
102
130
|
if (session.status === "expired") {
|
|
@@ -145,6 +173,7 @@ export class StripeHelper {
|
|
|
145
173
|
const totalPrice = payment.price;
|
|
146
174
|
|
|
147
175
|
let fee = 0;
|
|
176
|
+
let directCharge = false;
|
|
148
177
|
const vat = calculateVATPercentage(organization.address, organization.meta.VATNumber)
|
|
149
178
|
function calculateFee(fixed: number, percentageTimes100: number) {
|
|
150
179
|
return Math.round(Math.round(fixed + Math.max(1, totalPrice * percentageTimes100 / 100 / 100)) * (100 + vat) / 100); // € 0,21 + 0,2%
|
|
@@ -158,6 +187,12 @@ export class StripeHelper {
|
|
|
158
187
|
fee = calculateFee(25, 150); // € 0,25 + 1,5%
|
|
159
188
|
}
|
|
160
189
|
|
|
190
|
+
if (stripeAccount.meta.type === 'standard') {
|
|
191
|
+
// Submerchant is charged by Stripe for the fees directly
|
|
192
|
+
fee = 0;
|
|
193
|
+
directCharge = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
161
196
|
payment.transferFee = fee;
|
|
162
197
|
|
|
163
198
|
const fullMetadata = {
|
|
@@ -165,7 +200,7 @@ export class StripeHelper {
|
|
|
165
200
|
organizationVATNumber: organization.meta.VATNumber
|
|
166
201
|
}
|
|
167
202
|
|
|
168
|
-
const stripe = StripeHelper.getInstance()
|
|
203
|
+
const stripe = StripeHelper.getInstance(directCharge ? stripeAccount.accountId : null)
|
|
169
204
|
let paymentUrl: string
|
|
170
205
|
|
|
171
206
|
// Bancontact or iDEAL: use payment intends
|
|
@@ -185,12 +220,12 @@ export class StripeHelper {
|
|
|
185
220
|
payment_method_types: [payment.method.toLowerCase()],
|
|
186
221
|
statement_descriptor: Formatter.slug(statementDescriptor).substring(0, 22).toUpperCase(),
|
|
187
222
|
application_fee_amount: fee,
|
|
188
|
-
on_behalf_of: stripeAccount.accountId,
|
|
223
|
+
on_behalf_of: !directCharge ? stripeAccount.accountId : undefined,
|
|
189
224
|
confirm: true,
|
|
190
225
|
return_url: redirectUrl,
|
|
191
|
-
transfer_data: {
|
|
226
|
+
transfer_data: !directCharge ? {
|
|
192
227
|
destination: stripeAccount.accountId,
|
|
193
|
-
},
|
|
228
|
+
} : undefined,
|
|
194
229
|
metadata: fullMetadata,
|
|
195
230
|
payment_method_options: {bancontact: {preferred_language: ['nl', 'fr', 'de', 'en'].includes(i18n.language) ? i18n.language as 'en' : 'nl'}},
|
|
196
231
|
});
|
|
@@ -213,6 +248,10 @@ export class StripeHelper {
|
|
|
213
248
|
paymentIntentModel.paymentId = payment.id
|
|
214
249
|
paymentIntentModel.stripeIntentId = paymentIntent.id
|
|
215
250
|
paymentIntentModel.organizationId = organization.id
|
|
251
|
+
|
|
252
|
+
if (directCharge) {
|
|
253
|
+
paymentIntentModel.accountId = stripeAccount.accountId
|
|
254
|
+
}
|
|
216
255
|
await paymentIntentModel.save()
|
|
217
256
|
} else {
|
|
218
257
|
// Build Stripe line items
|
|
@@ -253,11 +292,11 @@ export class StripeHelper {
|
|
|
253
292
|
currency: 'eur',
|
|
254
293
|
locale: i18n.language as 'nl',
|
|
255
294
|
payment_intent_data: {
|
|
256
|
-
on_behalf_of: stripeAccount.accountId,
|
|
295
|
+
on_behalf_of: !directCharge ? stripeAccount.accountId : undefined,
|
|
257
296
|
application_fee_amount: fee,
|
|
258
|
-
transfer_data: {
|
|
297
|
+
transfer_data: !directCharge ? {
|
|
259
298
|
destination: stripeAccount.accountId,
|
|
260
|
-
},
|
|
299
|
+
} : undefined,
|
|
261
300
|
metadata: fullMetadata,
|
|
262
301
|
statement_descriptor: Formatter.slug(statementDescriptor).substring(0, 22).toUpperCase(),
|
|
263
302
|
},
|
|
@@ -281,6 +320,10 @@ export class StripeHelper {
|
|
|
281
320
|
paymentIntentModel.paymentId = payment.id
|
|
282
321
|
paymentIntentModel.stripeSessionId = session.id
|
|
283
322
|
paymentIntentModel.organizationId = organization.id
|
|
323
|
+
|
|
324
|
+
if (directCharge) {
|
|
325
|
+
paymentIntentModel.accountId = stripeAccount.accountId
|
|
326
|
+
}
|
|
284
327
|
await paymentIntentModel.save()
|
|
285
328
|
}
|
|
286
329
|
|
|
@@ -290,4 +333,4 @@ export class StripeHelper {
|
|
|
290
333
|
paymentUrl
|
|
291
334
|
}
|
|
292
335
|
}
|
|
293
|
-
}
|
|
336
|
+
}
|
|
@@ -9,7 +9,7 @@ export class StripePayoutChecker {
|
|
|
9
9
|
constructor({secretKey, stripeAccount}: { secretKey: string, stripeAccount?: string}) {
|
|
10
10
|
this.stripe = new Stripe(
|
|
11
11
|
secretKey, {
|
|
12
|
-
apiVersion: '
|
|
12
|
+
apiVersion: '2024-06-20',
|
|
13
13
|
typescript: true,
|
|
14
14
|
maxNetworkRetries: 1,
|
|
15
15
|
timeout: 10000,
|
|
@@ -18,7 +18,7 @@ export class StripePayoutChecker {
|
|
|
18
18
|
|
|
19
19
|
this.stripePlatform = new Stripe(
|
|
20
20
|
secretKey, {
|
|
21
|
-
apiVersion: '
|
|
21
|
+
apiVersion: '2024-06-20',
|
|
22
22
|
typescript: true,
|
|
23
23
|
maxNetworkRetries: 1,
|
|
24
24
|
timeout: 10000
|
|
@@ -147,6 +147,8 @@ export class StripePayoutChecker {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const applicationFee = balanceItem.source.application_fee_amount;
|
|
150
|
+
const otherFees = balanceItem.fee
|
|
151
|
+
const totalFees = otherFees + (applicationFee ?? 0);
|
|
150
152
|
|
|
151
153
|
// Cool, we can store this in the database now.
|
|
152
154
|
|
|
@@ -167,11 +169,11 @@ export class StripePayoutChecker {
|
|
|
167
169
|
settledAt: new Date(payout.arrival_date * 1000),
|
|
168
170
|
amount: payout.amount,
|
|
169
171
|
// Set only if application fee is witheld
|
|
170
|
-
fee:
|
|
172
|
+
fee: totalFees
|
|
171
173
|
});
|
|
172
174
|
|
|
173
175
|
payment.settlement = settlement;
|
|
174
|
-
payment.transferFee =
|
|
176
|
+
payment.transferFee = totalFees;
|
|
175
177
|
|
|
176
178
|
// Force an updatedAt timestamp of the related order
|
|
177
179
|
// Mark order as 'updated', or the frontend won't pull in the updates
|
|
@@ -185,4 +187,4 @@ export class StripePayoutChecker {
|
|
|
185
187
|
await payment.save();
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
}
|
|
190
|
+
}
|