diff --git a/src/binders/applePay/parameters.ts b/src/binders/applePay/parameters.ts index 5d50966c..5ebddd24 100644 --- a/src/binders/applePay/parameters.ts +++ b/src/binders/applePay/parameters.ts @@ -1,4 +1,6 @@ -export interface RequestPaymentSessionParameters { +import { IdempotencyParameter } from '../../types/parameters'; + +export interface RequestPaymentSessionParameters extends IdempotencyParameter { /** * The `validationUrl` you got from the [ApplePayValidateMerchant event](https://developer.apple.com/documentation/apple_pay_on_the_web/applepayvalidatemerchantevent). * diff --git a/src/binders/chargebacks/parameters.ts b/src/binders/chargebacks/parameters.ts index 769c8b24..fc593f47 100644 --- a/src/binders/chargebacks/parameters.ts +++ b/src/binders/chargebacks/parameters.ts @@ -1,9 +1,9 @@ import { ChargebackEmbed } from '../../data/chargebacks/Chargeback'; -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../types/parameters'; export type ListParameters = PaginationParameters & { profileId?: string; embed?: ChargebackEmbed[]; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/customers/mandates/parameters.ts b/src/binders/customers/mandates/parameters.ts index 2146cb7c..eabc6210 100644 --- a/src/binders/customers/mandates/parameters.ts +++ b/src/binders/customers/mandates/parameters.ts @@ -1,5 +1,5 @@ import { MandateData } from '../../../data/customers/mandates/data'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; interface ContextParameters { customerId: string; @@ -50,12 +50,12 @@ export type CreateParameters = ContextParameters & * @see https://docs.mollie.com/reference/v2/mandates-api/create-mandate?path=paypalBillingAgreementId#parameters */ paypalBillingAgreementId?: string; - }; + } & IdempotencyParameter; export type GetParameters = ContextParameters; export type ListParameters = ContextParameters & PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; -export type RevokeParameters = ContextParameters; +export type RevokeParameters = ContextParameters & IdempotencyParameter; diff --git a/src/binders/customers/parameters.ts b/src/binders/customers/parameters.ts index e9c3025f..ad8b6179 100644 --- a/src/binders/customers/parameters.ts +++ b/src/binders/customers/parameters.ts @@ -1,19 +1,19 @@ import { CustomerData } from '../../data/customers/Customer'; -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../types/parameters'; import PickOptional from '../../types/PickOptional'; -interface ContextParameters { +interface ContextParameter { testmode?: boolean; } -export type CreateParameters = ContextParameters & PickOptional; +export type CreateParameters = ContextParameter & PickOptional & IdempotencyParameter; -export type GetParameters = ContextParameters; +export type GetParameters = ContextParameter; -export type ListParameters = ContextParameters & PaginationParameters; +export type ListParameters = ContextParameter & PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; -export type UpdateParameters = ContextParameters & PickOptional; +export type UpdateParameters = ContextParameter & PickOptional; -export type DeleteParameters = ContextParameters; +export type DeleteParameters = ContextParameter & IdempotencyParameter; diff --git a/src/binders/customers/payments/parameters.ts b/src/binders/customers/payments/parameters.ts index f987554c..aeed8661 100644 --- a/src/binders/customers/payments/parameters.ts +++ b/src/binders/customers/payments/parameters.ts @@ -1,6 +1,6 @@ import { PaymentMethod } from '../../../data/global'; import { PaymentData } from '../../../data/payments/data'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; import PickOptional from '../../../types/PickOptional'; interface ContextParameters { @@ -22,8 +22,8 @@ export type CreateParameters = ContextParameters & * @see https://docs.mollie.com/reference/v2/payments-api/create-payment?path=method#parameters */ method?: PaymentMethod | PaymentMethod[]; - }; + } & IdempotencyParameter; export type ListParameters = ContextParameters & PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/customers/subscriptions/parameters.ts b/src/binders/customers/subscriptions/parameters.ts index 6fddfa27..7f37583d 100644 --- a/src/binders/customers/subscriptions/parameters.ts +++ b/src/binders/customers/subscriptions/parameters.ts @@ -1,5 +1,5 @@ import { SubscriptionData } from '../../../data/subscriptions/data'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; import PickOptional from '../../../types/PickOptional'; interface ContextParameters { @@ -9,16 +9,17 @@ interface ContextParameters { export type CreateParameters = ContextParameters & Pick & - PickOptional; + PickOptional & + IdempotencyParameter; export type GetParameters = ContextParameters; export type ListParameters = ContextParameters & PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; export type UpdateParameters = ContextParameters & Pick & PickOptional; -export type CancelParameters = ContextParameters; +export type CancelParameters = ContextParameters & IdempotencyParameter; diff --git a/src/binders/onboarding/OnboardingBinder.ts b/src/binders/onboarding/OnboardingBinder.ts index ad5395d9..f23eba31 100644 --- a/src/binders/onboarding/OnboardingBinder.ts +++ b/src/binders/onboarding/OnboardingBinder.ts @@ -34,7 +34,7 @@ export default class OnboardingBinder extends Binder */ public submit(parameters?: SubmitParameters): Promise; public submit(parameters: SubmitParameters, callback: Callback): void; - public submit(parameters: SubmitParameters) { + public submit(parameters: SubmitParameters = {}) { if (renege(this, this.submit, ...arguments)) return; return this.networkClient.post(pathSegments, parameters); } diff --git a/src/binders/onboarding/parameters.ts b/src/binders/onboarding/parameters.ts index 161b15c0..9ba4e4ac 100644 --- a/src/binders/onboarding/parameters.ts +++ b/src/binders/onboarding/parameters.ts @@ -1,6 +1,7 @@ import { Address } from '../../data/global'; +import { IdempotencyParameter } from '../../types/parameters'; -export interface SubmitParameters { +export interface SubmitParameters extends IdempotencyParameter { /** * Data of the organization you want to provide. * diff --git a/src/binders/orders/orderlines/parameters.ts b/src/binders/orders/orderlines/parameters.ts index f220a888..8a7313a0 100644 --- a/src/binders/orders/orderlines/parameters.ts +++ b/src/binders/orders/orderlines/parameters.ts @@ -1,5 +1,6 @@ import { Amount } from '../../../data/global'; import { OrderLineData } from '../../../data/orders/orderlines/OrderLine'; +import { IdempotencyParameter } from '../../../types/parameters'; import PickOptional from '../../../types/PickOptional'; interface ContextParameters { @@ -61,4 +62,4 @@ export type CancelParameters = ContextParameters & { */ amount?: Amount; }[]; -}; +} & IdempotencyParameter; diff --git a/src/binders/orders/parameters.ts b/src/binders/orders/parameters.ts index a228283c..d7dba714 100644 --- a/src/binders/orders/parameters.ts +++ b/src/binders/orders/parameters.ts @@ -1,7 +1,7 @@ import { PaymentMethod } from '../../data/global'; -import { OrderAddress, OrderData, OrderEmbed } from '../../data/orders/data'; +import { OrderData, OrderEmbed } from '../../data/orders/data'; import { OrderLineData } from '../../data/orders/orderlines/OrderLine'; -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../types/parameters'; import { CreateParameters as PaymentCreateParameters } from '../payments/parameters'; import PickOptional from '../../types/PickOptional'; @@ -82,7 +82,7 @@ export type CreateParameters = Pick & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; -export interface CancelParameters { +export interface CancelParameters extends IdempotencyParameter { testmode?: boolean; } diff --git a/src/binders/orders/shipments/parameters.ts b/src/binders/orders/shipments/parameters.ts index f622549f..6e74758f 100644 --- a/src/binders/orders/shipments/parameters.ts +++ b/src/binders/orders/shipments/parameters.ts @@ -1,5 +1,6 @@ import { Amount } from '../../../data/global'; import { ShipmentData } from '../../../data/orders/shipments/Shipment'; +import { IdempotencyParameter } from '../../../types/parameters'; import PickRequired from '../../../types/PickRequired'; interface ContextParameters { @@ -46,7 +47,7 @@ export type CreateParameters = ContextParameters & */ amount?: Amount; }[]; - }; + } & IdempotencyParameter; export type GetParameters = ContextParameters; diff --git a/src/binders/paymentLinks/parameters.ts b/src/binders/paymentLinks/parameters.ts index 95ebfb16..3bf145bf 100644 --- a/src/binders/paymentLinks/parameters.ts +++ b/src/binders/paymentLinks/parameters.ts @@ -1,10 +1,10 @@ import { PaymentLinkData } from '../../data/paymentLinks/data'; -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../types/parameters'; export type CreateParameters = Pick & { profileId?: string; testmode?: boolean; -}; +} & IdempotencyParameter; export interface GetParameters { testmode?: boolean; @@ -15,4 +15,4 @@ export type ListParameters = PaginationParameters & { testmode?: boolean; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/payments/captures/parameters.ts b/src/binders/payments/captures/parameters.ts index 23caaf55..07d04822 100644 --- a/src/binders/payments/captures/parameters.ts +++ b/src/binders/payments/captures/parameters.ts @@ -1,5 +1,5 @@ import { CaptureEmbed } from '../../../data/payments/captures/data'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; interface ContextParameters { paymentId: string; @@ -15,4 +15,4 @@ export type ListParameters = ContextParameters & embed?: CaptureEmbed[]; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/payments/chargebacks/parameters.ts b/src/binders/payments/chargebacks/parameters.ts index 1e4983b9..1663ef32 100644 --- a/src/binders/payments/chargebacks/parameters.ts +++ b/src/binders/payments/chargebacks/parameters.ts @@ -1,5 +1,5 @@ import { ChargebackEmbed } from '../../../data/chargebacks/Chargeback'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; interface ContextParameters { paymentId: string; @@ -14,4 +14,4 @@ export type ListParameters = ContextParameters & embed?: ChargebackEmbed[]; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/payments/orders/parameters.ts b/src/binders/payments/orders/parameters.ts index 307af82b..18dad7e0 100644 --- a/src/binders/payments/orders/parameters.ts +++ b/src/binders/payments/orders/parameters.ts @@ -1,5 +1,6 @@ import { PaymentMethod } from '../../../data/global'; import { PaymentData } from '../../../data/payments/data'; +import { IdempotencyParameter } from '../../../types/parameters'; interface ContextParameters { orderId: string; @@ -27,4 +28,4 @@ export type CreateParameters = ContextParameters & * @see https://docs.mollie.com/reference/v2/orders-api/create-order-payment?path=customerId#parameters */ customerId?: string; - }; + } & IdempotencyParameter; diff --git a/src/binders/payments/parameters.ts b/src/binders/payments/parameters.ts index 6b419efe..6aad21e1 100644 --- a/src/binders/payments/parameters.ts +++ b/src/binders/payments/parameters.ts @@ -1,7 +1,7 @@ import { Address, Amount, PaymentMethod } from '../../data/global'; import { Issuer } from '../../data/Issuer'; import { PaymentData, PaymentEmbed, PaymentInclude } from '../../data/payments/data'; -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../types/parameters'; import PickOptional from '../../types/PickOptional'; export type CreateParameters = Pick & @@ -157,7 +157,7 @@ export type CreateParameters = Pick & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; export type UpdateParameters = Pick & PickOptional & { @@ -187,6 +187,6 @@ export type UpdateParameters = Pick & restrictPaymentMethodsToCountry?: string; }; -export interface CancelParameters { +export interface CancelParameters extends IdempotencyParameter { testmode?: boolean; } diff --git a/src/binders/payments/refunds/parameters.ts b/src/binders/payments/refunds/parameters.ts index 9b0a35c6..54cdf268 100644 --- a/src/binders/payments/refunds/parameters.ts +++ b/src/binders/payments/refunds/parameters.ts @@ -1,5 +1,5 @@ import { RefundData, RefundEmbed } from '../../../data/refunds/data'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; import PickOptional from '../../../types/PickOptional'; interface ContextParameters { @@ -7,7 +7,7 @@ interface ContextParameters { testmode?: boolean; } -export type CreateParameters = ContextParameters & Pick & PickOptional; +export type CreateParameters = ContextParameters & Pick & PickOptional & IdempotencyParameter; export type GetParameters = ContextParameters & { embed?: RefundEmbed[]; @@ -18,6 +18,6 @@ export type ListParameters = ContextParameters & embed?: RefundEmbed[]; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; -export type CancelParameters = ContextParameters; +export type CancelParameters = ContextParameters & IdempotencyParameter; diff --git a/src/binders/profiles/ProfilesBinder.ts b/src/binders/profiles/ProfilesBinder.ts index 2aaf2cdf..bf1c9822 100644 --- a/src/binders/profiles/ProfilesBinder.ts +++ b/src/binders/profiles/ProfilesBinder.ts @@ -7,7 +7,7 @@ import checkId from '../../plumbing/checkId'; import renege from '../../plumbing/renege'; import Callback from '../../types/Callback'; import Binder from '../Binder'; -import { CreateParameters, IterateParameters, ListParameters, UpdateParameters } from './parameters'; +import { CreateParameters, DeleteParameters, IterateParameters, ListParameters, UpdateParameters } from './parameters'; const pathSegment = 'profiles'; @@ -124,13 +124,13 @@ export default class ProfilesBinder extends Binder { * @since 3.2.0 * @see https://docs.mollie.com/reference/v2/profiles-api/delete-profile */ - public delete(id: string): Promise; - public delete(id: string, callback: Callback>): void; - public delete(id: string) { + public delete(id: string, parameters?: DeleteParameters): Promise; + public delete(id: string, parameters: DeleteParameters, callback: Callback): void; + public delete(id: string, parameters?: DeleteParameters) { if (renege(this, this.delete, ...arguments)) return; if (!checkId(id, 'profile')) { throw new ApiError('The profile id is invalid'); } - return this.networkClient.delete(`${pathSegment}/${id}`); + return this.networkClient.delete(`${pathSegment}/${id}`, parameters); } } diff --git a/src/binders/profiles/giftcardIssuers/ProfileGiftcardIssuersBinder.ts b/src/binders/profiles/giftcardIssuers/ProfileGiftcardIssuersBinder.ts index ef084ada..348a859e 100644 --- a/src/binders/profiles/giftcardIssuers/ProfileGiftcardIssuersBinder.ts +++ b/src/binders/profiles/giftcardIssuers/ProfileGiftcardIssuersBinder.ts @@ -26,10 +26,11 @@ export default class ProfileGiftcardIssuersBinder extends Binder): void; public enable(parameters: Parameters) { if (renege(this, this.enable, ...arguments)) return; - if (!checkId(parameters.profileId, 'profile')) { + const { id, profileId, ...data } = parameters; + if (!checkId(profileId, 'profile')) { throw new ApiError('The profile id is invalid'); } - return this.networkClient.post(`${getPathSegments(parameters.profileId)}/${parameters.id}`, undefined); + return this.networkClient.post(`${getPathSegments(profileId)}/${id}`, data); } /** @@ -41,10 +42,11 @@ export default class ProfileGiftcardIssuersBinder extends Binder; public disable(parameters: Parameters, callback: Callback): void; public disable(parameters: Parameters) { - if (renege(this, this.enable, ...arguments)) return; - if (!checkId(parameters.profileId, 'profile')) { + if (renege(this, this.disable, ...arguments)) return; + const { id, profileId, ...context } = parameters; + if (!checkId(profileId, 'profile')) { throw new ApiError('The profile id is invalid'); } - return this.networkClient.delete(`${getPathSegments(parameters.profileId)}/${parameters.id}`); + return this.networkClient.delete(`${getPathSegments(profileId)}/${id}`, context); } } diff --git a/src/binders/profiles/giftcardIssuers/parameters.ts b/src/binders/profiles/giftcardIssuers/parameters.ts index 01d20f00..695e6e01 100644 --- a/src/binders/profiles/giftcardIssuers/parameters.ts +++ b/src/binders/profiles/giftcardIssuers/parameters.ts @@ -1,4 +1,6 @@ -export interface Parameters { +import { IdempotencyParameter } from '../../../types/parameters'; + +export interface Parameters extends IdempotencyParameter { /** * The ID of the profile, for example `pfl_v9hTwCvYqw`. */ diff --git a/src/binders/profiles/methods/ProfileMethodsBinder.ts b/src/binders/profiles/methods/ProfileMethodsBinder.ts index c11cb4a4..e695206a 100644 --- a/src/binders/profiles/methods/ProfileMethodsBinder.ts +++ b/src/binders/profiles/methods/ProfileMethodsBinder.ts @@ -29,10 +29,11 @@ export default class ProfileMethodsBinder extends Binder { public enable(parameters: Parameters, callback: Callback): void; public enable(parameters: Parameters) { if (renege(this, this.enable, ...arguments)) return; - if (!checkId(parameters.profileId, 'profile')) { + const { id, profileId, ...data } = parameters; + if (!checkId(profileId, 'profile')) { throw new ApiError('The profile id is invalid'); } - return this.networkClient.post(`${getPathSegments(parameters.profileId)}/${parameters.id}`, undefined); + return this.networkClient.post(`${getPathSegments(profileId)}/${id}`, data); } /** @@ -45,9 +46,10 @@ export default class ProfileMethodsBinder extends Binder { public disable(parameters: Parameters, callback: Callback): void; public disable(parameters: Parameters) { if (renege(this, this.disable, ...arguments)) return; - if (!checkId(parameters.profileId, 'profile')) { + const { id, profileId, ...context } = parameters; + if (!checkId(profileId, 'profile')) { throw new ApiError('The profile id is invalid'); } - return this.networkClient.delete(`${getPathSegments(parameters.profileId)}/${parameters.id}`); + return this.networkClient.delete(`${getPathSegments(profileId)}/${id}`, context); } } diff --git a/src/binders/profiles/methods/parameters.ts b/src/binders/profiles/methods/parameters.ts index 24e16fe7..79115f8f 100644 --- a/src/binders/profiles/methods/parameters.ts +++ b/src/binders/profiles/methods/parameters.ts @@ -1,6 +1,7 @@ import { PaymentMethod } from '../../../types'; +import { IdempotencyParameter } from '../../../types/parameters'; -export interface Parameters { +export interface Parameters extends IdempotencyParameter { /** * The ID of the profile, for example `pfl_v9hTwCvYqw`. */ diff --git a/src/binders/profiles/parameters.ts b/src/binders/profiles/parameters.ts index d851e205..a8b8f64d 100644 --- a/src/binders/profiles/parameters.ts +++ b/src/binders/profiles/parameters.ts @@ -1,11 +1,13 @@ import { ProfileData } from '../../data/profiles/data'; -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../types/parameters'; import PickOptional from '../../types/PickOptional'; -export type CreateParameters = Pick & PickOptional; +export type CreateParameters = Pick & PickOptional & IdempotencyParameter; export type ListParameters = PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; export type UpdateParameters = PickOptional; + +export interface DeleteParameters extends IdempotencyParameter {} diff --git a/src/binders/profiles/voucherIssuers/ProfileVoucherIssuersBinder.ts b/src/binders/profiles/voucherIssuers/ProfileVoucherIssuersBinder.ts index 08a9688d..0d51fb07 100644 --- a/src/binders/profiles/voucherIssuers/ProfileVoucherIssuersBinder.ts +++ b/src/binders/profiles/voucherIssuers/ProfileVoucherIssuersBinder.ts @@ -26,7 +26,7 @@ export default class ProfileVoucherIssuersBinder extends Binder): void; public enable(parameters: CreateParameters) { if (renege(this, this.enable, ...arguments)) return; - const { profileId, id, ...data } = parameters; + const { id, profileId, ...data } = parameters; if (!checkId(profileId, 'profile')) { throw new ApiError('The profile id is invalid'); } @@ -43,9 +43,10 @@ export default class ProfileVoucherIssuersBinder extends Binder): void; public disable(parameters: Parameters) { if (renege(this, this.disable, ...arguments)) return; - if (!checkId(parameters.profileId, 'profile')) { + const { id, profileId, ...context } = parameters; + if (!checkId(profileId, 'profile')) { throw new ApiError('The profile id is invalid'); } - return this.networkClient.delete(`${getPathSegments(parameters.profileId)}/${parameters.id}`); + return this.networkClient.delete(`${getPathSegments(profileId)}/${id}`, context); } } diff --git a/src/binders/profiles/voucherIssuers/parameters.ts b/src/binders/profiles/voucherIssuers/parameters.ts index 9e01b0d3..5de6fbc3 100644 --- a/src/binders/profiles/voucherIssuers/parameters.ts +++ b/src/binders/profiles/voucherIssuers/parameters.ts @@ -1,4 +1,6 @@ -export interface Parameters { +import { IdempotencyParameter } from '../../../types/parameters'; + +export interface Parameters extends IdempotencyParameter { /** * The ID of the profile, for example `pfl_v9hTwCvYqw`. */ @@ -9,7 +11,7 @@ export interface Parameters { id: string; } -export type CreateParameters = Pick & { +export type CreateParameters = Parameters & { /** * The contract id of the related contractor. Please note, for the first call that will be made to an issuer of the contractor, this field is required. You do not have to provide the same contract * id for other issuers of the same contractor. Update of the contract id will be possible through making the same call again with different contract ID value until the contract id is approved by diff --git a/src/binders/refunds/orders/parameters.ts b/src/binders/refunds/orders/parameters.ts index d23be908..96b03864 100644 --- a/src/binders/refunds/orders/parameters.ts +++ b/src/binders/refunds/orders/parameters.ts @@ -1,6 +1,6 @@ import { Amount } from '../../../data/global'; import { RefundData } from '../../../data/refunds/data'; -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { IdempotencyParameter, PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; interface ContextParameters { orderId: string; @@ -45,8 +45,8 @@ export type CreateParameters = ContextParameters & */ amount?: Amount; }[]; - }; + } & IdempotencyParameter; export type ListParameters = ContextParameters & PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/refunds/parameters.ts b/src/binders/refunds/parameters.ts index bfc303ac..e38d9e78 100644 --- a/src/binders/refunds/parameters.ts +++ b/src/binders/refunds/parameters.ts @@ -1,8 +1,8 @@ -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../types/parameters'; export type ListParameters = PaginationParameters & { profileId?: string; testmode?: boolean; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/settlements/captures/parameters.ts b/src/binders/settlements/captures/parameters.ts index 3c2b6528..1fe8fda0 100644 --- a/src/binders/settlements/captures/parameters.ts +++ b/src/binders/settlements/captures/parameters.ts @@ -1,8 +1,8 @@ -import { ThrottlingParameters } from '../../../types/parameters'; +import { ThrottlingParameter } from '../../../types/parameters'; import { ListParameters as CaptureListParameters } from '../../payments/captures/parameters'; export type ListParameters = Omit & { settlementId: string; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/settlements/chargebacks/parameters.ts b/src/binders/settlements/chargebacks/parameters.ts index 20cdc719..982c6a80 100644 --- a/src/binders/settlements/chargebacks/parameters.ts +++ b/src/binders/settlements/chargebacks/parameters.ts @@ -1,8 +1,8 @@ -import { ThrottlingParameters } from '../../../types/parameters'; +import { ThrottlingParameter } from '../../../types/parameters'; import { ListParameters as ChargebacksListParameters } from '../../chargebacks/parameters'; export type ListParameters = ChargebacksListParameters & { settlementId: string; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/settlements/parameters.ts b/src/binders/settlements/parameters.ts index 8584cb14..e2700cfb 100644 --- a/src/binders/settlements/parameters.ts +++ b/src/binders/settlements/parameters.ts @@ -1,5 +1,5 @@ -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../types/parameters'; export type ListParameters = PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/settlements/payments/parameters.ts b/src/binders/settlements/payments/parameters.ts index d738a557..f86eb8a0 100644 --- a/src/binders/settlements/payments/parameters.ts +++ b/src/binders/settlements/payments/parameters.ts @@ -1,8 +1,8 @@ -import { ThrottlingParameters } from '../../../types/parameters'; +import { ThrottlingParameter } from '../../../types/parameters'; import { ListParameters as PaymentListParameters } from '../../payments/parameters'; export type ListParameters = PaymentListParameters & { settlementId: string; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/settlements/refunds/parameters.ts b/src/binders/settlements/refunds/parameters.ts index 4f046565..25638e6e 100644 --- a/src/binders/settlements/refunds/parameters.ts +++ b/src/binders/settlements/refunds/parameters.ts @@ -1,8 +1,8 @@ -import { ThrottlingParameters } from '../../../types/parameters'; +import { ThrottlingParameter } from '../../../types/parameters'; import { ListParameters as RefundsListParameters } from '../../refunds/parameters'; export type ListParameters = RefundsListParameters & { settlementId: string; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/subscriptions/parameters.ts b/src/binders/subscriptions/parameters.ts index bfc303ac..e38d9e78 100644 --- a/src/binders/subscriptions/parameters.ts +++ b/src/binders/subscriptions/parameters.ts @@ -1,8 +1,8 @@ -import { PaginationParameters, ThrottlingParameters } from '../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../types/parameters'; export type ListParameters = PaginationParameters & { profileId?: string; testmode?: boolean; }; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/binders/subscriptions/payments/parameters.ts b/src/binders/subscriptions/payments/parameters.ts index bf374f70..7b39a375 100644 --- a/src/binders/subscriptions/payments/parameters.ts +++ b/src/binders/subscriptions/payments/parameters.ts @@ -1,4 +1,4 @@ -import { PaginationParameters, ThrottlingParameters } from '../../../types/parameters'; +import { PaginationParameters, ThrottlingParameter } from '../../../types/parameters'; interface ContextParameters { testmode?: boolean; @@ -8,4 +8,4 @@ interface ContextParameters { export type ListParameters = ContextParameters & PaginationParameters; -export type IterateParameters = Omit & ThrottlingParameters; +export type IterateParameters = Omit & ThrottlingParameter; diff --git a/src/communication/NetworkClient.ts b/src/communication/NetworkClient.ts index ae480f17..3ecb0bbf 100644 --- a/src/communication/NetworkClient.ts +++ b/src/communication/NetworkClient.ts @@ -1,7 +1,7 @@ import https from 'https'; import { SecureContextOptions } from 'tls'; -import axios, { AxiosInstance, AxiosResponse } from 'axios'; +import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios'; import List from '../data/list/List'; import ApiError from '../errors/ApiError'; @@ -10,10 +10,11 @@ import DemandingIterator from '../plumbing/iteration/DemandingIterator'; import HelpfulIterator from '../plumbing/iteration/HelpfulIterator'; import Throttler from '../plumbing/Throttler'; import Maybe from '../types/Maybe'; +import { IdempotencyParameter } from '../types/parameters'; import breakUrl from './breakUrl'; import buildUrl, { SearchParameters } from './buildUrl'; import dromedaryCase from './dromedaryCase'; -import makeRetrying from './makeRetrying'; +import makeRetrying, { idempotencyHeaderName } from './makeRetrying'; /** * Like `[].map` but with support for non-array inputs, in which case this function behaves as if an array was passed @@ -71,6 +72,9 @@ const throwApiError = (() => { }; })(); +interface Data {} +interface Context {} + /** * This class is essentially a wrapper around axios. It simplifies communication with the Mollie API over the network. */ @@ -110,8 +114,18 @@ export default class NetworkClient { makeRetrying(this.axiosInstance); } - async post(pathname: string, data: any, query?: SearchParameters): Promise { - const response = await this.axiosInstance.post(buildUrl(pathname, query), data).catch(throwApiError); + async post(pathname: string, data: Data & IdempotencyParameter, query?: SearchParameters): Promise { + // Take the idempotency key from the data, if any. It would be cleaner from a design perspective to have the + // idempotency key in a separate argument instead of cramming it into the data like this. However, having a + // separate argument would require every endpoint to split the input into those two arguments and thus cause a lot + // of boiler-plate code. + let config: AxiosRequestConfig | undefined = undefined; + if (data.idempotencyKey != undefined) { + const { idempotencyKey, ...rest } = data; + config = { headers: { [idempotencyHeaderName]: idempotencyKey } }; + data = rest; + } + const response = await this.axiosInstance.post(buildUrl(pathname, query), data, config).catch(throwApiError); if (response.status == 204) { return true; } @@ -211,13 +225,20 @@ export default class NetworkClient { }); } - async patch(pathname: string, data: any): Promise { + async patch(pathname: string, data: Data): Promise { const response = await this.axiosInstance.patch(pathname, data).catch(throwApiError); return response.data; } - async delete(pathname: string, context?: any): Promise { - const response = await this.axiosInstance.delete(pathname, { data: context }).catch(throwApiError); + async delete(pathname: string, context?: Context & IdempotencyParameter): Promise { + // Take the idempotency key from the context, if any. + let headers: AxiosRequestHeaders | undefined = undefined; + if (context?.idempotencyKey != undefined) { + const { idempotencyKey, ...rest } = context; + headers = { [idempotencyHeaderName]: idempotencyKey }; + context = rest; + } + const response = await this.axiosInstance.delete(pathname, { data: context, headers }).catch(throwApiError); if (response.status == 204) { return true; } diff --git a/src/communication/makeRetrying.ts b/src/communication/makeRetrying.ts index ec282c60..6835bf03 100644 --- a/src/communication/makeRetrying.ts +++ b/src/communication/makeRetrying.ts @@ -29,11 +29,11 @@ type AttemptState = { [attemptIndex]: number }; /** * The name of the header generated by `generateIdempotencyHeader`. */ -const idempotencyHeaderName = 'Idempotency-Key'; +export const idempotencyHeaderName = 'Idempotency-Key'; /** - * Returns an object with a single `'Idempotency-Key'` property, whose value is a random 24-character string which can - * be used as an idempotency key. + * Returns an object with a single property, whose value is a random 24-character string. + * The property's name is defined using the const `idempotencyHeaderName`. * * As the data encoded in said string is 144 bits long, the odds of two generated keys colliding is ±2% after * generating a sextillion (1000 billion billion, or 10^21) keys. @@ -87,7 +87,7 @@ export default function makeRetrying(axiosInstance: AxiosInstance) { axiosInstance.interceptors.request.use((config: AxiosRequestConfig & Partial) => { // If the request is a POST or DELETE one and does not yet have the idempotency header, add one now. if (unsafeMethods.has(config.method!) && config.headers?.[idempotencyHeaderName] == undefined) { - config.headers = { ...config.headers, ...generateIdempotencyHeader() }; + Object.assign((config.headers ??= {}), generateIdempotencyHeader()); } // Set the attempt (in the request configuration). config[attemptIndex] = (config[attemptIndex] ?? -1) + 1; diff --git a/src/createMollieClient.ts b/src/createMollieClient.ts index 1a186cf5..5175e19b 100644 --- a/src/createMollieClient.ts +++ b/src/createMollieClient.ts @@ -188,3 +188,4 @@ export { RefundEmbed, RefundStatus } from './data/refunds/data'; export { SubscriptionStatus } from './data/subscriptions/data'; export { ProfileStatus } from './data/profiles/data'; export { OnboardingStatus } from './data/onboarding/data'; +export { default as MollieApiError } from './errors/ApiError'; diff --git a/src/data/global.ts b/src/data/global.ts index 276f689b..48185d12 100644 --- a/src/data/global.ts +++ b/src/data/global.ts @@ -141,13 +141,3 @@ export enum SequenceType { first = 'first', recurring = 'recurring', } - -export type MollieApiErrorLinks = Record & Links; - -export interface MollieApiError { - status?: number; - title: string; - detail: string; - field?: string; - _links?: MollieApiErrorLinks; -} diff --git a/src/data/settlements/SettlementHelper.ts b/src/data/settlements/SettlementHelper.ts index c1d82b2d..77c35b10 100644 --- a/src/data/settlements/SettlementHelper.ts +++ b/src/data/settlements/SettlementHelper.ts @@ -1,6 +1,6 @@ import TransformingNetworkClient from '../../communication/TransformingNetworkClient'; import { Capture } from '../../types'; -import { ThrottlingParameters } from '../../types/parameters'; +import { ThrottlingParameter } from '../../types/parameters'; import Chargeback, { ChargebackData } from '../chargebacks/Chargeback'; import Helper from '../Helper'; import { CaptureData } from '../payments/captures/data'; @@ -21,7 +21,7 @@ export default class SettlementHelper extends Helper(this.links.payments.href, 'payments', undefined, parameters?.valuesPerMinute); } @@ -30,7 +30,7 @@ export default class SettlementHelper extends Helper(this.links.refunds.href, 'refunds', undefined, parameters?.valuesPerMinute); } @@ -39,7 +39,7 @@ export default class SettlementHelper extends Helper(this.links.chargebacks.href, 'chargebacks', undefined, parameters?.valuesPerMinute); } @@ -48,7 +48,7 @@ export default class SettlementHelper extends Helper(this.links.captures.href, 'captures', undefined, parameters?.valuesPerMinute); } } diff --git a/src/errors/ApiError.ts b/src/errors/ApiError.ts index e4a87ed5..3490231f 100644 --- a/src/errors/ApiError.ts +++ b/src/errors/ApiError.ts @@ -1,13 +1,31 @@ import { AxiosResponse } from 'axios'; -import { MollieApiErrorLinks, Url } from '../data/global'; +import { idempotencyHeaderName } from '../communication/makeRetrying'; +import { Links, Url } from '../data/global'; import Maybe from '../types/Maybe'; +type ApiErrorLinks = Record & Links; +type Info = { + field?: string; + statusCode?: number; + idempotencyKey?: string; + title?: string; + links?: ApiErrorLinks; +}; + export default class ApiError extends Error { - public constructor(message: string, protected title?: string, public readonly statusCode?: number, public readonly field?: string, protected links?: MollieApiErrorLinks) { + // Set the name to ApiError. + public readonly name: string = 'ApiError'; + public readonly field?: string; + public readonly statusCode?: number; + public readonly idempotencyKey?: string; + protected title?: string; + protected links?: ApiErrorLinks; + private readonly [Symbol.toStringTag] = this.name; + + public constructor(message: string, info: Info = {}) { super(message); - // Set the name to ApiError. - this.name = 'ApiError'; + Object.assign(this, info); // Ensure the message is enumerable, making it more likely to survive serialisation. Object.defineProperty(this, 'message', { enumerable: true }); } @@ -81,7 +99,7 @@ export default class ApiError extends Error { /** * @since 3.0.0 */ - public getUrl(key: keyof MollieApiErrorLinks): Maybe { + public getUrl(key: keyof ApiErrorLinks): Maybe { return this.links?.[key]?.href; } @@ -95,6 +113,8 @@ export default class ApiError extends Error { * @since 3.0.0 */ public static createFromResponse(response: AxiosResponse): ApiError { - return new ApiError(response.data.detail ?? 'Received an error without a message', response.data.title, response.data.status, response.data.field, response.data._links); + const { detail, title, status: statusCode, field, _links: links } = response.data; + const { headers } = response.config; + return new ApiError(detail ?? 'Received an error without a message', { title, statusCode, field, links, idempotencyKey: headers?.[idempotencyHeaderName] as string | undefined }); } } diff --git a/src/types/parameters.ts b/src/types/parameters.ts index 538e39ed..8302297f 100644 --- a/src/types/parameters.ts +++ b/src/types/parameters.ts @@ -3,10 +3,29 @@ export interface PaginationParameters { limit?: number; } -export interface ThrottlingParameters { +export interface ThrottlingParameter { /** * The number of values the iterator will produce per minute before throttling. Note that this value is not enforced * strictly; it is more of a hint. */ valuesPerMinute?: number; } + +export interface IdempotencyParameter { + /** + * The idempotency key sent to the Mollie API. The Mollie API uses this key to distinguish a single operation being + * attempted twice from two separate similarly-looking operations. When provided, you can safety re-attempt an + * operation without risk of said operation being performed twice. This is useful in case of network issues or your + * server going down unexpectedly, as it is uncertain whether the operation was actually performed during the first + * attempt in those situations. + * + * The Mollie API stores the response of every request made for a given idempotency key. A subsequent request with a + * known key will cause the previously stored response to be replayed; instead of having the effect the request would + * normally have. Note that the Mollie API will purge stored responses at some point. Re-attempts should happen + * within an hour for this reason. + * + * If this property is `undefined`, a random idempotency key will be generated and used internally. This property + * adds value only if the idempotency key is stored outside of the Mollie client. + */ + idempotencyKey?: string; +} diff --git a/tests/__nock-fixtures__/iteration.json b/tests/__nock-fixtures__/iteration.json index 6e640010..f9f08ee0 100644 --- a/tests/__nock-fixtures__/iteration.json +++ b/tests/__nock-fixtures__/iteration.json @@ -82,7 +82,7 @@ "scope": "https://api.mollie.com:443", "method": "POST", "path": "/v2/profiles/pfl_VmWrA97Mj4/methods/ideal", - "body": "", + "body": {}, "status": 201, "response": { "resource": "method", diff --git a/tests/__nock-fixtures__/profiles.json b/tests/__nock-fixtures__/profiles.json index 34129cb4..17ca4e10 100644 --- a/tests/__nock-fixtures__/profiles.json +++ b/tests/__nock-fixtures__/profiles.json @@ -168,7 +168,7 @@ "scope": "https://api.mollie.com:443", "method": "POST", "path": "/v2/profiles/pfl_XjBqKLWD7q/methods/giftcard/issuers/festivalcadeau", - "body": "", + "body": {}, "status": 201, "response": { "resource": "issuer", @@ -214,7 +214,7 @@ "scope": "https://api.mollie.com:443", "method": "DELETE", "path": "/v2/profiles/pfl_XjBqKLWD7q/methods/giftcard/issuers/festivalcadeau", - "body": "", + "body": {}, "status": 204, "response": "", "rawHeaders": [ @@ -299,7 +299,7 @@ "scope": "https://api.mollie.com:443", "method": "DELETE", "path": "/v2/profiles/pfl_XjBqKLWD7q/methods/voucher/issuers/appetiz", - "body": "", + "body": {}, "status": 204, "response": "", "rawHeaders": [ diff --git a/tests/communication/custom-idempotency-key.test.ts b/tests/communication/custom-idempotency-key.test.ts new file mode 100644 index 00000000..c4ef385e --- /dev/null +++ b/tests/communication/custom-idempotency-key.test.ts @@ -0,0 +1,107 @@ +import { Locale, MollieClient, SequenceType } from '../..'; +import NetworkMocker, { getApiKeyClientProvider } from '../NetworkMocker'; +import observePromise from '../matchers/observePromise'; +import tick from '../tick'; +import '../matchers/toBeDepleted'; + +const paymentResponse = { + resource: 'payment', + id: 'tr_WDqYK6vllg', + mode: 'test', + createdAt: '2018-03-20T13:13:37+00:00', + amount: { + value: '10.00', + currency: 'EUR', + }, + description: 'Order #12345', + method: null, + metadata: { + order_id: '12345', + }, + status: 'open', + isCancelable: false, + locale: 'nl_NL', + restrictPaymentMethodsToCountry: 'NL', + expiresAt: '2018-03-20T13:28:37+00:00', + details: null, + profileId: 'pfl_QkEhN94Ba', + sequenceType: 'oneoff', + redirectUrl: 'https://webshop.example.org/order/12345/', + webhookUrl: 'https://webshop.example.org/payments/webhook/', + _links: { + self: { + href: 'https://api.mollie.com/v2/payments/tr_WDqYK6vllg', + type: 'application/hal+json', + }, + checkout: { + href: 'https://www.mollie.com/payscreen/select-method/WDqYK6vllg', + type: 'text/html', + }, + dashboard: { + href: 'https://www.mollie.com/dashboard/org_12345678/payments/tr_WDqYK6vllg', + type: 'application/json', + }, + documentation: { + href: 'https://docs.mollie.com/reference/v2/payments-api/get-payment', + type: 'text/html', + }, + }, +}; + +describe('custom-idempotency-key', () => { + const networkMocker = new NetworkMocker(getApiKeyClientProvider(true)); + let mollieClient: MollieClient; + + beforeAll(async () => { + mollieClient = await networkMocker.prepare(); + }); + + test('custom-idempotency-key', async () => { + const errorInterceptor = networkMocker.intercept('POST', '/payments', 500, { + status: 500, + title: 'Internal Server Error', + detail: 'Mock error', + }); + errorInterceptor.matchHeader('Idempotency-Key', 'mock-key'); + const successInterceptor = networkMocker.intercept('POST', '/payments', 200, paymentResponse); + successInterceptor.matchHeader('Idempotency-Key', 'mock-key'); + + jest.useFakeTimers(); + + const paymentPromise = observePromise( + mollieClient.payments.create({ + amount: { + value: '10.00', + currency: 'EUR', + }, + description: 'Order #12345', + metadata: { + order_id: '12345', + }, + locale: Locale.nl_NL, + restrictPaymentMethodsToCountry: 'NL', + profileId: 'pfl_QkEhN94Ba', + sequenceType: SequenceType.oneoff, + redirectUrl: 'https://webshop.example.org/order/12345/', + webhookUrl: 'https://webshop.example.org/payments/webhook/', + idempotencyKey: 'mock-key', + }), + ); + + // Expect the first network request to have been made, and the promise to be pending. + await tick(); + expect(errorInterceptor).toBeDepleted(); + expect(paymentPromise).toBePending(); + + // Expect the second network request to have been made after two seconds, proving the Idempotency-Key header was + // consistent across these two network requests, and the promise to have fulfilled. + jest.advanceTimersByTime(2e3); + await tick(); + expect(successInterceptor).toBeDepleted(); + expect(paymentPromise).toBeFulfilledWith(expect.objectContaining({ id: 'tr_WDqYK6vllg' })); + + jest.useRealTimers(); + }); + + afterAll(() => networkMocker.cleanup()); +}); diff --git a/tests/communication/request-retrying.test.ts b/tests/communication/request-retrying.test.ts index 11d12f0f..6447ae2f 100644 --- a/tests/communication/request-retrying.test.ts +++ b/tests/communication/request-retrying.test.ts @@ -251,7 +251,7 @@ describe('request-retrying', () => { // Add a second interceptor which expects the same Idempotency-Key header. const successInterceptor = networkMocker.intercept('POST', '/payments', 200, paymentResponse); - successInterceptor.matchHeader('Idempotency-Key', idempotencyKey as string); + successInterceptor.matchHeader('Idempotency-Key', idempotencyKey!); // Expect the second network request to have been made after two seconds, proving the Idempotency-Key header was // consistent across these two network requests, and the promise to have fulfilled. diff --git a/tests/profiles/profiles.test.ts b/tests/profiles/profiles.test.ts index 19e72843..a0bc574d 100644 --- a/tests/profiles/profiles.test.ts +++ b/tests/profiles/profiles.test.ts @@ -1,4 +1,4 @@ -import { ApiMode, MollieClient } from '../..'; +import { ApiMode } from '../..'; import NetworkMocker, { getAccessTokenClientProvider } from '../NetworkMocker'; // 'record' ‒ This test interacts with the real Mollie API over the network, and records the communication. diff --git a/tests/unit/errors.test.ts b/tests/unit/errors.test.ts index 693067ea..b097685a 100644 --- a/tests/unit/errors.test.ts +++ b/tests/unit/errors.test.ts @@ -1,42 +1,60 @@ -import { PaymentCreateParams } from '../..'; +import { MollieApiError, PaymentCreateParams } from '../..'; import wireMockClient from '../wireMockClient'; -test('errorHandling', async () => { - expect.assertions(6); - +describe('errorHandling', () => { const { adapter, client } = wireMockClient(); - adapter.onGet('/customers/cst_chinchilla').reply(404, { - status: 404, - title: 'Not Found', - detail: 'No customer exists with token cst_chinchilla.', - _links: { documentation: { href: 'https://docs.mollie.com/guides/handling-errors', type: 'text/html' } }, - }); + test('data property passthrough', async () => { + expect.assertions(6); + adapter.onGet('/customers/cst_chinchilla').reply(404, { + status: 404, + title: 'Not Found', + detail: 'No customer exists with token cst_chinchilla.', + _links: { documentation: { href: 'https://docs.mollie.com/guides/handling-errors', type: 'text/html' } }, + }); - try { - await bluster(client.customers.get.bind(client.customers))('cst_chinchilla'); - } catch (error) { - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe('No customer exists with token cst_chinchilla.'); - // Ensure the message property survives conversion to and from JSON. - expect(JSON.parse(JSON.stringify(error)).message).toBe('No customer exists with token cst_chinchilla.'); - } + try { + await bluster(client.customers.get.bind(client.customers))('cst_chinchilla'); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toBe('No customer exists with token cst_chinchilla.'); + // Ensure the message property survives conversion to and from JSON. + expect(JSON.parse(JSON.stringify(error)).message).toBe('No customer exists with token cst_chinchilla.'); + } - adapter.onPost('/payments').reply(422, { - status: 422, - title: 'Unprocessable Entity', - detail: 'The amount is required for payments', - field: 'amount', - _links: { documentation: { href: 'https://docs.mollie.com/guides/handling-errors', type: 'text/html' } }, - }); + adapter.onPost('/payments').reply(422, { + status: 422, + title: 'Unprocessable Entity', + detail: 'The amount is required for payments', + field: 'amount', + _links: { documentation: { href: 'https://docs.mollie.com/guides/handling-errors', type: 'text/html' } }, + }); - try { const createPaymentParams = {}; + try { + await bluster(client.payments.create.bind(client.payments))(createPaymentParams as PaymentCreateParams); + } catch (error) { + expect(error).toBeInstanceOf(MollieApiError); + expect(error.field).toBe('amount'); + expect(error.message).toBe('The amount is required for payments'); + } + }); + + test('idempotency key retention', async () => { + expect.assertions(2); + adapter.onPost('/payments').reply(900, { + status: 900, + title: 'Custom failing error', + }); - await bluster(client.payments.create.bind(client.payments))(createPaymentParams as PaymentCreateParams); - } catch (error) { - expect(error).toBeInstanceOf(Error); - expect(error.field).toBe('amount'); - expect(error.message).toBe('The amount is required for payments'); - } + const createPaymentParams = { + idempotencyKey: 'mock-key', + }; + try { + await bluster(client.payments.create.bind(client.payments))(createPaymentParams as PaymentCreateParams); + } catch (error) { + expect(error).toBeInstanceOf(MollieApiError); + expect(error.idempotencyKey).toBe('mock-key'); + } + }); });