Skip to content

Commit

Permalink
[ACL-231] Support for generate idempotency keys when not provided
Browse files Browse the repository at this point in the history
  • Loading branch information
tl-Roberto-Mancinelli committed Nov 27, 2024
1 parent 2a59dfd commit 468dad2
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 45 deletions.
35 changes: 26 additions & 9 deletions src/TrueLayer/Payments/IPaymentsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,24 @@ public interface IPaymentsApi
/// <param name="idempotencyKey">
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
/// The value should be unique for each operation, e.g. a UUID, with the same key being sent on a retry of the same request.
/// If not provided a idempotency key is automatically generated.
/// </param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>An API response that includes details of the created payment if successful, otherwise problem details</returns>
Task<ApiResponse<CreatePaymentUnion>> CreatePayment(
CreatePaymentRequest paymentRequest, string idempotencyKey, CancellationToken cancellationToken = default);
CreatePaymentRequest paymentRequest,
string? idempotencyKey = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets the details of an existing payment
/// </summary>
/// <param name="id">The payment identifier</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>An API response that includes the payment details if successful, otherwise problem details</returns>
Task<ApiResponse<GetPaymentUnion>> GetPayment(string id, CancellationToken cancellationToken = default);
Task<ApiResponse<GetPaymentUnion>> GetPayment(
string id,
CancellationToken cancellationToken = default);

/// <summary>
/// Generates a link to the TrueLayer hosted payment page
Expand All @@ -66,7 +71,10 @@ Task<ApiResponse<CreatePaymentUnion>> CreatePayment(
/// Note this should be configured in the TrueLayer console under your application settings.
/// </param>
/// <returns>The HPP link you can redirect the end user to</returns>
string CreateHostedPaymentPageLink(string paymentId, string paymentToken, Uri returnUri);
string CreateHostedPaymentPageLink(
string paymentId,
string paymentToken,
Uri returnUri);

/// <summary>
/// Start the authorization flow for a payment.
Expand All @@ -75,13 +83,14 @@ Task<ApiResponse<CreatePaymentUnion>> CreatePayment(
/// <param name="idempotencyKey">
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
/// The value should be unique for each operation, e.g. a UUID, with the same key being sent on a retry of the same request.
/// If not provided a idempotency key is automatically generated.
/// </param>
/// <param name="request">The start authorization request details</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns></returns>
Task<ApiResponse<AuthorizationResponseUnion>> StartAuthorizationFlow(
string paymentId,
string idempotencyKey,
string? idempotencyKey,
StartAuthorizationFlowRequest request,
CancellationToken cancellationToken = default);

Expand All @@ -92,12 +101,14 @@ Task<ApiResponse<AuthorizationResponseUnion>> StartAuthorizationFlow(
/// <param name="idempotencyKey">
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
/// The value should be unique for each operation, e.g. a UUID, with the same key being sent on a retry of the same request.
/// If not provided a idempotency key is automatically generated.
/// </param>
/// <param name="request">The create payment refund request</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>The id of the created refund</returns>
Task<ApiResponse<CreatePaymentRefundResponse>> CreatePaymentRefund(string paymentId,
string idempotencyKey,
Task<ApiResponse<CreatePaymentRefundResponse>> CreatePaymentRefund(
string paymentId,
string? idempotencyKey,
CreatePaymentRefundRequest request,
CancellationToken cancellationToken = default);

Expand All @@ -107,7 +118,8 @@ Task<ApiResponse<CreatePaymentRefundResponse>> CreatePaymentRefund(string paymen
/// <param name="paymentId">The payment identifier</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>The list of refunds for a payment.</returns>
Task<ApiResponse<ListPaymentRefundsResponse>> ListPaymentRefunds(string paymentId,
Task<ApiResponse<ListPaymentRefundsResponse>> ListPaymentRefunds(
string paymentId,
CancellationToken cancellationToken = default);

/// <summary>
Expand All @@ -117,7 +129,8 @@ Task<ApiResponse<ListPaymentRefundsResponse>> ListPaymentRefunds(string paymentI
/// <param name="refundId">The refund identifier</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>The details of the selected refund</returns>
Task<ApiResponse<RefundUnion>> GetPaymentRefund(string paymentId,
Task<ApiResponse<RefundUnion>> GetPaymentRefund(
string paymentId,
string refundId,
CancellationToken cancellationToken = default);

Expand All @@ -128,9 +141,13 @@ Task<ApiResponse<RefundUnion>> GetPaymentRefund(string paymentId,
/// <param name="idempotencyKey">
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
/// The value should be unique for each operation, e.g. a UUID, with the same key being sent on a retry of the same request.
/// If not provided a idempotency key is automatically generated.
/// </param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>HTTP 202 Accepted if successful, otherwise problem details.</returns>
Task<ApiResponse> CancelPayment(string paymentId, string idempotencyKey, CancellationToken cancellationToken = default);
Task<ApiResponse> CancelPayment(
string paymentId,
string? idempotencyKey = null,
CancellationToken cancellationToken = default);
}
}
3 changes: 2 additions & 1 deletion src/TrueLayer/Payments/Model/CreatePaymentRefundRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace TrueLayer.Payments.Model;

public record CreatePaymentRefundRequest(string Reference,
public record CreatePaymentRefundRequest(
string Reference,
uint? AmountInMinor = null,
Dictionary<string, string>? Metadata = null);
36 changes: 21 additions & 15 deletions src/TrueLayer/Payments/PaymentsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,9 @@ public PaymentsApi(IApiClient apiClient, IAuthApi auth, TrueLayerOptions options
}

/// <inheritdoc />
public async Task<ApiResponse<CreatePaymentUnion>> CreatePayment(CreatePaymentRequest paymentRequest, string idempotencyKey, CancellationToken cancellationToken = default)
public async Task<ApiResponse<CreatePaymentUnion>> CreatePayment(CreatePaymentRequest paymentRequest, string? idempotencyKey = null, CancellationToken cancellationToken = default)
{
paymentRequest.NotNull(nameof(paymentRequest));
idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey));

var authResponse = await _auth.GetAuthToken(new GetAuthTokenRequest(AuthorizationScope.Payments), cancellationToken);

Expand All @@ -70,7 +69,7 @@ public async Task<ApiResponse<CreatePaymentUnion>> CreatePayment(CreatePaymentRe
return await _apiClient.PostAsync<CreatePaymentUnion>(
_baseUri,
paymentRequest,
idempotencyKey,
idempotencyKey ?? Guid.NewGuid().ToString(),
authResponse.Data!.AccessToken,
_options.Payments!.SigningKey,
cancellationToken
Expand Down Expand Up @@ -105,12 +104,11 @@ public string CreateHostedPaymentPageLink(string paymentId, string paymentToken,
/// <inheritdoc />
public async Task<ApiResponse<AuthorizationResponseUnion>> StartAuthorizationFlow(
string paymentId,
string idempotencyKey,
string? idempotencyKey,
StartAuthorizationFlowRequest request,
CancellationToken cancellationToken = default)
{
paymentId.NotNullOrWhiteSpace(nameof(paymentId));
idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey));
request.NotNull(nameof(request));

var authResponse = await _auth.GetAuthToken(
Expand All @@ -124,15 +122,18 @@ public async Task<ApiResponse<AuthorizationResponseUnion>> StartAuthorizationFlo
return await _apiClient.PostAsync<AuthorizationResponseUnion>(
_baseUri.Append(paymentId).Append(PaymentsEndpoints.AuthorizationFlow),
request,
idempotencyKey,
idempotencyKey ?? Guid.NewGuid().ToString(),
authResponse.Data!.AccessToken,
_options.Payments!.SigningKey,
cancellationToken
);
}

public async Task<ApiResponse<CreatePaymentRefundResponse>> CreatePaymentRefund(string paymentId,
string idempotencyKey, CreatePaymentRefundRequest request, CancellationToken cancellationToken = default)
public async Task<ApiResponse<CreatePaymentRefundResponse>> CreatePaymentRefund(
string paymentId,
string? idempotencyKey,
CreatePaymentRefundRequest request,
CancellationToken cancellationToken = default)
{
paymentId.NotNullOrWhiteSpace(nameof(paymentId));
request.NotNull(nameof(request));
Expand All @@ -148,14 +149,15 @@ public async Task<ApiResponse<CreatePaymentRefundResponse>> CreatePaymentRefund(
return await _apiClient.PostAsync<CreatePaymentRefundResponse>(
_baseUri.Append(paymentId).Append(PaymentsEndpoints.Refunds),
request,
idempotencyKey,
idempotencyKey ?? Guid.NewGuid().ToString(),
authResponse.Data!.AccessToken,
_options.Payments!.SigningKey,
cancellationToken
);
}

public async Task<ApiResponse<ListPaymentRefundsResponse>> ListPaymentRefunds(string paymentId,
public async Task<ApiResponse<ListPaymentRefundsResponse>> ListPaymentRefunds(
string paymentId,
CancellationToken cancellationToken = default)
{
paymentId.NotNullOrWhiteSpace(nameof(paymentId));
Expand All @@ -175,8 +177,10 @@ public async Task<ApiResponse<ListPaymentRefundsResponse>> ListPaymentRefunds(st
);
}

public async Task<ApiResponse<RefundUnion>> GetPaymentRefund(string paymentId,
string refundId, CancellationToken cancellationToken = default)
public async Task<ApiResponse<RefundUnion>> GetPaymentRefund(
string paymentId,
string refundId,
CancellationToken cancellationToken = default)
{
paymentId.NotNullOrWhiteSpace(nameof(paymentId));
refundId.NotNullOrWhiteSpace(nameof(refundId));
Expand All @@ -196,10 +200,12 @@ public async Task<ApiResponse<RefundUnion>> GetPaymentRefund(string paymentId,
);
}

public async Task<ApiResponse> CancelPayment(string paymentId, string idempotencyKey, CancellationToken cancellationToken = default)
public async Task<ApiResponse> CancelPayment(
string paymentId,
string? idempotencyKey = null,
CancellationToken cancellationToken = default)
{
paymentId.NotNullOrWhiteSpace(nameof(paymentId));
idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey));

var authResponse = await _auth.GetAuthToken(
new GetAuthTokenRequest(AuthorizationScope.Payments), cancellationToken);
Expand All @@ -211,7 +217,7 @@ public async Task<ApiResponse> CancelPayment(string paymentId, string idempotenc

return await _apiClient.PostAsync(
_baseUri.Append(paymentId).Append(PaymentsEndpoints.Cancel),
idempotencyKey: idempotencyKey,
idempotencyKey: idempotencyKey ?? Guid.NewGuid().ToString(),
accessToken: authResponse.Data!.AccessToken,
signingKey: _options.Payments!.SigningKey,
cancellationToken: cancellationToken);
Expand Down
9 changes: 7 additions & 2 deletions src/TrueLayer/Payouts/IPayoutsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,23 @@ public interface IPayoutsApi
/// <param name="idempotencyKey">
/// An idempotency key to allow safe retrying without the operation being performed multiple times.
/// The value should be unique for each operation, e.g. a UUID, with the same key being sent on a retry of the same request.
/// If not provided a idempotency key is automatically generated.
/// </param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>An API response that includes details of the created payout if successful, otherwise problem details</returns>
Task<ApiResponse<CreatePayoutResponse>> CreatePayout(
CreatePayoutRequest payoutRequest, string idempotencyKey, CancellationToken cancellationToken = default);
CreatePayoutRequest payoutRequest,
string? idempotencyKey = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Gets the details of an existing payment
/// </summary>
/// <param name="id">The payout identifier</param>
/// <param name="cancellationToken">The cancellation token to cancel the operation</param>
/// <returns>An API response that includes the payout details if successful, otherwise problem details</returns>
Task<ApiResponse<GetPayoutUnion>> GetPayout(string id, CancellationToken cancellationToken = default);
Task<ApiResponse<GetPayoutUnion>> GetPayout(
string id,
CancellationToken cancellationToken = default);
}
}
12 changes: 8 additions & 4 deletions src/TrueLayer/Payouts/PayoutsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ public PayoutsApi(IApiClient apiClient, IAuthApi auth, TrueLayerOptions options)
}

/// <inheritdoc />
public async Task<ApiResponse<CreatePayoutResponse>> CreatePayout(CreatePayoutRequest payoutRequest, string idempotencyKey, CancellationToken cancellationToken = default)
public async Task<ApiResponse<CreatePayoutResponse>> CreatePayout(
CreatePayoutRequest payoutRequest,
string? idempotencyKey = null,
CancellationToken cancellationToken = default)
{
payoutRequest.NotNull(nameof(payoutRequest));
idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey));

var authResponse = await _auth.GetAuthToken(new GetAuthTokenRequest(AuthorizationScope.Payments), cancellationToken);

Expand All @@ -52,14 +54,16 @@ public async Task<ApiResponse<CreatePayoutResponse>> CreatePayout(CreatePayoutRe
return await _apiClient.PostAsync<CreatePayoutResponse>(
_baseUri,
payoutRequest,
idempotencyKey,
idempotencyKey ?? Guid.NewGuid().ToString(),
authResponse.Data!.AccessToken,
_options.Payments!.SigningKey,
cancellationToken
);
}

public async Task<ApiResponse<GetPayoutUnion>> GetPayout(string id, CancellationToken cancellationToken = default)
public async Task<ApiResponse<GetPayoutUnion>> GetPayout(
string id,
CancellationToken cancellationToken = default)
{
id.NotNullOrWhiteSpace(nameof(id));
id.NotAUrl(nameof(id));
Expand Down
15 changes: 5 additions & 10 deletions test/TrueLayer.AcceptanceTests/PaymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ public PaymentTests(ApiTestFixture fixture)
[MemberData(nameof(ExternalAccountPaymentRequests))]
public async Task can_create_external_account_payment(CreatePaymentRequest paymentRequest)
{
var response = await _fixture.Client.Payments.CreatePayment(
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());
var response = await _fixture.Client.Payments.CreatePayment(paymentRequest);

response.StatusCode.Should().Be(HttpStatusCode.Created);
response.Data.IsT0.Should().BeTrue();
Expand Down Expand Up @@ -122,8 +121,7 @@ public async Task can_create_merchant_account_gbp_verification_Payment()
Verification = new Verification.Automated { RemitterName = true }
});

var response = await _fixture.Client.Payments.CreatePayment(
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());
var response = await _fixture.Client.Payments.CreatePayment(paymentRequest);

response.StatusCode.Should().Be(HttpStatusCode.Created);
response.Data.IsT0.Should().BeTrue();
Expand Down Expand Up @@ -206,8 +204,7 @@ public async Task Can_create_payment_with_auth_flow()
[MemberData(nameof(ExternalAccountPaymentRequests))]
public async Task Can_get_authorization_required_payment(CreatePaymentRequest paymentRequest)
{
var response = await _fixture.Client.Payments.CreatePayment(
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());
var response = await _fixture.Client.Payments.CreatePayment(paymentRequest);

response.IsSuccessful.Should().BeTrue();
response.Data.IsT0.Should().BeTrue();
Expand Down Expand Up @@ -280,7 +277,7 @@ public async Task Can_create_and_get_payment_refund()
// Act && assert
var createRefundResponse = await _fixture.Client.Payments.CreatePaymentRefund(
paymentId: payment.Id,
idempotencyKey: Guid.NewGuid().ToString(),
null,
new CreatePaymentRefundRequest(Reference: "a-reference"));
createRefundResponse.IsSuccessful.Should().BeTrue();
createRefundResponse.Data!.Id.Should().NotBeNullOrWhiteSpace();
Expand Down Expand Up @@ -334,9 +331,7 @@ public async Task Can_cancel_a_payment()
var paymentId = payment.Data.AsT0.Id;

// act
var cancelPaymentResponse = await _fixture.Client.Payments.CancelPayment(
paymentId,
idempotencyKey: Guid.NewGuid().ToString());
var cancelPaymentResponse = await _fixture.Client.Payments.CancelPayment(paymentId);

var getPaymentResponse = await _fixture.Client.Payments.GetPayment(paymentId);

Expand Down
7 changes: 3 additions & 4 deletions test/TrueLayer.AcceptanceTests/PayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ public async Task Can_create_payout()
{
CreatePayoutRequest payoutRequest = CreatePayoutRequest();

var response = await _fixture.Client.Payouts.CreatePayout(
payoutRequest, idempotencyKey: Guid.NewGuid().ToString());
var response = await _fixture.Client.Payouts.CreatePayout(payoutRequest);

response.StatusCode.Should().Be(HttpStatusCode.Accepted);
response.Data.Should().NotBeNull();
Expand Down Expand Up @@ -73,11 +72,11 @@ public async Task InitializeAsync()
throw new InvalidOperationException("You must have a merchant account in order to perform a payout");
}

_merchantAccount = accounts.Data.Items.Single(x => x.Currency == "GBP");
_merchantAccount = accounts.Data.Items.Single(x => x.Currency == Currencies.GBP);
}

private CreatePayoutRequest CreatePayoutRequest()
=> new CreatePayoutRequest(
=> new(
_merchantAccount!.Id,
100,
Currencies.GBP,
Expand Down

0 comments on commit 468dad2

Please sign in to comment.