From 686f478b47e29c5aef8b7a0774e216f28a84dd5d Mon Sep 17 00:00:00 2001 From: Roberto Mancinelli Date: Wed, 27 Nov 2024 15:09:34 +0100 Subject: [PATCH 1/6] [ACL-231] Support for generate idempotency keys when not provided --- src/TrueLayer/Payments/IPaymentsApi.cs | 35 +++++++++++++----- .../Model/CreatePaymentRefundRequest.cs | 3 +- src/TrueLayer/Payments/PaymentsApi.cs | 36 +++++++++++-------- src/TrueLayer/Payouts/IPayoutsApi.cs | 9 +++-- src/TrueLayer/Payouts/PayoutsApi.cs | 12 ++++--- .../TrueLayer.AcceptanceTests/PaymentTests.cs | 15 +++----- test/TrueLayer.AcceptanceTests/PayoutTests.cs | 7 ++-- 7 files changed, 72 insertions(+), 45 deletions(-) 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 b6c66ec9..7b438e37 100644 --- a/test/TrueLayer.AcceptanceTests/PaymentTests.cs +++ b/test/TrueLayer.AcceptanceTests/PaymentTests.cs @@ -67,8 +67,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); var authorizationRequired = response.Data.AsT0; @@ -131,8 +130,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); var authorizationRequired = response.Data.AsT0; @@ -239,8 +237,7 @@ public async Task GetPayment_Should_Return_Settled_Payment() [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(); @@ -312,7 +309,7 @@ public async Task Can_create_and_get_payment_refund() // Act && assert var createRefundResponse = await _fixture.Client.Payments.CreatePaymentRefund( paymentId: paymentId, - idempotencyKey: Guid.NewGuid().ToString(), + null, new CreatePaymentRefundRequest(Reference: "a-reference")); createRefundResponse.IsSuccessful.Should().BeTrue(); createRefundResponse.Data!.Id.Should().NotBeNullOrWhiteSpace(); @@ -366,9 +363,7 @@ public async Task Can_Cancel_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, From 0d047e7771bd7a431cbb76bb9f2ca1ac13ea4a95 Mon Sep 17 00:00:00 2001 From: Roberto Mancinelli Date: Wed, 27 Nov 2024 16:13:40 +0100 Subject: [PATCH 2/6] added idempotency key generation for mandates; added CreateHostedPaymentPageLink method --- src/TrueLayer/Mandates/IMandatesApi.cs | 63 ++++++++++++++++++--- src/TrueLayer/Mandates/MandatesApi.cs | 78 ++++++++++++++++++++------ 2 files changed, 114 insertions(+), 27 deletions(-) diff --git a/src/TrueLayer/Mandates/IMandatesApi.cs b/src/TrueLayer/Mandates/IMandatesApi.cs index 41d0c65a..42b55646 100644 --- a/src/TrueLayer/Mandates/IMandatesApi.cs +++ b/src/TrueLayer/Mandates/IMandatesApi.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using OneOf; @@ -32,7 +33,9 @@ public interface IMandatesApi /// The cancellation token to cancel the operation /// An API response that includes details of the created mandate if successful, otherwise problem details Task> CreateMandate( - CreateMandateRequest mandateRequest, string idempotencyKey, CancellationToken cancellationToken = default); + CreateMandateRequest mandateRequest, + string? idempotencyKey = null, + CancellationToken cancellationToken = default); /// /// Gets a mandate @@ -42,7 +45,24 @@ Task> CreateMandate( /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details Task> GetMandate( - string mandateId, MandateType mandateType, CancellationToken cancellationToken = default); + string mandateId, + MandateType mandateType, + CancellationToken cancellationToken = default); + + /// + /// Generates a link to the TrueLayer hosted payment page + /// + /// The mandate identifier + /// The resource token, returned from + /// + /// Your return URI to which the end user will be redirected after the mandate is completed. + /// 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 mandateId, + string resourceToken, + Uri returnUri); /// /// Lists mandates for a user @@ -52,7 +72,9 @@ Task> GetMandate( /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details Task>> ListMandates( - ListMandatesQuery query, MandateType mandateType, CancellationToken cancellationToken = default); + ListMandatesQuery query, + MandateType mandateType, + CancellationToken cancellationToken = default); /// /// Start the authorization flow for a mandate. @@ -67,7 +89,11 @@ Task>> ListMandates( /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details Task> StartAuthorizationFlow( - string mandateId, StartAuthorizationFlowRequest request, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default); + string mandateId, + StartAuthorizationFlowRequest request, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default); /// /// Submit the provider details selected by the PSU. @@ -82,7 +108,11 @@ Task> StartAuthorizationFlow( /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details Task> SubmitProviderSelection( - string mandateId, SubmitProviderSelectionRequest request, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default); + string mandateId, + SubmitProviderSelectionRequest request, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default); /// /// Submit the consent given by the user @@ -95,7 +125,11 @@ Task> SubmitProviderSelection( /// /// The cancellation token to cancel the operation /// An API response that includes the authorization flow action details if successful, otherwise problem details - Task> SubmitConsent(string mandateId, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default); + Task> SubmitConsent( + string mandateId, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default); /// /// Get Confirmation Of Funds @@ -107,7 +141,11 @@ Task> SubmitProviderSelection( /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details Task> GetConfirmationOfFunds( - string mandateId, int amountInMinor, string currency, MandateType mandateType, CancellationToken cancellationToken = default); + string mandateId, + int amountInMinor, + string currency, + MandateType mandateType, + CancellationToken cancellationToken = default); /// /// Gets a mandates constraints @@ -117,18 +155,25 @@ Task> GetConfirmationOfFunds( /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details Task> GetMandateConstraints( - string mandateId, MandateType mandateType, CancellationToken cancellationToken = default); + string mandateId, + MandateType mandateType, + CancellationToken cancellationToken = default); /// /// Revoke mandate /// /// The id of the mandate + /// The type of the mandate. Either sweeping or commercial /// /// 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. /// /// The cancellation token to cancel the operation /// An API response that includes the payment details if successful, otherwise problem details - Task RevokeMandate(string mandateId, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default); + Task RevokeMandate( + string mandateId, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default); } } diff --git a/src/TrueLayer/Mandates/MandatesApi.cs b/src/TrueLayer/Mandates/MandatesApi.cs index 505fe239..b79524be 100644 --- a/src/TrueLayer/Mandates/MandatesApi.cs +++ b/src/TrueLayer/Mandates/MandatesApi.cs @@ -6,6 +6,7 @@ using TrueLayer.Extensions; using TrueLayer.Mandates.Model; using TrueLayer.Models; +using TrueLayer.Payments; namespace TrueLayer.Mandates { @@ -25,12 +26,14 @@ internal class MandatesApi : IMandatesApi private readonly TrueLayerOptions _options; private readonly Uri _baseUri; private readonly IAuthApi _auth; + private readonly HppLinkBuilder _hppLinkBuilder; public MandatesApi(IApiClient apiClient, IAuthApi auth, TrueLayerOptions options) { _apiClient = apiClient.NotNull(nameof(apiClient)); _options = options.NotNull(nameof(options)); _auth = auth.NotNull(nameof(auth)); + _hppLinkBuilder = new HppLinkBuilder(options.Payments?.HppUri, options.UseSandbox ?? true); options.Payments.NotNull(nameof(options.Payments))!.Validate(); @@ -39,10 +42,13 @@ public MandatesApi(IApiClient apiClient, IAuthApi auth, TrueLayerOptions options } /// - public async Task> CreateMandate(CreateMandateRequest mandateRequest, string idempotencyKey, CancellationToken cancellationToken = default) + public async Task> CreateMandate( + CreateMandateRequest mandateRequest, + string? idempotencyKey = null, + CancellationToken cancellationToken = default) { mandateRequest.NotNull(nameof(mandateRequest)); - idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey)); + var mandateType = mandateRequest.Mandate.Match( vrpCommercial => vrpCommercial.Type, vrpSweeping => vrpSweeping.Type); @@ -56,7 +62,7 @@ public async Task> CreateMandate(CreateMandat return await _apiClient.PostAsync( _baseUri, mandateRequest, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken @@ -65,7 +71,10 @@ public async Task> CreateMandate(CreateMandat //TODO: is it correct that this method expects a mandate type? /// - public async Task> GetMandate(string mandateId, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task> GetMandate( + string mandateId, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); @@ -85,7 +94,14 @@ public async Task> GetMandate(string mandateId, } /// - public async Task>> ListMandates(ListMandatesQuery query, MandateType mandateType, CancellationToken cancellationToken = default) + public string CreateHostedPaymentPageLink(string mandateId, string resourceToken, Uri returnUri) + => _hppLinkBuilder.Build(mandateId, resourceToken, returnUri); + + /// + public async Task>> ListMandates( + ListMandatesQuery query, + MandateType mandateType, + CancellationToken cancellationToken = default) { var authResponse = await _auth.GetAuthToken(new GetAuthTokenRequest($"recurring_payments:{mandateType.AsString()}"), cancellationToken); @@ -108,12 +124,17 @@ public async Task>> ListManda } /// - public async Task> StartAuthorizationFlow(string mandateId, StartAuthorizationFlowRequest request, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task> StartAuthorizationFlow( + string mandateId, + StartAuthorizationFlowRequest request, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); request.NotNull(nameof(request)); - idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey)); + var authResponse = await _auth.GetAuthToken(new GetAuthTokenRequest($"recurring_payments:{mandateType.AsString()}"), cancellationToken); if (!authResponse.IsSuccessful) @@ -124,7 +145,7 @@ public async Task> StartAuthorizationFlo return await _apiClient.PostAsync( _baseUri.Append(mandateId).Append(MandatesEndpoints.AuthorizationFlow), request, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken @@ -132,12 +153,17 @@ public async Task> StartAuthorizationFlo } /// - public async Task> SubmitProviderSelection(string mandateId, SubmitProviderSelectionRequest request, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task> SubmitProviderSelection( + string mandateId, + SubmitProviderSelectionRequest request, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); request.NotNull(nameof(request)); - idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey)); + var authResponse = await _auth.GetAuthToken(new GetAuthTokenRequest($"recurring_payments:{mandateType.AsString()}"), cancellationToken); if (!authResponse.IsSuccessful) @@ -148,18 +174,22 @@ public async Task> SubmitProviderSelecti return await _apiClient.PostAsync( _baseUri.Append(mandateId).Append(MandatesEndpoints.ProviderSelection), request, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken ); } - public async Task> SubmitConsent(string mandateId, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task> SubmitConsent( + string mandateId, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); - idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey)); + var authResponse = await _auth.GetAuthToken(new GetAuthTokenRequest($"recurring_payments:{mandateType.AsString()}"), cancellationToken); if (!authResponse.IsSuccessful) @@ -170,7 +200,7 @@ public async Task> SubmitConsent(string return await _apiClient.PostAsync( _baseUri.Append(mandateId).Append(MandatesEndpoints.Consent), null, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken @@ -178,7 +208,12 @@ public async Task> SubmitConsent(string } /// - public async Task> GetConfirmationOfFunds(string mandateId, int amountInMinor, string currency, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task> GetConfirmationOfFunds( + string mandateId, + int amountInMinor, + string currency, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); @@ -198,7 +233,10 @@ public async Task> GetConfirmationOf } /// - public async Task> GetMandateConstraints(string mandateId, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task> GetMandateConstraints( + string mandateId, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); @@ -218,7 +256,11 @@ public async Task> GetMandateConstraints(str } /// - public async Task RevokeMandate(string mandateId, string idempotencyKey, MandateType mandateType, CancellationToken cancellationToken = default) + public async Task RevokeMandate( + string mandateId, + string? idempotencyKey, + MandateType mandateType, + CancellationToken cancellationToken = default) { mandateId.NotNullOrWhiteSpace(nameof(mandateId)); mandateId.NotAUrl(nameof(mandateId)); @@ -233,7 +275,7 @@ public async Task RevokeMandate(string mandateId, string idempotenc return await _apiClient.PostAsync( _baseUri.Append(mandateId).Append(MandatesEndpoints.Revoke), null, - idempotencyKey, + idempotencyKey ?? Guid.NewGuid().ToString(), authResponse.Data!.AccessToken, _options.Payments!.SigningKey, cancellationToken From efc53b6b2010891f0a224a58d41d76be63bb19bb Mon Sep 17 00:00:00 2001 From: Roberto Mancinelli Date: Wed, 18 Dec 2024 14:55:36 +0100 Subject: [PATCH 3/6] test improvements --- .github/workflows/build.yml | 4 ---- .github/workflows/publish.yml | 4 ---- .../TrueLayer.AcceptanceTests.csproj | 4 ++-- test/TrueLayer.Tests/TrueLayer.Tests.csproj | 4 ++-- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80a83337..ecf8218f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,10 +29,6 @@ jobs: uses: actions/setup-dotnet@v2 with: dotnet-version: 6.0.x - - name: Setup .NET 3.1 - uses: actions/setup-dotnet@v2 - with: - dotnet-version: 3.1.x - name: Restore tools run: dotnet tool restore - name: Run the build script diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 33961d4b..576acfd3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,10 +35,6 @@ jobs: uses: actions/setup-dotnet@v2 with: dotnet-version: 6.0.x - - name: Setup .NET 3.1 - uses: actions/setup-dotnet@v2 - with: - dotnet-version: 3.1.x - name: Restore tools run: dotnet tool restore - name: Run the build script diff --git a/test/TrueLayer.AcceptanceTests/TrueLayer.AcceptanceTests.csproj b/test/TrueLayer.AcceptanceTests/TrueLayer.AcceptanceTests.csproj index b35f951e..663b6093 100644 --- a/test/TrueLayer.AcceptanceTests/TrueLayer.AcceptanceTests.csproj +++ b/test/TrueLayer.AcceptanceTests/TrueLayer.AcceptanceTests.csproj @@ -8,8 +8,8 @@ nullable; - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/TrueLayer.Tests/TrueLayer.Tests.csproj b/test/TrueLayer.Tests/TrueLayer.Tests.csproj index 678d8008..96a328af 100644 --- a/test/TrueLayer.Tests/TrueLayer.Tests.csproj +++ b/test/TrueLayer.Tests/TrueLayer.Tests.csproj @@ -1,6 +1,6 @@ - net9.0;net8.0;net6.0;netcoreapp3.1 + net9.0;net8.0;net6.0 false 10.0 enable @@ -17,7 +17,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From a369fce40818002ef5a614941c5811f663f1f547 Mon Sep 17 00:00:00 2001 From: Roberto Mancinelli Date: Wed, 18 Dec 2024 15:02:57 +0100 Subject: [PATCH 4/6] update summary --- src/TrueLayer/Mandates/IMandatesApi.cs | 7 ++++++- src/TrueLayer/Payments/IPaymentsApi.cs | 8 ++++---- src/TrueLayer/Payouts/IPayoutsApi.cs | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/TrueLayer/Mandates/IMandatesApi.cs b/src/TrueLayer/Mandates/IMandatesApi.cs index 42b55646..4e8e1ab3 100644 --- a/src/TrueLayer/Mandates/IMandatesApi.cs +++ b/src/TrueLayer/Mandates/IMandatesApi.cs @@ -29,8 +29,10 @@ public interface IMandatesApi /// /// 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 an idempotency key is automatically generated. + /// + /// The cancellation token to cancel the operation /// - /// The cancellation token to cancel the operation /// An API response that includes details of the created mandate if successful, otherwise problem details Task> CreateMandate( CreateMandateRequest mandateRequest, @@ -85,6 +87,7 @@ Task>> ListMandates( /// /// 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 an idempotency key is automatically generated. /// /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details @@ -104,6 +107,7 @@ 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 an idempotency key is automatically generated. /// /// The cancellation token to cancel the operation /// An API response that includes details of the mandate if successful, otherwise problem details @@ -167,6 +171,7 @@ Task> GetMandateConstraints( /// /// 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 an idempotency key is automatically generated. /// /// The cancellation token to cancel the operation /// An API response that includes the payment details if successful, otherwise problem details diff --git a/src/TrueLayer/Payments/IPaymentsApi.cs b/src/TrueLayer/Payments/IPaymentsApi.cs index 87ad26e1..e2acba80 100644 --- a/src/TrueLayer/Payments/IPaymentsApi.cs +++ b/src/TrueLayer/Payments/IPaymentsApi.cs @@ -42,7 +42,7 @@ 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. + /// If not provided an 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 @@ -83,7 +83,7 @@ string CreateHostedPaymentPageLink( /// /// 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. + /// If not provided an idempotency key is automatically generated. /// /// The start authorization request details /// The cancellation token to cancel the operation @@ -101,7 +101,7 @@ 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. + /// If not provided an idempotency key is automatically generated. /// /// The create payment refund request /// The cancellation token to cancel the operation @@ -141,7 +141,7 @@ Task> GetPaymentRefund( /// /// 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. + /// If not provided an idempotency key is automatically generated. /// /// The cancellation token to cancel the operation /// HTTP 202 Accepted if successful, otherwise problem details. diff --git a/src/TrueLayer/Payouts/IPayoutsApi.cs b/src/TrueLayer/Payouts/IPayoutsApi.cs index ba85f718..6ecb7021 100644 --- a/src/TrueLayer/Payouts/IPayoutsApi.cs +++ b/src/TrueLayer/Payouts/IPayoutsApi.cs @@ -25,7 +25,7 @@ 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. + /// If not provided an 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 From 7e1c2ab815ad741c216cf1b38b0311a57983add0 Mon Sep 17 00:00:00 2001 From: Roberto Mancinelli Date: Wed, 18 Dec 2024 15:09:15 +0100 Subject: [PATCH 5/6] minor change for minver --- .github/workflows/build.yml | 3 +++ .github/workflows/publish.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ecf8218f..92cf836c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: tree:0 - name: Setup .NET 9.0 uses: actions/setup-dotnet@v2 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 576acfd3..47bafee1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,6 +23,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: tree:0 - name: Setup .NET 9.0 uses: actions/setup-dotnet@v2 with: From dc55757facf112ba3fc894b3f5755076bb1352a3 Mon Sep 17 00:00:00 2001 From: Roberto Mancinelli Date: Wed, 18 Dec 2024 16:26:04 +0100 Subject: [PATCH 6/6] switched from ob-natwest-vrp-sandbox to mock-payments-gb-redirect for commercial vrp tests --- .../MandatesTests.cs | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/test/TrueLayer.AcceptanceTests/MandatesTests.cs b/test/TrueLayer.AcceptanceTests/MandatesTests.cs index ddfc1b26..07ac54b1 100644 --- a/test/TrueLayer.AcceptanceTests/MandatesTests.cs +++ b/test/TrueLayer.AcceptanceTests/MandatesTests.cs @@ -38,10 +38,10 @@ public class MandatesTests : IClassFixture { private readonly ApiTestFixture _fixture; private TrueLayerOptions configuration; - public string RETURN_URI = "http://localhost:3000/callback"; - public static string PROVIDER_ID = "ob-uki-mock-bank-sbox"; // Beta provider in closed access, requires a whitelisted ClientId. - public static string COMMERCIAL_PROVIDER_ID = "ob-natwest-vrp-sandbox"; // Provider to satisfy commercial mandates creation. - public static AccountIdentifier.SortCodeAccountNumber accountIdentifier = new("140662", "10003957"); + private const string ReturnUri = "http://localhost:3000/callback"; + private const string ProviderId = "ob-uki-mock-bank-sbox"; // Beta provider in closed access, requires a whitelisted ClientId. + private const string CommercialProviderId = "mock-payments-gb-redirect"; // Provider to satisfy commercial mandates creation. + private static AccountIdentifier.SortCodeAccountNumber accountIdentifier = new("140662", "10003957"); public MandatesTests(ApiTestFixture fixture) { @@ -117,7 +117,7 @@ public async Task Can_start_authorization(CreateMandateRequest mandateRequest) var mandateId = createResponse.Data!.Id; StartAuthorizationFlowRequest authorizationRequest = new( new ProviderSelectionRequest(), - new Redirect(new Uri(RETURN_URI))); + new Redirect(new Uri(ReturnUri))); // Act var response = await _fixture.Client.Mandates.StartAuthorizationFlow( @@ -140,10 +140,10 @@ public async Task Can_submit_provider_selection(CreateMandateRequest mandateRequ var createResponse = await _fixture.Client.Mandates.CreateMandate( mandateRequest, idempotencyKey: Guid.NewGuid().ToString()); var mandateId = createResponse.Data!.Id; - SubmitProviderSelectionRequest request = new(COMMERCIAL_PROVIDER_ID); + SubmitProviderSelectionRequest request = new(CommercialProviderId); StartAuthorizationFlowRequest authorizationRequest = new( new ProviderSelectionRequest(), - new Redirect(new Uri(RETURN_URI))); + new Redirect(new Uri(ReturnUri))); await _fixture.Client.Mandates.StartAuthorizationFlow( mandateId, authorizationRequest, idempotencyKey: Guid.NewGuid().ToString(), MandateType.Sweeping); // Act @@ -161,23 +161,23 @@ await _fixture.Client.Mandates.StartAuthorizationFlow( public async Task Can_submit_consent(CreateMandateRequest mandateRequest) { // Arrange - var createResponse = await _fixture.Client.Mandates.CreateMandate( - mandateRequest, idempotencyKey: Guid.NewGuid().ToString()); + var createResponse = await _fixture.Client.Mandates.CreateMandate(mandateRequest); var mandateId = createResponse.Data!.Id; var mandateType = mandateRequest.Mandate.IsT0 ? MandateType.Commercial : MandateType.Sweeping; - StartAuthorizationFlowRequest authorizationRequest = new( + var authorizationRequest = new StartAuthorizationFlowRequest( new ProviderSelectionRequest(), - new Redirect(new Uri(RETURN_URI)), + new Redirect(new Uri(ReturnUri)), new Consent()); await _fixture.Client.Mandates.StartAuthorizationFlow( mandateId, authorizationRequest, idempotencyKey: Guid.NewGuid().ToString(), mandateType); - SubmitProviderSelectionRequest submitProviderRequest = new(mandateRequest.Mandate.Match( - c => COMMERCIAL_PROVIDER_ID, - s => PROVIDER_ID)); + var submitProviderRequest = new SubmitProviderSelectionRequest( + mandateRequest.Mandate.Match( + commercial => CommercialProviderId, + sweeping => ProviderId)); await _fixture.Client.Mandates.SubmitProviderSelection( mandateId, submitProviderRequest, idempotencyKey: Guid.NewGuid().ToString(), mandateType); @@ -200,7 +200,7 @@ public async Task Can_Get_Funds(CreateMandateRequest mandateRequest) var mandateId = createResponse.Data!.Id; StartAuthorizationFlowRequest authorizationRequest = new( new ProviderSelectionRequest(), - new Redirect(new Uri(RETURN_URI))); + new Redirect(new Uri(ReturnUri))); // Act var response = await _fixture.Client.Mandates.StartAuthorizationFlow( @@ -356,9 +356,9 @@ private async Task CreateAuthorizedSweepingMandate(CreateMandateRequest createResponse.StatusCode.Should().Be(HttpStatusCode.Created); - StartAuthorizationFlowRequest authorizationRequest = new( + var authorizationRequest = new StartAuthorizationFlowRequest( new ProviderSelectionRequest(), - new Redirect(new Uri(RETURN_URI))); + new Redirect(new Uri(ReturnUri))); var startAuthResponse = await _fixture.Client.Mandates.StartAuthorizationFlow( mandateId, authorizationRequest, idempotencyKey: Guid.NewGuid().ToString(), MandateType.Sweeping); @@ -369,64 +369,64 @@ private async Task CreateAuthorizedSweepingMandate(CreateMandateRequest public static IEnumerable CreateTestSweepingPreselectedMandateRequests() { - yield return new[] - { + yield return + [ CreateTestMandateRequest(MandateUnion.FromT1(new Mandate.VRPSweepingMandate( "sweeping", - ProviderUnion.FromT1(new Mandates.Model.Provider.Preselected("preselected", PROVIDER_ID)), + ProviderUnion.FromT1(new Mandates.Model.Provider.Preselected("preselected", ProviderId)), new Mandates.Model.Beneficiary.ExternalAccount( "external_account", "Bob NET SDK", - AccountIdentifierUnion.FromT0(accountIdentifier))))), - }; + AccountIdentifierUnion.FromT0(accountIdentifier))))) + ]; } public static IEnumerable CreateTestCommercialPreselectedMandateRequests() { - yield return new[] - { + yield return + [ CreateTestMandateRequest(MandateUnion.FromT0(new Mandate.VRPCommercialMandate( "commercial", - ProviderUnion.FromT1(new Mandates.Model.Provider.Preselected("preselected", COMMERCIAL_PROVIDER_ID)), + ProviderUnion.FromT1(new Mandates.Model.Provider.Preselected("preselected", CommercialProviderId)), new Mandates.Model.Beneficiary.ExternalAccount( "external_account", "My Bank Account", - AccountIdentifierUnion.FromT0(accountIdentifier))))), - }; + AccountIdentifierUnion.FromT0(accountIdentifier))))) + ]; } public static IEnumerable CreateTestSweepingUserSelectedMandateRequests() { - yield return new[] - { + yield return + [ CreateTestMandateRequest(MandateUnion.FromT1(new Mandate.VRPSweepingMandate( "sweeping", ProviderUnion.FromT0(new Payments.Model.Provider.UserSelected { - Filter = new ProviderFilter {Countries = new[] {"GB"}, ReleaseChannel = "general_availability"}, + Filter = new ProviderFilter {Countries = ["GB"], ReleaseChannel = "alpha"}, }), new Mandates.Model.Beneficiary.ExternalAccount( "external_account", "My Bank Account", - AccountIdentifierUnion.FromT0(accountIdentifier))))), - }; + AccountIdentifierUnion.FromT0(accountIdentifier))))) + ]; } public static IEnumerable CreateTestCommercialUserSelectedMandateRequests() { - yield return new[] - { + yield return + [ CreateTestMandateRequest(MandateUnion.FromT0(new Mandate.VRPCommercialMandate( "commercial", ProviderUnion.FromT0(new Payments.Model.Provider.UserSelected { - Filter = new ProviderFilter {Countries = new[] {"GB"}, ReleaseChannel = "general_availability"}, + Filter = new ProviderFilter {Countries = ["GB"], ReleaseChannel = "alpha"}, }), new Mandates.Model.Beneficiary.ExternalAccount( "external_account", "My Bank Account", - AccountIdentifierUnion.FromT0(accountIdentifier))))), - }; + AccountIdentifierUnion.FromT0(accountIdentifier))))) + ]; } } }