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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +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/).

## [17.5.0]
### Added
* Add support for Verified Payouts with user-determined beneficiary type
* Add HPP link builder support for payouts

## [17.4.0] - 2025-08-07
### Added
* Add support for `scheme_id` field in merchant account transaction responses for Payout and Refund transactions
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.4.0
version=17.5.0

# Artifacts properties
project_name=TrueLayer Java
Expand Down
16 changes: 11 additions & 5 deletions src/main/java/com/truelayer/java/Environment.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class Environment {

private final URI hppUri;

private final URI hp2Uri;

private static final String PAYMENTS_API_DEFAULT_VERSION = "v3";

/**
Expand All @@ -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"));
}

/**
Expand All @@ -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"));
}

/**
Expand All @@ -47,17 +51,19 @@ 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"));
}

/**
* Custom environment builder. Meant for testing purposes
* @param authApiUri the authentication API endpoint
* @param paymentsApiUri the Payments API endpoint
* @param hppUri the <i>Hosted Payment Page</i> endpoint
* @param hp2Uri the new <i>Hosted Payment Page</i> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -81,4 +81,18 @@ public URI build() {

return hppLink;
}

private URI getHppLinkForResourceType(ResourceType resourceType, Environment environment) {
URI hostedPageUri;
switch (resourceType.getHostedPageType()) {
case HP2:
hostedPageUri = environment.getHp2Uri();
break;
case HPP:
default:
hostedPageUri = environment.getHppUri();
break;
}
return hostedPageUri;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.truelayer.java.entities;

public enum HostedPageType {
HPP,
HP2
}
9 changes: 7 additions & 2 deletions src/main/java/com/truelayer/java/entities/ResourceType.java
Original file line number Diff line number Diff line change
@@ -1,15 +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"),
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand All @@ -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());
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AccountIdentifier> accountIdentifiers;

PayoutUser user;

Verification verification;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Copy link
Contributor Author

@dili91 dili91 Oct 23, 2025

Choose a reason for hiding this comment

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

I'm not considering this a breaking change, because:

  • library users are not expected to instantiate CreatePayoutResponse objects their own
  • the getter for the id field is left untouched

Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
27 changes: 27 additions & 0 deletions src/main/java/com/truelayer/java/payouts/entities/Payout.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -29,10 +31,21 @@ public abstract class Payout {
private Map<String, String> 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;
Expand All @@ -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());
Expand Down Expand Up @@ -84,6 +109,8 @@ private String buildErrorMessage() {
@Getter
@RequiredArgsConstructor
public enum Status {
AUTHORIZATION_REQUIRED("authorization_required"),
AUTHORIZING("authorizing"),
PENDING("pending"),
AUTHORIZED("authorized"),
EXECUTED("executed"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Loading