Skip to content

Commit

Permalink
#347 Add support for setting custom idempotency keys (#352)
Browse files Browse the repository at this point in the history
* #347 Add support for setting custom idempotency keys

* Downgrade Microsoft packages to version 6
  • Loading branch information
Viincenttt authored Mar 29, 2024
1 parent f63c9d8 commit e3c9bd6
Show file tree
Hide file tree
Showing 27 changed files with 146 additions and 59 deletions.
2 changes: 1 addition & 1 deletion src/Mollie.Api/Client/Abstract/IBalanceClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IBalanceClient : IDisposable {
public interface IBalanceClient : IBaseMollieClient {
Task<BalanceResponse> GetBalanceAsync(string balanceId);
Task<BalanceResponse> GetBalanceAsync(UrlObjectLink<BalanceResponse> url);
Task<BalanceResponse> GetPrimaryBalanceAsync();
Expand Down
9 changes: 9 additions & 0 deletions src/Mollie.Api/Client/Abstract/IBaseMollieClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

namespace Mollie.Api.Client.Abstract
{
public interface IBaseMollieClient : IDisposable
{
IDisposable WithIdempotencyKey(string value);
}
}
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/ICaptureClient.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Capture;
using Mollie.Api.Models.Capture.Request;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface ICaptureClient : IDisposable {
public interface ICaptureClient : IBaseMollieClient {
Task<CaptureResponse> GetCaptureAsync(string paymentId, string captureId, bool testmode = false);
Task<CaptureResponse> GetCaptureAsync(UrlObjectLink<CaptureResponse> url);
Task<ListResponse<CaptureResponse>> GetCapturesListAsync(string paymentId, bool testmode = false);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IChargebacksClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.Chargeback;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IChargebacksClient : IDisposable {
public interface IChargebacksClient : IBaseMollieClient {
Task<ChargebackResponse> GetChargebackAsync(string paymentId, string chargebackId, bool testmode = false);
Task<ListResponse<ChargebackResponse>> GetChargebacksListAsync(string paymentId, string from = null, int? limit = null, bool testmode = false);
Task<ListResponse<ChargebackResponse>> GetChargebacksListAsync(string profileId = null, bool testmode = false);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/ICustomerClient.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.Customer;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Payment.Request;
using Mollie.Api.Models.Payment.Response;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface ICustomerClient : IDisposable {
public interface ICustomerClient : IBaseMollieClient {
Task<CustomerResponse> CreateCustomerAsync(CustomerRequest request);
Task<CustomerResponse> UpdateCustomerAsync(string customerId, CustomerRequest request);
Task DeleteCustomerAsync(string customerId, bool testmode = false);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IInvoicesClient.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.Invoice;
using Mollie.Api.Models.List;

using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IInvoicesClient : IDisposable {
public interface IInvoicesClient : IBaseMollieClient {
Task<InvoiceResponse> GetInvoiceAsync(string invoiceId);
Task<InvoiceResponse> GetInvoiceAsync(UrlObjectLink<InvoiceResponse> url);
Task<ListResponse<InvoiceResponse>> GetInvoiceListAsync(
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IMandateClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Mandate;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IMandateClient : IDisposable {
public interface IMandateClient : IBaseMollieClient {
Task<MandateResponse> GetMandateAsync(string customerId, string mandateId, bool testmode = false);
Task<ListResponse<MandateResponse>> GetMandateListAsync(string customerId, string from = null, int? limit = null, bool testmode = false);
Task<MandateResponse> CreateMandateAsync(string customerId, MandateRequest request);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IOnboardingClient.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
using System;
using Mollie.Api.Models.Onboarding.Request;
using Mollie.Api.Models.Onboarding.Request;
using Mollie.Api.Models.Onboarding.Response;
using System.Threading.Tasks;

namespace Mollie.Api.Client.Abstract {
public interface IOnboardingClient : IDisposable {
public interface IOnboardingClient : IBaseMollieClient {
/// <summary>
/// Get the status of onboarding of the authenticated organization.
/// </summary>
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IOrderClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Order;
Expand All @@ -9,7 +8,7 @@
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IOrderClient : IDisposable {
public interface IOrderClient : IBaseMollieClient {
Task<OrderResponse> CreateOrderAsync(OrderRequest orderRequest);
Task<OrderResponse> GetOrderAsync(
string orderId, bool embedPayments = false, bool embedRefunds = false, bool embedShipments = false, bool testmode = false);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IOrganizationsClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Organization;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IOrganizationsClient : IDisposable {
public interface IOrganizationsClient : IBaseMollieClient {
Task<OrganizationResponse> GetCurrentOrganizationAsync();
Task<OrganizationResponse> GetOrganizationAsync(string organizationId);
Task<ListResponse<OrganizationResponse>> GetOrganizationsListAsync(string from = null, int? limit = null);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IPaymentClient.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Payment.Request;
using Mollie.Api.Models.Payment.Response;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IPaymentClient : IDisposable {
public interface IPaymentClient : IBaseMollieClient {
Task<PaymentResponse> CreatePaymentAsync(PaymentRequest paymentRequest, bool includeQrCode = false);

/// <summary>
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IPaymentLinkClient.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.PaymentLink.Request;
using Mollie.Api.Models.PaymentLink.Response;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IPaymentLinkClient : IDisposable {
public interface IPaymentLinkClient : IBaseMollieClient {
Task<PaymentLinkResponse> CreatePaymentLinkAsync(PaymentLinkRequest paymentLinkRequest);

/// <summary>
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IPaymentMethodClient.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Payment;
using Mollie.Api.Models.PaymentMethod;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IPaymentMethodClient : IDisposable {
public interface IPaymentMethodClient : IBaseMollieClient {
Task<PaymentMethodResponse> GetPaymentMethodAsync(
string paymentMethod,
bool includeIssuers = false,
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IPermissionsClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Permission;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IPermissionsClient : IDisposable {
public interface IPermissionsClient : IBaseMollieClient {
Task<PermissionResponse> GetPermissionAsync(string permissionId);
Task<PermissionResponse> GetPermissionAsync(UrlObjectLink<PermissionResponse> url);
Task<ListResponse<PermissionResponse>> GetPermissionListAsync();
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IProfileClient.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.PaymentMethod;
using Mollie.Api.Models.Profile.Request;
using Mollie.Api.Models.Profile.Response;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IProfileClient : IDisposable {
public interface IProfileClient : IBaseMollieClient {
Task<ProfileResponse> CreateProfileAsync(ProfileRequest request);
Task<ProfileResponse> GetProfileAsync(string profileId);
Task<ProfileResponse> GetProfileAsync(UrlObjectLink<ProfileResponse> url);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IRefundClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Refund;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IRefundClient : IDisposable {
public interface IRefundClient : IBaseMollieClient {
Task CancelRefundAsync(string paymentId, string refundId, bool testmode = false);
Task<RefundResponse> CreateRefundAsync(string paymentId, RefundRequest refundRequest);
Task<RefundResponse> GetRefundAsync(string paymentId, string refundId, bool testmode = false);
Expand Down
2 changes: 1 addition & 1 deletion src/Mollie.Api/Client/Abstract/ISettlementsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface ISettlementsClient : IDisposable {
public interface ISettlementsClient : IBaseMollieClient {
Task<SettlementResponse> GetSettlementAsync(string settlementId);
Task<SettlementResponse> GetNextSettlement();
Task<SettlementResponse> GetOpenSettlement();
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/IShipmentClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Shipment;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface IShipmentClient : IDisposable {
public interface IShipmentClient : IBaseMollieClient {
Task<ShipmentResponse> CreateShipmentAsync(string orderId, ShipmentRequest shipmentRequest);
Task<ShipmentResponse> GetShipmentAsync(string orderId, string shipmentId, bool testmode = false);
Task<ShipmentResponse> GetShipmentAsync(UrlObjectLink<ShipmentResponse> url);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/ISubscriptionClient.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Payment.Response;
using Mollie.Api.Models.Subscription;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client.Abstract {
public interface ISubscriptionClient : IDisposable {
public interface ISubscriptionClient : IBaseMollieClient {
Task CancelSubscriptionAsync(string customerId, string subscriptionId, bool testmode = false);
Task<SubscriptionResponse> CreateSubscriptionAsync(string customerId, SubscriptionRequest request);
Task<SubscriptionResponse> GetSubscriptionAsync(string customerId, string subscriptionId, bool testmode = false);
Expand Down
5 changes: 2 additions & 3 deletions src/Mollie.Api/Client/Abstract/ITerminalClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Mollie.Api.Models.List;
using Mollie.Api.Models.Terminal;
using Mollie.Api.Models.Url;
Expand All @@ -8,7 +7,7 @@ namespace Mollie.Api.Client.Abstract
{ /// <summary>
/// Calls in this class are documented in https://docs.mollie.com/reference/v2/terminals-api/overview
/// </summary>
public interface ITerminalClient : IDisposable {
public interface ITerminalClient : IBaseMollieClient {
Task<TerminalResponse> GetTerminalAsync(string terminalId);
Task<TerminalResponse> GetTerminalAsync(UrlObjectLink<TerminalResponse> url);
Task<ListResponse<TerminalResponse>> GetTerminalListAsync(string from = null, int? limit = null, string profileId = null, bool testmode = false);
Expand Down
2 changes: 1 addition & 1 deletion src/Mollie.Api/Client/Abstract/IWalletClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Mollie.Api.Models.Wallet.Response;

namespace Mollie.Api.Client.Abstract {
public interface IWalletClient {
public interface IWalletClient : IBaseMollieClient {
Task<ApplePayPaymentSessionResponse> RequestApplePayPaymentSessionAsync(ApplePayPaymentSessionRequest request);
}
}
11 changes: 10 additions & 1 deletion src/Mollie.Api/Client/BaseMollieClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Mollie.Api.Extensions;
using Mollie.Api.Framework;
using Mollie.Api.Framework.Idempotency;
using Mollie.Api.Models.Url;

namespace Mollie.Api.Client {
Expand All @@ -17,6 +18,8 @@ public abstract class BaseMollieClient : IDisposable {
private readonly string _apiKey;
private readonly HttpClient _httpClient;
private readonly JsonConverterService _jsonConverterService;

private readonly AsyncLocalVariable<string> _idempotencyKey = new AsyncLocalVariable<string>(null);

private readonly bool _createdHttpClient = false;

Expand All @@ -37,6 +40,11 @@ protected BaseMollieClient(HttpClient httpClient = null, string apiEndpoint = Ap
this._createdHttpClient = httpClient == null;
this._httpClient = httpClient ?? new HttpClient();
}

public IDisposable WithIdempotencyKey(string value) {
_idempotencyKey.Value = value;
return _idempotencyKey;
}

private async Task<T> SendHttpRequest<T>(HttpMethod httpMethod, string relativeUri, object data = null) {
HttpRequestMessage httpRequest = this.CreateHttpRequest(httpMethod, relativeUri);
Expand Down Expand Up @@ -127,7 +135,8 @@ protected virtual HttpRequestMessage CreateHttpRequest(HttpMethod method, string
httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", this._apiKey);
httpRequest.Headers.Add("User-Agent", this.GetUserAgent());
httpRequest.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
var idemPotencyKey = _idempotencyKey.Value ?? Guid.NewGuid().ToString();
httpRequest.Headers.Add("Idempotency-Key", idemPotencyKey);
httpRequest.Content = content;

return httpRequest;
Expand Down
26 changes: 26 additions & 0 deletions src/Mollie.Api/Framework/Idempotency/AsyncLocalVariable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Threading;

namespace Mollie.Api.Framework.Idempotency
{
public class AsyncLocalVariable<T> : IDisposable where T : class
{
private readonly AsyncLocal<T> _asyncLocal = new AsyncLocal<T>();

public T Value
{
get => _asyncLocal.Value;
set => _asyncLocal.Value = value;
}

public AsyncLocalVariable(T value)
{
_asyncLocal.Value = value;
}

public void Dispose()
{
_asyncLocal.Value = null;
}
}
}
3 changes: 1 addition & 2 deletions src/Mollie.Api/Mollie.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="7.0.10" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="6.0.28" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
</ItemGroup>
Expand Down
20 changes: 20 additions & 0 deletions tests/Mollie.Tests.Integration/Api/PaymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,26 @@ public async Task CanCreateDefaultPaymentWithOnlyRequiredFields() {
result.Description.Should().Be(paymentRequest.Description);
result.RedirectUrl.Should().Be(paymentRequest.RedirectUrl);
}

[DefaultRetryFact]
public async Task CanCreateDefaultPaymentWithCustomIdempotencyKey() {
// Given: we create a payment request with only the required parameters
PaymentRequest paymentRequest = new PaymentRequest() {
Amount = new Amount(Currency.EUR, "100.00"),
Description = "Description",
RedirectUrl = DefaultRedirectUrl
};

// When: We send the payment request to Mollie
using (_paymentClient.WithIdempotencyKey("my-idempotency-key"))
{
PaymentResponse firstAttempt = await _paymentClient.CreatePaymentAsync(paymentRequest);
PaymentResponse secondAttempt = await _paymentClient.CreatePaymentAsync(paymentRequest);

// Then: Make sure the responses have the same payment Id
firstAttempt.Id.Should().Be(secondAttempt.Id);
}
}

[DefaultRetryFact]
public async Task CanCreateDefaultPaymentWithAllFields() {
Expand Down
Loading

0 comments on commit e3c9bd6

Please sign in to comment.