@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.
Files changed (99) hide show
  1. package/package.json +19 -19
  2. package/src/crons/mollie-chargebacks.ts +31 -29
  3. package/src/crons/mollie-refunds.ts +61 -59
  4. package/src/endpoints/admin/organizations/PatchOrganizationsEndpoint.ts +16 -5
  5. package/src/endpoints/auth/CreateTokenEndpoint.test.ts +210 -4
  6. package/src/endpoints/auth/CreateTokenEndpoint.ts +9 -20
  7. package/src/endpoints/auth/DeleteTokenEndpoint.test.ts +29 -0
  8. package/src/endpoints/auth/DeleteTokenEndpoint.ts +2 -1
  9. package/src/endpoints/auth/GetUserEndpoint.test.ts +32 -11
  10. package/src/endpoints/auth/MFA.security.test.ts +9 -7
  11. package/src/endpoints/auth/MFA.test.ts +42 -15
  12. package/src/endpoints/auth/PatchUserEndpoint.ts +3 -2
  13. package/src/endpoints/auth/VerifyEmailEndpoint.ts +5 -4
  14. package/src/endpoints/global/email/GetAdminEmailsEndpoint.test.ts +4 -3
  15. package/src/endpoints/global/email/GetUserEmailsEndpoint.test.ts +6 -5
  16. package/src/endpoints/global/email/PatchEmailEndpoint.test.ts +5 -4
  17. package/src/endpoints/global/email-recipients/GetEmailRecipientsEndpoint.test.ts +5 -4
  18. package/src/endpoints/global/events/PatchEventNotificationsEndpoint.test.ts +2 -1
  19. package/src/endpoints/global/events/PatchEventsEndpoint.test.ts +2 -1
  20. package/src/endpoints/global/files/UploadFile.test.ts +2 -1
  21. package/src/endpoints/global/files/UploadImage.test.ts +2 -1
  22. package/src/endpoints/global/files/upload-security.test.ts +3 -2
  23. package/src/endpoints/global/groups/GetGroupsEndpoint.test.ts +4 -3
  24. package/src/endpoints/global/members/GetMembersEndpoint.test.ts +26 -25
  25. package/src/endpoints/global/members/PatchOrganizationMembersEndpoint.test.ts +57 -56
  26. package/src/endpoints/global/members/SendMemberSecurityCodeEndpoint.test.ts +4 -3
  27. package/src/endpoints/global/platform/GetPlatformEndpoint.test.ts +6 -4
  28. package/src/endpoints/global/platform/PatchPlatformEnpoint.test.ts +6 -6
  29. package/src/endpoints/global/platform/SignOutPlatformAdminsEndpoint.test.ts +9 -8
  30. package/src/endpoints/global/registration/GetRegistrationsEndpoint.test.ts +14 -13
  31. package/src/endpoints/global/registration/PatchUserMembersEndpoint.test.ts +24 -23
  32. package/src/endpoints/global/registration/RegisterMembersEndpoint.test.ts +4 -3
  33. package/src/endpoints/global/registration-invitations/GetRegistrationInvitationsEndpoint.test.ts +2 -1
  34. package/src/endpoints/global/registration-invitations/PatchRegistrationInvitationsEndpoint.test.ts +2 -1
  35. package/src/endpoints/global/webshops/GetWebshopsEndpoint.test.ts +20 -19
  36. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemBreakdownEndpoint.test.ts +2 -1
  37. package/src/endpoints/organization/dashboard/balance-items/GetBalanceItemsEndpoint.test.ts +2 -1
  38. package/src/endpoints/organization/dashboard/billing/ApplyRegisterCodeEndpoint.test.ts +3 -2
  39. package/src/endpoints/organization/dashboard/documents/PatchDocumentTemplatesEndpoint.test.ts +4 -3
  40. package/src/endpoints/organization/dashboard/email-templates/GetEmailTemplatesEndpoint.test.ts +5 -4
  41. package/src/endpoints/organization/dashboard/email-templates/PatchEmailTemplatesEndpoint.test.ts +5 -4
  42. package/src/endpoints/organization/dashboard/invoices/GetInvoicesEndpoint.test.ts +2 -1
  43. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.test.ts +2 -1
  44. package/src/endpoints/organization/dashboard/invoices/PatchInvoicesEndpoint.ts +0 -1
  45. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.test.ts +23 -22
  46. package/src/endpoints/organization/dashboard/organization/PatchOrganizationEndpoint.ts +4 -4
  47. package/src/endpoints/organization/dashboard/payments/GetPaymentBreakdownEndpoint.test.ts +2 -1
  48. package/src/endpoints/organization/dashboard/payments/GetPaymentsEndpoint.test.ts +4 -3
  49. package/src/endpoints/organization/dashboard/payments/PatchPaymentsEndpoint.ts +2 -2
  50. package/src/endpoints/organization/dashboard/receivable-balances/GetReceivableBalanceEndpoint.test.ts +2 -1
  51. package/src/endpoints/organization/dashboard/registration-periods/GetOrganizationRegistrationPeriodsEndpoint.test.ts +5 -4
  52. package/src/endpoints/organization/dashboard/registration-periods/MoveRegistrationPeriods.test.ts +7 -6
  53. package/src/endpoints/organization/dashboard/registration-periods/PatchOrganizationRegistrationPeriodsEndpoint.test.ts +9 -8
  54. package/src/endpoints/organization/dashboard/settlements/SettlementsExportEndpoint.test.ts +3 -2
  55. package/src/endpoints/organization/dashboard/settlements/SettlementsSyncEndpoint.test.ts +2 -1
  56. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.test.ts +5 -4
  57. package/src/endpoints/organization/dashboard/users/CreateApiUserEndpoint.ts +2 -1
  58. package/src/endpoints/organization/dashboard/users/GetOrganizationAdminsEndpoint.test.ts +3 -2
  59. package/src/endpoints/organization/dashboard/users/PatchApiUserEndpoint.test.ts +6 -5
  60. package/src/endpoints/organization/dashboard/users/SignOutOrganizationAdminsEndpoint.test.ts +16 -15
  61. package/src/endpoints/organization/dashboard/webshops/GetWebshopOrdersEndpoint.test.ts +2 -1
  62. package/src/endpoints/organization/dashboard/webshops/PatchWebshopEndpoint.test.ts +2 -1
  63. package/src/endpoints/organization/dashboard/webshops/PatchWebshopOrdersEndpoint.test.ts +2 -1
  64. package/src/endpoints/organization/dashboard/webshops/WebshopReservedPathSegments.test.ts +2 -1
  65. package/src/endpoints/organization/shared/auth/GetOrganizationEndpoint.test.ts +4 -3
  66. package/src/endpoints/organization/webshops/GetWebshopEndpoint.test.ts +6 -5
  67. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.test.ts +94 -8
  68. package/src/endpoints/organization/webshops/PlaceOrderEndpoint.ts +18 -18
  69. package/src/helpers/Context.ts +13 -6
  70. package/src/helpers/ContextStorage.ts +5 -0
  71. package/src/helpers/StripeSettlementSync.ts +10 -7
  72. package/src/helpers/TwoFactorHelper.ts +21 -22
  73. package/src/services/AdminSessionService.ts +5 -3
  74. package/src/services/InvoiceService.ts +2 -2
  75. package/src/services/MollieService.ts +168 -126
  76. package/src/services/PaymentService.ts +2 -2
  77. package/src/services/SSOService.test.ts +25 -1
  78. package/src/services/SSOService.ts +17 -6
  79. package/src/services/SessionService.test.ts +319 -0
  80. package/src/services/SessionService.ts +357 -0
  81. package/src/services/WebshopCrowdfundingService.test.ts +4 -3
  82. package/src/sql-filters/registrations.ts +59 -1
  83. package/tests/actions/patchUserMember.ts +2 -1
  84. package/tests/e2e/api-rate-limits.test.ts +2 -1
  85. package/tests/e2e/bundle-discounts.test.ts +4 -3
  86. package/tests/e2e/charge-members.test.ts +6 -5
  87. package/tests/e2e/documents.test.ts +4 -3
  88. package/tests/e2e/private-files.test.ts +9 -8
  89. package/tests/e2e/register.test.ts +4 -3
  90. package/tests/e2e/stock.test.ts +4 -3
  91. package/tests/e2e/tickets.test.ts +4 -3
  92. package/tests/helpers/ExportSlice.ts +2 -1
  93. package/tests/helpers/MollieMocker.ts +85 -23
  94. package/tests/init/initAdmin.ts +2 -1
  95. package/tests/init/initPlatformAdmin.ts +2 -1
  96. package/src/helpers/ViesHelper.test.ts +0 -401
  97. package/src/helpers/ViesHelper.ts +0 -222
  98. package/src/services/PeppolDirectoryService.test.ts +0 -139
  99. 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: ReturnType<typeof createMollieClient>;
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 = createMollieClient({ accessToken });
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
- organization: {
62
- name: this.sellingOrganization.name,
63
- address: {
64
- streetAndNumber: this.sellingOrganization.address.street + ' ' + this.sellingOrganization.address.number,
65
- postalCode: this.sellingOrganization.address.postalCode,
66
- city: this.sellingOrganization.address.city,
67
- country: this.sellingOrganization.address.country,
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(customerId, { testmode: this.testMode });
122
+ const c = await this.client.customers.get({ customerId });
118
123
 
119
- if ((c.mode === ApiMode.test) === this.testMode) {
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
- name: payingOrganization.name,
137
- email: customer.email ?? undefined,
138
- metadata: {
139
- organizationId: payingOrganization.id,
140
- userId: user?.id,
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 m = await this.client.customerMandates.page({
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 mandate of m) {
183
- const paymentMandate = MollieService.mollieManateToStamhoofd({ mandate, payingOrganization, user });
184
- if (paymentMandate) {
185
- mandates.push(paymentMandate);
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.customerMandates.revoke(
213
+ await this.client.mandates.revoke({
207
214
  mandateId,
208
- {
209
- customerId,
210
- testmode: this.testMode,
211
- },
212
- );
215
+ customerId,
216
+ });
213
217
  }
214
218
 
215
219
  private static mollieManateToStamhoofd({ mandate, payingOrganization, user }: {
216
- mandate: 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 MandateMethod.creditcard: {
226
+ case 'creditcard': {
223
227
  type = PaymentMandateType.CreditCard;
224
228
  break;
225
229
  }
226
230
 
227
- case MandateMethod.directdebit: {
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: Mandate): PaymentMandateStatus {
266
+ private static mollieMandateStatusToStamhoofd(mandate: MandateResponse | ListMandateResponse): PaymentMandateStatus {
263
267
  switch (mandate.status) {
264
- case MandateStatus.valid: {
268
+ case 'valid': {
265
269
  return PaymentMandateStatus.Valid;
266
270
  }
267
- case MandateStatus.invalid: {
271
+ case 'invalid': {
268
272
  return PaymentMandateStatus.Invalid;
269
273
  }
270
- case MandateStatus.pending: {
274
+ case 'pending': {
275
+ return PaymentMandateStatus.Pending;
276
+ }
277
+ default: {
271
278
  return PaymentMandateStatus.Pending;
272
279
  }
273
280
  }
274
281
  }
275
282
 
276
- private static paymentMethodToMollie(method: PaymentMethod) {
283
+ static paymentMethodToMollie(method: PaymentMethod) {
277
284
  switch (method) {
278
285
  case PaymentMethod.Bancontact:
279
- return molliePaymentMethod.bancontact;
286
+ return 'bancontact' as const;
280
287
  case PaymentMethod.CreditCard:
281
- return molliePaymentMethod.creditcard;
288
+ return 'creditcard' as const;
282
289
  case PaymentMethod.DirectDebit:
283
- return molliePaymentMethod.directdebit;
290
+ return 'directdebit' as const;
284
291
  case PaymentMethod.iDEAL:
285
- return molliePaymentMethod.ideal;
292
+ return 'ideal' as const;
286
293
  case PaymentMethod.Transfer:
287
- return molliePaymentMethod.banktransfer;
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 response = await this.client.profiles.page({
296
- limit: 250,
297
- });
298
- return response.map(p => MollieProfile.create({
299
- ...p,
300
- mode: p.mode === ApiMode.live ? MollieProfileMode.Live : MollieProfileMode.Test,
301
- status: p.status === ProfileStatus.unverified ? MollieProfileStatus.Unverified : (p.status === ProfileStatus.blocked ? MollieProfileStatus.Blocked : MollieProfileStatus.Verified),
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 === OnboardingStatus.needsData ? MollieStatus.NeedsData : (response.status === OnboardingStatus.inReview ? MollieStatus.InReview : (MollieStatus.Completed)),
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 profiles = await this.client.profiles.page({
331
- limit: 250,
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
- // Search profile with Stamhoofd as name
335
- if (website) {
336
- for (const profile of profiles) {
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
- // Search profile with Stamhoofd as name
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
- amount: {
401
- currency: 'EUR',
402
- value: (Math.round(payment.price / 100) / 100).toFixed(2),
403
- },
404
- method,
405
- testmode: mollieService.testMode,
406
- mandateId: mandate?.id,
407
- sequenceType: mandate ? SequenceType.recurring : (payment.createMandate ? SequenceType.first : SequenceType.oneoff),
408
- customerId,
409
- profileId,
410
- description,
411
- redirectUrl,
412
- cancelUrl,
413
- webhookUrl,
414
- metadata: {
415
- paymentId: payment.id,
416
- ...metadata,
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 = molliePayment.getCheckoutUrl();
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 async saveChargeInfo(mollieData: MolliePaymentType, payment: Payment) {
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: MolliePaymentType, payment: Payment, cancel = false): Promise<{ status: PaymentStatus }> {
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 === molliePaymentStatus.paid) {
481
+ if (mollieData.status === 'paid') {
469
482
  return {
470
483
  status: PaymentStatus.Succeeded,
471
484
  };
472
- } else if (mollieData.status === molliePaymentStatus.failed) {
485
+ } else if (mollieData.status === 'failed') {
473
486
  return {
474
487
  status: PaymentStatus.Failed,
475
488
  };
476
- } else if (mollieData.status === molliePaymentStatus.expired) {
489
+ } else if (mollieData.status === 'expired') {
477
490
  return {
478
491
  status: PaymentStatus.Failed,
479
492
  };
480
- } else if (mollieData.status === molliePaymentStatus.canceled) {
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(mollieData.id, {
493
- testmode: this.testMode,
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 === molliePaymentStatus.open) {
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: RefundStatus): PaymentStatus {
539
+ static refundStatusToPaymentStatus(status: EntityRefundResponseStatus | ListEntityRefundStatus): PaymentStatus {
527
540
  switch (status) {
528
- case RefundStatus.refunded:
541
+ case 'refunded':
529
542
  return PaymentStatus.Succeeded;
530
- case RefundStatus.failed:
531
- case RefundStatus.canceled:
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.paymentRefunds.create>[0] = {
567
+ const data: Parameters<typeof this.client.refunds.create>[0] = {
555
568
  paymentId: molliePayment.mollieId,
556
- amount: {
557
- currency: 'EUR',
558
- value: (Math.round(amount / 100) / 100).toFixed(2),
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.paymentRefunds.create(data);
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
- if (e instanceof MollieApiError) {
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: ' + e.message,
585
- human: $t('%ZZx', { message: e.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(molliePayment.mollieId, {
600
- testmode: this.testMode,
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.paymentRefunds.get(mollieRefund.mollieId, {
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 { ViesHelper } from '../helpers/ViesHelper.js';
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 ViesHelper.checkCompany(checkout.customer.company, customer.company);
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>) {