@stamhoofd/backend 2.144.0 → 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/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/UploadImage.test.ts +2 -1
- 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/Context.ts +13 -6
- package/src/helpers/ContextStorage.ts +5 -0
- package/src/helpers/StripeSettlementSync.ts +10 -7
- package/src/helpers/TwoFactorHelper.ts +21 -22
- 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/WebshopCrowdfundingService.test.ts +4 -3
- 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 +85 -23
- 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 -222
- package/src/services/PeppolDirectoryService.test.ts +0 -139
- package/src/services/PeppolDirectoryService.ts +0 -111
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { Mandate, Payment as MolliePaymentType } from '@mollie/api-client';
|
|
2
|
-
import { ApiMode, createMollieClient, MandateMethod, MandateStatus, MollieApiError, PaymentMethod as molliePaymentMethod, PaymentStatus as molliePaymentStatus, OnboardingStatus, ProfileStatus, RefundStatus, SequenceType } from '@mollie/api-client';
|
|
3
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
4
2
|
import type { Payment, User } from '@stamhoofd/models';
|
|
5
3
|
import type { Organization } from '@stamhoofd/models';
|
|
@@ -9,16 +7,22 @@ import { MollieOnboarding, MollieProfile, MollieProfileMode, MollieProfileStatus
|
|
|
9
7
|
import { PaymentMandate, PaymentMandateDetails, PaymentMandateStatus, PaymentMandateType } from '@stamhoofd/structures/PaymentMandate.js';
|
|
10
8
|
import { Formatter } from '@stamhoofd/utility';
|
|
11
9
|
import { DateTime } from 'luxon';
|
|
10
|
+
import { Client } from 'mollie-api-typescript';
|
|
11
|
+
import { ClientError, ErrorResponse } from 'mollie-api-typescript/models/errors';
|
|
12
|
+
import type { EntityRefundResponseStatus, ListEntityRefundStatus, ListMandateResponse, MandateResponse, PaymentResponse } from 'mollie-api-typescript/models';
|
|
12
13
|
import { Context } from '../helpers/Context.js';
|
|
13
14
|
|
|
14
15
|
export class MollieService {
|
|
15
|
-
client:
|
|
16
|
+
client: Client;
|
|
16
17
|
sellingOrganization: Organization;
|
|
17
18
|
createdAt: Date;
|
|
18
19
|
|
|
19
20
|
private constructor({ sellingOrganization, accessToken }: { sellingOrganization: Organization; accessToken: string }) {
|
|
20
21
|
this.sellingOrganization = sellingOrganization;
|
|
21
|
-
this.client =
|
|
22
|
+
this.client = new Client({
|
|
23
|
+
security: { advancedAccessToken: accessToken },
|
|
24
|
+
testmode: this.testMode,
|
|
25
|
+
});
|
|
22
26
|
this.createdAt = new Date();
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -58,21 +62,22 @@ export class MollieService {
|
|
|
58
62
|
if (this.sellingOrganization.privateMeta.mollieOnboarding && this.sellingOrganization.privateMeta.mollieOnboarding.status === MollieStatus.NeedsData) {
|
|
59
63
|
try {
|
|
60
64
|
await this.client.onboarding.submit({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
requestBody: {
|
|
66
|
+
organization: {
|
|
67
|
+
name: this.sellingOrganization.name,
|
|
68
|
+
address: {
|
|
69
|
+
streetAndNumber: this.sellingOrganization.address.street + ' ' + this.sellingOrganization.address.number,
|
|
70
|
+
postalCode: this.sellingOrganization.address.postalCode,
|
|
71
|
+
city: this.sellingOrganization.address.city,
|
|
72
|
+
country: this.sellingOrganization.address.country,
|
|
73
|
+
},
|
|
74
|
+
vatRegulation: 'shifted',
|
|
75
|
+
},
|
|
76
|
+
profile: {
|
|
77
|
+
name: this.sellingOrganization.name + ' via Stamhoofd',
|
|
78
|
+
description: $t(`%x1`),
|
|
79
|
+
businessCategory: 'SOCIAL_ASSOCIATIONS',
|
|
68
80
|
},
|
|
69
|
-
|
|
70
|
-
vatRegulation: 'shifted',
|
|
71
|
-
},
|
|
72
|
-
profile: {
|
|
73
|
-
name: this.sellingOrganization.name + ' via Stamhoofd',
|
|
74
|
-
description: $t(`%x1`),
|
|
75
|
-
categoryCode: 8398,
|
|
76
81
|
},
|
|
77
82
|
});
|
|
78
83
|
} catch (e) {
|
|
@@ -114,9 +119,9 @@ export class MollieService {
|
|
|
114
119
|
|
|
115
120
|
// check still valid
|
|
116
121
|
try {
|
|
117
|
-
const c = await this.client.customers.get(
|
|
122
|
+
const c = await this.client.customers.get({ customerId });
|
|
118
123
|
|
|
119
|
-
if ((c.mode ===
|
|
124
|
+
if ((c.mode === 'test') === this.testMode) {
|
|
120
125
|
this.#verifiedCustomerIds.add(customerId);
|
|
121
126
|
return customerId;
|
|
122
127
|
}
|
|
@@ -133,13 +138,14 @@ export class MollieService {
|
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
const mollieCustomer = await this.client.customers.create({
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
entityCustomer: {
|
|
142
|
+
name: payingOrganization.name,
|
|
143
|
+
email: customer.email ?? undefined,
|
|
144
|
+
metadata: {
|
|
145
|
+
organizationId: payingOrganization.id,
|
|
146
|
+
userId: user?.id,
|
|
147
|
+
},
|
|
141
148
|
},
|
|
142
|
-
testmode: this.testMode,
|
|
143
149
|
});
|
|
144
150
|
|
|
145
151
|
const customerId = mollieCustomer.id;
|
|
@@ -173,16 +179,17 @@ export class MollieService {
|
|
|
173
179
|
const mandates: PaymentMandate[] = [];
|
|
174
180
|
|
|
175
181
|
try {
|
|
176
|
-
const
|
|
182
|
+
const pages = await this.client.mandates.list({
|
|
177
183
|
customerId,
|
|
178
184
|
limit: 250,
|
|
179
|
-
testmode: this.testMode,
|
|
180
185
|
});
|
|
181
186
|
|
|
182
|
-
for (const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
187
|
+
for await (const page of pages) {
|
|
188
|
+
for (const mandate of page.result.embedded.mandates) {
|
|
189
|
+
const paymentMandate = MollieService.mollieManateToStamhoofd({ mandate, payingOrganization, user });
|
|
190
|
+
if (paymentMandate) {
|
|
191
|
+
mandates.push(paymentMandate);
|
|
192
|
+
}
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
} catch (e) {
|
|
@@ -203,28 +210,25 @@ export class MollieService {
|
|
|
203
210
|
return;
|
|
204
211
|
}
|
|
205
212
|
|
|
206
|
-
await this.client.
|
|
213
|
+
await this.client.mandates.revoke({
|
|
207
214
|
mandateId,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
testmode: this.testMode,
|
|
211
|
-
},
|
|
212
|
-
);
|
|
215
|
+
customerId,
|
|
216
|
+
});
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
private static mollieManateToStamhoofd({ mandate, payingOrganization, user }: {
|
|
216
|
-
mandate:
|
|
220
|
+
mandate: MandateResponse | ListMandateResponse;
|
|
217
221
|
payingOrganization: Organization | null;
|
|
218
222
|
user: User | null;
|
|
219
223
|
}): PaymentMandate | null {
|
|
220
224
|
let type: PaymentMandateType;
|
|
221
225
|
switch (mandate.method) {
|
|
222
|
-
case
|
|
226
|
+
case 'creditcard': {
|
|
223
227
|
type = PaymentMandateType.CreditCard;
|
|
224
228
|
break;
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
case
|
|
231
|
+
case 'directdebit': {
|
|
228
232
|
type = PaymentMandateType.DirectDebit;
|
|
229
233
|
break;
|
|
230
234
|
}
|
|
@@ -259,32 +263,35 @@ export class MollieService {
|
|
|
259
263
|
});
|
|
260
264
|
}
|
|
261
265
|
|
|
262
|
-
private static mollieMandateStatusToStamhoofd(mandate:
|
|
266
|
+
private static mollieMandateStatusToStamhoofd(mandate: MandateResponse | ListMandateResponse): PaymentMandateStatus {
|
|
263
267
|
switch (mandate.status) {
|
|
264
|
-
case
|
|
268
|
+
case 'valid': {
|
|
265
269
|
return PaymentMandateStatus.Valid;
|
|
266
270
|
}
|
|
267
|
-
case
|
|
271
|
+
case 'invalid': {
|
|
268
272
|
return PaymentMandateStatus.Invalid;
|
|
269
273
|
}
|
|
270
|
-
case
|
|
274
|
+
case 'pending': {
|
|
275
|
+
return PaymentMandateStatus.Pending;
|
|
276
|
+
}
|
|
277
|
+
default: {
|
|
271
278
|
return PaymentMandateStatus.Pending;
|
|
272
279
|
}
|
|
273
280
|
}
|
|
274
281
|
}
|
|
275
282
|
|
|
276
|
-
|
|
283
|
+
static paymentMethodToMollie(method: PaymentMethod) {
|
|
277
284
|
switch (method) {
|
|
278
285
|
case PaymentMethod.Bancontact:
|
|
279
|
-
return
|
|
286
|
+
return 'bancontact' as const;
|
|
280
287
|
case PaymentMethod.CreditCard:
|
|
281
|
-
return
|
|
288
|
+
return 'creditcard' as const;
|
|
282
289
|
case PaymentMethod.DirectDebit:
|
|
283
|
-
return
|
|
290
|
+
return 'directdebit' as const;
|
|
284
291
|
case PaymentMethod.iDEAL:
|
|
285
|
-
return
|
|
292
|
+
return 'ideal' as const;
|
|
286
293
|
case PaymentMethod.Transfer:
|
|
287
|
-
return
|
|
294
|
+
return 'banktransfer' as const;
|
|
288
295
|
}
|
|
289
296
|
|
|
290
297
|
return null;
|
|
@@ -292,14 +299,18 @@ export class MollieService {
|
|
|
292
299
|
|
|
293
300
|
async getProfiles(): Promise<MollieProfile[]> {
|
|
294
301
|
try {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
const pages = await this.client.profiles.list({ limit: 250 });
|
|
303
|
+
const profiles: MollieProfile[] = [];
|
|
304
|
+
for await (const page of pages) {
|
|
305
|
+
for (const profile of page.result.embedded.profiles) {
|
|
306
|
+
profiles.push(MollieProfile.create({
|
|
307
|
+
...profile,
|
|
308
|
+
mode: profile.mode === 'live' ? MollieProfileMode.Live : MollieProfileMode.Test,
|
|
309
|
+
status: profile.status === 'unverified' ? MollieProfileStatus.Unverified : (profile.status === 'blocked' ? MollieProfileStatus.Blocked : MollieProfileStatus.Verified),
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return profiles;
|
|
303
314
|
} catch (e) {
|
|
304
315
|
console.error('Failed to parse mollie profiles', e);
|
|
305
316
|
return [];
|
|
@@ -312,7 +323,7 @@ export class MollieService {
|
|
|
312
323
|
return MollieOnboarding.create({
|
|
313
324
|
canReceivePayments: !!response.canReceivePayments,
|
|
314
325
|
canReceiveSettlements: !!response.canReceiveSettlements,
|
|
315
|
-
status: response.status ===
|
|
326
|
+
status: response.status === 'needs-data' ? MollieStatus.NeedsData : (response.status === 'in-review' ? MollieStatus.InReview : (MollieStatus.Completed)),
|
|
316
327
|
});
|
|
317
328
|
} catch (e) {
|
|
318
329
|
console.error('Error when requesting Mollie onboarding status:');
|
|
@@ -327,27 +338,24 @@ export class MollieService {
|
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
try {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
341
|
+
const pages = await this.client.profiles.list({ limit: 250 });
|
|
342
|
+
|
|
343
|
+
let firstProfileId: string | undefined;
|
|
344
|
+
let stamhoofdProfileId: string | undefined;
|
|
333
345
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (profile.website.toLowerCase().includes(website)) {
|
|
346
|
+
for await (const page of pages) {
|
|
347
|
+
for (const profile of page.result.embedded.profiles) {
|
|
348
|
+
firstProfileId ??= profile.id;
|
|
349
|
+
if (website && profile.website.toLowerCase().includes(website)) {
|
|
338
350
|
return profile.id;
|
|
339
351
|
}
|
|
352
|
+
if (!stamhoofdProfileId && profile.name.toLowerCase().includes('stamhoofd')) {
|
|
353
|
+
stamhoofdProfileId = profile.id;
|
|
354
|
+
}
|
|
340
355
|
}
|
|
341
356
|
}
|
|
342
357
|
|
|
343
|
-
|
|
344
|
-
for (const profile of profiles) {
|
|
345
|
-
if (profile.name.toLowerCase().includes('stamhoofd')) {
|
|
346
|
-
return profile.id;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return profiles[0]?.id ?? undefined;
|
|
358
|
+
return stamhoofdProfileId ?? firstProfileId;
|
|
351
359
|
} catch (e) {
|
|
352
360
|
console.error('Error when requesting Mollie profile id:');
|
|
353
361
|
console.error(e);
|
|
@@ -397,31 +405,32 @@ export class MollieService {
|
|
|
397
405
|
}) ?? undefined;
|
|
398
406
|
|
|
399
407
|
const data: Parameters<typeof mollieService.client.payments.create>[0] = {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
408
|
+
paymentRequest: {
|
|
409
|
+
amount: {
|
|
410
|
+
currency: 'EUR',
|
|
411
|
+
value: (Math.round(payment.price / 100) / 100).toFixed(2),
|
|
412
|
+
},
|
|
413
|
+
method,
|
|
414
|
+
mandateId: mandate?.id,
|
|
415
|
+
sequenceType: mandate ? 'recurring' : (payment.createMandate ? 'first' : 'oneoff'),
|
|
416
|
+
customerId,
|
|
417
|
+
profileId,
|
|
418
|
+
description,
|
|
419
|
+
redirectUrl,
|
|
420
|
+
cancelUrl,
|
|
421
|
+
webhookUrl,
|
|
422
|
+
metadata: {
|
|
423
|
+
paymentId: payment.id,
|
|
424
|
+
...metadata,
|
|
425
|
+
},
|
|
426
|
+
locale: mollieService.locale,
|
|
417
427
|
},
|
|
418
|
-
locale: mollieService.locale,
|
|
419
428
|
};
|
|
420
429
|
console.log('Creating payment', data);
|
|
421
430
|
const molliePayment = await mollieService.client.payments.create(data);
|
|
422
431
|
console.log('Payment response', molliePayment);
|
|
423
432
|
|
|
424
|
-
const paymentUrl =
|
|
433
|
+
const paymentUrl = MollieService.getCheckoutUrl(molliePayment);
|
|
425
434
|
|
|
426
435
|
// Save payment
|
|
427
436
|
const dbPayment = new MolliePayment();
|
|
@@ -437,20 +446,24 @@ export class MollieService {
|
|
|
437
446
|
};
|
|
438
447
|
}
|
|
439
448
|
|
|
440
|
-
static
|
|
449
|
+
static getCheckoutUrl(molliePayment: PaymentResponse): string | null {
|
|
450
|
+
return molliePayment.links.checkout?.href ?? null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static async saveChargeInfo(mollieData: PaymentResponse, payment: Payment) {
|
|
441
454
|
try {
|
|
442
455
|
const details = mollieData.details;
|
|
443
456
|
if (details) {
|
|
444
|
-
if ('consumerName' in details) {
|
|
457
|
+
if ('consumerName' in details && details.consumerName) {
|
|
445
458
|
payment.ibanName = details.consumerName;
|
|
446
459
|
}
|
|
447
|
-
if ('consumerAccount' in details) {
|
|
460
|
+
if ('consumerAccount' in details && details.consumerAccount) {
|
|
448
461
|
payment.iban = details.consumerAccount;
|
|
449
462
|
}
|
|
450
|
-
if ('cardHolder' in details) {
|
|
463
|
+
if ('cardHolder' in details && details.cardHolder) {
|
|
451
464
|
payment.ibanName = details.cardHolder;
|
|
452
465
|
}
|
|
453
|
-
if ('cardNumber' in details) {
|
|
466
|
+
if ('cardNumber' in details && details.cardNumber) {
|
|
454
467
|
payment.iban = '•••• ' + details.cardNumber;
|
|
455
468
|
}
|
|
456
469
|
|
|
@@ -462,22 +475,22 @@ export class MollieService {
|
|
|
462
475
|
}
|
|
463
476
|
}
|
|
464
477
|
|
|
465
|
-
async getStatusFor(mollieData:
|
|
478
|
+
async getStatusFor(mollieData: PaymentResponse, payment: Payment, cancel = false): Promise<{ status: PaymentStatus }> {
|
|
466
479
|
await MollieService.saveChargeInfo(mollieData, payment);
|
|
467
480
|
|
|
468
|
-
if (mollieData.status ===
|
|
481
|
+
if (mollieData.status === 'paid') {
|
|
469
482
|
return {
|
|
470
483
|
status: PaymentStatus.Succeeded,
|
|
471
484
|
};
|
|
472
|
-
} else if (mollieData.status ===
|
|
485
|
+
} else if (mollieData.status === 'failed') {
|
|
473
486
|
return {
|
|
474
487
|
status: PaymentStatus.Failed,
|
|
475
488
|
};
|
|
476
|
-
} else if (mollieData.status ===
|
|
489
|
+
} else if (mollieData.status === 'expired') {
|
|
477
490
|
return {
|
|
478
491
|
status: PaymentStatus.Failed,
|
|
479
492
|
};
|
|
480
|
-
} else if (mollieData.status ===
|
|
493
|
+
} else if (mollieData.status === 'canceled') {
|
|
481
494
|
return {
|
|
482
495
|
status: PaymentStatus.Failed,
|
|
483
496
|
};
|
|
@@ -489,8 +502,8 @@ export class MollieService {
|
|
|
489
502
|
|
|
490
503
|
// Try to cancel
|
|
491
504
|
try {
|
|
492
|
-
const newData = await this.client.payments.cancel(
|
|
493
|
-
|
|
505
|
+
const newData = await this.client.payments.cancel({
|
|
506
|
+
paymentId: mollieData.id,
|
|
494
507
|
});
|
|
495
508
|
console.log('Cancelled Mollie Payment ' + payment.id);
|
|
496
509
|
return await this.getStatusFor(newData, payment, false);
|
|
@@ -501,7 +514,7 @@ export class MollieService {
|
|
|
501
514
|
console.log('Cannot cancel Mollie Payment ' + payment.id);
|
|
502
515
|
}
|
|
503
516
|
|
|
504
|
-
if (mollieData.status ===
|
|
517
|
+
if (mollieData.status === 'open') {
|
|
505
518
|
// Nothink happend yet
|
|
506
519
|
return {
|
|
507
520
|
status: PaymentStatus.Created,
|
|
@@ -523,12 +536,12 @@ export class MollieService {
|
|
|
523
536
|
* Map a Mollie refund status to a payment status. A refund only succeeds once Mollie
|
|
524
537
|
* reports it as refunded: before that it can still fail or be canceled.
|
|
525
538
|
*/
|
|
526
|
-
static refundStatusToPaymentStatus(status:
|
|
539
|
+
static refundStatusToPaymentStatus(status: EntityRefundResponseStatus | ListEntityRefundStatus): PaymentStatus {
|
|
527
540
|
switch (status) {
|
|
528
|
-
case
|
|
541
|
+
case 'refunded':
|
|
529
542
|
return PaymentStatus.Succeeded;
|
|
530
|
-
case
|
|
531
|
-
case
|
|
543
|
+
case 'failed':
|
|
544
|
+
case 'canceled':
|
|
532
545
|
return PaymentStatus.Failed;
|
|
533
546
|
default:
|
|
534
547
|
// queued, pending, processing
|
|
@@ -551,21 +564,22 @@ export class MollieService {
|
|
|
551
564
|
});
|
|
552
565
|
}
|
|
553
566
|
|
|
554
|
-
const data: Parameters<typeof this.client.
|
|
567
|
+
const data: Parameters<typeof this.client.refunds.create>[0] = {
|
|
555
568
|
paymentId: molliePayment.mollieId,
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
569
|
+
refundRequest: {
|
|
570
|
+
amount: {
|
|
571
|
+
currency: 'EUR',
|
|
572
|
+
value: (Math.round(amount / 100) / 100).toFixed(2),
|
|
573
|
+
},
|
|
574
|
+
description,
|
|
575
|
+
metadata: metadata ?? null,
|
|
559
576
|
},
|
|
560
|
-
description,
|
|
561
|
-
metadata,
|
|
562
|
-
testmode: this.testMode,
|
|
563
577
|
};
|
|
564
578
|
|
|
565
579
|
console.log('Creating Mollie refund', data);
|
|
566
580
|
|
|
567
581
|
try {
|
|
568
|
-
const refund = await this.client.
|
|
582
|
+
const refund = await this.client.refunds.create(data);
|
|
569
583
|
console.log('Mollie refund response', refund);
|
|
570
584
|
|
|
571
585
|
return {
|
|
@@ -576,13 +590,14 @@ export class MollieService {
|
|
|
576
590
|
} catch (e) {
|
|
577
591
|
console.error('Failed to create Mollie refund for payment ' + payment.id, e);
|
|
578
592
|
|
|
579
|
-
|
|
593
|
+
const detail = MollieService.getApiErrorDetail(e);
|
|
594
|
+
if (detail !== null) {
|
|
580
595
|
// E.g. the amount is higher than the remaining refundable amount, the payment
|
|
581
596
|
// was already (fully) refunded, or there is not enough balance available
|
|
582
597
|
throw new SimpleError({
|
|
583
598
|
code: 'refund_failed',
|
|
584
|
-
message: 'Mollie refused to create the refund: ' +
|
|
585
|
-
human: $t('%ZZx', { message:
|
|
599
|
+
message: 'Mollie refused to create the refund: ' + detail,
|
|
600
|
+
human: $t('%ZZx', { message: detail }),
|
|
586
601
|
statusCode: 400,
|
|
587
602
|
});
|
|
588
603
|
}
|
|
@@ -590,14 +605,41 @@ export class MollieService {
|
|
|
590
605
|
}
|
|
591
606
|
}
|
|
592
607
|
|
|
608
|
+
/**
|
|
609
|
+
* The human-readable detail of a Mollie API error response, or null if the error is not a
|
|
610
|
+
* Mollie 4xx refusal. The SDK only parses an ErrorResponse for the status codes an operation
|
|
611
|
+
* declares; other 4xx responses (e.g. 401 for a revoked token) throw a generic ClientError
|
|
612
|
+
* with the raw body.
|
|
613
|
+
*/
|
|
614
|
+
private static getApiErrorDetail(e: unknown): string | null {
|
|
615
|
+
if (e instanceof ErrorResponse) {
|
|
616
|
+
return e.detail;
|
|
617
|
+
}
|
|
618
|
+
if (e instanceof ClientError && e.statusCode >= 400 && e.statusCode < 500) {
|
|
619
|
+
try {
|
|
620
|
+
const body = JSON.parse(e.body) as { detail?: unknown; title?: unknown };
|
|
621
|
+
if (typeof body.detail === 'string') {
|
|
622
|
+
return body.detail;
|
|
623
|
+
}
|
|
624
|
+
if (typeof body.title === 'string') {
|
|
625
|
+
return body.title;
|
|
626
|
+
}
|
|
627
|
+
} catch {
|
|
628
|
+
// Not a JSON body
|
|
629
|
+
}
|
|
630
|
+
return e.message;
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
593
635
|
async getStatus(payment: Payment, cancel = false) {
|
|
594
636
|
const molliePayment = await MolliePayment.select().where('paymentId', payment.id).first(false);
|
|
595
637
|
if (!molliePayment) {
|
|
596
638
|
throw new Error('Mollie Payment not found for payment ' + payment.id);
|
|
597
639
|
}
|
|
598
640
|
|
|
599
|
-
const mollieData = await this.client.payments.get(
|
|
600
|
-
|
|
641
|
+
const mollieData = await this.client.payments.get({
|
|
642
|
+
paymentId: molliePayment.mollieId,
|
|
601
643
|
});
|
|
602
644
|
|
|
603
645
|
console.log(mollieData);
|
|
@@ -625,9 +667,9 @@ export class MollieService {
|
|
|
625
667
|
throw new Error('Mollie payment not found for source payment ' + refundPayment.reversingPaymentId);
|
|
626
668
|
}
|
|
627
669
|
|
|
628
|
-
const refund = await this.client.
|
|
670
|
+
const refund = await this.client.refunds.get({
|
|
671
|
+
refundId: mollieRefund.mollieId,
|
|
629
672
|
paymentId: sourceMolliePayment.mollieId,
|
|
630
|
-
testmode: this.testMode,
|
|
631
673
|
});
|
|
632
674
|
|
|
633
675
|
return {
|
|
@@ -14,7 +14,7 @@ import { BuckarooHelper } from '../helpers/BuckarooHelper.js';
|
|
|
14
14
|
import { Context } from '../helpers/Context.js';
|
|
15
15
|
import { ServiceFeeHelper } from '../helpers/ServiceFeeHelper.js';
|
|
16
16
|
import { StripeHelper } from '../helpers/StripeHelper.js';
|
|
17
|
-
import {
|
|
17
|
+
import { ViesService } from '@stamhoofd/vies';
|
|
18
18
|
import { AuditLogService } from './AuditLogService.js';
|
|
19
19
|
import { BalanceItemPaymentService } from './BalanceItemPaymentService.js';
|
|
20
20
|
import { BalanceItemService } from './BalanceItemService.js';
|
|
@@ -749,7 +749,7 @@ export class PaymentService {
|
|
|
749
749
|
} else {
|
|
750
750
|
if (checkout.customer && checkout.customer.company) {
|
|
751
751
|
customer.company = checkout.customer.company.clone();
|
|
752
|
-
await
|
|
752
|
+
await ViesService.checkCompany(customer.company, null, { forceValidation: true });
|
|
753
753
|
}
|
|
754
754
|
}
|
|
755
755
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SimpleError } from '@simonbackx/simple-errors';
|
|
2
2
|
import { Request } from '@simonbackx/simple-endpoints';
|
|
3
3
|
import { Platform } from '@stamhoofd/models';
|
|
4
|
-
import { LoginMethod, LoginMethodConfig, LoginProviderType, OpenIDClientConfiguration } from '@stamhoofd/structures';
|
|
4
|
+
import { LoginMethod, LoginMethodConfig, LoginProviderType, OpenIDClientConfiguration, SessionClientType, StartOpenIDFlowStruct } from '@stamhoofd/structures';
|
|
5
5
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
|
|
7
7
|
import { ContextInstance } from '../helpers/Context.js';
|
|
@@ -9,6 +9,7 @@ import { SSOService } from './SSOService.js';
|
|
|
9
9
|
|
|
10
10
|
describe('SSOService', () => {
|
|
11
11
|
afterEach(() => {
|
|
12
|
+
SSOService.sessionStorage.clear();
|
|
12
13
|
vi.restoreAllMocks();
|
|
13
14
|
});
|
|
14
15
|
|
|
@@ -68,6 +69,29 @@ describe('SSOService', () => {
|
|
|
68
69
|
expect(() => service.validateConfiguration()).not.toThrow();
|
|
69
70
|
});
|
|
70
71
|
});
|
|
72
|
+
|
|
73
|
+
it.each([
|
|
74
|
+
['web', SessionClientType.Browser],
|
|
75
|
+
['ios', SessionClientType.iOS],
|
|
76
|
+
['android', SessionClientType.Android],
|
|
77
|
+
])('keeps the %s client classification in the SSO session', async (clientPlatform, expectedClientType) => {
|
|
78
|
+
const service = createService({
|
|
79
|
+
ssoConfiguration: OpenIDClientConfiguration.create({}),
|
|
80
|
+
loginMethods: new Map([[LoginMethod.SSO, LoginMethodConfig.create({})]]),
|
|
81
|
+
provider: LoginProviderType.SSO,
|
|
82
|
+
});
|
|
83
|
+
vi.spyOn(service, 'getClient').mockResolvedValue({
|
|
84
|
+
authorizationUrl: vi.fn().mockReturnValue('https://idp.example/authorize'),
|
|
85
|
+
} as any);
|
|
86
|
+
|
|
87
|
+
await service.validateAndStartAuthCodeFlow(StartOpenIDFlowStruct.create({
|
|
88
|
+
spaState: 'a-valid-spa-state',
|
|
89
|
+
clientPlatform,
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
expect([...SSOService.sessionStorage.values()]).toHaveLength(1);
|
|
93
|
+
expect([...SSOService.sessionStorage.values()][0].clientType).toBe(expectedClientType);
|
|
94
|
+
});
|
|
71
95
|
});
|
|
72
96
|
|
|
73
97
|
async function runWithContext<T>(handler: () => Promise<T>) {
|