Skip to content

Commit 6ea53a4

Browse files
[ACL-242] Add CreditableAt in GetPaymentResult (#237)
1 parent 99d7cc1 commit 6ea53a4

File tree

6 files changed

+195
-9
lines changed

6 files changed

+195
-9
lines changed

src/TrueLayer/Payments/Model/GetPaymentResponse.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,21 @@ public record Authorizing : PaymentDetails;
7676

7777
/// <summary>
7878
/// Represents a payment that has been authorized by the end user
79+
/// <param name="CreditableAt">The date and time that TrueLayer determined that the payment was ready to be credited</param>
7980
/// </summary>
8081
/// <returns></returns>
8182
[JsonDiscriminator("authorized")]
82-
public record Authorized : PaymentDetails;
83+
public record Authorized(DateTime? CreditableAt) : PaymentDetails;
8384

8485
/// <summary>
8586
/// Represents a payment that has been executed
8687
/// For open loop payments this state is terminal. For closed-loop payments, wait for Settled.
8788
/// </summary>
8889
/// <param name="ExecutedAt">The date and time the payment executed</param>
90+
/// <param name="CreditableAt">The date and time that TrueLayer determined that the payment was ready to be credited</param>
8991
/// <returns></returns>
9092
[JsonDiscriminator("executed")]
91-
public record Executed(DateTime ExecutedAt) : PaymentDetails;
93+
public record Executed(DateTime ExecutedAt, DateTime? CreditableAt) : PaymentDetails;
9294

9395
/// <summary>
9496
/// Represents a payment that has settled
@@ -97,9 +99,10 @@ public record Executed(DateTime ExecutedAt) : PaymentDetails;
9799
/// <param name="ExecutedAt">The date and time the payment executed</param>
98100
/// <param name="SettledAt">The date and time the payment was settled</param>
99101
/// <param name="PaymentSource">Details of the source of funds for the payment</param>
102+
/// <param name="CreditableAt">The date and time that TrueLayer determined that the payment was ready to be credited</param>
100103
/// <returns></returns>
101104
[JsonDiscriminator("settled")]
102-
public record Settled(DateTime ExecutedAt, DateTime SettledAt, PaymentSource PaymentSource) : PaymentDetails;
105+
public record Settled(DateTime ExecutedAt, DateTime SettledAt, PaymentSource PaymentSource, DateTime? CreditableAt) : PaymentDetails;
103106

104107
/// <summary>
105108
/// Represents a payment that failed to complete. This is a terminal state.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading.Tasks;
4+
5+
namespace TrueLayer.AcceptanceTests.Helpers;
6+
7+
public static class Waiter
8+
{
9+
public static Task<T> WaitAsync<T>(
10+
Func<Task<T>> action,
11+
Predicate<T> predicate,
12+
TimeSpan? pause = null,
13+
TimeSpan? timeout = null)
14+
=> WaitAsync(action, x => Task.FromResult(predicate(x)), pause, timeout);
15+
16+
public static async Task<T> WaitAsync<T>(
17+
Func<Task<T>> action,
18+
Func<T, Task<bool>> predicate,
19+
TimeSpan? pause = null,
20+
TimeSpan? timeout = null)
21+
{
22+
var stopwatch = Stopwatch.StartNew();
23+
24+
T result;
25+
26+
do
27+
{
28+
result = await action();
29+
await Task.Delay(pause.GetValueOrDefault(TimeSpan.FromSeconds(1)));
30+
} while (!await predicate(result) && stopwatch.Elapsed < timeout.GetValueOrDefault(TimeSpan.FromSeconds(20)));
31+
32+
return result;
33+
}
34+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace TrueLayer.AcceptanceTests.MockBank;
2+
3+
public enum MockBankAction
4+
{
5+
Cancel,
6+
RejectAuthorisation,
7+
Execute,
8+
RejectExecution,
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Linq;
3+
using System.Net;
4+
using System.Net.Http;
5+
using System.Net.Mime;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using FluentAssertions;
9+
10+
namespace TrueLayer.AcceptanceTests.MockBank;
11+
12+
public class MockBankClient
13+
{
14+
private readonly HttpClient _httpClient;
15+
16+
public MockBankClient(HttpClient httpClient)
17+
{
18+
_httpClient = httpClient;
19+
}
20+
21+
public async Task<Uri> AuthorisePaymentAsync(
22+
Uri authUri,
23+
MockBankAction action,
24+
int settlementDelayInSeconds = 0)
25+
{
26+
var mockPaymentId = authUri.Segments.Last();
27+
var token = authUri.Fragment[7..];
28+
29+
var requestBody = $@"{{ ""action"": ""{action}"", ""settlement_delay_in_seconds"": {settlementDelayInSeconds} }}";
30+
31+
var request = new HttpRequestMessage(HttpMethod.Post, $"api/single-immediate-payments/{mockPaymentId}/action")
32+
{
33+
Headers = { { "Authorization", $"Bearer {token}" } },
34+
Content = new StringContent(requestBody, Encoding.UTF8, MediaTypeNames.Application.Json),
35+
};
36+
var response = await _httpClient.SendAsync(request);
37+
var responseBody = await response.Content.ReadAsStringAsync();
38+
response.StatusCode.Should().Be(HttpStatusCode.Accepted, "submit mock payment response should be 202");
39+
return new Uri(responseBody);
40+
}
41+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Net.Http;
2+
using System.Net.Http.Json;
3+
using System.Text.Json.Serialization;
4+
using System.Threading.Tasks;
5+
6+
namespace TrueLayer.AcceptanceTests;
7+
8+
public class PayApiClient
9+
{
10+
private readonly HttpClient _httpClient;
11+
12+
public PayApiClient(HttpClient httpClient)
13+
{
14+
_httpClient = httpClient;
15+
}
16+
17+
public async Task<HttpResponseMessage> GetJwksAsync()
18+
{
19+
var request = new HttpRequestMessage(HttpMethod.Get, "/.well-known/jwks.json");
20+
var response = await _httpClient.SendAsync(request);
21+
return response;
22+
}
23+
24+
public async Task<HttpResponseMessage> SubmitProviderReturnParametersAsync(string query, string fragment)
25+
{
26+
var requestBody = new SubmitProviderReturnParametersRequest { Query = query, Fragment = fragment };
27+
28+
var request = new HttpRequestMessage(HttpMethod.Post, "/spa/submit-provider-return-parameters")
29+
{
30+
Content = JsonContent.Create(requestBody)
31+
};
32+
var response = await _httpClient.SendAsync(request);
33+
return response;
34+
}
35+
}
36+
37+
public class SubmitProviderReturnParametersRequest
38+
{
39+
[JsonPropertyName("query")] public string? Query { get; set; }
40+
[JsonPropertyName("fragment")] public string? Fragment { get; set; }
41+
}
42+

test/TrueLayer.AcceptanceTests/PaymentTests.cs

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
using Microsoft.Extensions.DependencyInjection;
1010
using Microsoft.Extensions.Options;
1111
using OneOf;
12+
using TrueLayer.AcceptanceTests.Helpers;
13+
using TrueLayer.AcceptanceTests.MockBank;
1214
using TrueLayer.Common;
1315
using TrueLayer.Payments.Model;
1416
using TrueLayer.Payments.Model.AuthorizationFlow;
@@ -46,6 +48,8 @@ public partial class PaymentTests : IClassFixture<ApiTestFixture>
4648
private readonly TrueLayerOptions _configuration;
4749
private readonly string _gbpMerchantAccountId;
4850
private readonly string _eurMerchantSecretKey;
51+
private readonly MockBankClient _mockBankClient;
52+
private readonly PayApiClient _payApiClient;
4953

5054
public PaymentTests(ApiTestFixture fixture)
5155
{
@@ -54,11 +58,19 @@ public PaymentTests(ApiTestFixture fixture)
5458
(string gbpMerchantAccountId, string eurMerchantAccountId) = GetMerchantBeneficiaryAccountsAsync().Result;
5559
_gbpMerchantAccountId = gbpMerchantAccountId;
5660
_eurMerchantSecretKey = eurMerchantAccountId;
61+
_mockBankClient = new MockBankClient(new HttpClient
62+
{
63+
BaseAddress = new Uri("https://pay-mock-connect.truelayer-sandbox.com/")
64+
});
65+
_payApiClient = new PayApiClient(new HttpClient
66+
{
67+
BaseAddress = new Uri("https://pay-api.truelayer-sandbox.com")
68+
});
5769
}
5870

5971
[Theory]
6072
[MemberData(nameof(ExternalAccountPaymentRequests))]
61-
public async Task can_create_external_account_payment(CreatePaymentRequest paymentRequest)
73+
public async Task Can_Create_External_Account_Payment(CreatePaymentRequest paymentRequest)
6274
{
6375
var response = await _fixture.Client.Payments.CreatePayment(
6476
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());
@@ -77,7 +89,7 @@ public async Task can_create_external_account_payment(CreatePaymentRequest payme
7789
}
7890

7991
[Fact]
80-
public async Task can_create_merchant_account_gbp_Payment()
92+
public async Task Can_Create_Merchant_Account_Gbp_Payment()
8193
{
8294
var paymentRequest = CreateTestPaymentRequest(
8395
new Provider.UserSelected
@@ -139,7 +151,7 @@ public async Task can_create_merchant_account_gbp_verification_Payment()
139151
}
140152

141153
[Fact]
142-
public async Task can_create_merchant_account_eur_Payment()
154+
public async Task Can_Create_Merchant_Account_Eur_Payment()
143155
{
144156
var paymentRequest = CreateTestPaymentRequest(
145157
new Provider.Preselected("mock-payments-fr-redirect",
@@ -169,7 +181,7 @@ public async Task can_create_merchant_account_eur_Payment()
169181
}
170182

171183
[Fact]
172-
public async Task Can_create_payment_with_auth_flow()
184+
public async Task Can_Create_Payment_With_Auth_Flow()
173185
{
174186
var sortCodeAccountNumber = new AccountIdentifier.SortCodeAccountNumber("567890", "12345678");
175187
var providerSelection = new Provider.Preselected("mock-payments-gb-redirect", "faster_payments_service")
@@ -202,9 +214,42 @@ public async Task Can_create_payment_with_auth_flow()
202214
hppUri.Should().NotBeNullOrWhiteSpace();
203215
}
204216

217+
[Fact]
218+
public async Task GetPayment_Should_Return_Settled_Payment()
219+
{
220+
var providerSelection = new Provider.Preselected("mock-payments-gb-redirect", "faster_payments_service");
221+
222+
var paymentRequest = CreateTestPaymentRequest(
223+
beneficiary: new Beneficiary.MerchantAccount(_gbpMerchantAccountId),
224+
providerSelection: providerSelection,
225+
initAuthorizationFlow: true);
226+
227+
var response = await _fixture.Client.Payments.CreatePayment(paymentRequest, idempotencyKey: Guid.NewGuid().ToString());
228+
229+
response.StatusCode.Should().Be(HttpStatusCode.Created);
230+
var authorizing = response.Data.AsT3;
231+
var paymentId = authorizing.Id;
232+
233+
var providerReturnUri = await _mockBankClient.AuthorisePaymentAsync(
234+
authorizing.AuthorizationFlow!.Actions.Next.AsT2.Uri,
235+
MockBankAction.Execute);
236+
237+
await _payApiClient.SubmitProviderReturnParametersAsync(providerReturnUri.Query, providerReturnUri.Fragment);
238+
239+
var getPaymentResponse = await PollPaymentForTerminalStatusAsync(paymentId, typeof(GetPaymentResponse.Settled));
240+
241+
var executed = getPaymentResponse.AsT4;
242+
executed.AmountInMinor.Should().Be(paymentRequest.AmountInMinor);
243+
executed.Currency.Should().Be(paymentRequest.Currency);
244+
executed.Id.Should().NotBeNullOrWhiteSpace();
245+
executed.CreatedAt.Should().NotBe(default);
246+
executed.PaymentMethod.AsT0.Should().NotBeNull();
247+
executed.CreditableAt.Should().NotBeNull();
248+
}
249+
205250
[Theory]
206251
[MemberData(nameof(ExternalAccountPaymentRequests))]
207-
public async Task Can_get_authorization_required_payment(CreatePaymentRequest paymentRequest)
252+
public async Task Can_Get_Authorization_Required_Payment(CreatePaymentRequest paymentRequest)
208253
{
209254
var response = await _fixture.Client.Payments.CreatePayment(
210255
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());
@@ -250,7 +295,7 @@ var getPaymentResponse
250295
}
251296

252297
[Fact]
253-
public async Task Can_create_payment_with_retry_option_and_get_attemptFailed_error()
298+
public async Task Can_Create_Payment_With_Retry_Option_And_Get_AttemptFailed_Error()
254299
{
255300
// Arrange
256301
var paymentRequest = CreateTestPaymentRequest(
@@ -702,4 +747,16 @@ private GetPaymentResponse.PaymentDetails GetPaymentDetailsAndWaitForItToBeDone(
702747

703748
return (payment.Value as GetPaymentResponse.PaymentDetails)!;
704749
}
750+
751+
private async Task<GetPaymentUnion> PollPaymentForTerminalStatusAsync(
752+
string paymentId,
753+
Type expectedStatus)
754+
{
755+
var getPaymentResponseBody = await Waiter.WaitAsync(
756+
() => _fixture.Client.Payments.GetPayment(paymentId),
757+
r => r.Data.GetType() == expectedStatus);
758+
759+
getPaymentResponseBody.IsSuccessful.Should().BeTrue();
760+
return getPaymentResponseBody.Data;
761+
}
705762
}

0 commit comments

Comments
 (0)