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.6.0] - 2026-05-15
### Added
* Add support for `user_interaction` and `use_cases` fields in payment API for commercial mandates

## [17.5.1] - 2025-10-31
### Fixed
* Update Sonatype Central badge url in order to show latest version
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.5.1
version=17.6.0

# Artifacts properties
project_name=TrueLayer Java
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/truelayer/java/entities/UseCase.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.truelayer.java.entities;

import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum UseCase {
CHARITY("charity"),
FINANCIAL_SERVICES("financial_services"),
GOVERNMENT("government"),
UTILITIES("utilities");

@JsonValue
private final String useCase;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.truelayer.java.mandates.entities.mandate.Mandate.Type.COMMERCIAL;

import com.truelayer.java.entities.UseCase;
import com.truelayer.java.mandates.entities.beneficiary.Beneficiary;
import com.truelayer.java.payments.entities.providerselection.ProviderSelection;
import lombok.Builder;
Expand All @@ -20,4 +21,6 @@ public class VRPCommercialMandate extends Mandate {
private Beneficiary beneficiary;

private String reference;

private UseCase useCase;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.truelayer.java.TrueLayerException;
import com.truelayer.java.commonapi.entities.UserDetail;
import com.truelayer.java.entities.CurrencyCode;
import com.truelayer.java.entities.UseCase;
import com.truelayer.java.mandates.entities.Constraints;
import com.truelayer.java.mandates.entities.beneficiary.Beneficiary;
import com.truelayer.java.payments.entities.providerselection.ProviderSelection;
Expand Down Expand Up @@ -40,6 +41,10 @@ public abstract class MandateDetail {

private ProviderSelection providerSelection;

private UseCase useCase;

private String type;

Comment thread
tl-tai-tang marked this conversation as resolved.
public abstract Status getStatus();

public AuthorizationRequiredMandateDetail asAuthorizationRequiredMandateDetail() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.truelayer.java.payments.entities;

import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Getter
public enum UserInteraction {
PRESENT("present"),
NOT_PRESENT("not_present");

@JsonValue
private final String userInteraction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.truelayer.java.payments.entities.paymentmethod.PaymentMethod.Type.MANDATE;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.truelayer.java.payments.entities.UserInteraction;
import com.truelayer.java.payments.entities.retry.Retry;
import java.util.Optional;
import lombok.Builder;
Expand All @@ -29,4 +30,6 @@ public class Mandate extends PaymentMethod {
public Optional<String> getReference() {
return Optional.ofNullable(reference);
}

private UserInteraction userInteraction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@EqualsAndHashCode(callSuper = false)
public class BusinessClient extends UltimateCounterparty {
private final Type type = Type.BUSINESS_CLIENT;
private String id;
private String tradingName;
private String commercialName;
private String url;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.truelayer.java.paymentsproviders.entities;

import com.truelayer.java.entities.ProviderAvailability;
import com.truelayer.java.entities.UseCase;
import com.truelayer.java.payments.entities.ReleaseChannel;
import java.util.List;
import lombok.Value;

@Value
public class VrpCommercialCapabilities {
ReleaseChannel releaseChannel;

ProviderAvailability availability;

List<UseCase> useCases;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.truelayer.java.paymentsproviders.entities.searchproviders;

import com.truelayer.java.entities.UseCase;
import java.util.List;
import lombok.Value;

@Value
public class VrpCommercialCapabilities {}
public class VrpCommercialCapabilities {
List<UseCase> useCases;
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ public void itShouldCreateASweepingMandateWithSignupPlusIntention() {
ProviderSelection.preselected().providerId(PROVIDER_ID).build(),
RelatedProducts.builder()
.signupPlus(Collections.emptyMap())
.build()))
.build(),
null))
.get();

assertNotError(createMandateResponse);
Expand Down Expand Up @@ -309,15 +310,20 @@ public void itShouldCreateAndRevokeAMandate(String mandatesScope, Mandate.Type m
@ParameterizedTest(name = "with retry {0}")
@MethodSource("provideItShouldCreateAPaymentOnMandateTestParameters")
@SneakyThrows
public void itShouldCreateAPaymentOnMandate(String mandatesScope, Mandate.Type mandateType, Retry retry) {
public void itShouldCreateAPaymentOnMandate(
String mandatesScope,
Mandate.Type mandateType,
Retry retry,
UseCase useCase,
UserInteraction userInteraction) {
// create client with required scopes
var tlClient = buildMandatesTlClient(mandatesScope);

// create mandate
ProviderSelection preselectedProvider =
ProviderSelection.preselected().providerId(PROVIDER_ID).build();
ApiResponse<CreateMandateResponse> createMandateResponse = tlClient.mandates()
.createMandate(createMandateRequest(mandateType, preselectedProvider))
.createMandate(createMandateRequest(mandateType, preselectedProvider, null, useCase))
.get();
assertNotError(createMandateResponse);

Expand Down Expand Up @@ -375,6 +381,10 @@ public void itShouldCreateAPaymentOnMandate(String mandatesScope, Mandate.Type m
mandateBuilder.retry(retry);
}

if (ObjectUtils.isNotEmpty(userInteraction)) {
mandateBuilder.userInteraction(userInteraction);
}

// create a payment on mandate
CreatePaymentRequest createPaymentRequest = CreatePaymentRequest.builder()
.amountInMinor(getMandateResponse.getData().getConstraints().getMaximumIndividualAmount())
Expand All @@ -389,11 +399,11 @@ public void itShouldCreateAPaymentOnMandate(String mandatesScope, Mandate.Type m
}

private CreateMandateRequest createMandateRequest(Mandate.Type type, ProviderSelection providerSelection) {
return createMandateRequest(type, providerSelection, null);
return createMandateRequest(type, providerSelection, null, null);
}

private CreateMandateRequest createMandateRequest(
Mandate.Type type, ProviderSelection providerSelection, RelatedProducts relatedProducts) {
Mandate.Type type, ProviderSelection providerSelection, RelatedProducts relatedProducts, UseCase useCase) {
Mandate mandate = null;
if (type.equals(Mandate.Type.COMMERCIAL)) {
mandate = Mandate.vrpCommercialMandate()
Expand All @@ -405,6 +415,7 @@ private CreateMandateRequest createMandateRequest(
.build())
.accountHolderName("Andrea Java SDK")
.build())
.useCase(useCase)
.build();
} else {
mandate = Mandate.vrpSweepingMandate()
Expand Down Expand Up @@ -542,30 +553,47 @@ private static TrueLayerClient buildMandatesTlClient(String mandatesScope) {

private static Stream<Arguments> provideItShouldCreateAPaymentOnMandateTestParameters() {
return Stream.of(
Arguments.of(RECURRING_PAYMENTS_SWEEPING, SWEEPING, null),
Arguments.of(RECURRING_PAYMENTS_COMMERCIAL, COMMERCIAL, null),
Arguments.of(RECURRING_PAYMENTS_SWEEPING, SWEEPING, null, null, null),
Arguments.of(RECURRING_PAYMENTS_COMMERCIAL, COMMERCIAL, null, null, null),
Arguments.of(
RECURRING_PAYMENTS_SWEEPING,
SWEEPING,
Retry.standard().forDuration("30m").build()),
Retry.standard().forDuration("30m").build(),
null,
null),
Arguments.of(
RECURRING_PAYMENTS_COMMERCIAL,
COMMERCIAL,
Retry.standard().forDuration("30m").build()),
Retry.standard().forDuration("30m").build(),
null,
null),
Arguments.of(
RECURRING_PAYMENTS_SWEEPING,
SWEEPING,
Retry.smart()
.forDuration("90d")
.ensureMinimumBalanceInMinor(100)
.build()),
.build(),
null,
null),
Arguments.of(
RECURRING_PAYMENTS_COMMERCIAL,
COMMERCIAL,
Retry.smart()
.forDuration("90d")
.ensureMinimumBalanceInMinor(100)
.build(),
null,
null),
Arguments.of(
RECURRING_PAYMENTS_COMMERCIAL,
COMMERCIAL,
Retry.smart()
.forDuration("90d")
.ensureMinimumBalanceInMinor(100)
.build()));
.build(),
UseCase.FINANCIAL_SERVICES,
UserInteraction.PRESENT));
}

private static Stream<Arguments> provideMandatesScopesAndTypes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ private static Stream<Arguments> provideSubMerchantsScenarios() {
Arguments.of(
"BusinessClient",
BusinessClient.builder()
.id("client-123")
.tradingName("Test Trading Ltd")
.commercialName("Test Commercial Name")
.url("https://example.com")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,33 @@ public void shouldGetAMandateById(Status expectedStatus) {
assertEquals(expected, response.getData());
}

@Test
@SneakyThrows
@DisplayName("It should get the commercial mandate details")
public void shouldGetACommercialMandateById() {
String jsonResponseFile = "mandates/200.get_commercial_mandate_by_id.json";
RequestStub.New()
.method("post")
.path(urlPathEqualTo("/connect/token"))
.status(200)
.bodyFile("auth/200.access_token.json")
.build();
RequestStub.New()
.method("get")
.path(urlPathMatching("/mandates/" + A_MANDATE_ID))
.withAuthorization()
.status(200)
.bodyFile(jsonResponseFile)
.build();

ApiResponse<MandateDetail> response =
tlClient.mandates().getMandate(A_MANDATE_ID).get();

verifyGeneratedToken(Collections.singletonList(RECURRING_PAYMENTS_SWEEPING));
MandateDetail expected = deserializeJsonFileTo(jsonResponseFile, MandateDetail.class);
Comment thread
tl-tai-tang marked this conversation as resolved.
assertEquals(expected, response.getData());
}
Comment thread
tl-tai-tang marked this conversation as resolved.

@SneakyThrows
@ParameterizedTest(name = "and get a response of type {0}")
@ValueSource(strings = {"provider_selection", "redirect", "wait"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ void shouldDeserializePaymentWithSubMerchants() {
assertTrue(ultimateCounterparty.isBusinessClient());

BusinessClient businessClient = ultimateCounterparty.asBusinessClient();
assertEquals("client-123", businessClient.getId());
assertEquals("Example Trading Ltd", businessClient.getTradingName());
assertEquals("Example Commercial Name", businessClient.getCommercialName());
assertEquals("https://example.com", businessClient.getUrl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ void canCreateBusinessClient() {
.build();

BusinessClient businessClient = UltimateCounterparty.businessClient()
.id("client-123")
.tradingName("Test Trading Name")
.commercialName("Test Commercial Name")
.url("https://example.com")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"status": "authorized",
"id": "string",
"type": "commercial",
"use_case": "financial_services",
"authorization_flow":{
"actions":{
"next":{
"type":"provider_selection",
"providers":[
{
"provider_id":"ob-bank-name",
"display_name":"Bank Name",
"icon_uri":"https://truelayer-provider-assets.s3.amazonaws.com/global/icon/generic.svg",
"logo_uri":"https://truelayer-provider-assets.s3.amazonaws.com/global/logos/generic.svg",
"bg_color":"#000000",
"country_code":"GB"
}
]
}
}
},
"currency": "GBP",
"beneficiary": {
"type": "merchant_account",
"id": "string",
"account_holder_name": "string"
},
"reference": "a-mandate-ref",
"provider_selection":{
"type":"preselected",
"provider_id": "a-provider-id",
"remitter": {
"account_holder_name": "Andrea Di Lisio",
"account_identifier": {
"type":"sort_code_account_number",
"sort_code":"040662",
"account_number":"00002723"
}
}
},
"user": {
"id": "f9b48c9d-176b-46dd-b2da-fe1a2b77350c"
},
"created_at": "2022-01-17T17:13:18.214924Z",
"authorized_at": "2022-01-17T17:13:18.214924Z",
"constraints": {
"valid_from": "2022-01-17T17:13:18.214924Z",
"valid_to": "2023-01-17T17:13:18.214924Z",
"maximum_individual_amount": 1,
"periodic_limits": {
"day": {
"maximum_amount": 0,
"period_alignment": "consent"
},
"week": {
"maximum_amount": 0,
"period_alignment": "consent"
}
}
},
"metadata": {
"a_custom_key": "a-value"
},
"remitter": {
"account_holder_name": "Andrea Di Lisio",
"account_identifier": {
"type":"iban",
"iban":"GB53CLRB04066200002723"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"status": "authorization_required",
"id": "string",
"type": "sweeping",
"currency": "GBP",
"beneficiary": {
"type": "merchant_account",
Expand Down
Loading
Loading