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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [17.1.0] - 2025-03-14
### Added
* Add support for PLN payouts

## [17.0.0] - 2025-01-15
### Changed
* ⚠️ Breaking: removed deprecated HPP link builder
Expand Down
11 changes: 6 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
plugins {
id 'java-library'
// to unleash the lombok magic
id "io.freefair.lombok" version "8.11"
id "io.freefair.lombok" version "8.12.2.1"
// to make our tests output more fancy
id 'com.adarshr.test-logger' version '4.0.0'
// to publish packages
id 'maven-publish'
// code linting
id "com.diffplug.spotless" version "7.0.1"
id "com.diffplug.spotless" version "7.0.2"
// test coverage
id 'jacoco'
id 'com.github.kt3k.coveralls' version '2.12.2'
Expand Down Expand Up @@ -108,14 +108,15 @@ dependencies {
implementation group: 'org.tinylog', name: 'tinylog-impl', version: tinyLogVersion

// JUnit test framework.
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.11.4'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: '5.12.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Copy link
Contributor

Choose a reason for hiding this comment

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

Without this, tests tasks were failing with new Junit version


// Mocking libraries
testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.15.2'
testImplementation group: 'org.wiremock', name: 'wiremock', version: '3.10.0'
testImplementation group: 'org.wiremock', name: 'wiremock', version: '3.12.1'

// Wait test utility
testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.2.2'
testImplementation group: 'org.awaitility', name: 'awaitility', version: '4.3.0'

// Transitive dependencies constraints
constraints {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Main properties
group=com.truelayer
archivesBaseName=truelayer-java
version=17.0.0
version=17.1.0

# Artifacts properties
sonatype_repository_url=https://s01.oss.sonatype.org/service/local/
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
3 changes: 1 addition & 2 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
@Value
@EqualsAndHashCode(callSuper = false)
public class BusinessAccount extends Beneficiary {
private final Type type = BUSINESS_ACCOUNT;
Type type = BUSINESS_ACCOUNT;

private String reference;
String reference;

private String accountHolderName;
String accountHolderName;

private List<AccountIdentifier> accountIdentifiers;
List<AccountIdentifier> accountIdentifiers;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
@Value
@EqualsAndHashCode(callSuper = false)
public class ExternalAccount extends Beneficiary {
private final Type type = EXTERNAL_ACCOUNT;
Type type = EXTERNAL_ACCOUNT;

private String reference;
String reference;

private String accountHolderName;
String accountHolderName;

private List<AccountIdentifier> accountIdentifiers;
List<AccountIdentifier> accountIdentifiers;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
@Value
@EqualsAndHashCode(callSuper = false)
public class PaymentSource extends Beneficiary {
private final Type type = PAYMENT_SOURCE;
Type type = PAYMENT_SOURCE;

private String paymentSourceId;
String paymentSourceId;

private String userId;
String userId;

private String reference;
String reference;

private String accountHolderName;
String accountHolderName;

private List<AccountIdentifier> accountIdentifiers;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
*/
@Value
public class Remitter {
private List<AccountIdentifier> accountIdentifiers;
List<AccountIdentifier> accountIdentifiers;

private String accountHolderName;
String accountHolderName;

private String reference;
String reference;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@JsonSubTypes({
@JsonSubTypes.Type(value = SortCodeAccountNumberAccountIdentifier.class, name = "sort_code_account_number"),
@JsonSubTypes.Type(value = IbanAccountIdentifier.class, name = "iban"),
@JsonSubTypes.Type(value = NrbAccountIdentifier.class, name = "nrb"),
})
@ToString
@EqualsAndHashCode
Expand All @@ -40,6 +41,11 @@ public boolean isIbanIdentifier() {
return this instanceof IbanAccountIdentifier;
}

@JsonIgnore
public boolean isNrbIdentifier() {
return this instanceof NrbAccountIdentifier;
}

@JsonIgnore
public SortCodeAccountNumberAccountIdentifier asSortCodeAccountNumber() {
if (!isSortCodeAccountNumberIdentifier()) {
Expand All @@ -56,6 +62,14 @@ public IbanAccountIdentifier asIban() {
return (IbanAccountIdentifier) this;
}

@JsonIgnore
public NrbAccountIdentifier asNrb() {
if (!isNrbIdentifier()) {
throw new TrueLayerException(buildErrorMessage());
}
return (NrbAccountIdentifier) this;
}

private String buildErrorMessage() {
return String.format("Identifier is of type %s.", this.getClass().getSimpleName());
}
Expand All @@ -64,7 +78,8 @@ private String buildErrorMessage() {
@RequiredArgsConstructor
public enum Type {
SORT_CODE_ACCOUNT_NUMBER("sort_code_account_number"),
IBAN("iban");
IBAN("iban"),
NRB("nrb");

@JsonValue
private final String type;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class NrbAccountIdentifier extends AccountIdentifier {
private final Type type = Type.NRB;

private String nrb;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@JsonSubTypes({
@JsonSubTypes.Type(value = SortCodeAccountNumberAccountIdentifier.class, name = "sort_code_account_number"),
@JsonSubTypes.Type(value = IbanAccountIdentifier.class, name = "iban"),
@JsonSubTypes.Type(value = NrbAccountIdentifier.class, name = "nrb")
})
@ToString
@EqualsAndHashCode
Expand All @@ -40,6 +41,11 @@ public boolean isIbanIdentifier() {
return this instanceof IbanAccountIdentifier;
}

@JsonIgnore
public boolean isNrbIdentifier() {
return this instanceof NrbAccountIdentifier;
}

@JsonIgnore
public SortCodeAccountNumberAccountIdentifier asSortCodeAccountNumber() {
if (!isSortCodeAccountNumberIdentifier()) {
Expand All @@ -56,6 +62,14 @@ public IbanAccountIdentifier asIban() {
return (IbanAccountIdentifier) this;
}

@JsonIgnore
public NrbAccountIdentifier asNrb() {
if (!isNrbIdentifier()) {
throw new TrueLayerException(buildErrorMessage());
}
return (NrbAccountIdentifier) this;
}

private String buildErrorMessage() {
return String.format("Identifier is of type %s.", this.getClass().getSimpleName());
}
Expand All @@ -64,6 +78,7 @@ private String buildErrorMessage() {
@RequiredArgsConstructor
public enum Type {
SORT_CODE_ACCOUNT_NUMBER("sort_code_account_number"),
NRB("nrb"),
IBAN("iban");

@JsonValue
Expand All @@ -78,4 +93,8 @@ public enum Type {
public static IbanAccountIdentifier.IbanAccountIdentifierBuilder iban() {
return new IbanAccountIdentifier.IbanAccountIdentifierBuilder();
}

public static NrbAccountIdentifier.NrbAccountIdentifierBuilder nrb() {
return new NrbAccountIdentifier.NrbAccountIdentifierBuilder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.truelayer.java.payouts.entities.accountidentifier;

import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@Builder
@Getter
@EqualsAndHashCode(callSuper = false)
public class NrbAccountIdentifier extends AccountIdentifier {
private final Type type = Type.NRB;

private String nrb;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package com.truelayer.java.acceptance;

import com.truelayer.java.ClientCredentials;
import com.truelayer.java.Environment;
import com.truelayer.java.SigningOptions;
import com.truelayer.java.TrueLayerClient;
import static com.truelayer.java.TestUtils.assertNotError;

import com.truelayer.java.*;
import com.truelayer.java.entities.CurrencyCode;
import com.truelayer.java.http.entities.ApiResponse;
import com.truelayer.java.merchantaccounts.entities.ListMerchantAccountsResponse;
import com.truelayer.java.merchantaccounts.entities.MerchantAccount;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
Expand Down Expand Up @@ -51,11 +52,14 @@ protected MerchantAccount getMerchantAccount(CurrencyCode currencyCode) {
return merchantAccounts.get(currencyCode);
}

MerchantAccount merchantAccount =
tlClient.merchantAccounts().listMerchantAccounts().get().getData().getItems().stream()
.filter(m -> m.getCurrency().equals(currencyCode))
.findFirst()
.orElseThrow(() -> new RuntimeException("test merchant account not found"));
ApiResponse<ListMerchantAccountsResponse> merchantAccountList =
tlClient.merchantAccounts().listMerchantAccounts().get();
assertNotError(merchantAccountList);

MerchantAccount merchantAccount = merchantAccountList.getData().getItems().stream()
.filter(m -> m.getCurrency().equals(currencyCode))
.findFirst()
.orElseThrow(() -> new RuntimeException("test merchant account not found"));

merchantAccounts.put(currencyCode, merchantAccount);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,35 @@
import com.truelayer.java.payouts.entities.submerchants.SubMerchants;
import java.util.Collections;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Stream;
import lombok.SneakyThrows;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

@Tag("acceptance")
public class PayoutsAcceptanceTests extends AcceptanceTests {

@Test
@DisplayName("It should create a payout and get the details")
@ParameterizedTest(name = "It should create a {0} payout and get the details")
@MethodSource("providePayoutsArgumentsForDifferentCurrencies")
@SneakyThrows
public void shouldCreateAPayoutAndGetPayoutsDetails() {
public void shouldCreateAPayoutAndGetPayoutsDetails(
CurrencyCode currencyCode, AccountIdentifier accountIdentifier, SchemeSelection schemeSelection) {
// find a merchant to execute the payout from
MerchantAccount merchantAccount = getMerchantAccount(CurrencyCode.GBP);
MerchantAccount merchantAccount = getMerchantAccount(currencyCode);

// create the payout
CreatePayoutRequest createPayoutRequest = CreatePayoutRequest.builder()
.merchantAccountId(merchantAccount.getId())
.amountInMinor(ThreadLocalRandom.current().nextInt(10, 100))
.currency(CurrencyCode.GBP)
.currency(currencyCode)
.beneficiary(Beneficiary.externalAccount()
.accountIdentifier(AccountIdentifier.sortCodeAccountNumber()
.accountNumber("00009650")
.sortCode("040668")
.build())
.reference("Java SDK payout test")
.accountIdentifier(accountIdentifier)
.reference(String.format("Java SDK %s payout test", currencyCode.name()))
.accountHolderName("LucaB merchant")
.build())
.schemeSelection(SchemeSelection.instantPreferred().build())
.schemeSelection(schemeSelection)
.subMerchants(SubMerchants.builder()
.ultimateCounterparty(BusinessClient.businessClient()
.tradingName("A sub merchant trading name")
Expand All @@ -69,4 +69,21 @@ public void shouldCreateAPayoutAndGetPayoutsDetails() {
assertEquals(payoutId, getPayoutResponse.getData().getId());
assertEquals(merchantAccount.getId(), getPayoutResponse.getData().getMerchantAccountId());
}

public static Stream<Arguments> providePayoutsArgumentsForDifferentCurrencies() {
return Stream.of(
Arguments.of(
CurrencyCode.GBP,
AccountIdentifier.sortCodeAccountNumber()
.accountNumber("00009650")
.sortCode("040668")
.build(),
SchemeSelection.instantPreferred().build()),
Arguments.of(
CurrencyCode.PLN,
AccountIdentifier.iban().iban("GB25CLRB04066800046876").build(),
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't use the NRB here because the merchant account generated on the console has a GB IBAN, so NRB validation fails on MAP side (more info in this Slack thread)

SchemeSelection.preselected()
.schemeId("polish_domestic_express")
.build()));
}
}
Loading