@stamhoofd/backend 2.140.0 → 2.142.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 (65) hide show
  1. package/package.json +17 -17
  2. package/src/crons/fake-settlements.test.ts +129 -8
  3. package/src/crons/fake-settlements.ts +256 -9
  4. package/src/crons/index.ts +1 -1
  5. package/src/crons/invoices.ts +13 -8
  6. package/src/crons/settlement-sync.test.ts +39 -0
  7. package/src/crons/settlement-sync.ts +109 -0
  8. package/src/crons/stripe-invoices.ts +19 -13
  9. package/src/crons.ts +1 -5
  10. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.test.ts +165 -0
  11. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +18 -1
  12. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +154 -4
  13. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.ts +15 -8
  14. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +4 -4
  15. package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +2 -2
  16. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
  17. package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +3 -3
  18. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +150 -2
  19. package/src/endpoints/organization/dashboard/{stripe/GetStripePayoutsExportStatusEndpoint.ts → settlements/GetSettlementsSyncStatusEndpoint.ts} +9 -7
  20. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +157 -0
  21. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.ts +140 -0
  22. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +110 -0
  23. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.ts +104 -0
  24. package/src/excel-loaders/payments.ts +18 -1
  25. package/src/helpers/ApplicationFeeDetails.ts +66 -0
  26. package/src/helpers/ApplicationFeeInvoicer.test.ts +346 -0
  27. package/src/helpers/ApplicationFeeInvoicer.ts +424 -0
  28. package/src/helpers/AuthenticatedStructures.ts +14 -1
  29. package/src/helpers/MollieSettlementSync.test.ts +361 -0
  30. package/src/helpers/MollieSettlementSync.ts +323 -0
  31. package/src/helpers/MollieSettlementSyncRunner.ts +53 -0
  32. package/src/helpers/ProviderSettlementSyncRunner.ts +37 -0
  33. package/src/helpers/SettlementExporter.test.ts +388 -0
  34. package/src/helpers/SettlementExporter.ts +593 -0
  35. package/src/helpers/SettlementSyncRunner.test.ts +165 -0
  36. package/src/helpers/SettlementSyncRunner.ts +64 -0
  37. package/src/helpers/StripeHelper.ts +2 -1
  38. package/src/helpers/StripeSettlementSync.test.ts +997 -0
  39. package/src/helpers/StripeSettlementSync.ts +1053 -0
  40. package/src/helpers/StripeSettlementSyncRunner.test.ts +66 -0
  41. package/src/helpers/StripeSettlementSyncRunner.ts +160 -0
  42. package/src/helpers/WebmasterReport.test.ts +109 -0
  43. package/src/helpers/WebmasterReport.ts +115 -0
  44. package/src/helpers/getPaymentIdForStripeCharge.test.ts +91 -0
  45. package/src/helpers/getPaymentIdForStripeCharge.ts +71 -0
  46. package/src/services/ApplicationFeeService.test.ts +256 -0
  47. package/src/services/ApplicationFeeService.ts +301 -0
  48. package/src/services/InvoiceService.ts +18 -1
  49. package/src/services/SettlementService.test.ts +632 -0
  50. package/src/services/SettlementService.ts +650 -0
  51. package/src/sql-filters/payment-settlement.test.ts +2 -2
  52. package/src/sql-filters/payments.ts +73 -0
  53. package/tests/helpers/MollieMocker.ts +64 -6
  54. package/tests/helpers/StripeMocker.ts +209 -17
  55. package/src/crons/stripe-payout-reports.ts +0 -69
  56. package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.test.ts +0 -103
  57. package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.ts +0 -125
  58. package/src/helpers/CheckSettlements.test.ts +0 -190
  59. package/src/helpers/CheckSettlements.ts +0 -237
  60. package/src/helpers/StripeInvoicer.ts +0 -419
  61. package/src/helpers/StripePayoutChecker.ts +0 -193
  62. package/src/helpers/StripePayoutExportData.ts +0 -195
  63. package/src/helpers/StripePayoutExportExcel.ts +0 -280
  64. package/src/helpers/StripePayoutReporter.test.ts +0 -419
  65. package/src/helpers/StripePayoutReporter.ts +0 -585
@@ -1,419 +0,0 @@
1
- import { SimpleError } from '@simonbackx/simple-errors';
2
- import { Email } from '@stamhoofd/email';
3
- import { BalanceItem, BalanceItemPayment, Organization, Payment, StripeAccount, User } from '@stamhoofd/models';
4
- import { QueueHandler } from '@stamhoofd/queues';
5
- import { BalanceItemRelation, BalanceItemRelationType, BalanceItemStatus, BalanceItemType, getPaymentProviderName, PaymentCustomer, PaymentMethod, PaymentProvider, PaymentStatus, PaymentType, TranslatedString } from '@stamhoofd/structures';
6
- import { Formatter } from '@stamhoofd/utility';
7
- import Stripe from 'stripe';
8
- import { PaymentService } from '../services/PaymentService.js';
9
- import { VATService } from '../services/VATService.js';
10
- import { SQL } from '@stamhoofd/sql';
11
-
12
- export class ApplicationFeeDetails {
13
- transferFee = 0;
14
- serviceFee = 0;
15
- count = 0;
16
- minimumDate: Date | null = null;
17
- maximumDate: Date | null = null;
18
-
19
- constructor(details: { count?: number; transferFee: number; serviceFee: number; minimumDate: Date | null; maximumDate: Date | null }) {
20
- this.count = details.count ?? 0;
21
- this.transferFee = details.transferFee;
22
- this.serviceFee = details.serviceFee;
23
- this.minimumDate = details.minimumDate;
24
- this.maximumDate = details.maximumDate;
25
- }
26
-
27
- get amount() {
28
- return this.transferFee + this.serviceFee;
29
- }
30
-
31
- static fromStripe(transaction: Stripe.BalanceTransaction) {
32
- const source = transaction.source as Stripe.ApplicationFee;
33
- const metadata = (source.originating_transaction as Stripe.Charge).metadata;
34
-
35
- const serviceFeeStr = metadata.serviceFee as unknown;
36
- if (serviceFeeStr === undefined || typeof serviceFeeStr !== 'string') {
37
- throw new Error('Missing serviceFee metadata');
38
- }
39
-
40
- const parsed = parseInt(serviceFeeStr);
41
- if (isNaN(parsed) || !isFinite(parsed)) {
42
- throw new Error('Invalid serviceFee metadata');
43
- }
44
- const serviceFee = parsed * 100; // in cents
45
- const transferFee = transaction.amount * 100 - serviceFee;
46
-
47
- return new ApplicationFeeDetails({
48
- count: 1,
49
- serviceFee,
50
- transferFee,
51
- minimumDate: new Date(transaction.created * 1000),
52
- maximumDate: new Date(transaction.created * 1000),
53
- });
54
- }
55
-
56
- add(fee: Stripe.BalanceTransaction) {
57
- this.combine(ApplicationFeeDetails.fromStripe(fee));
58
- }
59
-
60
- combine(other: ApplicationFeeDetails) {
61
- this.serviceFee += other.serviceFee;
62
- this.transferFee += other.transferFee;
63
- this.count += other.count;
64
- if (other.minimumDate && (this.minimumDate === null || other.minimumDate < this.minimumDate)) {
65
- this.minimumDate = other.minimumDate;
66
- }
67
- if (other.maximumDate && (this.maximumDate === null || other.maximumDate > this.maximumDate)) {
68
- this.maximumDate = other.maximumDate;
69
- }
70
- }
71
- }
72
-
73
- class StripeReport {
74
- readonly applicationFeesPerAccount = new Map<string, ApplicationFeeDetails>();
75
-
76
- add(balanceTransaction: Stripe.BalanceTransaction) {
77
- if (balanceTransaction.type === 'application_fee' || balanceTransaction.type === 'application_fee_refund') {
78
- const source = balanceTransaction.source as Stripe.ApplicationFee;
79
- const account = typeof source.account === 'string' ? source.account : source.account.id;
80
-
81
- if (account) {
82
- // We don't included refunded fees because these are test payments from test account
83
- const currentAmount = this.applicationFeesPerAccount.get(account);
84
- if (currentAmount === undefined) {
85
- this.applicationFeesPerAccount.set(account, ApplicationFeeDetails.fromStripe(balanceTransaction));
86
- } else {
87
- currentAmount.add(balanceTransaction);
88
- }
89
- }
90
- }
91
- }
92
-
93
- combine(report: StripeReport) {
94
- for (const [account, amount] of report.applicationFeesPerAccount.entries()) {
95
- const currentAmount = this.applicationFeesPerAccount.get(account);
96
- if (currentAmount === undefined) {
97
- this.applicationFeesPerAccount.set(account, amount);
98
- } else {
99
- currentAmount.combine(amount);
100
- }
101
- }
102
- return this;
103
- }
104
- }
105
-
106
- export class StripeReportInvoicer {
107
- private readonly report: StripeReport;
108
- start: Date;
109
- end: Date;
110
-
111
- constructor(report: StripeReport, start: Date, end: Date) {
112
- this.report = report;
113
- this.start = start;
114
- this.end = end;
115
- }
116
-
117
- async generateInvoices(sellingOrganization: Organization, reference: string) {
118
- const payments: Payment[] = [];
119
- for (const [account, details] of this.report.applicationFeesPerAccount) {
120
- try {
121
- const payment = await this.generateInvoice(sellingOrganization, reference, account, details);
122
- if (payment) {
123
- payments.push(payment);
124
- }
125
- } catch (e) {
126
- console.error(e);
127
- // Send email notification of this error
128
- Email.sendWebmaster({
129
- subject: 'Aanmaken Stripe facturen voor ' + account + ' - ' + reference + ' mislukt',
130
- html: 'Aanmaken Stripe facturen voor ' + account + ' - ' + reference + ' mislukt. <br><br> ' + e.toString(),
131
- });
132
- }
133
- }
134
- return payments;
135
- }
136
-
137
- static async hasPayment(sellingOrganization: Organization, reference: string) {
138
- return !!await Payment.select()
139
- .where('organizationId', sellingOrganization.id)
140
- .where('reference', reference)
141
- .where('status', PaymentStatus.Succeeded)
142
- .where('method', PaymentMethod.AccountDeductions)
143
- .where('provider', PaymentProvider.Stripe)
144
- .first(false);
145
- }
146
-
147
- async generateInvoice(sellingOrganization: Organization, reference: string, accountId: string, applicationFee: ApplicationFeeDetails) {
148
- if (applicationFee.amount === 0) {
149
- return;
150
- }
151
-
152
- const seller = sellingOrganization.meta.companies[0];
153
- if (!seller) {
154
- return;
155
- }
156
-
157
- // Search for the organization in Stamhoofd
158
- const stripeAccount = await StripeAccount.select().where('accountId', accountId).first(false);
159
- if (!stripeAccount) {
160
- if (STAMHOOFD.environment === 'test') {
161
- console.error('No organization found for Stripe account ' + accountId);
162
- return;
163
- }
164
- throw new SimpleError({
165
- code: 'stripe_account_not_found',
166
- message: 'No organization found for Stripe account ' + accountId,
167
- });
168
- }
169
- const organization = await Organization.getByID(stripeAccount.organizationId);
170
-
171
- if (!organization) {
172
- throw new SimpleError({
173
- code: 'organization_not_found',
174
- message: 'No organization found for Stripe account ' + accountId,
175
- });
176
- }
177
-
178
- const existingPayments = await Payment.select()
179
- .where('organizationId', sellingOrganization.id)
180
- .where('payingOrganizationId', organization.id)
181
- .where(
182
- // required because the same organization can have multiple stripe accounts. Null = legacy
183
- SQL.where('stripeAccountId', stripeAccount.id)
184
- .or('stripeAccountId', null),
185
- )
186
- .where('reference', reference)
187
- .where('method', PaymentMethod.AccountDeductions)
188
- .where('provider', PaymentProvider.Stripe)
189
- .where('status', PaymentStatus.Succeeded)
190
- .fetch();
191
-
192
- for (const i of existingPayments) {
193
- if (i.price !== applicationFee.amount) {
194
- throw new SimpleError({
195
- code: 'payment_already_exists_with_different_amount',
196
- message: 'Payment already exists with different amount ' + organization.id + ' expected ' + applicationFee.amount + ' got ' + i.price + ' in payment ' + i.id,
197
- });
198
- }
199
- }
200
-
201
- if (existingPayments.length) {
202
- console.warn('Tried to create an already created stripe invoice payment ' + existingPayments.map(p => p.id).join(', '));
203
- return;
204
- }
205
-
206
- const customer = PaymentCustomer.create({
207
- company: organization.defaultCompanies[0],
208
- });
209
-
210
- if (customer.company!.isSameEntity(seller)) {
211
- throw new SimpleError({
212
- code: 'same_customer',
213
- message: 'Cannot invoice self',
214
- });
215
- }
216
-
217
- const balanceItems: BalanceItem[] = [];
218
-
219
- if (applicationFee.serviceFee !== 0) {
220
- const item = new BalanceItem();
221
- item.type = BalanceItemType.ServiceFee;
222
- item.description = $t('%1Wd', {
223
- startDate: Formatter.startDate(this.start, false, true),
224
- endDate: Formatter.endDate(this.end, false, true),
225
- });
226
- item.relations.set(BalanceItemRelationType.PaymentProvider, BalanceItemRelation.create({
227
- id: PaymentProvider.Stripe,
228
- name: TranslatedString.create(getPaymentProviderName(PaymentProvider.Stripe)),
229
- }));
230
- item.payingOrganizationId = organization.id;
231
- item.organizationId = sellingOrganization.id;
232
- item.VATPercentage = 21;
233
- item.VATExcempt = VATService.getVATExcempt({
234
- company: organization.defaultCompanies[0] ?? null,
235
- sellingOrganization,
236
- type: 'services',
237
- });
238
- item.VATIncluded = !item.VATExcempt; // Makes sure price with VAT always matches the charged amount
239
- item.quantity = 1;
240
- item.unitPrice = applicationFee.serviceFee;
241
- item.createdAt = new Date();
242
- item.status = BalanceItemStatus.Hidden;
243
- item.startDate = this.start;
244
- item.endDate = this.end;
245
- await item.save();
246
- balanceItems.push(item);
247
- }
248
-
249
- if (applicationFee.transferFee !== 0) {
250
- const item = new BalanceItem();
251
- item.type = BalanceItemType.TransferFee;
252
- item.description = $t('%1Xx', {
253
- startDate: Formatter.startDate(this.start, false, true),
254
- endDate: Formatter.endDate(this.end, false, true),
255
- });
256
- item.relations.set(BalanceItemRelationType.PaymentProvider, BalanceItemRelation.create({
257
- id: PaymentProvider.Stripe,
258
- name: TranslatedString.create(getPaymentProviderName(PaymentProvider.Stripe)),
259
- }));
260
- item.payingOrganizationId = organization.id;
261
- item.organizationId = sellingOrganization.id;
262
- item.VATPercentage = 21;
263
- item.VATExcempt = VATService.getVATExcempt({
264
- company: organization.defaultCompanies[0] ?? null,
265
- sellingOrganization,
266
- type: 'services',
267
- });
268
- item.VATIncluded = !item.VATExcempt; // Makes sure price with VAT always matches the charged amount
269
- item.quantity = 1;
270
- item.unitPrice = applicationFee.transferFee;
271
- item.createdAt = new Date();
272
- item.status = BalanceItemStatus.Hidden;
273
- item.startDate = this.start;
274
- item.endDate = this.end;
275
- await item.save();
276
- balanceItems.push(item);
277
- }
278
-
279
- let total = 0;
280
- for (const balanceItem of balanceItems) {
281
- total += balanceItem.priceWithVAT;
282
- }
283
- if (total !== applicationFee.amount) {
284
- throw new SimpleError({
285
- code: 'price_mismatched',
286
- message: 'The charged amount does not match the total application fee for the payment',
287
- });
288
- }
289
-
290
- const systemUser = await User.getSystem();
291
-
292
- // Done validation
293
- const payment = new Payment();
294
- payment.adminUserId = systemUser.id;
295
-
296
- // Who will receive this money?
297
- payment.organizationId = sellingOrganization.id;
298
-
299
- // Who paid
300
- payment.payingOrganizationId = organization.id;
301
- payment.customer = customer;
302
-
303
- payment.status = PaymentStatus.Pending;
304
- payment.price = applicationFee.amount;
305
- payment.roundingAmount = 0;
306
- payment.method = PaymentMethod.AccountDeductions;
307
- payment.type = PaymentType.Payment;
308
- payment.createMandate = null;
309
- payment.reference = reference;
310
-
311
- payment.provider = PaymentProvider.Stripe;
312
- payment.stripeAccountId = stripeAccount.id;
313
- await payment.save();
314
-
315
- for (const balanceItem of balanceItems) {
316
- // Create one balance item payment to pay it in one payment
317
- const balanceItemPayment = new BalanceItemPayment();
318
- balanceItemPayment.balanceItemId = balanceItem.id;
319
- balanceItemPayment.paymentId = payment.id;
320
- balanceItemPayment.organizationId = payment.organizationId;
321
- balanceItemPayment.price = balanceItem.priceWithVAT;
322
- await balanceItemPayment.save();
323
- }
324
-
325
- await PaymentService.handlePaymentStatusUpdate(payment, sellingOrganization, PaymentStatus.Succeeded, this.start);
326
- return payment;
327
- }
328
- }
329
-
330
- export class StripeInvoicer {
331
- private stripe: Stripe;
332
-
333
- constructor({ secretKey }: { secretKey: string }) {
334
- this.stripe = new Stripe(secretKey, { apiVersion: '2024-06-20', typescript: true, maxNetworkRetries: 1, timeout: 10000 });
335
- }
336
-
337
- static getMonthUnixStartEnd(date: Date) {
338
- const start = Math.floor((new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0).getTime()) / 1000);
339
- const end = Math.ceil((new Date(date.getFullYear(), date.getMonth() + 1, 1, 0, 0, 0, 0).getTime() - 1000) / 1000);
340
- return { start, end };
341
- }
342
-
343
- // Loops all months until all invoices are generated
344
- async generateAllInvoices(sellingOrganization: Organization, options?: { force?: boolean; start?: Date; forceLast?: boolean }) {
345
- const startMonth = options?.start ?? new Date(2026, 0, 1);
346
- const stopAt = new Date(Date.now() + (STAMHOOFD.environment === 'production' ? (-60 * 60 * 24 * 1000) : (60 * 60 * 24 * 1000 * 31))); // One day margin before creating invoices
347
- let currentMonth = new Date(startMonth);
348
-
349
- while (true) {
350
- const { start, end } = StripeInvoicer.getMonthUnixStartEnd(currentMonth);
351
-
352
- if (end >= stopAt.getTime() / 1000) {
353
- // Stop
354
- break;
355
- }
356
- const nextMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
357
- const { end: endNext } = StripeInvoicer.getMonthUnixStartEnd(nextMonth);
358
- const isLastMonth = endNext >= stopAt.getTime() / 1000;
359
- await this.generateInvoices(sellingOrganization, currentMonth, { ...options, force: (isLastMonth && !!options?.forceLast) || !!options?.force });
360
- currentMonth = nextMonth;
361
- }
362
- }
363
-
364
- async generateInvoices(sellingOrganization: Organization, month: Date, options?: { force?: boolean }) {
365
- const { start, end } = StripeInvoicer.getMonthUnixStartEnd(month);
366
- const reference = 'stripe-fees-' + Formatter.dateIso(new Date(start * 1000));
367
-
368
- try {
369
- await QueueHandler.schedule(reference, async () => {
370
- if (!options?.force && await StripeReportInvoicer.hasPayment(sellingOrganization, reference)) {
371
- console.log('Already invoiced month', reference);
372
- return;
373
- }
374
-
375
- console.log('Generating invoices for ', reference, start, end);
376
- const reportFees = await this.fetchBalanceItems({
377
- created: {
378
- gte: start,
379
- lte: end,
380
- },
381
- type: 'application_fee',
382
- expand: ['data.source', 'data.source.originating_transaction'],
383
- });
384
- const reportRefund = await this.fetchBalanceItems({
385
- created: {
386
- gte: start,
387
- lte: end,
388
- },
389
- type: 'application_fee_refund',
390
- expand: ['data.source', 'data.source.originating_transaction'],
391
- });
392
-
393
- const report = reportFees.combine(reportRefund);
394
- const invoicer = new StripeReportInvoicer(report, new Date(start * 1000), new Date(end * 1000));
395
- await invoicer.generateInvoices(sellingOrganization, reference);
396
- });
397
- } catch (e) {
398
- console.error(e);
399
-
400
- // Send email notification of this error
401
- Email.sendWebmaster({
402
- subject: 'Aanmaken Stripe facturen voor ' + Formatter.dateNumber(month) + ' mislukt',
403
- html: 'Aanmaken Stripe facturen voor ' + Formatter.dateNumber(month) + ' mislukt. <br><br> ' + e.toString(),
404
- });
405
- }
406
- }
407
-
408
- private async fetchBalanceItems(options: Stripe.BalanceTransactionListParams) {
409
- // For the given payout, fetch all balance items
410
- const params = { ...options };
411
- const report = new StripeReport();
412
-
413
- for await (const balanceItem of this.stripe.balanceTransactions.list(params)) {
414
- report.add(balanceItem);
415
- }
416
-
417
- return report;
418
- }
419
- }
@@ -1,193 +0,0 @@
1
- import { Order, Payment, StripeCheckoutSession, StripePaymentIntent } from '@stamhoofd/models';
2
- import { Settlement } from '@stamhoofd/structures';
3
- import Stripe from 'stripe';
4
- import { passthroughFetch } from './passthroughFetch.js';
5
-
6
- export class StripePayoutChecker {
7
- private stripe: Stripe;
8
- private stripePlatform: Stripe;
9
-
10
- constructor({ secretKey, stripeAccount }: { secretKey: string; stripeAccount?: string }) {
11
- this.stripe = new Stripe(
12
- secretKey, {
13
- apiVersion: '2024-06-20',
14
- typescript: true,
15
- maxNetworkRetries: 1,
16
- timeout: 10000,
17
- stripeAccount,
18
- httpClient: STAMHOOFD.environment === 'test'
19
- ? Stripe.createFetchHttpClient(passthroughFetch)
20
- : undefined,
21
- });
22
-
23
- this.stripePlatform = new Stripe(
24
- secretKey, {
25
- apiVersion: '2024-06-20',
26
- typescript: true,
27
- maxNetworkRetries: 1,
28
- timeout: 10000,
29
- httpClient: STAMHOOFD.environment === 'test'
30
- ? Stripe.createFetchHttpClient(passthroughFetch)
31
- : undefined,
32
- });
33
- }
34
-
35
- async checkSettlements(checkAll = false) {
36
- // Check last 2 weeks + 3 day margin, unless we check them all
37
- const d = new Date();
38
- d.setDate(d.getDate() - 20);
39
-
40
- if (checkAll) {
41
- d.setFullYear(2022, 11, 1);
42
- }
43
-
44
- // Loop all payouts
45
- try {
46
- // Fetch all payouts that are paid out
47
- for await (const payout of this.stripe.payouts.list({
48
- status: 'paid',
49
- arrival_date: {
50
- gte: Math.floor(d.getTime() / 1000),
51
- },
52
- })) {
53
- // Get all payments for this payout
54
- try {
55
- await this.fetchBalanceItems(payout);
56
- } catch (e) {
57
- console.error('Error for payout ' + payout.id);
58
- console.error(e);
59
- }
60
- }
61
- } catch (e) {
62
- console.error(e);
63
- }
64
- }
65
-
66
- private async fetchBalanceItems(payout: Stripe.Payout) {
67
- // For the given payout, fetch all balance items
68
- const params = {
69
- payout: payout.id,
70
- // Via the Application Fee object, we can get the original payment metadata
71
- expand: ['data.source', 'data.source.application_fee', 'data.source.application_fee.originating_transaction'],
72
- };
73
-
74
- for await (const balanceItem of this.stripe.balanceTransactions.list(params)) {
75
- if (balanceItem.type === 'charge' || balanceItem.type === 'payment') {
76
- await this.handleBalanceItem(payout, balanceItem);
77
- }
78
- }
79
-
80
- return;
81
- }
82
-
83
- private async handleBalanceItem(payout: Stripe.Payout, balanceItem: Stripe.BalanceTransaction) {
84
- if (!balanceItem.source || typeof balanceItem.source === 'string') {
85
- return;
86
- }
87
- if (balanceItem.source.object !== 'charge') {
88
- console.log('No payment id set for charge ' + balanceItem.source.id);
89
- return;
90
- }
91
-
92
- let paymentId = balanceItem.source.metadata.payment;
93
-
94
- if (!paymentId) {
95
- // Search in the metadata of the application fee, originating transaction
96
- if (typeof balanceItem.source.application_fee !== 'string' && balanceItem.source.application_fee) {
97
- const applicationFee = balanceItem.source.application_fee;
98
-
99
- if (applicationFee.originating_transaction !== 'string' && applicationFee.originating_transaction) {
100
- const originatingTransaction = applicationFee.originating_transaction as Stripe.Charge;
101
- paymentId = originatingTransaction.metadata.payment;
102
-
103
- if (!paymentId) {
104
- // Historical bug where we didn't save payment in metadata
105
- // Try to look it up by payment intent id
106
-
107
- if (originatingTransaction.payment_intent) {
108
- const paymentIntentId = typeof originatingTransaction.payment_intent === 'string' ? originatingTransaction.payment_intent : originatingTransaction.payment_intent.id;
109
- const stripePayments = await StripePaymentIntent.where({
110
- stripeIntentId: paymentIntentId,
111
- }, { limit: 1 });
112
-
113
- if (stripePayments.length === 1) {
114
- paymentId = stripePayments[0].paymentId;
115
- console.log('Found missing payment metadata for payment intent ', originatingTransaction.payment_intent, paymentId);
116
- } else {
117
- // Probably a card payment
118
- // Search for the checkout session
119
- const checkoutSession = await this.stripePlatform.checkout.sessions.list({
120
- payment_intent: paymentIntentId,
121
- });
122
- if (checkoutSession.data.length === 1) {
123
- const session = checkoutSession.data[0];
124
- console.log('Found checkout session for payment intent ', paymentIntentId, session);
125
-
126
- // Search
127
- const stripeCheckoutSessions = await StripeCheckoutSession.where({
128
- stripeSessionId: session.id,
129
- }, { limit: 1 });
130
-
131
- if (stripeCheckoutSessions.length === 1) {
132
- paymentId = stripeCheckoutSessions[0].paymentId;
133
- console.log('Found missing payment metadata for payment intent ', originatingTransaction.payment_intent, paymentId);
134
- } else {
135
- console.log('No payment found for checkout session ' + session.id);
136
- }
137
- } else {
138
- console.log('No Stripe Checkout Sessions found for payment intent ' + paymentIntentId);
139
- }
140
- }
141
- }
142
- }
143
- }
144
- }
145
- }
146
-
147
- if (!paymentId) {
148
- console.log(balanceItem);
149
- console.log('No payment id set for charge ' + balanceItem.source.id);
150
- return;
151
- }
152
-
153
- const applicationFee = balanceItem.source.application_fee_amount;
154
- const otherFees = balanceItem.fee;
155
- const totalFees = Math.max(otherFees, (applicationFee ?? 0));
156
-
157
- // Cool, we can store this in the database now.
158
-
159
- const payment = await Payment.getByID(paymentId);
160
- if (!payment) {
161
- console.log('Invalid payment id set for charge ' + balanceItem.source.id + ': ' + paymentId);
162
- return;
163
- }
164
-
165
- if (payment.price !== balanceItem.amount * 100) {
166
- console.log('Amount mismatch for payment ' + payment.id + ': ' + payment.price + ' !== ' + (balanceItem.amount * 100));
167
- return;
168
- }
169
-
170
- const settlement = Settlement.create({
171
- id: payout.id,
172
- reference: payout.statement_descriptor ?? '',
173
- settledAt: new Date(payout.arrival_date * 1000),
174
- amount: payout.amount * 100,
175
- // Set only if application fee is witheld
176
- fee: totalFees * 100,
177
- });
178
-
179
- payment.settlement = settlement;
180
- payment.transferFee = totalFees * 100 - payment.serviceFeePayout;
181
-
182
- // Force an updatedAt timestamp of the related order
183
- // Mark order as 'updated', or the frontend won't pull in the updates
184
- const order = await Order.getForPayment(null, payment.id);
185
- if (order) {
186
- order.updatedAt = new Date();
187
- order.forceSaveProperty('updatedAt');
188
- await order.save();
189
- }
190
-
191
- await payment.save();
192
- }
193
- }