@stamhoofd/backend 2.139.0 → 2.141.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 +17 -17
- package/src/crons/fake-settlements.test.ts +129 -8
- package/src/crons/fake-settlements.ts +256 -9
- package/src/crons/index.ts +1 -1
- package/src/crons/invoices.ts +13 -8
- package/src/crons/settlement-sync.test.ts +39 -0
- package/src/crons/settlement-sync.ts +109 -0
- package/src/crons/stripe-invoices.ts +19 -13
- package/src/crons.ts +1 -5
- package/src/endpoints/auth/MFA.security.test.ts +76 -1
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +14 -0
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +16 -16
- package/src/endpoints/organization/dashboard/mollie/ConnectMollieEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +1 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +14 -14
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +150 -2
- package/src/endpoints/organization/dashboard/{stripe/GetStripePayoutsExportStatusEndpoint.ts → settlements/GetSettlementsSyncStatusEndpoint.ts} +9 -7
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +157 -0
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.ts +140 -0
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +110 -0
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.ts +104 -0
- package/src/excel-loaders/payments.ts +18 -1
- package/src/helpers/ApplicationFeeDetails.ts +66 -0
- package/src/helpers/ApplicationFeeInvoicer.test.ts +285 -0
- package/src/helpers/ApplicationFeeInvoicer.ts +419 -0
- package/src/helpers/AuthenticatedStructures.ts +14 -1
- package/src/helpers/MollieSettlementSync.test.ts +361 -0
- package/src/helpers/MollieSettlementSync.ts +323 -0
- package/src/helpers/MollieSettlementSyncRunner.ts +53 -0
- package/src/helpers/ProviderSettlementSyncRunner.ts +37 -0
- package/src/helpers/SettlementExporter.test.ts +366 -0
- package/src/helpers/SettlementExporter.ts +583 -0
- package/src/helpers/SettlementSyncRunner.test.ts +165 -0
- package/src/helpers/SettlementSyncRunner.ts +64 -0
- package/src/helpers/StripeHelper.ts +2 -1
- package/src/helpers/StripeSettlementSync.test.ts +828 -0
- package/src/helpers/StripeSettlementSync.ts +947 -0
- package/src/helpers/StripeSettlementSyncRunner.test.ts +66 -0
- package/src/helpers/StripeSettlementSyncRunner.ts +152 -0
- package/src/helpers/TwoFactorHelper.ts +1 -1
- package/src/helpers/WebmasterReport.test.ts +109 -0
- package/src/helpers/WebmasterReport.ts +115 -0
- package/src/helpers/getPaymentIdForStripeCharge.test.ts +91 -0
- package/src/helpers/getPaymentIdForStripeCharge.ts +71 -0
- package/src/helpers/streamForBreakdown.ts +2 -2
- package/src/services/ApplicationFeeService.test.ts +256 -0
- package/src/services/ApplicationFeeService.ts +283 -0
- package/src/services/DocumentRenderService.test.ts +72 -1
- package/src/services/InvoiceService.ts +18 -1
- package/src/services/PaymentService.ts +8 -0
- package/src/services/SettlementService.test.ts +559 -0
- package/src/services/SettlementService.ts +607 -0
- package/src/sql-filters/orders.ts +6 -2
- package/src/sql-filters/payment-settlement.test.ts +2 -2
- package/src/sql-filters/payments.ts +73 -0
- package/tests/filters/orders.test.ts +635 -0
- package/tests/helpers/MollieMocker.ts +64 -6
- package/tests/helpers/StripeMocker.ts +209 -17
- package/src/crons/stripe-payout-reports.ts +0 -69
- package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.test.ts +0 -103
- package/src/endpoints/organization/dashboard/stripe/StripePayoutsExportEndpoint.ts +0 -125
- package/src/helpers/CheckSettlements.test.ts +0 -190
- package/src/helpers/CheckSettlements.ts +0 -237
- package/src/helpers/StripeInvoicer.ts +0 -419
- package/src/helpers/StripePayoutChecker.ts +0 -193
- package/src/helpers/StripePayoutExportData.ts +0 -195
- package/src/helpers/StripePayoutExportExcel.ts +0 -280
- package/src/helpers/StripePayoutReporter.test.ts +0 -419
- package/src/helpers/StripePayoutReporter.ts +0 -585
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { Decoder } from '@simonbackx/simple-encoding';
|
|
2
|
-
import { AutoEncoder, DateDecoder, field, IntegerDecoder } from '@simonbackx/simple-encoding';
|
|
3
|
-
import type { DecodedRequest, Request } from '@simonbackx/simple-endpoints';
|
|
4
|
-
import { Endpoint, Response } from '@simonbackx/simple-endpoints';
|
|
5
|
-
import { SimpleError } from '@simonbackx/simple-errors';
|
|
6
|
-
import { Platform } from '@stamhoofd/models';
|
|
7
|
-
import { QueueHandler } from '@stamhoofd/queues';
|
|
8
|
-
|
|
9
|
-
import { Context } from '../../../../helpers/Context.js';
|
|
10
|
-
import { StripePayoutReporter } from '../../../../helpers/StripePayoutReporter.js';
|
|
11
|
-
|
|
12
|
-
export class PayoutExportStatus extends AutoEncoder {
|
|
13
|
-
@field({ decoder: DateDecoder })
|
|
14
|
-
start: Date;
|
|
15
|
-
|
|
16
|
-
@field({ decoder: DateDecoder })
|
|
17
|
-
end: Date;
|
|
18
|
-
|
|
19
|
-
@field({ decoder: IntegerDecoder })
|
|
20
|
-
count = 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type Params = Record<string, never>;
|
|
24
|
-
class Body extends AutoEncoder {
|
|
25
|
-
@field({ decoder: DateDecoder })
|
|
26
|
-
start: Date;
|
|
27
|
-
|
|
28
|
-
@field({ decoder: DateDecoder })
|
|
29
|
-
end: Date;
|
|
30
|
-
}
|
|
31
|
-
type Query = undefined;
|
|
32
|
-
type ResponseBody = undefined;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Manually build a Stripe payout report for a given period and email it (to check whether
|
|
36
|
-
* everything we charged via Stripe application fees was invoiced correctly).
|
|
37
|
-
*/
|
|
38
|
-
export class StripePayoutsExportEndpoint extends Endpoint<Params, Query, Body, ResponseBody> {
|
|
39
|
-
bodyDecoder = Body as Decoder<Body>;
|
|
40
|
-
|
|
41
|
-
static queue: PayoutExportStatus[] = [];
|
|
42
|
-
|
|
43
|
-
protected doesMatch(request: Request): [true, Params] | [false] {
|
|
44
|
-
if (request.method !== 'POST') {
|
|
45
|
-
return [false];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const params = Endpoint.parseParameters(request.url, '/stripe/payouts', {});
|
|
49
|
-
|
|
50
|
-
if (params) {
|
|
51
|
-
return [true, params as Params];
|
|
52
|
-
}
|
|
53
|
-
return [false];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
static async authenticate() {
|
|
57
|
-
const organization = await Context.setOrganizationScope();
|
|
58
|
-
const { user } = await Context.authenticate();
|
|
59
|
-
|
|
60
|
-
if (!Context.auth.hasPlatformFullAccess()) {
|
|
61
|
-
throw Context.auth.error();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const platform = await Platform.getShared();
|
|
65
|
-
if (!platform.membershipOrganizationId || platform.membershipOrganizationId !== organization.id) {
|
|
66
|
-
throw new SimpleError({
|
|
67
|
-
code: 'not_available',
|
|
68
|
-
message: 'Stripe payout exports are only available for the platform membership organization',
|
|
69
|
-
statusCode: 400,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { organization, user };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async handle(request: DecodedRequest<Params, Query, Body>) {
|
|
77
|
-
const { organization, user } = await StripePayoutsExportEndpoint.authenticate();
|
|
78
|
-
|
|
79
|
-
if (!STAMHOOFD.STRIPE_SECRET_KEY) {
|
|
80
|
-
throw new SimpleError({
|
|
81
|
-
code: 'not_configured',
|
|
82
|
-
message: 'Stripe is not configured',
|
|
83
|
-
statusCode: 400,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
const secretKey = STAMHOOFD.STRIPE_SECRET_KEY;
|
|
87
|
-
|
|
88
|
-
const start = request.body.start;
|
|
89
|
-
const end = request.body.end;
|
|
90
|
-
|
|
91
|
-
const item = PayoutExportStatus.create({
|
|
92
|
-
start,
|
|
93
|
-
end,
|
|
94
|
-
count: 0,
|
|
95
|
-
});
|
|
96
|
-
StripePayoutsExportEndpoint.queue.push(item);
|
|
97
|
-
|
|
98
|
-
// Schedule
|
|
99
|
-
QueueHandler.schedule('stripe-payout-export', async () => {
|
|
100
|
-
try {
|
|
101
|
-
const reporter = new StripePayoutReporter({
|
|
102
|
-
secretKey,
|
|
103
|
-
sellingOrganization: organization,
|
|
104
|
-
});
|
|
105
|
-
let count = 0;
|
|
106
|
-
reporter.callback = () => {
|
|
107
|
-
count++;
|
|
108
|
-
item.count = count;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
await reporter.build(start, end);
|
|
112
|
-
|
|
113
|
-
// Send the report to the user that requested it
|
|
114
|
-
await reporter.sendEmail({
|
|
115
|
-
to: [{ email: user.email, name: user.name }],
|
|
116
|
-
});
|
|
117
|
-
} finally {
|
|
118
|
-
// Remove from queue
|
|
119
|
-
StripePayoutsExportEndpoint.queue.splice(StripePayoutsExportEndpoint.queue.indexOf(item), 1);
|
|
120
|
-
}
|
|
121
|
-
}).catch(console.error);
|
|
122
|
-
|
|
123
|
-
return new Response(undefined);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { MolliePayment, OrganizationFactory, Payment } from '@stamhoofd/models';
|
|
2
|
-
import { PaymentMethod, PaymentProvider, PaymentStatus, PaymentType } from '@stamhoofd/structures';
|
|
3
|
-
import type { MollieMockPayment, MollieMockRefund } from '../../tests/helpers/MollieMocker.js';
|
|
4
|
-
import { MollieMocker } from '../../tests/helpers/MollieMocker.js';
|
|
5
|
-
import { checkMollieSettlementsFor } from './CheckSettlements.js';
|
|
6
|
-
|
|
7
|
-
describe('Helper.CheckSettlements', () => {
|
|
8
|
-
let mollieMocker: MollieMocker;
|
|
9
|
-
|
|
10
|
-
beforeAll(() => {
|
|
11
|
-
mollieMocker = new MollieMocker();
|
|
12
|
-
mollieMocker.start();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterAll(() => {
|
|
16
|
-
mollieMocker.stop();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
mollieMocker.reset();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Create an organization with a Mollie token, a succeeded Mollie payment and a Mollie refund
|
|
25
|
-
* payment reversing it. Both are linked to their Mollie ids (tr_... / re_...) like the real crons do.
|
|
26
|
-
*/
|
|
27
|
-
const init = async () => {
|
|
28
|
-
const organization = await new OrganizationFactory({}).create();
|
|
29
|
-
const token = await mollieMocker.setupToken(organization);
|
|
30
|
-
|
|
31
|
-
// Source payment
|
|
32
|
-
const payment = new Payment();
|
|
33
|
-
payment.organizationId = organization.id;
|
|
34
|
-
payment.method = PaymentMethod.Bancontact;
|
|
35
|
-
payment.provider = PaymentProvider.Mollie;
|
|
36
|
-
payment.status = PaymentStatus.Succeeded;
|
|
37
|
-
payment.type = PaymentType.Payment;
|
|
38
|
-
payment.price = 50_0000;
|
|
39
|
-
payment.paidAt = new Date();
|
|
40
|
-
await payment.save();
|
|
41
|
-
|
|
42
|
-
const mockPayment: MollieMockPayment = {
|
|
43
|
-
id: mollieMocker.createId('tr'),
|
|
44
|
-
status: 'paid',
|
|
45
|
-
amount: { currency: 'EUR', value: '50.00' },
|
|
46
|
-
internalPaymentId: payment.id,
|
|
47
|
-
redirectUrl: null,
|
|
48
|
-
sequenceType: 'oneoff',
|
|
49
|
-
customerId: null,
|
|
50
|
-
mandateId: null,
|
|
51
|
-
isCancelable: false,
|
|
52
|
-
details: null,
|
|
53
|
-
};
|
|
54
|
-
mollieMocker.payments.push(mockPayment);
|
|
55
|
-
|
|
56
|
-
const paymentLink = new MolliePayment();
|
|
57
|
-
paymentLink.paymentId = payment.id;
|
|
58
|
-
paymentLink.mollieId = mockPayment.id;
|
|
59
|
-
await paymentLink.save();
|
|
60
|
-
|
|
61
|
-
// Refund payment reversing the source payment
|
|
62
|
-
const refundPayment = new Payment();
|
|
63
|
-
refundPayment.organizationId = organization.id;
|
|
64
|
-
refundPayment.method = PaymentMethod.Bancontact;
|
|
65
|
-
refundPayment.provider = PaymentProvider.Mollie;
|
|
66
|
-
refundPayment.status = PaymentStatus.Succeeded;
|
|
67
|
-
refundPayment.type = PaymentType.Refund;
|
|
68
|
-
refundPayment.price = -20_0000;
|
|
69
|
-
refundPayment.reversingPaymentId = payment.id;
|
|
70
|
-
refundPayment.paidAt = new Date();
|
|
71
|
-
await refundPayment.save();
|
|
72
|
-
|
|
73
|
-
const mockRefund = mollieMocker.createRefund(mockPayment, { value: '20.00', status: 'refunded' });
|
|
74
|
-
|
|
75
|
-
const refundLink = new MolliePayment();
|
|
76
|
-
refundLink.paymentId = refundPayment.id;
|
|
77
|
-
refundLink.mollieId = mockRefund.id;
|
|
78
|
-
await refundLink.save();
|
|
79
|
-
|
|
80
|
-
return { organization, token, payment, refundPayment, mockPayment, mockRefund };
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const runCron = async (token: { accessToken: string }) => {
|
|
84
|
-
await checkMollieSettlementsFor(token.accessToken, true);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Add a Mollie chargeback payment reversing the given source payment, linked to its Mollie
|
|
89
|
-
* chargeback id (chb_...) like the mollie-chargebacks cron does.
|
|
90
|
-
*/
|
|
91
|
-
const addChargeback = async (organizationId: string, sourcePayment: Payment, mockPayment: MollieMockPayment) => {
|
|
92
|
-
const chargebackPayment = new Payment();
|
|
93
|
-
chargebackPayment.organizationId = organizationId;
|
|
94
|
-
chargebackPayment.method = PaymentMethod.Bancontact;
|
|
95
|
-
chargebackPayment.provider = PaymentProvider.Mollie;
|
|
96
|
-
chargebackPayment.status = PaymentStatus.Succeeded;
|
|
97
|
-
chargebackPayment.type = PaymentType.Chargeback;
|
|
98
|
-
chargebackPayment.price = -sourcePayment.price;
|
|
99
|
-
chargebackPayment.reversingPaymentId = sourcePayment.id;
|
|
100
|
-
chargebackPayment.paidAt = new Date();
|
|
101
|
-
await chargebackPayment.save();
|
|
102
|
-
|
|
103
|
-
const mockChargeback = mollieMocker.createChargeback(mockPayment);
|
|
104
|
-
|
|
105
|
-
const chargebackLink = new MolliePayment();
|
|
106
|
-
chargebackLink.paymentId = chargebackPayment.id;
|
|
107
|
-
chargebackLink.mollieId = mockChargeback.id;
|
|
108
|
-
await chargebackLink.save();
|
|
109
|
-
|
|
110
|
-
return { chargebackPayment, mockChargeback };
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
test('The settlement of a refund settled at Mollie is stored on the refund payment', async () => {
|
|
114
|
-
const { token, payment, refundPayment, mockPayment, mockRefund } = await init();
|
|
115
|
-
|
|
116
|
-
const settlement = mollieMocker.createSettlement({
|
|
117
|
-
payments: [mockPayment],
|
|
118
|
-
refunds: [mockRefund],
|
|
119
|
-
value: '100.00',
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
await runCron(token);
|
|
123
|
-
|
|
124
|
-
// The source payment gets the settlement metadata (existing behaviour)
|
|
125
|
-
const updatedPayment = await Payment.getByID(payment.id);
|
|
126
|
-
expect(updatedPayment!.settlement).toMatchObject({
|
|
127
|
-
id: settlement.id,
|
|
128
|
-
reference: settlement.reference,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// The refund payment gets the same settlement metadata (new behaviour)
|
|
132
|
-
const updatedRefund = await Payment.getByID(refundPayment.id);
|
|
133
|
-
expect(updatedRefund!.settlement).toMatchObject({
|
|
134
|
-
id: settlement.id,
|
|
135
|
-
reference: settlement.reference,
|
|
136
|
-
amount: 100_0000,
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('A refund that is not part of any settlement keeps no settlement', async () => {
|
|
141
|
-
const { token, refundPayment, mockPayment } = await init();
|
|
142
|
-
|
|
143
|
-
// A settlement that only contains the source payment, not the refund
|
|
144
|
-
mollieMocker.createSettlement({ payments: [mockPayment], value: '50.00' });
|
|
145
|
-
|
|
146
|
-
await runCron(token);
|
|
147
|
-
|
|
148
|
-
const updatedRefund = await Payment.getByID(refundPayment.id);
|
|
149
|
-
expect(updatedRefund!.settlement).toBeNull();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('The settlement of a chargeback settled at Mollie is stored on the chargeback payment', async () => {
|
|
153
|
-
const { organization, token, payment, mockPayment } = await init();
|
|
154
|
-
const { chargebackPayment, mockChargeback } = await addChargeback(organization.id, payment, mockPayment);
|
|
155
|
-
|
|
156
|
-
const settlement = mollieMocker.createSettlement({
|
|
157
|
-
payments: [mockPayment],
|
|
158
|
-
chargebacks: [mockChargeback],
|
|
159
|
-
value: '100.00',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
await runCron(token);
|
|
163
|
-
|
|
164
|
-
const updatedChargeback = await Payment.getByID(chargebackPayment.id);
|
|
165
|
-
expect(updatedChargeback!.settlement).toMatchObject({
|
|
166
|
-
id: settlement.id,
|
|
167
|
-
reference: settlement.reference,
|
|
168
|
-
amount: 100_0000,
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test('An unlinked refund entry in a settlement is skipped without affecting the known refund', async () => {
|
|
173
|
-
const { token, refundPayment, mockPayment, mockRefund } = await init();
|
|
174
|
-
|
|
175
|
-
// A refund that belongs to a different system: it exists at Mollie but has no MolliePayment link
|
|
176
|
-
const unlinkedRefund: MollieMockRefund = mollieMocker.createRefund(mockPayment, { value: '5.00', status: 'refunded' });
|
|
177
|
-
|
|
178
|
-
const settlement = mollieMocker.createSettlement({
|
|
179
|
-
payments: [mockPayment],
|
|
180
|
-
refunds: [unlinkedRefund, mockRefund],
|
|
181
|
-
value: '100.00',
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
await runCron(token);
|
|
185
|
-
|
|
186
|
-
// The known refund still gets its settlement, the unlinked one is silently ignored
|
|
187
|
-
const updatedRefund = await Payment.getByID(refundPayment.id);
|
|
188
|
-
expect(updatedRefund!.settlement).toMatchObject({ id: settlement.id });
|
|
189
|
-
});
|
|
190
|
-
});
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
import { MolliePayment, MollieToken, Order, Organization, PayconiqPayment, Payment, StripeAccount } from '@stamhoofd/models';
|
|
2
|
-
import { Settlement } from '@stamhoofd/structures';
|
|
3
|
-
import axios from 'axios';
|
|
4
|
-
|
|
5
|
-
import { StripePayoutChecker } from './StripePayoutChecker.js';
|
|
6
|
-
|
|
7
|
-
type MollieSettlement = {
|
|
8
|
-
id: string;
|
|
9
|
-
reference: string;
|
|
10
|
-
createdAt: string;
|
|
11
|
-
settledAt: string;
|
|
12
|
-
status: 'open' | 'pending' | 'paidout' | 'failed';
|
|
13
|
-
amount: {
|
|
14
|
-
currenty: string;
|
|
15
|
-
value: string;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Both payments (tr_...) and refunds (re_...) settled in a settlement are matched to a local
|
|
21
|
-
* payment through the mollieId of their MolliePayment link, so we only need their id here.
|
|
22
|
-
*/
|
|
23
|
-
type MollieSettlementEntryJSON = {
|
|
24
|
-
id: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
let lastSettlementCheck: Date | null = null;
|
|
28
|
-
|
|
29
|
-
export async function checkAllStripePayouts(checkAll = false) {
|
|
30
|
-
if (STAMHOOFD.environment !== 'production' || !STAMHOOFD.STRIPE_SECRET_KEY) {
|
|
31
|
-
console.log('Skip settlement check');
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Stripe payouts
|
|
36
|
-
const stripeAccounts = await StripeAccount.where({ status: 'active' });
|
|
37
|
-
for (const account of stripeAccounts) {
|
|
38
|
-
try {
|
|
39
|
-
console.log('Checking settlements for ', account.accountId);
|
|
40
|
-
|
|
41
|
-
const checker = new StripePayoutChecker({
|
|
42
|
-
secretKey: STAMHOOFD.STRIPE_SECRET_KEY,
|
|
43
|
-
stripeAccount: account.accountId,
|
|
44
|
-
});
|
|
45
|
-
await checker.checkSettlements(checkAll);
|
|
46
|
-
} catch (e) {
|
|
47
|
-
console.error(e);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function checkSettlements(checkAll = false) {
|
|
53
|
-
if (STAMHOOFD.environment !== 'production') {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!checkAll && lastSettlementCheck && (lastSettlementCheck > new Date(new Date().getTime() - 24 * 60 * 60 * 1000))) {
|
|
58
|
-
console.log('Skip settlement check');
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log('Checking settlements...');
|
|
63
|
-
lastSettlementCheck = new Date();
|
|
64
|
-
|
|
65
|
-
// Mollie payment is required
|
|
66
|
-
const token = STAMHOOFD.MOLLIE_ORGANIZATION_TOKEN;
|
|
67
|
-
if (!token) {
|
|
68
|
-
console.error('Missing mollie organization token');
|
|
69
|
-
} else {
|
|
70
|
-
await checkMollieSettlementsFor(token, checkAll);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Loop all mollie tokens created after given date (when settlement permission was added)
|
|
74
|
-
try {
|
|
75
|
-
// Stripe payouts
|
|
76
|
-
await checkAllStripePayouts(checkAll);
|
|
77
|
-
|
|
78
|
-
const mollieTokens = await MollieToken.all();
|
|
79
|
-
for (const token of mollieTokens) {
|
|
80
|
-
if (token.createdAt < new Date(2021, 8 /* september! */, 8)) {
|
|
81
|
-
console.log('Skipped mollie token that is too old');
|
|
82
|
-
} else {
|
|
83
|
-
try {
|
|
84
|
-
await token.refreshIfNeeded();
|
|
85
|
-
await checkMollieSettlementsFor(token.accessToken, checkAll);
|
|
86
|
-
} catch (e) {
|
|
87
|
-
console.error(e);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.error(e);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check settlements once a week on tuesday morning/night
|
|
97
|
-
export async function checkMollieSettlementsFor(token: string, checkAll = false) {
|
|
98
|
-
// Check last 2 weeks + 3 day margin, unless we check them all
|
|
99
|
-
const d = new Date();
|
|
100
|
-
d.setDate(d.getDate() - 17);
|
|
101
|
-
|
|
102
|
-
console.log('Checking settlements for given token...');
|
|
103
|
-
|
|
104
|
-
// Loop all organizations with online paymetns the last week
|
|
105
|
-
try {
|
|
106
|
-
const request = await axios.get('https://api.mollie.com/v2/settlements?limit=' + (checkAll ? 250 : 14), {
|
|
107
|
-
headers: {
|
|
108
|
-
Authorization: 'Bearer ' + token,
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
if (request.status === 200) {
|
|
112
|
-
// get data
|
|
113
|
-
try {
|
|
114
|
-
const data = request.data;
|
|
115
|
-
// Read the data
|
|
116
|
-
|
|
117
|
-
if (data._embedded?.settlements) {
|
|
118
|
-
const settlements = data._embedded.settlements as MollieSettlement[];
|
|
119
|
-
|
|
120
|
-
for (const settlement of settlements) {
|
|
121
|
-
if (settlement.settledAt === null) {
|
|
122
|
-
// Skip: this is the open settlement
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const settledAt = new Date(settlement.settledAt);
|
|
127
|
-
|
|
128
|
-
if (isNaN(settledAt.getTime())) {
|
|
129
|
-
console.error('Received an invalid settledAt from Mollie', settlement, 'for token', token);
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (checkAll || settledAt > d) {
|
|
134
|
-
await updateSettlement(token, settlement);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
console.error('Unreadable settlements');
|
|
139
|
-
}
|
|
140
|
-
} catch (e) {
|
|
141
|
-
console.error(request.data);
|
|
142
|
-
throw e;
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
console.error('Failed to fetch settlements');
|
|
146
|
-
console.error(request.data);
|
|
147
|
-
}
|
|
148
|
-
} catch (e) {
|
|
149
|
-
console.error(e);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function updateSettlement(token: string, settlement: MollieSettlement) {
|
|
154
|
-
// Regular payments settled in this settlement
|
|
155
|
-
await updateSettlementResource(token, settlement, 'payments');
|
|
156
|
-
|
|
157
|
-
// Refunds settled in this settlement. These are negative entries linked to a refund payment
|
|
158
|
-
// (created by the mollie-refunds cron), so we can set their settlement metadata too.
|
|
159
|
-
await updateSettlementResource(token, settlement, 'refunds');
|
|
160
|
-
|
|
161
|
-
// Chargebacks settled in this settlement. Like refunds, these are negative entries linked to a
|
|
162
|
-
// chargeback payment (created by the mollie-chargebacks cron).
|
|
163
|
-
await updateSettlementResource(token, settlement, 'chargebacks');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Loop over all entries (payments, refunds or chargebacks) that are part of a settlement and set the
|
|
168
|
-
* settlement metadata on the matching local payment. Every resource exposes its entries under
|
|
169
|
-
* `_embedded[resource]` and uses the same pagination, so they share this logic.
|
|
170
|
-
*/
|
|
171
|
-
async function updateSettlementResource(token: string, settlement: MollieSettlement, resource: 'payments' | 'refunds' | 'chargebacks', fromId?: string) {
|
|
172
|
-
const limit = 250;
|
|
173
|
-
|
|
174
|
-
const request = await axios.get('https://api.mollie.com/v2/settlements/' + settlement.id + '/' + resource + '?limit=' + limit + (fromId ? ('&from=' + encodeURIComponent(fromId)) : ''), {
|
|
175
|
-
headers: {
|
|
176
|
-
Authorization: 'Bearer ' + token,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (request.status === 200) {
|
|
181
|
-
const entries = request.data._embedded[resource] as MollieSettlementEntryJSON[];
|
|
182
|
-
|
|
183
|
-
for (const entry of entries) {
|
|
184
|
-
await applySettlementToPayment(settlement, entry.id);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Check next page
|
|
188
|
-
if (request.data._links.next && entries.length > 0) {
|
|
189
|
-
await updateSettlementResource(token, settlement, resource, entries[entries.length - 1].id);
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
console.error(request.data);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Find the local payment linked to a Mollie payment or refund id and store the settlement metadata.
|
|
198
|
-
*/
|
|
199
|
-
async function applySettlementToPayment(settlement: MollieSettlement, mollieId: string) {
|
|
200
|
-
// Search payment
|
|
201
|
-
const mps = await MolliePayment.where({ mollieId });
|
|
202
|
-
if (mps.length === 1) {
|
|
203
|
-
const mp = mps[0];
|
|
204
|
-
const payment = await Payment.getByID(mp.paymentId);
|
|
205
|
-
if (payment) {
|
|
206
|
-
payment.settlement = Settlement.create({
|
|
207
|
-
id: settlement.id,
|
|
208
|
-
reference: settlement.reference,
|
|
209
|
-
settledAt: new Date(settlement.settledAt),
|
|
210
|
-
amount: Math.round(parseFloat(settlement.amount.value) * 100) * 100,
|
|
211
|
-
});
|
|
212
|
-
const saved = await payment.save();
|
|
213
|
-
|
|
214
|
-
if (saved) {
|
|
215
|
-
// Mark order as 'updated', or the frontend won't pull in the updates
|
|
216
|
-
const order = await Order.getForPayment(null, payment.id);
|
|
217
|
-
if (order) {
|
|
218
|
-
order.updatedAt = new Date();
|
|
219
|
-
order.forceSaveProperty('updatedAt');
|
|
220
|
-
await order.save();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// TODO: Mark registrations as 'saved'
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (STAMHOOFD.environment === 'development') {
|
|
227
|
-
console.log('Updated settlement of payment ' + payment.id);
|
|
228
|
-
console.log(payment.settlement);
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
console.log('Missing payment ' + mp.paymentId);
|
|
232
|
-
}
|
|
233
|
-
} else {
|
|
234
|
-
// Probably a payment in a different system/platform
|
|
235
|
-
// console.log("No mollie payment found for id "+mollieId)
|
|
236
|
-
}
|
|
237
|
-
}
|