Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.cake
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// Install .NET Core Global tools.
#tool "dotnet:?package=dotnet-reportgenerator-globaltool&version=5.4.1"
#tool "dotnet:?package=dotnet-reportgenerator-globaltool&version=5.4.9"
#tool "dotnet:?package=coveralls.net&version=4.0.1"

// Install addins
#addin nuget:?package=Cake.Coverlet&version=4.0.1
#addin nuget:?package=Cake.Coverlet&version=5.1.1

///////////////////////////////////////////////////////////////////////////////
// ARGUMENTS
Expand Down
10 changes: 9 additions & 1 deletion src/TrueLayer/Payments/Model/CreatePaymentRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class CreatePaymentRequest
/// If provided, the start authorization flow endpoint does not need to be called</param>
/// <param name="metadata">Add to the payment a list of custom key-value pairs as metadata</param>
/// <param name="riskAssessment">The risk assessment and the payment_creditable webhook configuration.</param>
/// <param name="subMerchants">Sub-merchants information for the payment</param>
public CreatePaymentRequest(
long amountInMinor,
string currency,
Expand All @@ -32,7 +33,8 @@ public CreatePaymentRequest(
RelatedProducts? relatedProducts = null,
StartAuthorizationFlowRequest? authorizationFlow = null,
Dictionary<string, string>? metadata = null,
RiskAssessment? riskAssessment = null)
RiskAssessment? riskAssessment = null,
SubMerchants? subMerchants = null)
{
AmountInMinor = amountInMinor.GreaterThan(0, nameof(amountInMinor));
Currency = currency.NotNullOrWhiteSpace(nameof(currency));
Expand All @@ -42,6 +44,7 @@ public CreatePaymentRequest(
AuthorizationFlow = authorizationFlow;
Metadata = metadata;
RiskAssessment = riskAssessment;
SubMerchants = subMerchants;
}

/// <summary>
Expand Down Expand Up @@ -84,5 +87,10 @@ public CreatePaymentRequest(
/// Gets the risk assessment configuration
/// </summary>
public RiskAssessment? RiskAssessment { get; }

/// <summary>
/// Gets the sub-merchants information for the payment
/// </summary>
public SubMerchants? SubMerchants { get; }
}
}
130 changes: 130 additions & 0 deletions src/TrueLayer/Payments/Model/SubMerchants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using OneOf;
using TrueLayer.Common;
using TrueLayer.Serialization;

namespace TrueLayer.Payments.Model
{
using UltimateCounterpartyUnion = OneOf<SubMerchants.BusinessDivision, SubMerchants.BusinessClient>;

/// <summary>
/// Represents sub-merchants information for payment requests
/// </summary>
public class SubMerchants
{
/// <summary>
/// Creates a new <see cref="SubMerchants"/> instance
/// </summary>
/// <param name="ultimateCounterparty">The ultimate counterparty information</param>
public SubMerchants(UltimateCounterpartyUnion ultimateCounterparty)
{
UltimateCounterparty = ultimateCounterparty;
}

/// <summary>
/// Gets the ultimate counterparty information
/// </summary>
public UltimateCounterpartyUnion UltimateCounterparty { get; }

/// <summary>
/// Represents a business division counterparty
/// </summary>
[JsonDiscriminator("business_division")]
public class BusinessDivision
{
/// <summary>
/// Creates a new <see cref="BusinessDivision"/> instance
/// </summary>
/// <param name="id">UUID generated by you</param>
/// <param name="name">Name of the division</param>
public BusinessDivision(string id, string name)
{
Type = "business_division";
Id = id.NotNullOrWhiteSpace(nameof(id));
Name = name.NotNullOrWhiteSpace(nameof(name));
}

/// <summary>
/// Gets the type of the counterparty
/// </summary>
public string Type { get; }

/// <summary>
/// Gets the UUID generated by you
/// </summary>
public string Id { get; }

/// <summary>
/// Gets the name of the division
/// </summary>
public string Name { get; }
}

/// <summary>
/// Represents a business client counterparty
/// </summary>
[JsonDiscriminator("business_client")]
public class BusinessClient
{
/// <summary>
/// Creates a new <see cref="BusinessClient"/> instance
/// </summary>
/// <param name="tradingName">Trading name of the merchant</param>
/// <param name="commercialName">Commercial name different from trading name (optional)</param>
/// <param name="url">Business website URL (optional)</param>
/// <param name="mcc">Merchant category code (optional)</param>
/// <param name="registrationNumber">Business registration number (optional if address provided)</param>
/// <param name="address">Business address (optional)</param>
public BusinessClient(
string tradingName,
string? commercialName = null,
string? url = null,
string? mcc = null,
string? registrationNumber = null,
Address? address = null)
{
Type = "business_client";
TradingName = tradingName.NotNullOrWhiteSpace(nameof(tradingName));
CommercialName = commercialName.NotEmptyOrWhiteSpace(nameof(commercialName));
Url = url.NotEmptyOrWhiteSpace(nameof(url));
Mcc = mcc.NotEmptyOrWhiteSpace(nameof(mcc));
RegistrationNumber = registrationNumber.NotEmptyOrWhiteSpace(nameof(registrationNumber));
Address = address;
}

/// <summary>
/// Gets the type of the counterparty
/// </summary>
public string Type { get; }

/// <summary>
/// Gets the trading name of the merchant
/// </summary>
public string TradingName { get; }

/// <summary>
/// Gets the commercial name different from trading name
/// </summary>
public string? CommercialName { get; }

/// <summary>
/// Gets the business website URL
/// </summary>
public string? Url { get; }

/// <summary>
/// Gets the merchant category code
/// </summary>
public string? Mcc { get; }

/// <summary>
/// Gets the business registration number
/// </summary>
public string? RegistrationNumber { get; }

/// <summary>
/// Gets the business address
/// </summary>
public Address? Address { get; }
}
}
}
34 changes: 34 additions & 0 deletions src/TrueLayer/Payouts/Model/AccountIdentifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,39 @@ public SortCodeAccountNumber(string sortCode, string accountNumber)
public string AccountNumber { get; }

}

/// <summary>
/// Defines a bank account identified by a Polish NRB
/// </summary>
/// <value></value>
[JsonDiscriminator(Discriminator)]
public record Nrb : IDiscriminated
{
public const string Discriminator = "nrb";

/// <summary>
/// Creates a new <see cref="Nrb"/> instance
/// </summary>
/// <param name="value">
/// Valid Polish NRB (no spaces).
/// Consists of 2 check digits, followed by an 8 digit bank branch number, and then by a 16 digit bank account number.
/// Equivalent to a Polish IBAN with the country code removed.
/// </param>
public Nrb(string value)
{
Value = value.NotNullOrWhiteSpace(nameof(value));
}

/// <summary>
/// Gets the scheme identifier type
/// </summary>
public string Type => Discriminator;

/// <summary>
/// Gets the NRB value
/// </summary>
[JsonPropertyName(Discriminator)]
public string Value { get; }
}
}
}
2 changes: 1 addition & 1 deletion src/TrueLayer/Payouts/Model/Beneficiary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

namespace TrueLayer.Payouts.Model
{
using AccountIdentifierUnion = OneOf<Iban, SortCodeAccountNumber>;
using AccountIdentifierUnion = OneOf<Iban, SortCodeAccountNumber, Nrb>;

public static class Beneficiary
{
Expand Down
67 changes: 65 additions & 2 deletions test/TrueLayer.AcceptanceTests/PaymentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,67 @@
hppUri.Should().NotBeNullOrWhiteSpace();
}

[Fact]
public async Task Can_Create_Payment_With_SubMerchants_BusinessDivision()
{
var subMerchants = new SubMerchants(new SubMerchants.BusinessDivision(
id: Guid.NewGuid().ToString(),
name: "Test Division"));

var paymentRequest = CreateTestPaymentRequest(
new Provider.UserSelected
{
Filter = new ProviderFilter { ProviderIds = ["mock-payments-gb-redirect"] },
SchemeSelection = new SchemeSelection.InstantOnly { AllowRemitterFee = true },
},
subMerchants: subMerchants);

var response = await _fixture.TlClients[0].Payments.CreatePayment(
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());

response.StatusCode.Should().Be(HttpStatusCode.Created);
var authorizationRequired = response.Data.AsT0;

authorizationRequired.Id.Should().NotBeNullOrWhiteSpace();
authorizationRequired.ResourceToken.Should().NotBeNullOrWhiteSpace();
authorizationRequired.User.Should().NotBeNull();
authorizationRequired.User.Id.Should().NotBeNullOrWhiteSpace();
authorizationRequired.Status.Should().Be("authorization_required");
}

[Fact]
public async Task Can_Create_Payment_With_SubMerchants_BusinessClient()
{
var address = new Address("London", "England", "EC1R 4RB", "GB", "1 Hardwick St");
var subMerchants = new SubMerchants(new SubMerchants.BusinessClient(
tradingName: "Test Trading Company",
commercialName: "Test Commercial Name",
url: "https://example.com",
mcc: "1234",
registrationNumber: "REG123456",
address: address));

var paymentRequest = CreateTestPaymentRequest(
new Provider.UserSelected
{
Filter = new ProviderFilter { ProviderIds = ["mock-payments-gb-redirect"] },
SchemeSelection = new SchemeSelection.InstantOnly { AllowRemitterFee = true },
},
subMerchants: subMerchants);

var response = await _fixture.TlClients[0].Payments.CreatePayment(
paymentRequest, idempotencyKey: Guid.NewGuid().ToString());

response.StatusCode.Should().Be(HttpStatusCode.Created);
var authorizationRequired = response.Data.AsT0;

authorizationRequired.Id.Should().NotBeNullOrWhiteSpace();
authorizationRequired.ResourceToken.Should().NotBeNullOrWhiteSpace();
authorizationRequired.User.Should().NotBeNull();
authorizationRequired.User.Id.Should().NotBeNullOrWhiteSpace();
authorizationRequired.Status.Should().Be("authorization_required");
}

[Fact]
public async Task Can_Create_Payment_With_Auth_Flow()
{
Expand Down Expand Up @@ -250,12 +311,12 @@
userSelected.Filter.Should().BeEquivalentTo(providerSelectionReq.Filter);
// Provider selection hasn't happened yet
userSelected.ProviderId.Should().BeNullOrEmpty();
userSelected.SchemeId.Should().BeNullOrEmpty();

Check warning on line 314 in test/TrueLayer.AcceptanceTests/PaymentTests.cs

View workflow job for this annotation

GitHub Actions / build

'Provider.UserSelected.SchemeId' is obsolete: 'The field will be removed soon. Please start using the new <see cref="SchemeSelection"/> field.'
},
preselected =>
{
Provider.Preselected providerSelectionReq = bankTransfer.ProviderSelection.AsT1;
AssertSchemeSelection(preselected.SchemeSelection, providerSelectionReq.SchemeSelection, preselected.SchemeId, providerSelectionReq.SchemeId);

Check warning on line 319 in test/TrueLayer.AcceptanceTests/PaymentTests.cs

View workflow job for this annotation

GitHub Actions / build

'Provider.Preselected.SchemeId' is obsolete: 'The field will be removed soon. Please start using the new <see cref="SchemeSelection"/> field.'

Check warning on line 319 in test/TrueLayer.AcceptanceTests/PaymentTests.cs

View workflow job for this annotation

GitHub Actions / build

'Provider.Preselected.SchemeId' is obsolete: 'The field will be removed soon. Please start using the new <see cref="SchemeSelection"/> field.'
preselected.ProviderId.Should().Be(providerSelectionReq.ProviderId);
preselected.Remitter.Should().Be(providerSelectionReq.Remitter);
});
Expand Down Expand Up @@ -530,7 +591,8 @@
RelatedProducts? relatedProducts = null,
BeneficiaryUnion? beneficiary = null,
Retry.BaseRetry? retry = null,
bool initAuthorizationFlow = false)
bool initAuthorizationFlow = false,
SubMerchants? subMerchants = null)
{
accountIdentifier ??= new AccountIdentifier.SortCodeAccountNumber("567890", "12345678");
providerSelection ??= new Provider.Preselected("mock-payments-gb-redirect",
Expand Down Expand Up @@ -568,7 +630,8 @@
["test-key-1"] = "test-value-1",
["test-key-2"] = "test-value-2",
},
riskAssessment: new RiskAssessment("test")
riskAssessment: new RiskAssessment("test"),
subMerchants: subMerchants
);
}

Expand Down
27 changes: 27 additions & 0 deletions test/TrueLayer.AcceptanceTests/PayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ public async Task Can_create_payout()
response.Data!.Id.Should().NotBeNullOrWhiteSpace();
}

[Fact]
public async Task Can_create_pln_payout()
{
CreatePayoutRequest payoutRequest = CreatePlnPayoutRequest();

var response = await _fixture.TlClients[0].Payouts.CreatePayout(payoutRequest);

response.StatusCode.Should().Be(HttpStatusCode.Accepted);
response.Data.Should().NotBeNull();
response.Data!.Id.Should().NotBeNullOrWhiteSpace();
}

[Fact]
public async Task Can_get_payout()
{
Expand Down Expand Up @@ -83,5 +95,20 @@ private CreatePayoutRequest CreatePayoutRequest()
metadata: new() { { "a", "b" } },
schemeSelection: new SchemeSelection.InstantOnly()
);

private static CreatePayoutRequest CreatePlnPayoutRequest()
=> new(
"fdb6007b-78c0-dbc0-60dd-d4c6f6908e3b", //pln merchant account
100,
Currencies.PLN,
new Beneficiary.ExternalAccount(
"Ms. Lucky",
"truelayer-dotnet",
new AccountIdentifier.Iban("GB25CLRB04066800046876"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using the new Nrb type here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately we can't yet, thread

dateOfBirth: new DateTime(1970, 12, 31),
address: new Address("London", "England", "EC1R 4RB", "GB", "1 Hardwick St")),
metadata: new() { { "a", "b" } },
schemeSelection: new SchemeSelection.InstantOnly()
);
}
}
Loading
Loading