@stamhoofd/backend 2.5.0 → 2.7.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/GetEventsEndpoint.ts +5 -2
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +16 -9
- package/src/endpoints/global/members/GetMembersEndpoint.ts +57 -22
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +94 -27
- 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 +55 -37
- 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.7.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": "319a4de9fac39a31110cddbfa9a580d1b9c8f730"
|
|
54
54
|
}
|
|
@@ -23,7 +23,7 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
23
23
|
startDate: createSQLColumnFilterCompiler('startDate'),
|
|
24
24
|
endDate: createSQLColumnFilterCompiler('endDate'),
|
|
25
25
|
groupIds: createSQLExpressionFilterCompiler(
|
|
26
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.
|
|
26
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.groups[*].id'),
|
|
27
27
|
undefined,
|
|
28
28
|
true,
|
|
29
29
|
true
|
|
@@ -35,7 +35,10 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
35
35
|
true
|
|
36
36
|
),
|
|
37
37
|
organizationTagIds: createSQLExpressionFilterCompiler(
|
|
38
|
-
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds')
|
|
38
|
+
SQL.jsonValue(SQL.column('meta'), '$.value.organizationTagIds'),
|
|
39
|
+
undefined,
|
|
40
|
+
true,
|
|
41
|
+
true
|
|
39
42
|
)
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -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
|
|
|
@@ -3,8 +3,8 @@ import { Decoder } from '@simonbackx/simple-encoding';
|
|
|
3
3
|
import { DecodedRequest, Endpoint, Request, Response } from '@simonbackx/simple-endpoints';
|
|
4
4
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
5
5
|
import { Email, Member, MemberWithRegistrations, Platform } from '@stamhoofd/models';
|
|
6
|
-
import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions,
|
|
7
|
-
import { CountFilteredRequest, EmailRecipientFilterType,
|
|
6
|
+
import { SQL, SQLAge, SQLConcat, SQLFilterDefinitions, SQLOrderBy, SQLOrderByDirection, SQLScalar, SQLSortDefinitions, baseSQLFilterCompilers, compileToSQLFilter, compileToSQLSorter, createSQLColumnFilterCompiler, createSQLExpressionFilterCompiler, createSQLFilterNamespace, createSQLRelationFilterCompiler, joinSQLQuery } from "@stamhoofd/sql";
|
|
7
|
+
import { CountFilteredRequest, EmailRecipientFilterType, LimitedFilteredRequest, MembersBlob, PaginatedResponse, PermissionLevel, StamhoofdFilter, getSortFilter, mergeFilters } from '@stamhoofd/structures';
|
|
8
8
|
import { DataValidator, Formatter } from '@stamhoofd/utility';
|
|
9
9
|
|
|
10
10
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures';
|
|
@@ -36,6 +36,7 @@ Email.recipientLoaders.set(EmailRecipientFilterType.Members, {
|
|
|
36
36
|
return await q.count();
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
|
+
|
|
39
40
|
Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
|
|
40
41
|
fetch: async (query: LimitedFilteredRequest) => {
|
|
41
42
|
const result = await GetMembersEndpoint.buildData(query)
|
|
@@ -54,6 +55,24 @@ Email.recipientLoaders.set(EmailRecipientFilterType.MemberParents, {
|
|
|
54
55
|
}
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
Email.recipientLoaders.set(EmailRecipientFilterType.MemberUnverified, {
|
|
59
|
+
fetch: async (query: LimitedFilteredRequest) => {
|
|
60
|
+
const result = await GetMembersEndpoint.buildData(query)
|
|
61
|
+
|
|
62
|
+
return new PaginatedResponse({
|
|
63
|
+
results: result.results.members.flatMap(m => m.getEmailRecipients(['unverified'])),
|
|
64
|
+
next: result.next
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
count: async (query: LimitedFilteredRequest) => {
|
|
69
|
+
const q = await GetMembersEndpoint.buildQuery(query)
|
|
70
|
+
return await q.sum(
|
|
71
|
+
SQL.jsonLength(SQL.column('details'), '$.value.unverifiedEmails')
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
57
76
|
const registrationFilterCompilers: SQLFilterDefinitions = {
|
|
58
77
|
...baseSQLFilterCompilers,
|
|
59
78
|
"price": createSQLColumnFilterCompiler('price'),
|
|
@@ -157,6 +176,15 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
157
176
|
.where(
|
|
158
177
|
SQL.column('memberId'),
|
|
159
178
|
SQL.column('members', 'id'),
|
|
179
|
+
).whereNot(
|
|
180
|
+
SQL.column('registeredAt'),
|
|
181
|
+
null,
|
|
182
|
+
).where(
|
|
183
|
+
SQL.column('deactivatedAt'),
|
|
184
|
+
null,
|
|
185
|
+
).where(
|
|
186
|
+
SQL.column('groups', 'deletedAt'),
|
|
187
|
+
null
|
|
160
188
|
),
|
|
161
189
|
{
|
|
162
190
|
...registrationFilterCompilers,
|
|
@@ -245,9 +273,12 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
245
273
|
).whereNot(
|
|
246
274
|
SQL.column('registeredAt'),
|
|
247
275
|
null,
|
|
248
|
-
).
|
|
249
|
-
SQL.column('
|
|
250
|
-
|
|
276
|
+
).where(
|
|
277
|
+
SQL.column('deactivatedAt'),
|
|
278
|
+
null,
|
|
279
|
+
).where(
|
|
280
|
+
SQL.column('groups', 'deletedAt'),
|
|
281
|
+
null
|
|
251
282
|
),
|
|
252
283
|
registrationFilterCompilers
|
|
253
284
|
),
|
|
@@ -276,9 +307,9 @@ const filterCompilers: SQLFilterDefinitions = {
|
|
|
276
307
|
).whereNot(
|
|
277
308
|
SQL.column('registeredAt'),
|
|
278
309
|
null,
|
|
279
|
-
).
|
|
280
|
-
SQL.column('groups', '
|
|
281
|
-
|
|
310
|
+
).where(
|
|
311
|
+
SQL.column('groups', 'deletedAt'),
|
|
312
|
+
null
|
|
282
313
|
),
|
|
283
314
|
organizationFilterCompilers
|
|
284
315
|
),
|
|
@@ -366,9 +397,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
366
397
|
}
|
|
367
398
|
},
|
|
368
399
|
periodId: platform.periodId,
|
|
369
|
-
registeredAt: {
|
|
370
|
-
$neq: null
|
|
371
|
-
},
|
|
372
400
|
group: {
|
|
373
401
|
defaultAgeGroupId: {
|
|
374
402
|
$neq: null
|
|
@@ -385,16 +413,26 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
385
413
|
const groups = await Context.auth.getAccessibleGroups(organization.id)
|
|
386
414
|
|
|
387
415
|
if (groups === 'all') {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
416
|
+
if (await Context.auth.hasFullAccess(organization.id)) {
|
|
417
|
+
// Can access full history for now
|
|
418
|
+
scopeFilter = {
|
|
419
|
+
registrations: {
|
|
420
|
+
$elemMatch: {
|
|
421
|
+
organizationId: organization.id,
|
|
394
422
|
}
|
|
395
423
|
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
424
|
+
};
|
|
425
|
+
} else {
|
|
426
|
+
// Can only access current period
|
|
427
|
+
scopeFilter = {
|
|
428
|
+
registrations: {
|
|
429
|
+
$elemMatch: {
|
|
430
|
+
organizationId: organization.id,
|
|
431
|
+
periodId: organization.periodId,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
398
436
|
} else {
|
|
399
437
|
scopeFilter = {
|
|
400
438
|
registrations: {
|
|
@@ -403,9 +441,6 @@ export class GetMembersEndpoint extends Endpoint<Params, Query, Body, ResponseBo
|
|
|
403
441
|
periodId: organization.periodId,
|
|
404
442
|
groupId: {
|
|
405
443
|
$in: groups
|
|
406
|
-
},
|
|
407
|
-
registeredAt: {
|
|
408
|
-
$neq: null
|
|
409
444
|
}
|
|
410
445
|
}
|
|
411
446
|
}
|
|
@@ -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,14 +6,13 @@ 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';
|
|
13
13
|
import { BuckarooHelper } from '../../../helpers/BuckarooHelper';
|
|
14
14
|
import { Context } from '../../../helpers/Context';
|
|
15
15
|
import { StripeHelper } from '../../../helpers/StripeHelper';
|
|
16
|
-
import { ExchangePaymentEndpoint } from '../../organization/shared/ExchangePaymentEndpoint';
|
|
17
16
|
type Params = Record<string, never>;
|
|
18
17
|
type Query = undefined;
|
|
19
18
|
type Body = IDRegisterCheckout
|
|
@@ -99,7 +98,9 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
99
98
|
}
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
const memberIds = Formatter.uniqueArray(
|
|
101
|
+
const memberIds = Formatter.uniqueArray(
|
|
102
|
+
[...request.body.cart.items.map(i => i.memberId), ...request.body.cart.deleteRegistrations.map(i => i.member.id)]
|
|
103
|
+
)
|
|
103
104
|
const members = await Member.getBlobByIds(...memberIds)
|
|
104
105
|
const groupIds = Formatter.uniqueArray(request.body.cart.items.map(i => i.groupId))
|
|
105
106
|
const groups = await Group.getByIDs(...groupIds)
|
|
@@ -141,11 +142,19 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
141
142
|
platformMembers.push(...family.members)
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
const organizationStruct = await AuthenticatedStructures.organization(organization)
|
|
144
146
|
const checkout = request.body.hydrate({
|
|
145
147
|
members: platformMembers,
|
|
146
148
|
groups: await AuthenticatedStructures.groups(groups),
|
|
147
|
-
organizations: [
|
|
149
|
+
organizations: [organizationStruct]
|
|
148
150
|
})
|
|
151
|
+
|
|
152
|
+
// Set circular references
|
|
153
|
+
for (const member of platformMembers) {
|
|
154
|
+
member.family.checkout = checkout
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
checkout.setDefaultOrganization(organizationStruct)
|
|
149
158
|
|
|
150
159
|
const registrations: RegistrationWithMemberAndGroup[] = []
|
|
151
160
|
const payRegistrations: {registration: RegistrationWithMemberAndGroup, item: RegisterItem}[] = []
|
|
@@ -178,6 +187,8 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
178
187
|
memberBalanceItems = await BalanceItem.getMemberStructure(balanceItems)
|
|
179
188
|
}
|
|
180
189
|
|
|
190
|
+
console.log('isAdminFromSameOrganization', checkout.isAdminFromSameOrganization)
|
|
191
|
+
|
|
181
192
|
// Validate the cart
|
|
182
193
|
checkout.validate({memberBalanceItems})
|
|
183
194
|
|
|
@@ -230,7 +241,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
230
241
|
.setRelation(Registration.group, group)
|
|
231
242
|
|
|
232
243
|
|
|
233
|
-
if (existingRegistration.registeredAt !== null) {
|
|
244
|
+
if (existingRegistration.registeredAt !== null && existingRegistration.deactivatedAt === null) {
|
|
234
245
|
throw new SimpleError({
|
|
235
246
|
code: "already_registered",
|
|
236
247
|
message: "Dit lid is reeds ingeschreven. Herlaad de pagina en probeer opnieuw."
|
|
@@ -260,18 +271,18 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
260
271
|
}
|
|
261
272
|
|
|
262
273
|
// Who is going to pay?
|
|
263
|
-
let
|
|
274
|
+
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
275
|
|
|
265
276
|
if (request.body.asOrganizationId && request.body.asOrganizationId === organization.id) {
|
|
266
277
|
// Will get added to the outstanding amount of the member
|
|
267
|
-
|
|
278
|
+
whoWillPayNow = 'nobody'
|
|
268
279
|
} else if (request.body.asOrganizationId && request.body.asOrganizationId !== organization.id) {
|
|
269
280
|
// 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
|
-
|
|
281
|
+
whoWillPayNow = 'organization'
|
|
271
282
|
}
|
|
272
283
|
|
|
273
284
|
// Validate payment method
|
|
274
|
-
if (totalPrice > 0 &&
|
|
285
|
+
if (totalPrice > 0 && whoWillPayNow !== 'nobody') {
|
|
275
286
|
const allowedPaymentMethods = organization.meta.registrationPaymentConfiguration.paymentMethods
|
|
276
287
|
|
|
277
288
|
if (!checkout.paymentMethod || !allowedPaymentMethods.includes(checkout.paymentMethod)) {
|
|
@@ -292,10 +303,10 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
292
303
|
checkout.paymentMethod = PaymentMethod.Unknown
|
|
293
304
|
}
|
|
294
305
|
|
|
295
|
-
console.log('Registering members using
|
|
306
|
+
console.log('Registering members using whoWillPayNow', whoWillPayNow, checkout.paymentMethod, totalPrice)
|
|
296
307
|
|
|
297
308
|
const items: BalanceItem[] = []
|
|
298
|
-
const shouldMarkValid =
|
|
309
|
+
const shouldMarkValid = whoWillPayNow === 'nobody' || checkout.paymentMethod === PaymentMethod.Transfer || checkout.paymentMethod === PaymentMethod.PointOfSale
|
|
299
310
|
|
|
300
311
|
// Save registrations and add extra data if needed
|
|
301
312
|
for (const bundle of payRegistrations) {
|
|
@@ -330,7 +341,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
330
341
|
|
|
331
342
|
// Who is responsible for payment?
|
|
332
343
|
let balanceItem2: BalanceItem | null = null
|
|
333
|
-
if (
|
|
344
|
+
if (whoWillPayNow === 'organization' && request.body.asOrganizationId) {
|
|
334
345
|
// Create a separate balance item for this meber to pay back the paying organization
|
|
335
346
|
// this is not yet associated with a payment but will be added to the outstanding balance of the member
|
|
336
347
|
|
|
@@ -391,7 +402,7 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
391
402
|
items.push(balanceItem)
|
|
392
403
|
}
|
|
393
404
|
|
|
394
|
-
if (checkout.administrationFee &&
|
|
405
|
+
if (checkout.administrationFee && whoWillPayNow !== 'nobody') {
|
|
395
406
|
// Create balance item
|
|
396
407
|
const balanceItem = new BalanceItem();
|
|
397
408
|
balanceItem.price = checkout.administrationFee
|
|
@@ -412,17 +423,68 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
412
423
|
balanceItem.status = shouldMarkValid ? BalanceItemStatus.Pending : BalanceItemStatus.Hidden
|
|
413
424
|
await balanceItem.save();
|
|
414
425
|
|
|
415
|
-
items.push(balanceItem)
|
|
426
|
+
items.push(balanceItem);
|
|
416
427
|
}
|
|
417
428
|
|
|
418
|
-
if (checkout.cart.balanceItems.length &&
|
|
419
|
-
throw new Error('Not possible to pay balance items when
|
|
429
|
+
if (checkout.cart.balanceItems.length && whoWillPayNow === 'nobody') {
|
|
430
|
+
throw new Error('Not possible to pay balance items when whoWillPayNow is nobody')
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Create negative balance items
|
|
434
|
+
for (const registrationStruct of checkout.cart.deleteRegistrations) {
|
|
435
|
+
if (whoWillPayNow !== 'nobody') {
|
|
436
|
+
// this also fixes the issue that we cannot delete the registration right away if we would need to wait for a payment
|
|
437
|
+
throw new SimpleError({
|
|
438
|
+
code: "forbidden",
|
|
439
|
+
message: "Permission denied: you are not allowed to delete registrations",
|
|
440
|
+
human: "Oeps, je hebt geen toestemming om inschrijvingen te verwijderen.",
|
|
441
|
+
statusCode: 403
|
|
442
|
+
})
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const existingRegistration = await Registration.getByID(registrationStruct.id)
|
|
446
|
+
if (!existingRegistration || existingRegistration.organizationId !== organization.id) {
|
|
447
|
+
throw new SimpleError({
|
|
448
|
+
code: "invalid_data",
|
|
449
|
+
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen lijken niet meer te bestaan. Herlaad de pagina en probeer opnieuw."
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (!await Context.auth.canAccessRegistration(existingRegistration, PermissionLevel.Write)) {
|
|
454
|
+
throw new SimpleError({
|
|
455
|
+
code: "forbidden",
|
|
456
|
+
message: "Je hebt geen toegaansrechten om deze inschrijving te verwijderen.",
|
|
457
|
+
statusCode: 403
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (existingRegistration.deactivatedAt || !existingRegistration.registeredAt) {
|
|
462
|
+
throw new SimpleError({
|
|
463
|
+
code: "invalid_data",
|
|
464
|
+
message: "Oeps, één of meerdere inschrijvingen die je probeert te verwijderen was al verwijderd. Herlaad de pagina en probeer opnieuw."
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// We can alter right away since whoWillPayNow is nobody, and shouldMarkValid will always be true
|
|
469
|
+
// Find all balance items of this registration and set them to zero
|
|
470
|
+
await BalanceItem.deleteForDeletedRegistration(existingRegistration.id)
|
|
471
|
+
|
|
472
|
+
// Clear the registration
|
|
473
|
+
await existingRegistration.deactivate()
|
|
474
|
+
|
|
475
|
+
const group = groups.find(g => g.id === existingRegistration.groupId)
|
|
476
|
+
if (!group) {
|
|
477
|
+
const g = await Group.getByID(existingRegistration.groupId)
|
|
478
|
+
if (g) {
|
|
479
|
+
groups.push(g)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
420
482
|
}
|
|
421
483
|
|
|
422
484
|
let paymentUrl: string | null = null
|
|
423
485
|
let payment: Payment | null = null
|
|
424
486
|
|
|
425
|
-
if (
|
|
487
|
+
if (whoWillPayNow !== 'nobody') {
|
|
426
488
|
const mappedBalanceItems = new Map<BalanceItem, number>()
|
|
427
489
|
|
|
428
490
|
for (const item of items) {
|
|
@@ -452,11 +514,11 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
452
514
|
}
|
|
453
515
|
}
|
|
454
516
|
|
|
455
|
-
await
|
|
517
|
+
await BalanceItem.updateOutstanding(items, organization.id)
|
|
456
518
|
|
|
457
519
|
// Update occupancy
|
|
458
520
|
for (const group of groups) {
|
|
459
|
-
if (registrations.find(p => p.groupId === group.id)) {
|
|
521
|
+
if (registrations.find(p => p.groupId === group.id) || checkout.cart.deleteRegistrations.find(p => p.groupId === group.id)) {
|
|
460
522
|
await group.updateOccupancy()
|
|
461
523
|
await group.save()
|
|
462
524
|
}
|
|
@@ -501,10 +563,12 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
501
563
|
}
|
|
502
564
|
|
|
503
565
|
if (totalPrice < 0) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
566
|
+
// No payment needed: the outstanding balance will be negative and can be used in the future
|
|
567
|
+
return;
|
|
568
|
+
// throw new SimpleError({
|
|
569
|
+
// code: "empty_data",
|
|
570
|
+
// message: "Oeps! De totaalprijs is negatief."
|
|
571
|
+
// })
|
|
508
572
|
}
|
|
509
573
|
|
|
510
574
|
if (totalPrice === 0) {
|
|
@@ -595,11 +659,14 @@ export class RegisterMembersEndpoint extends Endpoint<Params, Query, Body, Respo
|
|
|
595
659
|
}
|
|
596
660
|
|
|
597
661
|
const _redirectUrl = new URL(checkout.redirectUrl)
|
|
598
|
-
_redirectUrl.searchParams.set('
|
|
599
|
-
|
|
662
|
+
_redirectUrl.searchParams.set('paymentId', payment.id);
|
|
663
|
+
_redirectUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
|
|
664
|
+
|
|
600
665
|
const _cancelUrl = new URL(checkout.cancelUrl)
|
|
601
|
-
_cancelUrl.searchParams.set('
|
|
602
|
-
|
|
666
|
+
_cancelUrl.searchParams.set('paymentId', payment.id);
|
|
667
|
+
_cancelUrl.searchParams.set('cancel', 'true');
|
|
668
|
+
_cancelUrl.searchParams.set('organizationId', organization.id); // makes sure the client uses the token associated with this organization when fetching payment polling status
|
|
669
|
+
|
|
603
670
|
const redirectUrl = _redirectUrl.href
|
|
604
671
|
const cancelUrl = _cancelUrl.href
|
|
605
672
|
|
|
@@ -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
|
}
|
|
@@ -917,6 +916,10 @@ export class AdminPermissionChecker {
|
|
|
917
916
|
const isUserManager = this.isUserManager(member)
|
|
918
917
|
if (isUserManager) {
|
|
919
918
|
// For the user manager, we don't delete data, because when registering a new member, it doesn't have any organizations yet...
|
|
919
|
+
|
|
920
|
+
// Notes are not visible for the member.
|
|
921
|
+
data.details.notes = null;
|
|
922
|
+
|
|
920
923
|
return data;
|
|
921
924
|
}
|
|
922
925
|
|
|
@@ -956,48 +959,63 @@ export class AdminPermissionChecker {
|
|
|
956
959
|
})
|
|
957
960
|
}
|
|
958
961
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
throw new SimpleError({
|
|
962
|
-
code: 'invalid_request',
|
|
963
|
-
message: 'Cannot PUT recordAnswers',
|
|
964
|
-
statusCode: 400
|
|
965
|
-
})
|
|
966
|
-
}
|
|
967
|
-
const isUserManager = this.isUserManager(member)
|
|
968
|
-
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
962
|
+
const hasRecordAnswers = !!data.details.recordAnswers;
|
|
963
|
+
const hasNotes = data.details.notes !== undefined;
|
|
969
964
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
if (value) {
|
|
973
|
-
if (value.isPatch()) {
|
|
974
|
-
throw new SimpleError({
|
|
975
|
-
code: 'invalid_request',
|
|
976
|
-
message: 'Cannot PATCH a record answer object',
|
|
977
|
-
statusCode: 400
|
|
978
|
-
})
|
|
979
|
-
}
|
|
965
|
+
if(hasRecordAnswers || hasNotes) {
|
|
966
|
+
const isUserManager = this.isUserManager(member);
|
|
980
967
|
|
|
981
|
-
|
|
968
|
+
if (hasRecordAnswers) {
|
|
969
|
+
if (!(data.details.recordAnswers instanceof PatchMap)) {
|
|
970
|
+
throw new SimpleError({
|
|
971
|
+
code: 'invalid_request',
|
|
972
|
+
message: 'Cannot PUT recordAnswers',
|
|
973
|
+
statusCode: 400
|
|
974
|
+
})
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const records = isUserManager ? new Set() : await this.getAccessibleRecordSet(member, PermissionLevel.Write)
|
|
978
|
+
|
|
979
|
+
for (const [key, value] of data.details.recordAnswers.entries()) {
|
|
980
|
+
let name: string | undefined = undefined
|
|
981
|
+
if (value) {
|
|
982
|
+
if (value.isPatch()) {
|
|
983
|
+
throw new SimpleError({
|
|
984
|
+
code: 'invalid_request',
|
|
985
|
+
message: 'Cannot PATCH a record answer object',
|
|
986
|
+
statusCode: 400
|
|
987
|
+
})
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const id = value.settings.id
|
|
991
|
+
|
|
992
|
+
if (id !== key) {
|
|
993
|
+
throw new SimpleError({
|
|
994
|
+
code: 'invalid_request',
|
|
995
|
+
message: 'Record answer key does not match record id',
|
|
996
|
+
statusCode: 400
|
|
997
|
+
})
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
name = value.settings.name
|
|
1001
|
+
}
|
|
982
1002
|
|
|
983
|
-
if (
|
|
1003
|
+
if (!isUserManager && !records.has(key)) {
|
|
984
1004
|
throw new SimpleError({
|
|
985
|
-
code: '
|
|
986
|
-
message: '
|
|
1005
|
+
code: 'permission_denied',
|
|
1006
|
+
message: `Je hebt geen toegangsrechten om het antwoord op ${name ?? 'deze vraag'} aan te passen voor dit lid`,
|
|
987
1007
|
statusCode: 400
|
|
988
1008
|
})
|
|
989
1009
|
}
|
|
990
|
-
|
|
991
|
-
name = value.settings.name
|
|
992
1010
|
}
|
|
1011
|
+
}
|
|
993
1012
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
}
|
|
1013
|
+
if(hasNotes && isUserManager) {
|
|
1014
|
+
throw new SimpleError({
|
|
1015
|
+
code: 'permission_denied',
|
|
1016
|
+
message: 'Cannot edit notes',
|
|
1017
|
+
statusCode: 400
|
|
1018
|
+
})
|
|
1001
1019
|
}
|
|
1002
1020
|
}
|
|
1003
1021
|
|
|
@@ -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 unverifiedEmails: string[] = member.details.unverifiedEmails;
|
|
19
|
+
const parentAndUnverifiedEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails).concat(unverifiedEmails) : []
|
|
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 parentAndUnverifiedEmails) {
|
|
28
|
+
// Link parents and unverified 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) && !parentAndUnverifiedEmails.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
|
+
}
|