From 0218b95a30a8c27b7075b72aa1300a6a3bc0a11c Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Fri, 17 Oct 2025 18:12:34 +0200 Subject: [PATCH 01/10] Add Verified Payout entities to Create and Get Payout request/response --- .../entities/beneficiary/Beneficiary.java | 19 ++++- .../entities/beneficiary/UserDetermined.java | 26 +++++++ .../entities/AuthorizationRequiredPayout.java | 16 ++++ .../AuthorizationRequiredPayoutDetail.java | 10 +++ .../payouts/entities/AuthorizingPayout.java | 10 +++ .../entities/CreatePayoutResponse.java | 53 ++++++++++++- .../java/payouts/entities/CreatedPayout.java | 10 +++ .../java/payouts/entities/Payout.java | 27 +++++++ .../java/payouts/entities/PayoutUser.java | 12 +++ .../entities/beneficiary/Beneficiary.java | 21 +++++- .../TransactionSearchCriteria.java | 23 ++++++ .../entities/beneficiary/UserDetermined.java | 24 ++++++ .../entities/beneficiary/Verification.java | 16 ++++ .../PreselectedProviderSelection.java | 14 ++++ .../providerselection/ProviderSelection.java | 74 +++++++++++++++++++ .../UserSelectedProviderSelection.java | 15 ++++ 16 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/truelayer/java/entities/beneficiary/UserDetermined.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayout.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayoutDetail.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/AuthorizingPayout.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/CreatedPayout.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/PayoutUser.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/UserDetermined.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/Verification.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/PreselectedProviderSelection.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelection.java create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/UserSelectedProviderSelection.java diff --git a/src/main/java/com/truelayer/java/entities/beneficiary/Beneficiary.java b/src/main/java/com/truelayer/java/entities/beneficiary/Beneficiary.java index f970947a1..7770b73ad 100644 --- a/src/main/java/com/truelayer/java/entities/beneficiary/Beneficiary.java +++ b/src/main/java/com/truelayer/java/entities/beneficiary/Beneficiary.java @@ -20,7 +20,8 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = ExternalAccount.class, name = "external_account"), @JsonSubTypes.Type(value = BusinessAccount.class, name = "business_account"), - @JsonSubTypes.Type(value = PaymentSource.class, name = "payment_source") + @JsonSubTypes.Type(value = PaymentSource.class, name = "payment_source"), + @JsonSubTypes.Type(value = UserDetermined.class, name = "user_determined") }) @ToString @EqualsAndHashCode @@ -45,6 +46,11 @@ public boolean isBusinessAccount() { return this instanceof BusinessAccount; } + @JsonIgnore + public boolean isUserDetermined() { + return this instanceof UserDetermined; + } + @JsonIgnore public BusinessAccount asBusinessAccount() { if (!isBusinessAccount()) { @@ -69,6 +75,14 @@ public PaymentSource asPaymentSource() { return (PaymentSource) this; } + @JsonIgnore + public UserDetermined asUserDetermined() { + if (!isUserDetermined()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (UserDetermined) this; + } + private String buildErrorMessage() { return String.format("Beneficiary is of type %s.", this.getClass().getSimpleName()); } @@ -78,7 +92,8 @@ private String buildErrorMessage() { public enum Type { EXTERNAL_ACCOUNT("external_account"), PAYMENT_SOURCE("payment_source"), - BUSINESS_ACCOUNT("business_account"); + BUSINESS_ACCOUNT("business_account"), + USER_DETERMINED("user_determined"); @JsonValue private final String type; diff --git a/src/main/java/com/truelayer/java/entities/beneficiary/UserDetermined.java b/src/main/java/com/truelayer/java/entities/beneficiary/UserDetermined.java new file mode 100644 index 000000000..4e5b92751 --- /dev/null +++ b/src/main/java/com/truelayer/java/entities/beneficiary/UserDetermined.java @@ -0,0 +1,26 @@ +package com.truelayer.java.entities.beneficiary; + +import static com.truelayer.java.entities.beneficiary.Beneficiary.Type.USER_DETERMINED; + +import com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier.AccountIdentifier; +import com.truelayer.java.payouts.entities.PayoutUser; +import com.truelayer.java.payouts.entities.beneficiary.Verification; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class UserDetermined extends Beneficiary { + Type type = USER_DETERMINED; + + String reference; + + String accountHolderName; + + List accountIdentifiers; + + PayoutUser user; + + Verification verification; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayout.java b/src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayout.java new file mode 100644 index 000000000..77f4ac7ef --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayout.java @@ -0,0 +1,16 @@ +package com.truelayer.java.payouts.entities; + +import com.truelayer.java.payouts.entities.beneficiary.Beneficiary; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class AuthorizationRequiredPayout extends CreatePayoutResponse { + private final Status status = Status.AUTHORIZATION_REQUIRED; + private String resourceToken; + private PayoutUser user; + private Beneficiary beneficiary; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayoutDetail.java b/src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayoutDetail.java new file mode 100644 index 000000000..7603085a5 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/AuthorizationRequiredPayoutDetail.java @@ -0,0 +1,10 @@ +package com.truelayer.java.payouts.entities; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AuthorizationRequiredPayoutDetail extends Payout { + Status status = Status.AUTHORIZATION_REQUIRED; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/AuthorizingPayout.java b/src/main/java/com/truelayer/java/payouts/entities/AuthorizingPayout.java new file mode 100644 index 000000000..a81e3f645 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/AuthorizingPayout.java @@ -0,0 +1,10 @@ +package com.truelayer.java.payouts.entities; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class AuthorizingPayout extends Payout { + Status status = Status.AUTHORIZING; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/CreatePayoutResponse.java b/src/main/java/com/truelayer/java/payouts/entities/CreatePayoutResponse.java index 7674a644a..923e5e47e 100644 --- a/src/main/java/com/truelayer/java/payouts/entities/CreatePayoutResponse.java +++ b/src/main/java/com/truelayer/java/payouts/entities/CreatePayoutResponse.java @@ -1,12 +1,63 @@ package com.truelayer.java.payouts.entities; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonValue; +import com.truelayer.java.TrueLayerException; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.ToString; +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "status", defaultImpl = CreatedPayout.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = CreatedPayout.class, name = "created"), + @JsonSubTypes.Type(value = AuthorizationRequiredPayout.class, name = "authorization_required") +}) @ToString @EqualsAndHashCode @Getter -public class CreatePayoutResponse { +public abstract class CreatePayoutResponse { private String id; + + @JsonIgnore + public abstract Status getStatus(); + + @JsonIgnore + public boolean isCreated() { + return this instanceof CreatedPayout; + } + + @JsonIgnore + public boolean isAuthorizationRequired() { + return this instanceof AuthorizationRequiredPayout; + } + + @JsonIgnore + public CreatedPayout asCreated() { + if (!isCreated()) throw new TrueLayerException(buildErrorMessage()); + return (CreatedPayout) this; + } + + @JsonIgnore + public AuthorizationRequiredPayout asAuthorizationRequired() { + if (!isAuthorizationRequired()) throw new TrueLayerException(buildErrorMessage()); + return (AuthorizationRequiredPayout) this; + } + + private String buildErrorMessage() { + return String.format( + "Create payout response is of type %s.", this.getClass().getSimpleName()); + } + + @Getter + @RequiredArgsConstructor + public enum Status { + CREATED("created"), + AUTHORIZATION_REQUIRED("authorization_required"); + + @JsonValue + private final String status; + } } diff --git a/src/main/java/com/truelayer/java/payouts/entities/CreatedPayout.java b/src/main/java/com/truelayer/java/payouts/entities/CreatedPayout.java new file mode 100644 index 000000000..480a6cd81 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/CreatedPayout.java @@ -0,0 +1,10 @@ +package com.truelayer.java.payouts.entities; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class CreatedPayout extends CreatePayoutResponse { + Status status = Status.CREATED; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/Payout.java b/src/main/java/com/truelayer/java/payouts/entities/Payout.java index c9d957297..e701fb06f 100644 --- a/src/main/java/com/truelayer/java/payouts/entities/Payout.java +++ b/src/main/java/com/truelayer/java/payouts/entities/Payout.java @@ -14,6 +14,8 @@ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "status", defaultImpl = PendingPayout.class) @JsonSubTypes({ + @JsonSubTypes.Type(value = AuthorizationRequiredPayoutDetail.class, name = "authorization_required"), + @JsonSubTypes.Type(value = AuthorizingPayout.class, name = "authorizing"), @JsonSubTypes.Type(value = PendingPayout.class, name = "pending"), @JsonSubTypes.Type(value = AuthorizedPayout.class, name = "authorized"), @JsonSubTypes.Type(value = ExecutedPayout.class, name = "executed"), @@ -29,10 +31,21 @@ public abstract class Payout { private Map metadata; private SchemeId schemeId; private ZonedDateTime createdAt; + private PayoutUser user; @JsonIgnore public abstract Status getStatus(); + @JsonIgnore + public boolean isAuthorizationRequired() { + return this instanceof AuthorizationRequiredPayoutDetail; + } + + @JsonIgnore + public boolean isAuthorizing() { + return this instanceof AuthorizingPayout; + } + @JsonIgnore public boolean isPending() { return this instanceof PendingPayout; @@ -53,6 +66,18 @@ public boolean isFailed() { return this instanceof FailedPayout; } + @JsonIgnore + public AuthorizationRequiredPayoutDetail asAuthorizationRequiredPayout() { + if (!isAuthorizationRequired()) throw new TrueLayerException(buildErrorMessage()); + return (AuthorizationRequiredPayoutDetail) this; + } + + @JsonIgnore + public AuthorizingPayout asAuthorizingPayout() { + if (!isAuthorizing()) throw new TrueLayerException(buildErrorMessage()); + return (AuthorizingPayout) this; + } + @JsonIgnore public PendingPayout asPendingPayout() { if (!isPending()) throw new TrueLayerException(buildErrorMessage()); @@ -84,6 +109,8 @@ private String buildErrorMessage() { @Getter @RequiredArgsConstructor public enum Status { + AUTHORIZATION_REQUIRED("authorization_required"), + AUTHORIZING("authorizing"), PENDING("pending"), AUTHORIZED("authorized"), EXECUTED("executed"), diff --git a/src/main/java/com/truelayer/java/payouts/entities/PayoutUser.java b/src/main/java/com/truelayer/java/payouts/entities/PayoutUser.java new file mode 100644 index 000000000..0b730c388 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/PayoutUser.java @@ -0,0 +1,12 @@ +package com.truelayer.java.payouts.entities; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +public class PayoutUser { + private String id; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Beneficiary.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Beneficiary.java index 4291e77ca..ae7a13080 100644 --- a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Beneficiary.java +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Beneficiary.java @@ -16,7 +16,8 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = ExternalAccount.class, name = "external_account"), @JsonSubTypes.Type(value = PaymentSource.class, name = "payment_source"), - @JsonSubTypes.Type(value = BusinessAccount.class, name = "business_account") + @JsonSubTypes.Type(value = BusinessAccount.class, name = "business_account"), + @JsonSubTypes.Type(value = UserDetermined.class, name = "user_determined") }) public abstract class Beneficiary { @JsonIgnore @@ -37,6 +38,11 @@ public boolean isBusinessAccount() { return this instanceof BusinessAccount; } + @JsonIgnore + public boolean isUserDetermined() { + return this instanceof UserDetermined; + } + @JsonIgnore public ExternalAccount asExternalAccount() { if (!isExternalAccount()) throw new TrueLayerException(buildErrorMessage()); @@ -61,12 +67,22 @@ public static BusinessAccount.BusinessAccountBuilder businessAccount() { return new BusinessAccount.BusinessAccountBuilder(); } + public static UserDetermined.UserDeterminedBuilder userDetermined() { + return new UserDetermined.UserDeterminedBuilder(); + } + @JsonIgnore public BusinessAccount asBusinessAccount() { if (!isBusinessAccount()) throw new TrueLayerException(buildErrorMessage()); return (BusinessAccount) this; } + @JsonIgnore + public UserDetermined asUserDetermined() { + if (!isUserDetermined()) throw new TrueLayerException(buildErrorMessage()); + return (UserDetermined) this; + } + private String buildErrorMessage() { return String.format("Beneficiary is of type %s.", this.getClass().getSimpleName()); } @@ -76,7 +92,8 @@ private String buildErrorMessage() { public enum Type { EXTERNAL_ACCOUNT("external_account"), PAYMENT_SOURCE("payment_source"), - BUSINESS_ACCOUNT("business_account"); + BUSINESS_ACCOUNT("business_account"), + USER_DETERMINED("user_determined"); @JsonValue private final String type; diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java new file mode 100644 index 000000000..b99eae3ed --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java @@ -0,0 +1,23 @@ +package com.truelayer.java.payouts.entities.beneficiary; + +import com.truelayer.java.entities.CurrencyCode; +import java.time.ZonedDateTime; +import java.util.List; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +@EqualsAndHashCode +public class TransactionSearchCriteria { + private List tokens; + + private Integer amountInMinor; + + private CurrencyCode currency; + + private ZonedDateTime createdAt; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/UserDetermined.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/UserDetermined.java new file mode 100644 index 000000000..abcef3c8e --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/UserDetermined.java @@ -0,0 +1,24 @@ +package com.truelayer.java.payouts.entities.beneficiary; + +import com.truelayer.java.entities.User; +import com.truelayer.java.payouts.entities.beneficiary.providerselection.ProviderSelection; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString(callSuper = false) +@EqualsAndHashCode(callSuper = false) +public class UserDetermined extends Beneficiary { + private final Type type = Type.USER_DETERMINED; + + private String reference; + + private User user; + + private Verification verification; + + private ProviderSelection providerSelection; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Verification.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Verification.java new file mode 100644 index 000000000..7a5d5b733 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/Verification.java @@ -0,0 +1,16 @@ +package com.truelayer.java.payouts.entities.beneficiary; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +@EqualsAndHashCode +public class Verification { + private Boolean verifyName; + + private TransactionSearchCriteria transactionSearchCriteria; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/PreselectedProviderSelection.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/PreselectedProviderSelection.java new file mode 100644 index 000000000..ef8747a2b --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/PreselectedProviderSelection.java @@ -0,0 +1,14 @@ +package com.truelayer.java.payouts.entities.beneficiary.providerselection; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Getter +@Builder +@EqualsAndHashCode(callSuper = false) +public class PreselectedProviderSelection extends ProviderSelection { + private final Type type = Type.PRESELECTED; + + private String providerId; +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelection.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelection.java new file mode 100644 index 000000000..5e4b822b2 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelection.java @@ -0,0 +1,74 @@ +package com.truelayer.java.payouts.entities.beneficiary.providerselection; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonValue; +import com.truelayer.java.TrueLayerException; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = UserSelectedProviderSelection.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = UserSelectedProviderSelection.class, name = "user_selected"), + @JsonSubTypes.Type(value = PreselectedProviderSelection.class, name = "preselected") +}) +@ToString +@EqualsAndHashCode +@Getter +public abstract class ProviderSelection { + + @JsonIgnore + public abstract Type getType(); + + public static UserSelectedProviderSelection.UserSelectedProviderSelectionBuilder userSelected() { + return UserSelectedProviderSelection.builder(); + } + + public static PreselectedProviderSelection.PreselectedProviderSelectionBuilder preselected() { + return PreselectedProviderSelection.builder(); + } + + @JsonIgnore + public boolean isUserSelected() { + return this instanceof UserSelectedProviderSelection; + } + + @JsonIgnore + public boolean isPreselected() { + return this instanceof PreselectedProviderSelection; + } + + @JsonIgnore + public UserSelectedProviderSelection asUserSelected() { + if (!isUserSelected()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (UserSelectedProviderSelection) this; + } + + @JsonIgnore + public PreselectedProviderSelection asPreselected() { + if (!isPreselected()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (PreselectedProviderSelection) this; + } + + private String buildErrorMessage() { + return String.format( + "Provider selection is of type %s.", this.getClass().getSimpleName()); + } + + @RequiredArgsConstructor + @Getter + public enum Type { + USER_SELECTED("user_selected"), + PRESELECTED("preselected"); + + @JsonValue + private final String type; + } +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/UserSelectedProviderSelection.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/UserSelectedProviderSelection.java new file mode 100644 index 000000000..b23eaf904 --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/UserSelectedProviderSelection.java @@ -0,0 +1,15 @@ +package com.truelayer.java.payouts.entities.beneficiary.providerselection; + +import com.truelayer.java.entities.ProviderFilter; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; + +@Builder +@Getter +@EqualsAndHashCode(callSuper = false) +public class UserSelectedProviderSelection extends ProviderSelection { + private final Type type = Type.USER_SELECTED; + + private ProviderFilter filter; +} From 56159bbdac5ce9e8047f190e804fcfbfde392c38 Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Fri, 17 Oct 2025 18:31:21 +0200 Subject: [PATCH 02/10] Adds unit and integration tests for Create and Get payout endpoints --- .../beneficiary/BeneficiaryTests.java | 39 ++++++++++ .../integration/PayoutsIntegrationTests.java | 34 ++++++++- .../entities/CreatePayoutResponseTests.java | 66 +++++++++++++++++ .../java/payouts/entities/PayoutTests.java | 52 ++++++++++++++ .../beneficiary/BeneficiaryTests.java | 38 ++++++++++ .../ProviderSelectionTests.java | 72 +++++++++++++++++++ .../200.get_transactions.json | 30 ++++---- ...t_payout_by_id.authorization_required.json | 28 ++++++++ .../200.get_payout_by_id.authorizing.json | 28 ++++++++ ....create_payout.authorization_required.json | 8 +++ 10 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java create mode 100644 src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java create mode 100644 src/test/resources/__files/payouts/200.get_payout_by_id.authorization_required.json create mode 100644 src/test/resources/__files/payouts/200.get_payout_by_id.authorizing.json create mode 100644 src/test/resources/__files/payouts/202.create_payout.authorization_required.json diff --git a/src/test/java/com/truelayer/java/entities/beneficiary/BeneficiaryTests.java b/src/test/java/com/truelayer/java/entities/beneficiary/BeneficiaryTests.java index 7bd0a7b34..0d6b6ef8d 100644 --- a/src/test/java/com/truelayer/java/entities/beneficiary/BeneficiaryTests.java +++ b/src/test/java/com/truelayer/java/entities/beneficiary/BeneficiaryTests.java @@ -122,4 +122,43 @@ public void shouldNotConvertToPaymentSource() { assertEquals(String.format("Beneficiary is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } + + @Test + @DisplayName("It should yield true if instance is of type UserDetermined") + public void shouldYieldTrueIfUserDetermined() { + Beneficiary sut = new UserDetermined( + "a-reference", + "an-account-holder-name", + Arrays.asList(IbanAccountIdentifier.builder().build()), + null, + null); + + assertTrue(sut.isUserDetermined()); + } + + @Test + @DisplayName("It should convert to an instance of class UserDetermined") + public void shouldConvertToUserDetermined() { + Beneficiary sut = new UserDetermined( + "a-reference", + "an-account-holder-name", + Arrays.asList(IbanAccountIdentifier.builder().build()), + null, + null); + + assertDoesNotThrow(sut::asUserDetermined); + } + + @Test + @DisplayName("It should throw an error when converting to UserDetermined") + public void shouldNotConvertToUserDetermined() { + Beneficiary sut = new BusinessAccount( + "a-reference", + "a-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asUserDetermined); + + assertEquals(String.format("Beneficiary is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } } diff --git a/src/test/java/com/truelayer/java/integration/PayoutsIntegrationTests.java b/src/test/java/com/truelayer/java/integration/PayoutsIntegrationTests.java index 9e876ddd2..e20fed222 100644 --- a/src/test/java/com/truelayer/java/integration/PayoutsIntegrationTests.java +++ b/src/test/java/com/truelayer/java/integration/PayoutsIntegrationTests.java @@ -55,9 +55,41 @@ public void shouldCreateAPayout() { assertEquals(expected, response.getData()); } + @Test + @DisplayName("It should create a payout with authorization required") + @SneakyThrows + public void shouldCreateAPayoutWithAuthorizationRequired() { + String jsonResponseFile = "payouts/202.create_payout.authorization_required.json"; + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/connect/token")) + .status(200) + .bodyFile("auth/200.access_token.json") + .build(); + RequestStub.New() + .method("post") + .path(urlPathEqualTo("/payouts")) + .withAuthorization() + .withSignature() + .withIdempotencyKey() + .status(202) + .bodyFile(jsonResponseFile) + .build(); + CreatePayoutRequest payoutRequest = CreatePayoutRequest.builder().build(); + + ApiResponse response = + tlClient.payouts().createPayout(payoutRequest).get(); + + verifyGeneratedToken(Collections.singletonList(PAYMENTS)); + assertNotError(response); + CreatePayoutResponse expected = TestUtils.deserializeJsonFileTo(jsonResponseFile, CreatePayoutResponse.class); + assertEquals(expected, response.getData()); + assertTrue(response.getData().isAuthorizationRequired()); + } + @DisplayName("It should get payout details") @ParameterizedTest(name = "of a payout with status {0}") - @ValueSource(strings = {"PENDING", "AUTHORIZED", "EXECUTED", "FAILED"}) + @ValueSource(strings = {"PENDING", "AUTHORIZED", "EXECUTED", "FAILED", "AUTHORIZATION_REQUIRED", "AUTHORIZING"}) @SneakyThrows public void shouldReturnPayoutDetails(Payout.Status expectedStatus) { String jsonResponseFile = "payouts/200.get_payout_by_id." + expectedStatus.getStatus() + ".json"; diff --git a/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java b/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java new file mode 100644 index 000000000..bc523419d --- /dev/null +++ b/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java @@ -0,0 +1,66 @@ +package com.truelayer.java.payouts.entities; + +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.TrueLayerException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CreatePayoutResponseTests { + + @Test + @DisplayName("It should yield true if instance is of type CreatedPayout") + public void shouldYieldTrueIfCreatedPayout() { + CreatePayoutResponse sut = new CreatedPayout(); + + assertTrue(sut.isCreated()); + } + + @Test + @DisplayName("It should convert to an instance of class CreatedPayout") + public void shouldConvertToCreatedPayout() { + CreatePayoutResponse sut = new CreatedPayout(); + + assertDoesNotThrow(sut::asCreated); + } + + @Test + @DisplayName("It should throw an error when converting to CreatedPayout") + public void shouldNotConvertToCreatedPayout() { + CreatePayoutResponse sut = new AuthorizationRequiredPayout(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asCreated); + + assertEquals( + String.format("Create payout response is of type %s.", sut.getClass().getSimpleName()), + thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type AuthorizationRequiredPayout") + public void shouldYieldTrueIfAuthorizationRequiredPayout() { + CreatePayoutResponse sut = new AuthorizationRequiredPayout(); + + assertTrue(sut.isAuthorizationRequired()); + } + + @Test + @DisplayName("It should convert to an instance of class AuthorizationRequiredPayout") + public void shouldConvertToAuthorizationRequiredPayout() { + CreatePayoutResponse sut = new AuthorizationRequiredPayout(); + + assertDoesNotThrow(sut::asAuthorizationRequired); + } + + @Test + @DisplayName("It should throw an error when converting to AuthorizationRequiredPayout") + public void shouldNotConvertToAuthorizationRequiredPayout() { + CreatePayoutResponse sut = new CreatedPayout(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asAuthorizationRequired); + + assertEquals( + String.format("Create payout response is of type %s.", sut.getClass().getSimpleName()), + thrown.getMessage()); + } +} diff --git a/src/test/java/com/truelayer/java/payouts/entities/PayoutTests.java b/src/test/java/com/truelayer/java/payouts/entities/PayoutTests.java index 14ea2c2ab..3cf629e4d 100644 --- a/src/test/java/com/truelayer/java/payouts/entities/PayoutTests.java +++ b/src/test/java/com/truelayer/java/payouts/entities/PayoutTests.java @@ -113,4 +113,56 @@ public void shouldNotConvertToFailedPayout() { assertEquals(String.format("Payout is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } + + @Test + @DisplayName("It should yield true if instance is of type AuthorizationRequiredPayoutDetail") + public void shouldYieldTrueIfAuthorizationRequiredPayoutDetail() { + Payout sut = new AuthorizationRequiredPayoutDetail(); + + assertTrue(sut.isAuthorizationRequired()); + } + + @Test + @DisplayName("It should convert to an instance of class AuthorizationRequiredPayoutDetail") + public void shouldConvertToAuthorizationRequiredPayoutDetail() { + Payout sut = new AuthorizationRequiredPayoutDetail(); + + assertDoesNotThrow(sut::asAuthorizationRequiredPayout); + } + + @Test + @DisplayName("It should throw an error when converting to AuthorizationRequiredPayoutDetail") + public void shouldNotConvertToAuthorizationRequiredPayoutDetail() { + Payout sut = new PendingPayout(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asAuthorizationRequiredPayout); + + assertEquals(String.format("Payout is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type AuthorizingPayout") + public void shouldYieldTrueIfAuthorizingPayout() { + Payout sut = new AuthorizingPayout(); + + assertTrue(sut.isAuthorizing()); + } + + @Test + @DisplayName("It should convert to an instance of class AuthorizingPayout") + public void shouldConvertToAuthorizingPayout() { + Payout sut = new AuthorizingPayout(); + + assertDoesNotThrow(sut::asAuthorizingPayout); + } + + @Test + @DisplayName("It should throw an error when converting to AuthorizingPayout") + public void shouldNotConvertToAuthorizingPayout() { + Payout sut = new PendingPayout(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asAuthorizingPayout); + + assertEquals(String.format("Payout is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } } diff --git a/src/test/java/com/truelayer/java/payouts/entities/beneficiary/BeneficiaryTests.java b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/BeneficiaryTests.java index 20d101405..1c18b0142 100644 --- a/src/test/java/com/truelayer/java/payouts/entities/beneficiary/BeneficiaryTests.java +++ b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/BeneficiaryTests.java @@ -122,4 +122,42 @@ public void shouldNotConvertToPaymentSource() { assertEquals(String.format("Beneficiary is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } + + @Test + @DisplayName("It should yield true if instance is of type UserDetermined") + public void shouldYieldTrueIfUserDetermined() { + Beneficiary sut = Beneficiary.userDetermined() + .reference("a-reference") + .user(com.truelayer.java.entities.User.builder() + .name("John Doe") + .build()) + .build(); + + assertTrue(sut.isUserDetermined()); + } + + @Test + @DisplayName("It should convert to an instance of class UserDetermined") + public void shouldConvertToUserDetermined() { + Beneficiary sut = Beneficiary.userDetermined() + .reference("a-reference") + .user(com.truelayer.java.entities.User.builder() + .name("John Doe") + .build()) + .build(); + + assertDoesNotThrow(sut::asUserDetermined); + } + + @Test + @DisplayName("It should throw an error when converting to UserDetermined") + public void shouldNotConvertToUserDetermined() { + Beneficiary sut = new BusinessAccount.BusinessAccountBuilder() + .reference("a-reference") + .build(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asUserDetermined); + + assertEquals(String.format("Beneficiary is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } } diff --git a/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java new file mode 100644 index 000000000..fb7d38d1f --- /dev/null +++ b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java @@ -0,0 +1,72 @@ +package com.truelayer.java.payouts.entities.beneficiary.providerselection; + +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.TrueLayerException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class ProviderSelectionTests { + + @Test + @DisplayName("It should yield true if instance is of type UserSelectedProviderSelection") + public void shouldYieldTrueIfUserSelectedProviderSelection() { + ProviderSelection sut = ProviderSelection.userSelected().build(); + + assertTrue(sut.isUserSelected()); + } + + @Test + @DisplayName("It should convert to an instance of class UserSelectedProviderSelection") + public void shouldConvertToUserSelectedProviderSelection() { + ProviderSelection sut = ProviderSelection.userSelected().build(); + + assertDoesNotThrow(sut::asUserSelected); + } + + @Test + @DisplayName("It should throw an error when converting to UserSelectedProviderSelection") + public void shouldNotConvertToUserSelectedProviderSelection() { + ProviderSelection sut = ProviderSelection.preselected() + .providerId("a-provider-id") + .build(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asUserSelected); + + assertEquals( + String.format("Provider selection is of type %s.", sut.getClass().getSimpleName()), + thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type PreselectedProviderSelection") + public void shouldYieldTrueIfPreselectedProviderSelection() { + ProviderSelection sut = ProviderSelection.preselected() + .providerId("a-provider-id") + .build(); + + assertTrue(sut.isPreselected()); + } + + @Test + @DisplayName("It should convert to an instance of class PreselectedProviderSelection") + public void shouldConvertToPreselectedProviderSelection() { + ProviderSelection sut = ProviderSelection.preselected() + .providerId("a-provider-id") + .build(); + + assertDoesNotThrow(sut::asPreselected); + } + + @Test + @DisplayName("It should throw an error when converting to PreselectedProviderSelection") + public void shouldNotConvertToPreselectedProviderSelection() { + ProviderSelection sut = ProviderSelection.userSelected().build(); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asPreselected); + + assertEquals( + String.format("Provider selection is of type %s.", sut.getClass().getSimpleName()), + thrown.getMessage()); + } +} diff --git a/src/test/resources/__files/merchant_accounts/200.get_transactions.json b/src/test/resources/__files/merchant_accounts/200.get_transactions.json index 8a8f52848..ab3cd96f7 100644 --- a/src/test/resources/__files/merchant_accounts/200.get_transactions.json +++ b/src/test/resources/__files/merchant_accounts/200.get_transactions.json @@ -117,24 +117,28 @@ "created_at":"2022-02-10T15:47:17.403Z", "executed_at":"2022-02-10T15:47:17.403Z", "beneficiary":{ - "type":"business_account", - "merchant_account_id":"e83c4c20-b2ad-4b73-8a32-***", + "type":"user_determined", + "reference":"verified-payout-ref", "account_holder_name":"john smith", - "account_identifiers":[ - { - "type":"sort_code_account_number", - "sort_code":"040662", - "account_number":"00002723" - }, - { - "type":"iban", - "iban":"GB53CLRB04066200002723" + "user":{ + "id":"user-123456" + }, + "verification":{ + "verify_name":true, + "transaction_search_criteria":{ + "tokens":["token123","token456"], + "amount_in_minor":1, + "currency":"GBP", + "created_at":"2022-02-10T15:47:17.403Z" } - ] + } }, "context_code":"withdrawal", "payout_id":"string", - "scheme_id":"internal_transfer" + "scheme_id":"faster_payments_service", + "user":{ + "id":"user-123456" + } }, { "type":"refund", diff --git a/src/test/resources/__files/payouts/200.get_payout_by_id.authorization_required.json b/src/test/resources/__files/payouts/200.get_payout_by_id.authorization_required.json new file mode 100644 index 000000000..bad28441d --- /dev/null +++ b/src/test/resources/__files/payouts/200.get_payout_by_id.authorization_required.json @@ -0,0 +1,28 @@ +{ + "id": "d234567-890b-cdef-0123-***", + "merchant_account_id": "f84d5d31-c3be-5c84-9b43-***", + "amount_in_minor": 250, + "currency": "GBP", + "beneficiary": { + "type": "user_determined", + "reference": "verified-payout-ref", + "account_holder_name": "Jane Smith", + "user": { + "id": "user-789012" + }, + "verification": { + "verify_name": true, + "transaction_search_criteria": { + "tokens": ["token123", "token456"], + "amount_in_minor": 250, + "currency": "GBP", + "created_at": "2025-10-17T16:30:00.000000Z" + } + } + }, + "status": "authorization_required", + "user": { + "id": "user-789012" + }, + "created_at": "2025-10-17T16:30:00.000000Z" +} diff --git a/src/test/resources/__files/payouts/200.get_payout_by_id.authorizing.json b/src/test/resources/__files/payouts/200.get_payout_by_id.authorizing.json new file mode 100644 index 000000000..37f3fa065 --- /dev/null +++ b/src/test/resources/__files/payouts/200.get_payout_by_id.authorizing.json @@ -0,0 +1,28 @@ +{ + "id": "e345678-901c-def0-1234-***", + "merchant_account_id": "f84d5d31-c3be-5c84-9b43-***", + "amount_in_minor": 300, + "currency": "GBP", + "beneficiary": { + "type": "user_determined", + "reference": "verified-payout-ref", + "account_holder_name": "John Smith", + "user": { + "id": "user-890123" + }, + "verification": { + "verify_name": true, + "transaction_search_criteria": { + "tokens": ["token789", "token012"], + "amount_in_minor": 300, + "currency": "GBP", + "created_at": "2025-10-17T16:45:00.000000Z" + } + } + }, + "status": "authorizing", + "user": { + "id": "user-890123" + }, + "created_at": "2025-10-17T16:45:00.000000Z" +} diff --git a/src/test/resources/__files/payouts/202.create_payout.authorization_required.json b/src/test/resources/__files/payouts/202.create_payout.authorization_required.json new file mode 100644 index 000000000..86c868221 --- /dev/null +++ b/src/test/resources/__files/payouts/202.create_payout.authorization_required.json @@ -0,0 +1,8 @@ +{ + "id": "c123456-789a-bcde-f012-***", + "status": "authorization_required", + "resource_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U", + "user": { + "id": "user-123456" + } +} From dcb9fcef43519063229c11d2c9acad9e4822335b Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Fri, 17 Oct 2025 18:37:02 +0200 Subject: [PATCH 03/10] Apply code formatting --- .../entities/CreatePayoutResponseTests.java | 6 ++++-- .../ProviderSelectionTests.java | 21 +++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java b/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java index bc523419d..6b1c51391 100644 --- a/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java +++ b/src/test/java/com/truelayer/java/payouts/entities/CreatePayoutResponseTests.java @@ -32,7 +32,8 @@ public void shouldNotConvertToCreatedPayout() { Throwable thrown = assertThrows(TrueLayerException.class, sut::asCreated); assertEquals( - String.format("Create payout response is of type %s.", sut.getClass().getSimpleName()), + String.format( + "Create payout response is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } @@ -60,7 +61,8 @@ public void shouldNotConvertToAuthorizationRequiredPayout() { Throwable thrown = assertThrows(TrueLayerException.class, sut::asAuthorizationRequired); assertEquals( - String.format("Create payout response is of type %s.", sut.getClass().getSimpleName()), + String.format( + "Create payout response is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } } diff --git a/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java index fb7d38d1f..40b5eae8c 100644 --- a/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java +++ b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/providerselection/ProviderSelectionTests.java @@ -27,23 +27,22 @@ public void shouldConvertToUserSelectedProviderSelection() { @Test @DisplayName("It should throw an error when converting to UserSelectedProviderSelection") public void shouldNotConvertToUserSelectedProviderSelection() { - ProviderSelection sut = ProviderSelection.preselected() - .providerId("a-provider-id") - .build(); + ProviderSelection sut = + ProviderSelection.preselected().providerId("a-provider-id").build(); Throwable thrown = assertThrows(TrueLayerException.class, sut::asUserSelected); assertEquals( - String.format("Provider selection is of type %s.", sut.getClass().getSimpleName()), + String.format( + "Provider selection is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } @Test @DisplayName("It should yield true if instance is of type PreselectedProviderSelection") public void shouldYieldTrueIfPreselectedProviderSelection() { - ProviderSelection sut = ProviderSelection.preselected() - .providerId("a-provider-id") - .build(); + ProviderSelection sut = + ProviderSelection.preselected().providerId("a-provider-id").build(); assertTrue(sut.isPreselected()); } @@ -51,9 +50,8 @@ public void shouldYieldTrueIfPreselectedProviderSelection() { @Test @DisplayName("It should convert to an instance of class PreselectedProviderSelection") public void shouldConvertToPreselectedProviderSelection() { - ProviderSelection sut = ProviderSelection.preselected() - .providerId("a-provider-id") - .build(); + ProviderSelection sut = + ProviderSelection.preselected().providerId("a-provider-id").build(); assertDoesNotThrow(sut::asPreselected); } @@ -66,7 +64,8 @@ public void shouldNotConvertToPreselectedProviderSelection() { Throwable thrown = assertThrows(TrueLayerException.class, sut::asPreselected); assertEquals( - String.format("Provider selection is of type %s.", sut.getClass().getSimpleName()), + String.format( + "Provider selection is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } } From 11a8f0cb465ec9e4f653d9361a011b083fecff65 Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Wed, 22 Oct 2025 19:38:00 +0200 Subject: [PATCH 04/10] Add payout support to HPP link builder --- .../java/com/truelayer/java/Environment.java | 16 ++++++--- .../java/HostedPaymentPageLinkBuilder.java | 6 +++- .../truelayer/java/entities/ResourceType.java | 1 + .../com/truelayer/java/EnvironmentTests.java | 3 +- .../HostedPaymentPageLinkBuilderTests.java | 36 +++++++++++++++++-- .../java/com/truelayer/java/TestUtils.java | 2 +- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/truelayer/java/Environment.java b/src/main/java/com/truelayer/java/Environment.java index d3c22e2c5..c393e101f 100644 --- a/src/main/java/com/truelayer/java/Environment.java +++ b/src/main/java/com/truelayer/java/Environment.java @@ -14,6 +14,8 @@ public class Environment { private final URI hppUri; + private final URI hp2Uri; + private static final String PAYMENTS_API_DEFAULT_VERSION = "v3"; /** @@ -24,7 +26,8 @@ public static Environment development() { return new Environment( URI.create("https://auth.t7r.dev"), URI.create(MessageFormat.format("https://api.t7r.dev/{0}/", PAYMENTS_API_DEFAULT_VERSION)), - URI.create("https://payment.t7r.dev")); + URI.create("https://payment.t7r.dev"), + URI.create("https://app.t7r.dev")); } /** @@ -36,7 +39,8 @@ public static Environment sandbox() { URI.create("https://auth.truelayer-sandbox.com"), URI.create( MessageFormat.format("https://api.truelayer-sandbox.com/{0}/", PAYMENTS_API_DEFAULT_VERSION)), - URI.create("https://payment.truelayer-sandbox.com")); + URI.create("https://payment.truelayer-sandbox.com"), + URI.create("https://app.truelayer-sandbox.com")); } /** @@ -47,7 +51,8 @@ public static Environment live() { return new Environment( URI.create("https://auth.truelayer.com"), URI.create(MessageFormat.format("https://api.truelayer.com/{0}/", PAYMENTS_API_DEFAULT_VERSION)), - URI.create("https://payment.truelayer.com")); + URI.create("https://payment.truelayer.com"), + URI.create("https://app.truelayer.com")); } /** @@ -55,9 +60,10 @@ public static Environment live() { * @param authApiUri the authentication API endpoint * @param paymentsApiUri the Payments API endpoint * @param hppUri the Hosted Payment Page endpoint + * @param hp2Uri the new Hosted Payment Page endpoint * @return a custom environment object */ - public static Environment custom(URI authApiUri, URI paymentsApiUri, URI hppUri) { - return new Environment(authApiUri, paymentsApiUri, hppUri); + public static Environment custom(URI authApiUri, URI paymentsApiUri, URI hppUri, URI hp2Uri) { + return new Environment(authApiUri, paymentsApiUri, hppUri, hp2Uri); } } diff --git a/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java b/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java index c0b3efb28..8fa5332d2 100644 --- a/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java +++ b/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java @@ -64,7 +64,7 @@ public URI build() { URI hppLink = URI.create(MessageFormat.format( "{0}/{1}#{2}={3}&resource_token={4}&return_uri={5}", - environment.getHppUri(), + getHppLinkForResourceType(resourceType, environment), resourceType.getHppLinkPath(), resourceType.getHppLinkQueryParameter(), resourceId, @@ -81,4 +81,8 @@ public URI build() { return hppLink; } + + private URI getHppLinkForResourceType(ResourceType resourceType, Environment environment) { + return resourceType == ResourceType.PAYOUT ? environment.getHp2Uri() : environment.getHppUri(); + } } diff --git a/src/main/java/com/truelayer/java/entities/ResourceType.java b/src/main/java/com/truelayer/java/entities/ResourceType.java index 626017c8c..fd2ff0119 100644 --- a/src/main/java/com/truelayer/java/entities/ResourceType.java +++ b/src/main/java/com/truelayer/java/entities/ResourceType.java @@ -8,6 +8,7 @@ public enum ResourceType { PAYMENT("payments", "payment_id"), MANDATE("mandates", "mandate_id"), + PAYOUT("payouts", "payout_id"), ; private final String hppLinkPath; diff --git a/src/test/java/com/truelayer/java/EnvironmentTests.java b/src/test/java/com/truelayer/java/EnvironmentTests.java index 88c67f3d5..bc5cc4fd7 100644 --- a/src/test/java/com/truelayer/java/EnvironmentTests.java +++ b/src/test/java/com/truelayer/java/EnvironmentTests.java @@ -50,7 +50,8 @@ public void shouldCreateACustomEnvironment() { URI customAuthUri = URI.create("http://localhost/auth"); URI customPaymentsUri = URI.create("http://localhost/pay"); URI customHppUri = URI.create("http://localhost/hpp"); - Environment environment = Environment.custom(customAuthUri, customPaymentsUri, customHppUri); + URI customHp2Uri = URI.create("http://localhost/hp2"); + Environment environment = Environment.custom(customAuthUri, customPaymentsUri, customHppUri, customHp2Uri); assertEquals(customAuthUri, environment.getAuthApiUri()); assertEquals(customPaymentsUri, environment.getPaymentsApiUri()); diff --git a/src/test/java/com/truelayer/java/HostedPaymentPageLinkBuilderTests.java b/src/test/java/com/truelayer/java/HostedPaymentPageLinkBuilderTests.java index 092946634..faf04bd32 100644 --- a/src/test/java/com/truelayer/java/HostedPaymentPageLinkBuilderTests.java +++ b/src/test/java/com/truelayer/java/HostedPaymentPageLinkBuilderTests.java @@ -64,8 +64,10 @@ public void itShouldYieldAPaymentsHppLinkByDefault() { } @ParameterizedTest - @DisplayName("It should yield an HPP link for a resource of type") - @EnumSource(ResourceType.class) + @DisplayName("It should yield an HPP link for payments and mandates") + @EnumSource( + value = ResourceType.class, + names = {"PAYMENT", "MANDATE"}) public void itShouldYieldAnHppLink(ResourceType resourceType) { Environment environment = Environment.live(); String resourceId = UUID.randomUUID().toString(); @@ -91,6 +93,36 @@ public void itShouldYieldAnHppLink(ResourceType resourceType) { uri.toString()); } + @ParameterizedTest + @DisplayName("It should yield an HP2 link for payouts") + @EnumSource( + value = ResourceType.class, + names = {"PAYOUT"}) + public void itShouldYieldAnHp2Link(ResourceType resourceType) { + Environment environment = Environment.live(); + String resourceId = UUID.randomUUID().toString(); + String resourceToken = UUID.randomUUID().toString(); + URI returnUri = URI.create("https://example.com"); + + URI uri = new HostedPaymentPageLinkBuilder(environment) + .resourceType(resourceType) + .resourceId(resourceId) + .resourceToken(resourceToken) + .returnUri(returnUri) + .build(); + + assertEquals( + MessageFormat.format( + "{0}/{1}#{2}={3}&resource_token={4}&return_uri={5}", + environment.getHp2Uri().toString(), + resourceType.getHppLinkPath(), + resourceType.getHppLinkQueryParameter(), + resourceId, + resourceToken, + returnUri), + uri.toString()); + } + @Test @DisplayName("It should yield an HPP link with extra optional parameters") public void itShouldYieldAPaymentsHppLinkWithExtraOptionalParameters() { diff --git a/src/test/java/com/truelayer/java/TestUtils.java b/src/test/java/com/truelayer/java/TestUtils.java index 1d1dac856..0cefcb4b6 100644 --- a/src/test/java/com/truelayer/java/TestUtils.java +++ b/src/test/java/com/truelayer/java/TestUtils.java @@ -69,7 +69,7 @@ public static VersionInfo getVersionInfo() { } public static Environment getTestEnvironment(URI endpointUrl) { - return Environment.custom(endpointUrl, endpointUrl, endpointUrl); + return Environment.custom(endpointUrl, endpointUrl, endpointUrl, endpointUrl); } private TestUtils() {} From 66d1df155f419c50c801b6d94089b97bf46beecc Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Wed, 22 Oct 2025 20:28:31 +0200 Subject: [PATCH 05/10] Add acceptance tests for verified payouts --- .../LocalDateFlexibleDeserializer.java | 33 +++++++ .../TransactionSearchCriteria.java | 8 +- .../java/acceptance/AcceptanceTests.java | 12 +++ .../acceptance/PaymentsAcceptanceTests.java | 7 -- .../acceptance/PayoutsAcceptanceTests.java | 97 ++++++++++++++++++- .../LocalDateFlexibleDeserializerTests.java | 76 +++++++++++++++ 6 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializer.java create mode 100644 src/test/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializerTests.java diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializer.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializer.java new file mode 100644 index 000000000..6de4cb18b --- /dev/null +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializer.java @@ -0,0 +1,33 @@ +package com.truelayer.java.payouts.entities.beneficiary; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +public class LocalDateFlexibleDeserializer extends JsonDeserializer { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter TIMESTAMP_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); + + @Override + public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + String dateString = p.getText(); + + // Try to parse as date first (yyyy-MM-dd) + try { + return LocalDate.parse(dateString, DATE_FORMATTER); + } catch (DateTimeParseException e) { + // If that fails, try to parse as timestamp and extract the date part + try { + return LocalDate.parse(dateString, TIMESTAMP_FORMATTER); + } catch (DateTimeParseException ex) { + // If both fail, return epoch date as default, as this is not critical + return LocalDate.ofEpochDay(0); + } + } + } +} diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java index b99eae3ed..c33fca63c 100644 --- a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java @@ -1,7 +1,9 @@ package com.truelayer.java.payouts.entities.beneficiary; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.truelayer.java.entities.CurrencyCode; -import java.time.ZonedDateTime; +import java.time.LocalDate; import java.util.List; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -19,5 +21,7 @@ public class TransactionSearchCriteria { private CurrencyCode currency; - private ZonedDateTime createdAt; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + @JsonDeserialize(using = LocalDateFlexibleDeserializer.class) + private LocalDate createdAt; } diff --git a/src/test/java/com/truelayer/java/acceptance/AcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/AcceptanceTests.java index faf8864bf..25f7319dd 100644 --- a/src/test/java/com/truelayer/java/acceptance/AcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/AcceptanceTests.java @@ -1,17 +1,22 @@ package com.truelayer.java.acceptance; import static com.truelayer.java.TestUtils.assertNotError; +import static com.truelayer.java.TestUtils.getHttpClientInstance; +import static org.junit.jupiter.api.Assertions.assertTrue; 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.net.URI; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import lombok.SneakyThrows; import lombok.Synchronized; +import okhttp3.Request; +import okhttp3.Response; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; @@ -65,4 +70,11 @@ protected MerchantAccount getMerchantAccount(CurrencyCode currencyCode) { return merchantAccount; } + + @SneakyThrows + protected void assertCanBrowseLink(URI link) { + Request hppRequest = new Request.Builder().url(link.toURL()).build(); + Response hppResponse = getHttpClientInstance().newCall(hppRequest).execute(); + assertTrue(hppResponse.isSuccessful()); + } } diff --git a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java index 8743ad82b..6ae14c409 100644 --- a/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java @@ -774,13 +774,6 @@ private CreatePaymentRequest buildPaymentRequest(CurrencyCode currencyCode) { return buildPaymentRequestWithProviderSelection(userSelectionProvider, currencyCode); } - @SneakyThrows - private void assertCanBrowseLink(URI link) { - Request hppRequest = new Request.Builder().url(link.toURL()).build(); - Response hppResponse = getHttpClientInstance().newCall(hppRequest).execute(); - assertTrue(hppResponse.isSuccessful()); - } - // since the StartAuthorizationFlowRequest object does not support retry property // we need to do a raw HTTP call with a JSON string request body @SneakyThrows diff --git a/src/test/java/com/truelayer/java/acceptance/PayoutsAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/PayoutsAcceptanceTests.java index c904d15b3..fd6a23ec3 100644 --- a/src/test/java/com/truelayer/java/acceptance/PayoutsAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/PayoutsAcceptanceTests.java @@ -4,21 +4,31 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import com.truelayer.java.entities.CurrencyCode; +import com.truelayer.java.entities.ResourceType; +import com.truelayer.java.entities.User; import com.truelayer.java.http.entities.ApiResponse; import com.truelayer.java.merchantaccounts.entities.MerchantAccount; +import com.truelayer.java.payments.entities.CountryCode; import com.truelayer.java.payouts.entities.CreatePayoutRequest; import com.truelayer.java.payouts.entities.CreatePayoutResponse; import com.truelayer.java.payouts.entities.Payout; import com.truelayer.java.payouts.entities.accountidentifier.AccountIdentifier; import com.truelayer.java.payouts.entities.beneficiary.Beneficiary; +import com.truelayer.java.payouts.entities.beneficiary.TransactionSearchCriteria; +import com.truelayer.java.payouts.entities.beneficiary.Verification; +import com.truelayer.java.payouts.entities.beneficiary.providerselection.ProviderSelection; import com.truelayer.java.payouts.entities.schemeselection.SchemeSelection; import com.truelayer.java.payouts.entities.submerchants.BusinessClient; import com.truelayer.java.payouts.entities.submerchants.SubMerchants; +import java.net.URI; +import java.time.LocalDate; +import java.util.Arrays; import java.util.Collections; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Stream; import lombok.SneakyThrows; 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; @@ -26,10 +36,12 @@ @Tag("acceptance") public class PayoutsAcceptanceTests extends AcceptanceTests { - @ParameterizedTest(name = "It should create a {0} payout and get the details") - @MethodSource("providePayoutsArgumentsForDifferentCurrencies") + public static final String RETURN_URI = "http://localhost:3000/callback"; + + @ParameterizedTest(name = "It should create a standard {0} payout and get the details") + @MethodSource("provideStandardPayoutsArgumentsForDifferentCurrencies") @SneakyThrows - public void shouldCreateAPayoutAndGetPayoutsDetails( + public void shouldCreateAStandardPayoutAndGetDetails( CurrencyCode currencyCode, AccountIdentifier accountIdentifier, SchemeSelection schemeSelection) { // find a merchant to execute the payout from MerchantAccount merchantAccount = getMerchantAccount(currencyCode); @@ -70,7 +82,84 @@ public void shouldCreateAPayoutAndGetPayoutsDetails( assertEquals(merchantAccount.getId(), getPayoutResponse.getData().getMerchantAccountId()); } - public static Stream providePayoutsArgumentsForDifferentCurrencies() { + @Test + @SneakyThrows + public void shouldCreateAVerifiedPayoutGetDetailsAndOpenHp2Link() { + // find a merchant to execute the payout from + MerchantAccount merchantAccount = getMerchantAccount(CurrencyCode.GBP); + + int amountInMinor = ThreadLocalRandom.current().nextInt(10, 100); + + LocalDate transactionCreatedAt = LocalDate.now().minusDays(7); + + // create the verified payout with verification including verify name and transaction criteria + CreatePayoutRequest createPayoutRequest = CreatePayoutRequest.builder() + .merchantAccountId(merchantAccount.getId()) + .amountInMinor(amountInMinor) + .currency(CurrencyCode.GBP) + .beneficiary(Beneficiary.userDetermined() + .reference("java-lib-test") + .user(User.builder() + .name("John Doe") + .email("john.doe@example.com") + .build()) + .verification(Verification.builder() + .verifyName(true) + .transactionSearchCriteria(TransactionSearchCriteria.builder() + .tokens(Arrays.asList("test-token-1", "test-token-2")) + .amountInMinor(amountInMinor) + .currency(CurrencyCode.GBP) + .createdAt(transactionCreatedAt) + .build()) + .build()) + .providerSelection(ProviderSelection.userSelected() + .filter(com.truelayer.java.entities.ProviderFilter.builder() + .countries(Collections.singletonList(CountryCode.GB)) + .build()) + .build()) + .build()) + .metadata(Collections.singletonMap("test_type", "verified_payout")) + .build(); + + ApiResponse createPayoutResponse = + tlClient.payouts().createPayout(createPayoutRequest).get(); + + assertNotError(createPayoutResponse); + + CreatePayoutResponse response = createPayoutResponse.getData(); + assertEquals(true, response.isAuthorizationRequired()); + + String payoutId = response.getId(); + String resourceToken = response.asAuthorizationRequired().getResourceToken(); + + // get payout details + ApiResponse getPayoutResponse = + tlClient.payouts().getPayout(payoutId).get(); + + assertNotError(getPayoutResponse); + assertEquals(payoutId, getPayoutResponse.getData().getId()); + assertEquals(merchantAccount.getId(), getPayoutResponse.getData().getMerchantAccountId()); + assertEquals( + transactionCreatedAt, + getPayoutResponse + .getData() + .asAuthorizationRequiredPayout() + .getBeneficiary() + .asUserDetermined() + .getVerification() + .getTransactionSearchCriteria() + .getCreatedAt()); + + URI hp2Uri = tlClient.hppLinkBuilder() + .resourceType(ResourceType.PAYOUT) + .resourceId(payoutId) + .resourceToken(resourceToken) + .returnUri(URI.create(RETURN_URI)) + .build(); + assertCanBrowseLink(hp2Uri); + } + + public static Stream provideStandardPayoutsArgumentsForDifferentCurrencies() { return Stream.of( Arguments.of( CurrencyCode.GBP, diff --git a/src/test/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializerTests.java b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializerTests.java new file mode 100644 index 000000000..c6cf33b16 --- /dev/null +++ b/src/test/java/com/truelayer/java/payouts/entities/beneficiary/LocalDateFlexibleDeserializerTests.java @@ -0,0 +1,76 @@ +package com.truelayer.java.payouts.entities.beneficiary; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.time.LocalDate; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class LocalDateFlexibleDeserializerTests { + + private final LocalDateFlexibleDeserializer deserializer = new LocalDateFlexibleDeserializer(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + @DisplayName("It should deserialize a date in yyyy-MM-dd format") + public void shouldDeserializeDateFormat() throws IOException { + String json = "\"2025-10-15\""; + JsonParser parser = objectMapper.getFactory().createParser(json); + parser.nextToken(); + + LocalDate result = deserializer.deserialize(parser, null); + + assertEquals(LocalDate.of(2025, 10, 15), result); + } + + @Test + @DisplayName("It should deserialize a timestamp in yyyy-MM-dd'T'HH:mm:ssXXX format") + public void shouldDeserializeTimestampFormat() throws IOException { + String json = "\"2025-10-15T00:00:00+00:00\""; + JsonParser parser = objectMapper.getFactory().createParser(json); + parser.nextToken(); + + LocalDate result = deserializer.deserialize(parser, null); + + assertEquals(LocalDate.of(2025, 10, 15), result); + } + + @Test + @DisplayName("It should deserialize a timestamp with different timezone") + public void shouldDeserializeTimestampWithTimezone() throws IOException { + String json = "\"2025-10-15T14:30:00+01:00\""; + JsonParser parser = objectMapper.getFactory().createParser(json); + parser.nextToken(); + + LocalDate result = deserializer.deserialize(parser, null); + + assertEquals(LocalDate.of(2025, 10, 15), result); + } + + @Test + @DisplayName("It should return epoch date when given an invalid format") + public void shouldReturnEpochDateForInvalidFormat() throws IOException { + String json = "\"invalid-date-string\""; + JsonParser parser = objectMapper.getFactory().createParser(json); + parser.nextToken(); + + LocalDate result = deserializer.deserialize(parser, null); + + assertEquals(LocalDate.ofEpochDay(0), result); + } + + @Test + @DisplayName("It should return epoch date when given an empty string") + public void shouldReturnEpochDateForEmptyString() throws IOException { + String json = "\"\""; + JsonParser parser = objectMapper.getFactory().createParser(json); + parser.nextToken(); + + LocalDate result = deserializer.deserialize(parser, null); + + assertEquals(LocalDate.ofEpochDay(0), result); + } +} From 54d146dc1b5976f9b78412bf970b29f8b17be7a0 Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Thu, 23 Oct 2025 09:36:47 +0200 Subject: [PATCH 06/10] refactor --- .../truelayer/java/HostedPaymentPageLinkBuilder.java | 12 +++++++++++- .../com/truelayer/java/entities/HostedPageType.java | 6 ++++++ .../com/truelayer/java/entities/ResourceType.java | 10 +++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/truelayer/java/entities/HostedPageType.java diff --git a/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java b/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java index 8fa5332d2..e84a77ba6 100644 --- a/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java +++ b/src/main/java/com/truelayer/java/HostedPaymentPageLinkBuilder.java @@ -83,6 +83,16 @@ public URI build() { } private URI getHppLinkForResourceType(ResourceType resourceType, Environment environment) { - return resourceType == ResourceType.PAYOUT ? environment.getHp2Uri() : environment.getHppUri(); + URI hostedPageUri; + switch (resourceType.getHostedPageType()) { + case HP2: + hostedPageUri = environment.getHp2Uri(); + break; + case HPP: + default: + hostedPageUri = environment.getHppUri(); + break; + } + return hostedPageUri; } } diff --git a/src/main/java/com/truelayer/java/entities/HostedPageType.java b/src/main/java/com/truelayer/java/entities/HostedPageType.java new file mode 100644 index 000000000..397545b51 --- /dev/null +++ b/src/main/java/com/truelayer/java/entities/HostedPageType.java @@ -0,0 +1,6 @@ +package com.truelayer.java.entities; + +public enum HostedPageType { + HPP, + HP2 +} diff --git a/src/main/java/com/truelayer/java/entities/ResourceType.java b/src/main/java/com/truelayer/java/entities/ResourceType.java index fd2ff0119..7117272b1 100644 --- a/src/main/java/com/truelayer/java/entities/ResourceType.java +++ b/src/main/java/com/truelayer/java/entities/ResourceType.java @@ -1,16 +1,20 @@ package com.truelayer.java.entities; +import static com.truelayer.java.entities.HostedPageType.HP2; +import static com.truelayer.java.entities.HostedPageType.HPP; + import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Getter public enum ResourceType { - PAYMENT("payments", "payment_id"), - MANDATE("mandates", "mandate_id"), - PAYOUT("payouts", "payout_id"), + PAYMENT("payments", "payment_id", HPP), + MANDATE("mandates", "mandate_id", HPP), + PAYOUT("payouts", "payout_id", HP2), ; private final String hppLinkPath; private final String hppLinkQueryParameter; + private final HostedPageType hostedPageType; } From 7a11703f710496cd2c1c377d99d9e2e122749c33 Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Thu, 23 Oct 2025 11:40:43 +0200 Subject: [PATCH 07/10] version change --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index db67393a2..989d9ea82 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Main properties group=com.truelayer archivesBaseName=truelayer-java -version=17.4.0 +version=18.0.0 # Artifacts properties project_name=TrueLayer Java From 5db9a1b5067503f0abb89aeb3bf51ec8e33f5dda Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Thu, 23 Oct 2025 12:34:24 +0200 Subject: [PATCH 08/10] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d38f73495..e36528ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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/). +## [18.0.0] +### Added +* Add support for Verified Payouts with user-determined beneficiary type +* Add HPP link builder support for payouts + +### Changed +* ⚠️ Breaking: `CreatePayoutResponse` changed from concrete class to abstract class with polymorphic subtypes + ## [17.4.0] - 2025-08-07 ### Added * Add support for `scheme_id` field in merchant account transaction responses for Payout and Refund transactions From 738218a3d9a9ea1807a6b2d928c1ff1737be2420 Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Thu, 23 Oct 2025 17:15:49 +0200 Subject: [PATCH 09/10] Change version to 17.5.0 (non-breaking) --- CHANGELOG.md | 5 +---- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e36528ac3..9da341963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,11 @@ 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/). -## [18.0.0] +## [17.5.0] ### Added * Add support for Verified Payouts with user-determined beneficiary type * Add HPP link builder support for payouts -### Changed -* ⚠️ Breaking: `CreatePayoutResponse` changed from concrete class to abstract class with polymorphic subtypes - ## [17.4.0] - 2025-08-07 ### Added * Add support for `scheme_id` field in merchant account transaction responses for Payout and Refund transactions diff --git a/gradle.properties b/gradle.properties index 989d9ea82..06c8870b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Main properties group=com.truelayer archivesBaseName=truelayer-java -version=18.0.0 +version=17.5.0 # Artifacts properties project_name=TrueLayer Java From 4cce977349faf0b0924da81ed57362d320466ea3 Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Fri, 24 Oct 2025 15:20:27 +0200 Subject: [PATCH 10/10] primitive type used --- .../payouts/entities/beneficiary/TransactionSearchCriteria.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java index c33fca63c..a9e1b7bb3 100644 --- a/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java +++ b/src/main/java/com/truelayer/java/payouts/entities/beneficiary/TransactionSearchCriteria.java @@ -17,7 +17,7 @@ public class TransactionSearchCriteria { private List tokens; - private Integer amountInMinor; + private int amountInMinor; private CurrencyCode currency;