Skip to content

Commit

Permalink
feat(io-20): implemented StartAuthorizationFlow endpoint
Browse files Browse the repository at this point in the history
- Added CreatePayment tests with auth flow
  • Loading branch information
tl-mauro-franchi committed Jul 1, 2024
1 parent 69d6806 commit 661362e
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 4 deletions.
25 changes: 25 additions & 0 deletions src/TrueLayer/Payments/IPaymentsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
using System.Threading.Tasks;
using OneOf;
using TrueLayer.Payments.Model;
using TrueLayer.Payments.Model.AuthorizationFlow;

namespace TrueLayer.Payments
{
using CreatePaymentUnion = OneOf<
CreatePaymentResponse.Authorizing,
CreatePaymentResponse.AuthorizationRequired,
CreatePaymentResponse.Authorized,
CreatePaymentResponse.Failed
Expand All @@ -21,6 +23,11 @@ namespace TrueLayer.Payments
GetPaymentResponse.Failed
>;

using AuthorizationResponseUnion = OneOf<
AuthorizationFlowResponse.AuthorizationFlowAuthorizing,
AuthorizationFlowResponse.AuthorizationFlowAuthorizationFailed
>;

/// <summary>
/// Provides access to the TrueLayer Payments API
/// </summary>
Expand Down Expand Up @@ -58,5 +65,23 @@ Task<ApiResponse<CreatePaymentUnion>> CreatePayment(
/// </param>
/// <returns>The HPP link you can redirect the end user to</returns>
string CreateHostedPaymentPageLink(string paymentId, string paymentToken, Uri returnUri);

/// <summary>
/// Start the authorization flow for a payment.
/// </summary>
/// <param name="paymentId">The payment identifier</param>
/// <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.
/// </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,
StartAuthorizationFlowRequest request,
CancellationToken cancellationToken = default);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using TrueLayer.Serialization;
using OneOf;

namespace TrueLayer.Payments.Model.AuthorizationFlow;

using AuthorizationFlowActionUnion = OneOf<
Models.AuthorizationFlowAction.ProviderSelection,
AuthorizationFlowAction.Consent,
Models.AuthorizationFlowAction.Form,
Models.AuthorizationFlowAction.WaitForOutcome,
Models.AuthorizationFlowAction.Redirect
>;

/// <summary>
/// Contains information regarding the next action to be taken in the authorization flow.
/// </summary>
/// <param name="Next">The next action that can be performed.</param>
public record Actions(AuthorizationFlowActionUnion Next);

/// <summary>
/// Contains information regarding the nature and the state of the authorization flow.
/// </summary>
/// <param name="Actions">Contains the next action to be taken in the authorization flow.</param>
public record AuthorizationFlow(Actions Actions);

/// <summary>
/// This static class contains the different types of actions that can be taken during the authorization flow.
/// It contains only the actions/types that are for the payments flow and not already defined in the <see cref="TrueLayer.Models.AuthorizationFlowAction"/> class.
/// </summary>
public static class AuthorizationFlowAction
{
public enum SubsequentActionHint { Redirect = 0, Form = 1, Wait = 2 };

[JsonDiscriminator("consent")]
public record Consent(
string Type,
ConsentRequirements Requirements,
SubsequentActionHint SubsequentActionHint) : IDiscriminated;

public record ConsentAisRequirement(
List<ConsentAisScopes> RequiredScopes,
List<ConsentAisScopes> OptionalScopes);

public record ConsentRequirements(ConsentPisRequirement Pis, ConsentAisRequirement Ais);

public record AdjacentConsent(ConsentRequirements Requirements);

public record AdjacentAction(AdjacentConsent Consent);

[JsonDiscriminator("user_account_selection")]
public record UserAccountSelection(
string Type,
Models.Provider Provider,
string MaskedAccountIdentifier,
DateTime LastUsedAt) : IDiscriminated;

[JsonDiscriminator("scheme_selection")]
public record SchemeSelection(
string Type,
List<Scheme> Schemes) : IDiscriminated;

public record Scheme(
string Id,
bool Recommended,
Fee Fee);

public record Fee(int AmountInMinor, string Currency);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using TrueLayer.Serialization;

namespace TrueLayer.Payments.Model.AuthorizationFlow
{
public static class AuthorizationFlowResponse
{
/// <summary>
/// Mandate Authorization Flow
/// </summary>
/// <param name="Status">authorizing</param>
/// <param name="AuthorizationFlow">Contains information regarding the nature and the state of the authorization flow.</param>
[JsonDiscriminator("authorizing")]
public record AuthorizationFlowAuthorizing(string Status, AuthorizationFlow AuthorizationFlow);

/// <summary>
/// Mandate Authorization Flow
/// </summary>
/// <param name="Status">failed</param>
/// <param name="FailureStage">The status the mandate was in when it failed./param>
/// <param name="FailureReason">A readable detail for why the mandate failed.</param>
[JsonDiscriminator("failed")]
public record AuthorizationFlowAuthorizationFailed(string Status, string FailureStage, string FailureReason);
}
}
9 changes: 9 additions & 0 deletions src/TrueLayer/Payments/Model/CreatePaymentResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public record AuthorizationRequired : PaymentDetails;
[JsonDiscriminator("authorized")]
public record Authorized : PaymentDetails;

/// <summary>
/// Represents a payment that is being authorizing by the end user
/// </summary>
[JsonDiscriminator("authorizing")]
public record Authorizing : PaymentDetails
{
public AuthorizationFlow.AuthorizationFlow AuthorizationFlow { get; init; } = null!;
}

/// <summary>
/// Represents a payment that failed to complete. This is a terminal state.
/// </summary>
Expand Down
36 changes: 36 additions & 0 deletions src/TrueLayer/Payments/PaymentsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
using TrueLayer.Common;
using TrueLayer.Extensions;
using TrueLayer.Payments.Model;
using TrueLayer.Payments.Model.AuthorizationFlow;

namespace TrueLayer.Payments
{
using CreatePaymentUnion = OneOf<
CreatePaymentResponse.Authorizing,
CreatePaymentResponse.AuthorizationRequired,
CreatePaymentResponse.Authorized,
CreatePaymentResponse.Failed
Expand All @@ -24,6 +26,11 @@ namespace TrueLayer.Payments
GetPaymentResponse.Failed
>;

using AuthorizationResponseUnion = OneOf<
AuthorizationFlowResponse.AuthorizationFlowAuthorizing,
AuthorizationFlowResponse.AuthorizationFlowAuthorizationFailed
>;

internal class PaymentsApi : IPaymentsApi
{
private readonly IApiClient _apiClient;
Expand Down Expand Up @@ -96,5 +103,34 @@ public async Task<ApiResponse<GetPaymentUnion>> GetPayment(string id, Cancellati
/// <inheritdoc />
public string CreateHostedPaymentPageLink(string paymentId, string paymentToken, Uri returnUri)
=> _hppLinkBuilder.Build(paymentId, paymentToken, returnUri);

/// <inheritdoc />
public async Task<ApiResponse<AuthorizationResponseUnion>> StartAuthorizationFlow(
string paymentId,
string idempotencyKey,
StartAuthorizationFlowRequest request,
CancellationToken cancellationToken = default)
{
paymentId.NotNullOrWhiteSpace(nameof(paymentId));
idempotencyKey.NotNullOrWhiteSpace(nameof(idempotencyKey));
request.NotNull(nameof(request));

ApiResponse<GetAuthTokenResponse> authResponse = await _auth.GetAuthToken(
new GetAuthTokenRequest("payments"), cancellationToken);

if (!authResponse.IsSuccessful)
{
return new(authResponse.StatusCode, authResponse.TraceId);
}

return await _apiClient.PostAsync<AuthorizationResponseUnion>(
_baseUri,
request,
idempotencyKey,
authResponse.Data!.AccessToken,
_options.Payments!.SigningKey,
cancellationToken
);
}
}
}
47 changes: 43 additions & 4 deletions test/TrueLayer.AcceptanceTests/PaymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Shouldly;
using TrueLayer.Common;
using TrueLayer.Payments.Model;
using TrueLayer.Payments.Model.AuthorizationFlow;
using Xunit;

namespace TrueLayer.AcceptanceTests
Expand All @@ -16,8 +17,11 @@ namespace TrueLayer.AcceptanceTests
AccountIdentifier.Iban,
AccountIdentifier.Bban,
AccountIdentifier.Nrb>;
using PreselectedProviderSchemeSelectionUnion = OneOf<SchemeSelection.InstantOnly, SchemeSelection.InstantPreferred, SchemeSelection.Preselected, SchemeSelection.UserSelected>;
using UserSelectedProviderSchemeSelectionUnion = OneOf<SchemeSelection.InstantOnly, SchemeSelection.InstantPreferred, SchemeSelection.UserSelected>;
using PreselectedProviderSchemeSelectionUnion = OneOf<
SchemeSelection.InstantOnly,
SchemeSelection.InstantPreferred,
SchemeSelection.Preselected,
SchemeSelection.UserSelected>;

public class PaymentTests : IClassFixture<ApiTestFixture>
{
Expand Down Expand Up @@ -48,6 +52,38 @@ public async Task Can_create_payment(CreatePaymentRequest paymentRequest)
hppUri.ShouldNotBeNullOrWhiteSpace();
}

[Fact]
public async Task Can_create_payment_with_auth_flow()
{
var sortCodeAccountNumber = new AccountIdentifier.SortCodeAccountNumber("567890", "12345678");
var providerSelection = new Provider.Preselected("mock-payments-gb-redirect", "faster_payments_service")
{
Remitter = new RemitterAccount("John Doe", sortCodeAccountNumber),
};
var paymentRequest = CreateTestPaymentRequest(
providerSelection,
sortCodeAccountNumber,
authorizationFlow: new StartAuthorizationFlowRequest(
providerSelection, new SchemeSelection.Preselected(),
new Redirect(new Uri("http://localhost:3000/callback")))
);

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

response.StatusCode.ShouldBe(HttpStatusCode.Created);
response.Data.IsT0.ShouldBeTrue();
response.Data.AsT0.Id.ShouldNotBeNullOrWhiteSpace();
response.Data.AsT0.ResourceToken.ShouldNotBeNullOrWhiteSpace();
response.Data.AsT0.User.ShouldNotBeNull();
response.Data.AsT0.User.Id.ShouldNotBeNullOrWhiteSpace();
response.Data.AsT0.Status.ShouldBe("authorizing");

string hppUri = _fixture.Client.Payments.CreateHostedPaymentPageLink(
response.Data.AsT0.Id, response.Data.AsT0.ResourceToken, new Uri("https://redirect.mydomain.com"));
hppUri.ShouldNotBeNullOrWhiteSpace();
}

[Theory]
[MemberData(nameof(CreateTestPaymentRequests))]
public async Task Can_get_authorization_required_payment(CreatePaymentRequest paymentRequest)
Expand Down Expand Up @@ -95,6 +131,7 @@ var getPaymentResponse
payment.User.Id.ShouldBe(authorizationRequiredResponse.User.Id);
}


private static void AssertSchemeSelection(
PreselectedProviderSchemeSelectionUnion? actualSchemeSelection,
PreselectedProviderSchemeSelectionUnion? expectedSchemeSelection,
Expand All @@ -121,7 +158,8 @@ private static CreatePaymentRequest CreateTestPaymentRequest(
ProviderUnion providerSelection,
AccountIdentifierUnion accountIdentifier,
string currency = Currencies.GBP,
RelatedProducts? relatedProducts = null)
RelatedProducts? relatedProducts = null,
StartAuthorizationFlowRequest? authorizationFlow = null)
=> new CreatePaymentRequest(
100,
currency,
Expand All @@ -138,7 +176,8 @@ private static CreatePaymentRequest CreateTestPaymentRequest(
phone: "+442079460087",
dateOfBirth: new DateTime(1999, 1, 1),
address: new Address("London", "England", "EC1R 4RB", "GB", "1 Hardwick St")),
relatedProducts
relatedProducts,
authorizationFlow
);

private static IEnumerable<object[]> CreateTestPaymentRequests()
Expand Down

0 comments on commit 661362e

Please sign in to comment.