@stamhoofd/backend 2.143.1 → 2.145.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 +19 -19
- package/src/crons/fake-settlements.test.ts +13 -11
- package/src/crons/fake-settlements.ts +22 -38
- package/src/crons/mollie-chargebacks.ts +31 -29
- package/src/crons/mollie-refunds.ts +61 -59
- package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
- package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
- package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
- package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
- package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
- package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
- package/src/endpoints/auth/MFA.security.test.ts +9 -7
- package/src/endpoints/auth/MFA.test.ts +42 -15
- package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
- package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
- package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
- package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
- package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
- package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
- package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.test.ts +2 -1
- package/src/endpoints/global/files/UploadFile.ts +53 -25
- package/src/endpoints/global/files/UploadImage.test.ts +2 -1
- package/src/endpoints/global/files/UploadImage.ts +32 -6
- package/src/endpoints/global/files/upload-security.test.ts +3 -2
- package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
- package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
- package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
- package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
- package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
- package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
- package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
- package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
- package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
- package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
- package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
- package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
- package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
- package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
- package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
- package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
- package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
- package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
- package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
- package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
- package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
- package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
- package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
- package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
- package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
- package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
- package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
- package/src/helpers/ApplicationFeeInvoicer.ts +1 -1
- package/src/helpers/Context.ts +14 -21
- package/src/helpers/ContextStorage.ts +5 -0
- package/src/helpers/MollieSettlementSync.test.ts +94 -41
- package/src/helpers/MollieSettlementSync.ts +174 -78
- package/src/helpers/StripeSettlementSync.test.ts +0 -85
- package/src/helpers/StripeSettlementSync.ts +47 -70
- package/src/helpers/TwoFactorHelper.ts +21 -22
- package/src/helpers/streamForBreakdown.ts +1 -1
- package/src/services/AdminSessionService.ts +5 -3
- package/src/services/InvoiceService.ts +2 -2
- package/src/services/MollieService.ts +168 -126
- package/src/services/PaymentService.ts +2 -2
- package/src/services/SSOService.test.ts +25 -1
- package/src/services/SSOService.ts +17 -6
- package/src/services/SessionService.test.ts +319 -0
- package/src/services/SessionService.ts +357 -0
- package/src/services/SettlementService.test.ts +13 -85
- package/src/services/SettlementService.ts +5 -113
- package/src/services/WebshopCrowdfundingService.test.ts +4 -3
- package/src/sql-filters/balance-items.ts +10 -0
- package/src/sql-filters/registrations.ts +59 -1
- package/tests/actions/patchUserMember.ts +2 -1
- package/tests/e2e/api-rate-limits.test.ts +2 -1
- package/tests/e2e/bundle-discounts.test.ts +4 -3
- package/tests/e2e/charge-members.test.ts +6 -5
- package/tests/e2e/documents.test.ts +4 -3
- package/tests/e2e/private-files.test.ts +9 -8
- package/tests/e2e/register.test.ts +4 -3
- package/tests/e2e/stock.test.ts +4 -3
- package/tests/e2e/tickets.test.ts +4 -3
- package/tests/helpers/ExportSlice.ts +2 -1
- package/tests/helpers/MollieMocker.ts +173 -38
- package/tests/init/initAdmin.ts +2 -1
- package/tests/init/initPlatformAdmin.ts +2 -1
- package/src/helpers/ViesHelper.test.ts +0 -401
- package/src/helpers/ViesHelper.ts +0 -215
- package/src/services/PeppolDirectoryService.test.ts +0 -139
- package/src/services/PeppolDirectoryService.ts +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stamhoofd/backend",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.145.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -58,27 +58,27 @@
|
|
|
58
58
|
"@aws-sdk/client-sqs": "3.1090.0",
|
|
59
59
|
"@aws-sdk/s3-request-presigner": "3.1090.0",
|
|
60
60
|
"@bwip-js/node": "4.11.2",
|
|
61
|
-
"@mollie/api-client": "4.6.0",
|
|
62
61
|
"@simonbackx/simple-database": "1.37.1",
|
|
63
62
|
"@simonbackx/simple-encoding": "2.26.11",
|
|
64
63
|
"@simonbackx/simple-endpoints": "1.22.0",
|
|
65
64
|
"@simonbackx/simple-errors": "1.5.0",
|
|
66
65
|
"@simonbackx/simple-logging": "1.0.1",
|
|
67
66
|
"@simplewebauthn/server": "13.3.2",
|
|
68
|
-
"@stamhoofd/backend-env": "2.
|
|
69
|
-
"@stamhoofd/backend-i18n": "2.
|
|
70
|
-
"@stamhoofd/backend-middleware": "2.
|
|
71
|
-
"@stamhoofd/crons": "2.
|
|
72
|
-
"@stamhoofd/email": "2.
|
|
73
|
-
"@stamhoofd/excel-writer": "2.
|
|
74
|
-
"@stamhoofd/logging": "2.
|
|
75
|
-
"@stamhoofd/models": "2.
|
|
76
|
-
"@stamhoofd/object-differ": "2.
|
|
77
|
-
"@stamhoofd/queues": "2.
|
|
78
|
-
"@stamhoofd/sql": "2.
|
|
79
|
-
"@stamhoofd/structures": "2.
|
|
80
|
-
"@stamhoofd/types": "2.
|
|
81
|
-
"@stamhoofd/utility": "2.
|
|
67
|
+
"@stamhoofd/backend-env": "2.145.0",
|
|
68
|
+
"@stamhoofd/backend-i18n": "2.145.0",
|
|
69
|
+
"@stamhoofd/backend-middleware": "2.145.0",
|
|
70
|
+
"@stamhoofd/crons": "2.145.0",
|
|
71
|
+
"@stamhoofd/email": "2.145.0",
|
|
72
|
+
"@stamhoofd/excel-writer": "2.145.0",
|
|
73
|
+
"@stamhoofd/logging": "2.145.0",
|
|
74
|
+
"@stamhoofd/models": "2.145.0",
|
|
75
|
+
"@stamhoofd/object-differ": "2.145.0",
|
|
76
|
+
"@stamhoofd/queues": "2.145.0",
|
|
77
|
+
"@stamhoofd/sql": "2.145.0",
|
|
78
|
+
"@stamhoofd/structures": "2.145.0",
|
|
79
|
+
"@stamhoofd/types": "2.145.0",
|
|
80
|
+
"@stamhoofd/utility": "2.145.0",
|
|
81
|
+
"@stamhoofd/vies": "2.145.0",
|
|
82
82
|
"archiver": "7.0.1",
|
|
83
83
|
"argon2": "0.44.0",
|
|
84
84
|
"axios": "1.18.1",
|
|
@@ -89,11 +89,11 @@
|
|
|
89
89
|
"handlebars": "4.7.9",
|
|
90
90
|
"jose": "5.10.0",
|
|
91
91
|
"jsonwebtoken": "9.0.3",
|
|
92
|
-
"jsvat-next": "3.0.4",
|
|
93
92
|
"libphonenumber-js": "1.13.9",
|
|
94
93
|
"luxon": "3.7.2",
|
|
95
94
|
"mailparser": "3.9.14",
|
|
96
95
|
"mockdate": "3.0.5",
|
|
96
|
+
"mollie-api-typescript": "1.12.23",
|
|
97
97
|
"mysql2": "3.23.1",
|
|
98
98
|
"node-rsa": "2.0.0",
|
|
99
99
|
"openid-client": "5.7.1",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"uuid": "14.0.1"
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@stamhoofd/test-utils": "2.
|
|
105
|
+
"@stamhoofd/test-utils": "2.145.0",
|
|
106
106
|
"@types/cookie": "0.6.0",
|
|
107
107
|
"@types/luxon": "3.7.2",
|
|
108
108
|
"@types/mailparser": "3.4.6",
|
|
@@ -117,5 +117,5 @@
|
|
|
117
117
|
"publishConfig": {
|
|
118
118
|
"access": "public"
|
|
119
119
|
},
|
|
120
|
-
"gitHead": "
|
|
120
|
+
"gitHead": "ee58cd6383c03df39218d8589679638019241e15"
|
|
121
121
|
}
|
|
@@ -90,8 +90,8 @@ describe('Cron.fake-settlements', () => {
|
|
|
90
90
|
settledAt: Formatter.luxon(inWeek(2)).startOf('week').plus({ weeks: 1, days: 2 }).toJSDate(),
|
|
91
91
|
|
|
92
92
|
// Refunds are taken out of the payout, just like the provider does, and so are the
|
|
93
|
-
// fake Mollie
|
|
94
|
-
amount: 65_0000 -
|
|
93
|
+
// fake Mollie fees: 3 payments x 0.363
|
|
94
|
+
amount: 65_0000 - 3 * 36_30,
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
// All payments of the week point to the same payout
|
|
@@ -135,8 +135,7 @@ describe('Cron.fake-settlements', () => {
|
|
|
135
135
|
];
|
|
136
136
|
|
|
137
137
|
// Same week, but four accounts: four payouts, each holding only its own money. The Mollie
|
|
138
|
-
// payouts are reduced by the fake
|
|
139
|
-
// carry no fees here
|
|
138
|
+
// payouts are reduced by the fake fee of one payment; the Stripe payments carry no fees here
|
|
140
139
|
expect(settlements.map(s => s!.amount)).toEqual([50_0000 - 36_30, 30_0000 - 36_30, 20_0000, 15_0000]);
|
|
141
140
|
expect(new Set(settlements.map(s => s!.id)).size).toBe(4);
|
|
142
141
|
expect(new Set(settlements.map(s => s!.reference)).size).toBe(4);
|
|
@@ -192,7 +191,7 @@ describe('Cron.fake-settlements', () => {
|
|
|
192
191
|
return await Settlement.select().where('externalId', reference!.id).first(true);
|
|
193
192
|
};
|
|
194
193
|
|
|
195
|
-
test('A Mollie payout stores payment lines and
|
|
194
|
+
test('A Mollie payout stores payment lines and fee rows, and reconciles to zero', async () => {
|
|
196
195
|
const first = await createPayment({ price: 50_0000 });
|
|
197
196
|
const second = await createPayment({ price: -10_0000, type: PaymentType.Refund });
|
|
198
197
|
|
|
@@ -201,7 +200,7 @@ describe('Cron.fake-settlements', () => {
|
|
|
201
200
|
const settlement = await getSettlementRow(first);
|
|
202
201
|
expect(settlement.provider).toBe(PaymentProvider.Mollie);
|
|
203
202
|
expect(settlement.organizationId).toBe(organization.id);
|
|
204
|
-
expect(settlement.amount).toBe(40_0000 -
|
|
203
|
+
expect(settlement.amount).toBe(40_0000 - 2 * 36_30);
|
|
205
204
|
expect(settlement.unexplainedAmount).toBe(0);
|
|
206
205
|
expect(settlement.syncedAt).not.toBeNull();
|
|
207
206
|
|
|
@@ -211,12 +210,15 @@ describe('Cron.fake-settlements', () => {
|
|
|
211
210
|
[second.id, -10_0000],
|
|
212
211
|
].sort());
|
|
213
212
|
|
|
214
|
-
const settledMonth = settlement.settledAt.getFullYear() + '-' + (settlement.settledAt.getMonth() + 1).toString().padStart(2, '0');
|
|
215
213
|
const charges = await SettlementCharge.select().where('settlementId', settlement.id).fetch();
|
|
216
|
-
expect(charges.map(c =>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
]);
|
|
214
|
+
expect(charges.map(c => [c.paymentId, c.type, c.amount]).sort()).toEqual([
|
|
215
|
+
[first.id, SettlementChargeType.ProviderTransactionFee, -36_30],
|
|
216
|
+
[second.id, SettlementChargeType.ProviderTransactionFee, -36_30],
|
|
217
|
+
].sort());
|
|
218
|
+
|
|
219
|
+
// Each settled payment holds its own fee
|
|
220
|
+
expect((await Payment.getByID(first.id))!.transferFee).toBe(36_30);
|
|
221
|
+
expect((await Payment.getByID(second.id))!.transferFee).toBe(36_30);
|
|
220
222
|
});
|
|
221
223
|
|
|
222
224
|
test('A Stripe application fee is mirrored on the payout and a monthly platform payout', async () => {
|
|
@@ -34,10 +34,9 @@ const PAYOUT_DELAY_DAYS = 2;
|
|
|
34
34
|
const BATCH_SIZE = 1000;
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Fake Mollie transaction fee per settled payment,
|
|
37
|
+
* Fake Mollie transaction fee per settled payment, including VAT.
|
|
38
38
|
*/
|
|
39
|
-
const MOLLIE_FEE_PER_PAYMENT =
|
|
40
|
-
const MOLLIE_FEE_VAT_PERCENTAGE = 21;
|
|
39
|
+
const MOLLIE_FEE_PER_PAYMENT = 36_30;
|
|
41
40
|
|
|
42
41
|
export async function createFakeSettlements() {
|
|
43
42
|
if (STAMHOOFD.environment !== 'development') {
|
|
@@ -183,8 +182,7 @@ async function settleWeek(payments: Payment[], weekStart: DateTime, settledAt: D
|
|
|
183
182
|
return total + serviceFee + transferFee;
|
|
184
183
|
}, 0);
|
|
185
184
|
} else if (provider === PaymentProvider.Mollie) {
|
|
186
|
-
|
|
187
|
-
fees = net + Math.round(net * MOLLIE_FEE_VAT_PERCENTAGE / 100);
|
|
185
|
+
fees = MOLLIE_FEE_PER_PAYMENT * group.length;
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
const grossAmount = group.reduce((total, payment) => total + payment.price, 0);
|
|
@@ -223,7 +221,7 @@ async function settleWeek(payments: Payment[], weekStart: DateTime, settledAt: D
|
|
|
223
221
|
if (provider === PaymentProvider.Stripe) {
|
|
224
222
|
await createStripeFeeRows(settlement, group, settledAt, platformOrganizationId);
|
|
225
223
|
} else if (provider === PaymentProvider.Mollie) {
|
|
226
|
-
await
|
|
224
|
+
await createMollieFeeRows(settlement, group);
|
|
227
225
|
}
|
|
228
226
|
|
|
229
227
|
await finishFakeSync(settlement);
|
|
@@ -322,43 +320,29 @@ async function createStripeFeeRows(settlement: Settlement, group: Payment[], set
|
|
|
322
320
|
|
|
323
321
|
// A dev database can contain fee payments (e.g. copied history): keep their derived lines
|
|
324
322
|
// in sync like a real payout walk does
|
|
325
|
-
await SettlementService.
|
|
323
|
+
await SettlementService.updatePaymentSettlementsForApplicationFeeBalanceItems([...invoicedFeeBalanceItemIds]);
|
|
326
324
|
|
|
327
325
|
await finishFakeSync(platformSettlement);
|
|
328
326
|
}
|
|
329
327
|
|
|
330
328
|
/**
|
|
331
|
-
* Mollie deducts its
|
|
332
|
-
* the settlement reconciles to 0
|
|
329
|
+
* Mollie deducts its fee per transaction: one fake fee charge per settled payment, linked to it
|
|
330
|
+
* like the real balance transaction walk stores, so the settlement reconciles to 0.
|
|
333
331
|
*/
|
|
334
|
-
async function
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
amount: -net,
|
|
347
|
-
settlementId: settlement.id,
|
|
348
|
-
organizationId: settlement.organizationId,
|
|
349
|
-
providerInvoiceId,
|
|
350
|
-
description: 'DEV Mollie transactiekosten',
|
|
351
|
-
occurredAt: settledAt,
|
|
352
|
-
});
|
|
332
|
+
async function createMollieFeeRows(settlement: Settlement, group: Payment[]) {
|
|
333
|
+
for (const payment of group) {
|
|
334
|
+
await SettlementService.upsertCharge({
|
|
335
|
+
type: SettlementChargeType.ProviderTransactionFee,
|
|
336
|
+
externalId: 'fake-baltr-' + payment.id,
|
|
337
|
+
amount: -MOLLIE_FEE_PER_PAYMENT,
|
|
338
|
+
settlementId: settlement.id,
|
|
339
|
+
paymentId: payment.id,
|
|
340
|
+
organizationId: settlement.organizationId,
|
|
341
|
+
description: 'DEV Mollie transactiekosten',
|
|
342
|
+
occurredAt: payment.paidAt!,
|
|
343
|
+
});
|
|
353
344
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
amount: -vat,
|
|
358
|
-
settlementId: settlement.id,
|
|
359
|
-
organizationId: settlement.organizationId,
|
|
360
|
-
providerInvoiceId,
|
|
361
|
-
description: 'DEV BTW op Mollie transactiekosten',
|
|
362
|
-
occurredAt: settledAt,
|
|
363
|
-
});
|
|
345
|
+
payment.transferFee = MOLLIE_FEE_PER_PAYMENT;
|
|
346
|
+
await payment.save();
|
|
347
|
+
}
|
|
364
348
|
}
|
|
@@ -42,40 +42,42 @@ export async function checkMollieChargebacksFor(service: MollieService, checkAll
|
|
|
42
42
|
// Check last 3 days
|
|
43
43
|
const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 4);
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
47
|
-
for await (const chargeback of service.client.chargebacks.iterate({ sort: 'desc', testmode: service.testMode } as any)) {
|
|
48
|
-
if (!checkAll && new Date(chargeback.createdAt) < offset) {
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
45
|
+
const pages = await service.client.chargebacks.all({ sort: 'desc', limit: 250 });
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
pagesLoop: for await (const page of pages) {
|
|
48
|
+
for (const chargeback of page.result.embedded.chargebacks) {
|
|
49
|
+
if (!checkAll && new Date(chargeback.createdAt) < offset) {
|
|
50
|
+
break pagesLoop;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if this chargeback has been handled
|
|
54
|
+
const existingChargeback = await MolliePayment.select().where('mollieId', chargeback.id).first(false);
|
|
55
|
+
if (existingChargeback) {
|
|
56
|
+
// We can break because all older chargebacks will also be handled
|
|
57
|
+
break pagesLoop;
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
if (chargeback.paymentId) {
|
|
61
|
+
const molliePayment = await MolliePayment.select().where('mollieId', chargeback.paymentId).first(false);
|
|
62
|
+
if (molliePayment) {
|
|
63
|
+
const payment = await Payment.getByID(molliePayment.paymentId);
|
|
64
|
+
if (payment) {
|
|
65
|
+
try {
|
|
66
|
+
const amount = Math.round(parseFloat(chargeback.amount.value) * 100) * 100;
|
|
67
|
+
const createdPayment = await PaymentService.registerChargeback(payment, amount, new Date(chargeback.createdAt));
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
// Link Mollie chargeback ID (so we can set settlement later in the settlements cron)
|
|
70
|
+
const molliePayment = new MolliePayment();
|
|
71
|
+
molliePayment.paymentId = createdPayment.id;
|
|
72
|
+
molliePayment.mollieId = chargeback.id;
|
|
73
|
+
await molliePayment.save();
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('Failed to register chargeback ' + chargeback.id, e);
|
|
76
|
+
}
|
|
75
77
|
}
|
|
78
|
+
} else {
|
|
79
|
+
console.error('Invalid chargeback payment id ' + chargeback.paymentId + ', not found');
|
|
76
80
|
}
|
|
77
|
-
} else {
|
|
78
|
-
console.error('Invalid chargeback payment id ' + chargeback.paymentId + ', not found');
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { RefundStatus } from '@mollie/api-client';
|
|
2
1
|
import { registerCron } from '@stamhoofd/crons';
|
|
3
2
|
import { MolliePayment, MollieToken, Organization, Payment } from '@stamhoofd/models';
|
|
4
3
|
import { PaymentStatus, PaymentType } from '@stamhoofd/structures';
|
|
4
|
+
import type { ListEntityRefundStatus } from 'mollie-api-typescript/models';
|
|
5
5
|
import { MollieService } from '../services/MollieService.js';
|
|
6
6
|
import { PaymentService } from '../services/PaymentService.js';
|
|
7
7
|
|
|
@@ -49,73 +49,75 @@ export async function checkMollieRefundsFor(service: MollieService, checkAll = f
|
|
|
49
49
|
// Check last 7 days
|
|
50
50
|
const offset = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7);
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
54
|
-
for await (const refund of service.client.refunds.iterate({ sort: 'desc', testmode: service.testMode } as any)) {
|
|
55
|
-
const createdAt = new Date(refund.createdAt);
|
|
52
|
+
const pages = await service.client.refunds.all({ sort: 'desc', limit: 250 });
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
pagesLoop: for await (const page of pages) {
|
|
55
|
+
for (const refund of page.result.embedded.refunds) {
|
|
56
|
+
const createdAt = new Date(refund.createdAt);
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
if (createdAt < MOLLIE_REFUNDS_MINIMUM_DATE) {
|
|
59
|
+
break pagesLoop;
|
|
60
|
+
}
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// refunds can still fail or be canceled until they are actually executed.
|
|
69
|
-
const existingRefund = await MolliePayment.select().where('mollieId', refund.id).first(false);
|
|
70
|
-
if (existingRefund) {
|
|
71
|
-
await reconcileRefundStatus(existingRefund.paymentId, refund.status, createdAt);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
62
|
+
if (!checkAll && createdAt < offset) {
|
|
63
|
+
break pagesLoop;
|
|
64
|
+
}
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
// Check if this refund is already registered (refunds created via the API are linked on
|
|
67
|
+
// creation, so we cannot break here: an older refund created in the Mollie UI could still
|
|
68
|
+
// be unhandled). Registered refunds that are still pending locally are reconciled: Mollie
|
|
69
|
+
// refunds can still fail or be canceled until they are actually executed.
|
|
70
|
+
const existingRefund = await MolliePayment.select().where('mollieId', refund.id).first(false);
|
|
71
|
+
if (existingRefund) {
|
|
72
|
+
await reconcileRefundStatus(existingRefund.paymentId, refund.status, createdAt);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// the refund twice (the metadata is set in PaymentService.createRefund).
|
|
83
|
-
const metadata = refund.metadata as { refundPaymentId?: string } | null;
|
|
84
|
-
if (metadata?.refundPaymentId) {
|
|
85
|
-
const localRefund = await Payment.getByID(metadata.refundPaymentId);
|
|
86
|
-
if (localRefund && localRefund.type === PaymentType.Refund) {
|
|
87
|
-
console.error('Restoring missing link for Mollie refund ' + refund.id + ' to payment ' + localRefund.id);
|
|
88
|
-
const link = new MolliePayment();
|
|
89
|
-
link.paymentId = localRefund.id;
|
|
90
|
-
link.mollieId = refund.id;
|
|
91
|
-
await link.save();
|
|
92
|
-
|
|
93
|
-
await reconcileRefundStatus(localRefund.id, refund.status, createdAt);
|
|
76
|
+
// Refunds that were canceled or failed before we ever registered them can be ignored
|
|
77
|
+
if (refund.status === 'canceled' || refund.status === 'failed') {
|
|
94
78
|
continue;
|
|
95
79
|
}
|
|
96
|
-
}
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
81
|
+
// Safety net: when creating a refund via the API succeeded, but saving the link afterwards
|
|
82
|
+
// failed, the local refund payment already exists. Restore the link instead of registering
|
|
83
|
+
// the refund twice (the metadata is set in PaymentService.createRefund).
|
|
84
|
+
const metadata = refund.metadata as { refundPaymentId?: string } | null;
|
|
85
|
+
if (metadata?.refundPaymentId) {
|
|
86
|
+
const localRefund = await Payment.getByID(metadata.refundPaymentId);
|
|
87
|
+
if (localRefund && localRefund.type === PaymentType.Refund) {
|
|
88
|
+
console.error('Restoring missing link for Mollie refund ' + refund.id + ' to payment ' + localRefund.id);
|
|
89
|
+
const link = new MolliePayment();
|
|
90
|
+
link.paymentId = localRefund.id;
|
|
91
|
+
link.mollieId = refund.id;
|
|
92
|
+
await link.save();
|
|
93
|
+
|
|
94
|
+
await reconcileRefundStatus(localRefund.id, refund.status, createdAt);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (refund.paymentId) {
|
|
100
|
+
const molliePayment = await MolliePayment.select().where('mollieId', refund.paymentId).first(false);
|
|
101
|
+
if (molliePayment) {
|
|
102
|
+
const payment = await Payment.getByID(molliePayment.paymentId);
|
|
103
|
+
if (payment) {
|
|
104
|
+
try {
|
|
105
|
+
const amount = Math.round(parseFloat(refund.amount.value) * 100) * 100;
|
|
106
|
+
const status = MollieService.refundStatusToPaymentStatus(refund.status) === PaymentStatus.Succeeded ? PaymentStatus.Succeeded : PaymentStatus.Pending;
|
|
107
|
+
const createdPayment = await PaymentService.registerRefund(payment, amount, createdAt, status);
|
|
108
|
+
|
|
109
|
+
// Link Mollie refund ID (so we don't register it twice and can set the settlement later)
|
|
110
|
+
const molliePayment = new MolliePayment();
|
|
111
|
+
molliePayment.paymentId = createdPayment.id;
|
|
112
|
+
molliePayment.mollieId = refund.id;
|
|
113
|
+
await molliePayment.save();
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error('Failed to register refund ' + refund.id, e);
|
|
116
|
+
}
|
|
115
117
|
}
|
|
118
|
+
} else {
|
|
119
|
+
console.error('Invalid refund payment id ' + refund.paymentId + ', not found');
|
|
116
120
|
}
|
|
117
|
-
} else {
|
|
118
|
-
console.error('Invalid refund payment id ' + refund.paymentId + ', not found');
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
}
|
|
@@ -129,7 +131,7 @@ export async function checkMollieRefundsFor(service: MollieService, checkAll = f
|
|
|
129
131
|
* Update a locally registered refund that is still pending when Mollie reports a final status
|
|
130
132
|
* (refunded, failed or canceled).
|
|
131
133
|
*/
|
|
132
|
-
async function reconcileRefundStatus(paymentId: string, mollieStatus:
|
|
134
|
+
async function reconcileRefundStatus(paymentId: string, mollieStatus: ListEntityRefundStatus, createdAt: Date) {
|
|
133
135
|
const payment = await Payment.getByID(paymentId);
|
|
134
136
|
if (!payment || !payment.organizationId) {
|
|
135
137
|
return;
|
|
@@ -10,6 +10,7 @@ import { Organization as OrganizationStruct } from '@stamhoofd/structures';
|
|
|
10
10
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
11
|
import { AuthenticatedStructures } from '../../../helpers/AuthenticatedStructures.js';
|
|
12
12
|
import { Context } from '../../../helpers/Context.js';
|
|
13
|
+
import { SQL } from '@stamhoofd/sql';
|
|
13
14
|
|
|
14
15
|
type Params = Record<string, never>;
|
|
15
16
|
type Query = undefined;
|
|
@@ -64,13 +65,23 @@ export class PatchOrganizationsEndpoint extends Endpoint<Params, Query, Body, Re
|
|
|
64
65
|
|
|
65
66
|
// Financial records may not disappear together with the organization that had to pay them
|
|
66
67
|
const payerRecordCounts = await Promise.all([
|
|
67
|
-
BalanceItem.select().where('payingOrganizationId', id).
|
|
68
|
-
Payment.select().where('payingOrganizationId', id).
|
|
69
|
-
ApplicationFee.select().where('payingOrganizationId', id).
|
|
70
|
-
Invoice.select().where('payingOrganizationId', id).
|
|
68
|
+
BalanceItem.select().where('payingOrganizationId', id).first(false),
|
|
69
|
+
Payment.select().where('payingOrganizationId', id).first(false),
|
|
70
|
+
ApplicationFee.select().where('payingOrganizationId', id).first(false),
|
|
71
|
+
Invoice.select().where('payingOrganizationId', id).first(false),
|
|
72
|
+
Payment.select()
|
|
73
|
+
.where('organizationId', id)
|
|
74
|
+
.where(
|
|
75
|
+
SQL.where('serviceFeePayout', '!=', 0)
|
|
76
|
+
.or('serviceFeeManual', '!=', 0)
|
|
77
|
+
.or('serviceFeeManualCharged', '!=', 0)
|
|
78
|
+
.or('transferFeeManual', '!=', 0)
|
|
79
|
+
.or('transferFeeManualCharged', '!=', 0),
|
|
80
|
+
)
|
|
81
|
+
.first(false),
|
|
71
82
|
]);
|
|
72
83
|
|
|
73
|
-
if (payerRecordCounts.some(
|
|
84
|
+
if (payerRecordCounts.some(hasSome => hasSome !== null)) {
|
|
74
85
|
throw new SimpleError({
|
|
75
86
|
code: 'organization_has_financial_records',
|
|
76
87
|
message: 'Organization is still referenced as paying organization',
|