@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.
Files changed (23) hide show
  1. package/package.json +3 -3
  2. package/src/endpoints/admin/invoices/GetInvoicesEndpoint.ts +1 -1
  3. package/src/endpoints/global/events/PatchEventsEndpoint.ts +16 -9
  4. package/src/endpoints/global/members/GetMembersEndpoint.ts +18 -6
  5. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.ts +21 -8
  6. package/src/endpoints/global/organizations/GetOrganizationFromDomainEndpoint.ts +3 -2
  7. package/src/endpoints/global/organizations/GetOrganizationFromUriEndpoint.ts +2 -1
  8. package/src/endpoints/global/organizations/SearchOrganizationEndpoint.ts +2 -1
  9. package/src/endpoints/global/payments/StripeWebhookEndpoint.ts +6 -0
  10. package/src/endpoints/global/registration/RegisterMembersEndpoint.ts +417 -178
  11. package/src/endpoints/global/webshops/GetWebshopFromDomainEndpoint.ts +5 -5
  12. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.ts +88 -11
  13. package/src/endpoints/organization/dashboard/stripe/ConnectStripeEndpoint.ts +10 -4
  14. package/src/endpoints/organization/dashboard/stripe/GetStripeAccountLinkEndpoint.ts +4 -1
  15. package/src/endpoints/organization/dashboard/stripe/GetStripeLoginLinkEndpoint.ts +6 -0
  16. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.ts +7 -2
  17. package/src/endpoints/organization/shared/ExchangePaymentEndpoint.ts +13 -28
  18. package/src/helpers/AdminPermissionChecker.ts +9 -4
  19. package/src/helpers/AuthenticatedStructures.ts +89 -28
  20. package/src/helpers/MemberUserSyncer.ts +5 -5
  21. package/src/helpers/StripeHelper.ts +83 -40
  22. package/src/helpers/StripePayoutChecker.ts +7 -5
  23. 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 parentEmails = member.details.parentsHaveAccess ? member.details.parents.flatMap(p => p.email ? [p.email, ...p.alternativeEmails] : p.alternativeEmails) : []
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 parentEmails) {
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) && !parentEmails.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: '2022-11-15', typescript: true, maxNetworkRetries: 0, timeout: 10000});
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
- try {
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: '2022-11-15',
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: '2022-11-15',
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: applicationFee ?? 0
172
+ fee: totalFees
171
173
  });
172
174
 
173
175
  payment.settlement = settlement;
174
- payment.transferFee = applicationFee ?? 0;
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
+ })