@stamhoofd/backend 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.ts +16 -9
- package/src/endpoints/global/members/GetMembersEndpoint.ts +18 -6
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -8
- package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
- package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
- package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
- package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
- package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +417 -178
- package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +88 -11
- package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +10 -4
- package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +4 -1
- package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +6 -0
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -2
- package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -28
- package/src/helpers/AdminPermissionChecker.ts +9 -4
- package/src/helpers/AuthenticatedStructures.ts +89 -28
- package/src/helpers/MemberUserSyncer.ts +5 -5
- package/src/helpers/StripeHelper.ts +83 -40
- package/src/helpers/StripePayoutChecker.ts +7 -5
- package/src/seeds/1722344160-update-membership.ts +57 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Migration } from '@simonbackx/simple-database';
|
|
2
|
+
import { Member } from '@stamhoofd/models';
|
|
3
|
+
|
|
4
|
+
export default new Migration(async () => {
|
|
5
|
+
if (STAMHOOFD.environment == "test") {
|
|
6
|
+
console.log("skipped in tests")
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if(STAMHOOFD.userMode !== "platform") {
|
|
11
|
+
console.log("skipped seed update-membership because usermode not platform")
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process.stdout.write('\n');
|
|
16
|
+
let c = 0;
|
|
17
|
+
let id: string = '';
|
|
18
|
+
|
|
19
|
+
while(true) {
|
|
20
|
+
const rawMembers = await Member.where({
|
|
21
|
+
id: {
|
|
22
|
+
value: id,
|
|
23
|
+
sign: '>'
|
|
24
|
+
}
|
|
25
|
+
}, {limit: 100, sort: ['id']});
|
|
26
|
+
|
|
27
|
+
// const members = await Member.getByIDs(...rawMembers.map(m => m.id));
|
|
28
|
+
|
|
29
|
+
for (const member of rawMembers) {
|
|
30
|
+
const memberWithRegistrations = await Member.getWithRegistrations(member.id);
|
|
31
|
+
if(memberWithRegistrations) {
|
|
32
|
+
await memberWithRegistrations.updateMemberships();
|
|
33
|
+
await memberWithRegistrations.save();
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error("Member with registrations not found: " + member.id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
c++;
|
|
39
|
+
|
|
40
|
+
if (c%1000 === 0) {
|
|
41
|
+
process.stdout.write('.');
|
|
42
|
+
}
|
|
43
|
+
if (c%10000 === 0) {
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (rawMembers.length === 0) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
id = rawMembers[rawMembers.length - 1].id;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Do something here
|
|
56
|
+
return Promise.resolve()
|
|
57
|
+
})
|