Skip to content

Commit

Permalink
Implement mandates APIs (#185)
Browse files Browse the repository at this point in the history
Create mandates api #177
Get and List mandates #179
Release StartAuthorizationFlow, SubmitProviderSelection, GetConfirmationOfFunds Mandates #180
Release GetMandateConstraints and RevokeMandate #181
Release SubmitConsent #184
Release Mandate Payment Method #186

---------

Co-authored-by: TANDEM\ryan.palmer <[email protected]>
Co-authored-by: mohammedmiah99 <[email protected]>
Co-authored-by: Ryan Palmer <[email protected]>
Co-authored-by: mohammedmiah99 <[email protected]>
Co-authored-by: Marcin Przyłęcki <[email protected]>
Co-authored-by: tl-antonio-valentini <[email protected]>
Co-authored-by: ubunoir <[email protected]>
  • Loading branch information
8 people authored Oct 17, 2023
1 parent 7fdc6f2 commit 47a4e49
Show file tree
Hide file tree
Showing 54 changed files with 2,235 additions and 59 deletions.
9 changes: 5 additions & 4 deletions examples/MvcExample/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Diagnostics;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -104,7 +104,7 @@ public async Task<IActionResult> Complete([FromQuery(Name = "payment_id")]string

var apiResponse = await _truelayer.Payments.GetPayment(paymentId);

IActionResult Failed(string status, OneOf<PaymentMethod.BankTransfer>? paymentMethod)
IActionResult Failed(string status, OneOf<PaymentMethod.BankTransfer, PaymentMethod.Mandate>? paymentMethod)
{
ViewData["Status"] = status;

Expand All @@ -119,13 +119,14 @@ IActionResult SuccessOrPending(PaymentDetails payment)
return View("Success");
}

void SetProviderAndSchemeId(OneOf<PaymentMethod.BankTransfer>? paymentMethod)
void SetProviderAndSchemeId(OneOf<PaymentMethod.BankTransfer, PaymentMethod.Mandate>? paymentMethod)
{
(string providerId, string schemeId) = paymentMethod?.Match(
bankTransfer => bankTransfer.ProviderSelection.Match(
userSelected => (userSelected.ProviderId, userSelected.SchemeId),
preselected => (preselected.ProviderId, preselected.SchemeId)
)) ?? ("unavailable", "unavailable");
),
mandate => ("unavailable", "unavailable")) ?? ("unavailable", "unavailable");

ViewData["ProviderId"] = providerId;
ViewData["SchemeId"] = schemeId;
Expand Down
91 changes: 71 additions & 20 deletions src/TrueLayer/ApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
Expand Down Expand Up @@ -88,12 +89,46 @@ public async Task<ApiResponse<TData>> PostAsync<TData>(Uri uri, object? request
return await CreateResponseAsync<TData>(httpResponse, cancellationToken);
}

public async Task<ApiResponse> PostAsync(Uri uri, HttpContent? httpContent = null, string? accessToken = null, CancellationToken cancellationToken = default)
{
if (uri is null) throw new ArgumentNullException(nameof(uri));

using var httpResponse = await SendRequestAsync(
httpMethod: HttpMethod.Post,
uri: uri,
idempotencyKey: null,
accessToken: accessToken,
httpContent: httpContent,
signature: null,
cancellationToken: cancellationToken
);

return await CreateResponseAsync(httpResponse, cancellationToken);
}

public async Task<ApiResponse> PostAsync(Uri uri, object? request = null, string? idempotencyKey = null, string? accessToken = null, SigningKey? signingKey = null, CancellationToken cancellationToken = default)
{
if (uri is null) throw new ArgumentNullException(nameof(uri));

using var httpResponse = await SendJsonRequestAsync(
httpMethod: HttpMethod.Post,
uri: uri,
idempotencyKey: idempotencyKey,
accessToken: accessToken,
request: request,
signingKey: signingKey,
cancellationToken: cancellationToken
);

return await CreateResponseAsync(httpResponse, cancellationToken);
}

private async Task<ApiResponse<TData>> CreateResponseAsync<TData>(HttpResponseMessage httpResponse, CancellationToken cancellationToken)
{
httpResponse.Headers.TryGetValues(CustomHeaders.TraceId, out var traceIdHeader);
string? traceId = traceIdHeader?.FirstOrDefault();

if (httpResponse.IsSuccessStatusCode)
if (httpResponse.IsSuccessStatusCode && httpResponse.StatusCode != HttpStatusCode.NoContent)
{
var data = await DeserializeJsonAsync<TData>(httpResponse, traceId, cancellationToken);
return new ApiResponse<TData>(data, httpResponse.StatusCode, traceId);
Expand All @@ -109,6 +144,21 @@ private async Task<ApiResponse<TData>> CreateResponseAsync<TData>(HttpResponseMe
return new ApiResponse<TData>(httpResponse.StatusCode, traceId);
}

private async Task<ApiResponse> CreateResponseAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken)
{
httpResponse.Headers.TryGetValues(CustomHeaders.TraceId, out var traceIdHeader);
string? traceId = traceIdHeader?.FirstOrDefault();

// In .NET Standard 2.1 HttpResponse.Content can be null
if (httpResponse.Content?.Headers.ContentType?.MediaType == "application/problem+json")
{
var problemDetails = await DeserializeJsonAsync<ProblemDetails>(httpResponse, traceId, cancellationToken);
return new ApiResponse(problemDetails, httpResponse.StatusCode, traceId);
}

return new ApiResponse(httpResponse.StatusCode, traceId);
}

private async Task<TData> DeserializeJsonAsync<TData>(HttpResponseMessage httpResponse, string? traceId, CancellationToken cancellationToken)
{
TData? data = default;
Expand Down Expand Up @@ -145,37 +195,38 @@ private Task<HttpResponseMessage> SendJsonRequestAsync(
HttpContent? httpContent = null;
string? signature = null;

if (request is { })
if (signingKey != null)
{
if (signingKey != null)
var signer = Signer.SignWith(signingKey.KeyId, signingKey.Value)
.Method(httpMethod.Method)
.Path(uri.AbsolutePath.TrimEnd('/'));

if (request is { })
{
// Only serialize to string if signing is required,
string json = JsonSerializer.Serialize(request, request.GetType(), SerializerOptions.Default);

var signer = Signer.SignWith(signingKey.KeyId, signingKey.Value)
.Method(httpMethod.Method)
.Path(uri.AbsolutePath.TrimEnd('/'))
.Body(json);

if (!string.IsNullOrWhiteSpace(idempotencyKey))
{
signer.Header(CustomHeaders.IdempotencyKey, idempotencyKey);
}

signature = signer.Sign();
signer.Body(json);

httpContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json);
}
else // Otherwise we can serialize directly to stream for .NET 5.0 onwards

if (!string.IsNullOrWhiteSpace(idempotencyKey))
{
signer.Header(CustomHeaders.IdempotencyKey, idempotencyKey);
}

signature = signer.Sign();
}
else if (request is { }) // Otherwise we can serialize directly to stream for .NET 5.0 onwards
{
#if (NET6_0 || NET6_0_OR_GREATER)
httpContent = JsonContent.Create(request, request.GetType(), options: SerializerOptions.Default);
httpContent = JsonContent.Create(request, request.GetType(), options: SerializerOptions.Default);
#else
// for older versions of .NET we'll have to fall back to using StringContent
string json = JsonSerializer.Serialize(request, request.GetType(), SerializerOptions.Default);
httpContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json);
// for older versions of .NET we'll have to fall back to using StringContent
string json = JsonSerializer.Serialize(request, request.GetType(), SerializerOptions.Default);
httpContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json);
#endif
}
}

return SendRequestAsync(httpMethod, uri, idempotencyKey, accessToken, httpContent, signature, cancellationToken);
Expand Down
4 changes: 2 additions & 2 deletions src/TrueLayer/ApiResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ namespace TrueLayer
/// </summary>
public class ApiResponse
{
internal ApiResponse(HttpStatusCode statusCode, string? traceId)
public ApiResponse(HttpStatusCode statusCode, string? traceId)
{
StatusCode = statusCode;
TraceId = traceId;
}

internal ApiResponse(ProblemDetails problemDetails, HttpStatusCode statusCode, string? traceId)
public ApiResponse(ProblemDetails problemDetails, HttpStatusCode statusCode, string? traceId)
{
StatusCode = statusCode;
TraceId = traceId;
Expand Down
7 changes: 3 additions & 4 deletions src/TrueLayer/ApiResponseOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ namespace TrueLayer
/// <typeparam name="TData">The expected type for the response</typeparam>
public class ApiResponse<TData> : ApiResponse
{
internal ApiResponse(HttpStatusCode statusCode, string? traceId)
public ApiResponse(HttpStatusCode statusCode, string? traceId)
: base(statusCode, traceId)
{

}

internal ApiResponse(ProblemDetails problemDetails, HttpStatusCode statusCode, string? traceId)
public ApiResponse(ProblemDetails problemDetails, HttpStatusCode statusCode, string? traceId)
: base(problemDetails, statusCode, traceId)
{
}

internal ApiResponse(TData data, HttpStatusCode statusCode, string? traceId)
public ApiResponse(TData data, HttpStatusCode statusCode, string? traceId)
: base(statusCode, traceId)
{
Data = data.NotNull(nameof(data));
Expand Down
22 changes: 22 additions & 0 deletions src/TrueLayer/IApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,27 @@ internal interface IApiClient
/// <typeparam name="TData">The expected response type to be deserialized.</typeparam>
/// <returns>A task that upon completion contains the specified API response data.</returns>
Task<ApiResponse<TData>> PostAsync<TData>(Uri uri, object? request = null, string? idempotencyKey = null, string? accessToken = null, SigningKey? signingKey = null, CancellationToken cancellationToken = default);

/// <summary>
/// Executes a POST request to the specified <paramref name="uri"/>.
/// </summary>
/// <param name="uri">The API resource path.</param>
/// <param name="httpContent">Optional data that should be sent in the request body.</param>
/// <param name="accessToken">The access token used to authenticate the request.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the underlying HTTP request.</param>
/// <returns>A task that upon completion contains the API content-less response.</returns>
Task<ApiResponse> PostAsync(Uri uri, HttpContent? httpContent = null, string? accessToken = null, CancellationToken cancellationToken = default);

/// <summary>
/// Executes a POST request to the specified <paramref name="uri"/>.
/// </summary>
/// <param name="uri">The API resource path.</param>
/// <param name="request">Optional data that should be sent in the request body.</param>
/// <param name="idempotencyKey">Unique identifier for the request that allows it to be safely retried.</param>
/// <param name="accessToken">The access token used to authenticate the request.</param>
/// <param name="signingKey">ES512 signing key used to sign the request with a JSON Web Signature</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the underlying HTTP request.</param>
/// <returns>A task that upon completion contains the API content-less response.</returns>
Task<ApiResponse> PostAsync(Uri uri, object? request = null, string? idempotencyKey = null, string? accessToken = null, SigningKey? signingKey = null, CancellationToken cancellationToken = default);
}
}
7 changes: 7 additions & 0 deletions src/TrueLayer/ITrueLayerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

namespace TrueLayer
{
using TrueLayer.Mandates;

/// <summary>
/// Provides access to TrueLayer APIs
/// </summary>
Expand Down Expand Up @@ -35,5 +37,10 @@ public interface ITrueLayerClient
/// Gets the Merchant Accounts API resource
/// </summary>
IMerchantAccountsApi MerchantAccounts { get; }

/// <summary>
/// Gets the Mandates API resource
/// </summary>
IMandatesApi Mandates { get; }
}
}
Loading

0 comments on commit 47a4e49

Please sign in to comment.