diff --git a/src/TrueLayer/Payments/IPaymentsApi.cs b/src/TrueLayer/Payments/IPaymentsApi.cs index a8ff9a59..87ad26e1 100644 --- a/src/TrueLayer/Payments/IPaymentsApi.cs +++ b/src/TrueLayer/Payments/IPaymentsApi.cs @@ -42,11 +42,14 @@ public interface IPaymentsApi /// /// 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. /// /// The cancellation token to cancel the operation /// An API response that includes details of the created payment if successful, otherwise problem details Task> CreatePayment( - CreatePaymentRequest paymentRequest, string idempotencyKey, CancellationToken cancellationToken = default); + CreatePaymentRequest paymentRequest, + string? idempotencyKey = null, + CancellationToken cancellationToken = default); /// /// Gets the details of an existing payment @@ -54,7 +57,9 @@ Task> CreatePayment( /// The payment identifier /// The cancellation token to cancel the operation /// An API response that includes the payment details if successful, otherwise problem details - Task> GetPayment(string id, CancellationToken cancellationToken = default); + Task> GetPayment( + string id, + CancellationToken cancellationToken = default); /// /// Generates a link to the TrueLayer hosted payment page @@ -66,7 +71,10 @@ Task> CreatePayment( /// Note this should be configured in the TrueLayer console under your application settings. /// /// The HPP link you can redirect the end user to - string CreateHostedPaymentPageLink(string paymentId, string paymentToken, Uri returnUri); + string CreateHostedPaymentPageLink( + string paymentId, + string paymentToken, + Uri returnUri); /// /// Start the authorization flow for a payment. @@ -75,13 +83,14 @@ Task> CreatePayment( /// /// 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. /// /// The start authorization request details /// The cancellation token to cancel the operation /// Task> StartAuthorizationFlow( string paymentId, - string idempotencyKey, + string? idempotencyKey, StartAuthorizationFlowRequest request, CancellationToken cancellationToken = default); @@ -92,12 +101,14 @@ Task> StartAuthorizationFlow( /// /// 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. /// /// The create payment refund request /// The cancellation token to cancel the operation /// The id of the created refund - Task> CreatePaymentRefund(string paymentId, - string idempotencyKey, + Task> CreatePaymentRefund( + string paymentId, + string? idempotencyKey, CreatePaymentRefundRequest request, CancellationToken cancellationToken = default); @@ -107,7 +118,8 @@ Task> CreatePaymentRefund(string paymen /// The payment identifier /// The cancellation token to cancel the operation /// The list of refunds for a payment. - Task> ListPaymentRefunds(string paymentId, + Task> ListPaymentRefunds( + string paymentId, CancellationToken cancellationToken = default); /// @@ -117,7 +129,8 @@ Task> ListPaymentRefunds(string paymentI /// The refund identifier /// The cancellation token to cancel the operation /// The details of the selected refund - Task> GetPaymentRefund(string paymentId, + Task> GetPaymentRefund( + string paymentId, string refundId, CancellationToken cancellationToken = default); @@ -128,9 +141,13 @@ Task> GetPaymentRefund(string paymentId, /// /// 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. /// /// The cancellation token to cancel the operation /// HTTP 202 Accepted if successful, otherwise problem details. - Task CancelPayment(string paymentId, string idempotencyKey, CancellationToken cancellationToken = default); + Task CancelPayment( + string paymentId, + string? idempotencyKey = null, + CancellationToken cancellationToken = default); } } diff --git a/src/TrueLayer/Payments/Model/CreatePaymentRefundRequest.cs b/src/TrueLayer/Payments/Model/CreatePaymentRefundRequest.cs index b6140063..d3a84cab 100644 --- a/src/TrueLayer/Payments/Model/CreatePaymentRefundRequest.cs +++ b/src/TrueLayer/Payments/Model/CreatePaymentRefundRequest.cs @@ -2,6 +2,7 @@ namespace TrueLayer.Payments.Model; -public record CreatePaymentRefundRequest(string Reference, +public record CreatePaymentRefundRequest( + string Reference, uint? AmountInMinor = null, Dictionary? Metadata = null); diff --git a/src/TrueLayer/Payments/PaymentsApi.cs b/src/TrueLayer/Payments/PaymentsApi.cs index 958a6ecd..d5e092b9 100644 --- a/src/TrueLayer/Payments/PaymentsApi.cs +++ b/src/TrueLayer/Payments/PaymentsApi.cs @@ -55,10 +55,9 @@ public PaymentsApi(IApiClient apiClient, IAuthApi auth, TrueLayerOptions options } /// - public async Task> CreatePayment(CreatePaymentRequest paymentRequest, string idempotencyKey, CancellationToken cancellationToken = default) + public async Task> 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); @@ -70,7 +69,7 @@ public async Task> CreatePayment(CreatePaymentRe return await _apiClient.PostAsync( _baseUri, paymentRequest, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken @@ -105,12 +104,11 @@ public string CreateHostedPaymentPageLink(string paymentId, string paymentToken, /// public async Task> 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( @@ -124,15 +122,18 @@ public async Task> StartAuthorizationFlo return await _apiClient.PostAsync( _baseUri.Append(paymentId).Append(PaymentsEndpoints.AuthorizationFlow), request, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken ); } - public async Task> CreatePaymentRefund(string paymentId, - string idempotencyKey, CreatePaymentRefundRequest request, CancellationToken cancellationToken = default) + public async Task> CreatePaymentRefund( + string paymentId, + string? idempotencyKey, + CreatePaymentRefundRequest request, + CancellationToken cancellationToken = default) { paymentId.NotNullOrWhiteSpace(nameof(paymentId)); request.NotNull(nameof(request)); @@ -148,14 +149,15 @@ public async Task> CreatePaymentRefund( return await _apiClient.PostAsync( _baseUri.Append(paymentId).Append(PaymentsEndpoints.Refunds), request, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken ); } - public async Task> ListPaymentRefunds(string paymentId, + public async Task> ListPaymentRefunds( + string paymentId, CancellationToken cancellationToken = default) { paymentId.NotNullOrWhiteSpace(nameof(paymentId)); @@ -175,8 +177,10 @@ public async Task> ListPaymentRefunds(st ); } - public async Task> GetPaymentRefund(string paymentId, - string refundId, CancellationToken cancellationToken = default) + public async Task> GetPaymentRefund( + string paymentId, + string refundId, + CancellationToken cancellationToken = default) { paymentId.NotNullOrWhiteSpace(nameof(paymentId)); refundId.NotNullOrWhiteSpace(nameof(refundId)); @@ -196,10 +200,12 @@ public async Task> GetPaymentRefund(string paymentId, ); } - public async Task CancelPayment(string paymentId, string idempotencyKey, CancellationToken cancellationToken = default) + public async Task 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); @@ -211,7 +217,7 @@ public async Task 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); diff --git a/src/TrueLayer/Payouts/IPayoutsApi.cs b/src/TrueLayer/Payouts/IPayoutsApi.cs index d1266268..ba85f718 100644 --- a/src/TrueLayer/Payouts/IPayoutsApi.cs +++ b/src/TrueLayer/Payouts/IPayoutsApi.cs @@ -25,11 +25,14 @@ public interface IPayoutsApi /// /// 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. /// /// The cancellation token to cancel the operation /// An API response that includes details of the created payout if successful, otherwise problem details Task> CreatePayout( - CreatePayoutRequest payoutRequest, string idempotencyKey, CancellationToken cancellationToken = default); + CreatePayoutRequest payoutRequest, + string? idempotencyKey = null, + CancellationToken cancellationToken = default); /// /// Gets the details of an existing payment @@ -37,6 +40,8 @@ Task> CreatePayout( /// The payout identifier /// The cancellation token to cancel the operation /// An API response that includes the payout details if successful, otherwise problem details - Task> GetPayout(string id, CancellationToken cancellationToken = default); + Task> GetPayout( + string id, + CancellationToken cancellationToken = default); } } diff --git a/src/TrueLayer/Payouts/PayoutsApi.cs b/src/TrueLayer/Payouts/PayoutsApi.cs index 937a7041..a39232a4 100644 --- a/src/TrueLayer/Payouts/PayoutsApi.cs +++ b/src/TrueLayer/Payouts/PayoutsApi.cs @@ -37,10 +37,12 @@ public PayoutsApi(IApiClient apiClient, IAuthApi auth, TrueLayerOptions options) } /// - public async Task> CreatePayout(CreatePayoutRequest payoutRequest, string idempotencyKey, CancellationToken cancellationToken = default) + public async Task> 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); @@ -52,14 +54,16 @@ public async Task> CreatePayout(CreatePayoutRe return await _apiClient.PostAsync( _baseUri, payoutRequest, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken ); } - public async Task> GetPayout(string id, CancellationToken cancellationToken = default) + public async Task> GetPayout( + string id, + CancellationToken cancellationToken = default) { id.NotNullOrWhiteSpace(nameof(id)); id.NotAUrl(nameof(id)); diff --git a/test/TrueLayer.AcceptanceTests/PaymentTests.cs b/test/TrueLayer.AcceptanceTests/PaymentTests.cs index 9e91b408..61930996 100644 --- a/test/TrueLayer.AcceptanceTests/PaymentTests.cs +++ b/test/TrueLayer.AcceptanceTests/PaymentTests.cs @@ -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(); @@ -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(); @@ -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(); @@ -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(); @@ -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); diff --git a/test/TrueLayer.AcceptanceTests/PayoutTests.cs b/test/TrueLayer.AcceptanceTests/PayoutTests.cs index a902530b..47f87861 100644 --- a/test/TrueLayer.AcceptanceTests/PayoutTests.cs +++ b/test/TrueLayer.AcceptanceTests/PayoutTests.cs @@ -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(); @@ -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,