Skip to content

Commit 398daae

Browse files
federico1525claudedili91
authored
[ACL-290] Add sub_merchants support to Payments module (#360)
Co-authored-by: Claude <[email protected]> Co-authored-by: Andrea Di Lisio <[email protected]>
1 parent 63fa3aa commit 398daae

File tree

13 files changed

+511
-1
lines changed

13 files changed

+511
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ build
1010
#OSX
1111
.DS_Store
1212

13+
#Claude Code
14+
.claude/
15+
1316
# Claude local configuration
1417
CLAUDE.local.md

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Main properties
22
group=com.truelayer
33
archivesBaseName=truelayer-java
4-
version=17.2.0
4+
version=17.3.0
55

66
# Artifacts properties
77
sonatype_repository_url=https://s01.oss.sonatype.org/service/local/

src/main/java/com/truelayer/java/payments/entities/CreatePaymentRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.truelayer.java.entities.RelatedProducts;
55
import com.truelayer.java.entities.User;
66
import com.truelayer.java.payments.entities.paymentmethod.PaymentMethod;
7+
import com.truelayer.java.payments.entities.submerchants.SubMerchants;
78
import java.util.Map;
89
import lombok.Builder;
910
import lombok.EqualsAndHashCode;
@@ -34,4 +35,9 @@ public class CreatePaymentRequest {
3435
* Optional field for configuring risk assessment and the payment_creditable webhook
3536
*/
3637
private RiskAssessment riskAssessment;
38+
39+
/**
40+
* Optional field for sub-merchant details
41+
*/
42+
private SubMerchants subMerchants;
3743
}

src/main/java/com/truelayer/java/payments/entities/paymentdetail/PaymentDetail.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.truelayer.java.commonapi.entities.UserDetail;
88
import com.truelayer.java.entities.CurrencyCode;
99
import com.truelayer.java.payments.entities.paymentmethod.PaymentMethod;
10+
import com.truelayer.java.payments.entities.submerchants.SubMerchants;
1011
import java.time.ZonedDateTime;
1112
import java.util.Map;
1213
import lombok.*;
@@ -37,6 +38,11 @@ public abstract class PaymentDetail {
3738

3839
private Map<String, String> metadata;
3940

41+
/**
42+
* Optional sub-merchants information for payment processing.
43+
*/
44+
private SubMerchants subMerchants;
45+
4046
@JsonIgnore
4147
public abstract Status getStatus();
4248

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.truelayer.java.payments.entities.submerchants;
2+
3+
import com.truelayer.java.entities.Address;
4+
import lombok.Builder;
5+
import lombok.EqualsAndHashCode;
6+
import lombok.Getter;
7+
import lombok.ToString;
8+
9+
@Builder
10+
@Getter
11+
@ToString
12+
@EqualsAndHashCode(callSuper = false)
13+
public class BusinessClient extends UltimateCounterparty {
14+
private final Type type = Type.BUSINESS_CLIENT;
15+
private String tradingName;
16+
private String commercialName;
17+
private String url;
18+
private String mcc;
19+
private String registrationNumber;
20+
private Address address;
21+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.truelayer.java.payments.entities.submerchants;
2+
3+
import lombok.Builder;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Getter;
6+
import lombok.ToString;
7+
8+
@Builder
9+
@Getter
10+
@ToString
11+
@EqualsAndHashCode(callSuper = false)
12+
public class BusinessDivision extends UltimateCounterparty {
13+
private final Type type = Type.BUSINESS_DIVISION;
14+
private String id;
15+
private String name;
16+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.truelayer.java.payments.entities.submerchants;
2+
3+
import lombok.Builder;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.Getter;
6+
import lombok.ToString;
7+
8+
@Builder
9+
@Getter
10+
@ToString
11+
@EqualsAndHashCode
12+
public class SubMerchants {
13+
private UltimateCounterparty ultimateCounterparty;
14+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.truelayer.java.payments.entities.submerchants;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
import com.fasterxml.jackson.annotation.JsonSubTypes;
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6+
import com.fasterxml.jackson.annotation.JsonValue;
7+
import com.truelayer.java.TrueLayerException;
8+
import lombok.Getter;
9+
import lombok.RequiredArgsConstructor;
10+
11+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = BusinessClient.class)
12+
@JsonSubTypes({
13+
@JsonSubTypes.Type(value = BusinessClient.class, name = "business_client"),
14+
@JsonSubTypes.Type(value = BusinessDivision.class, name = "business_division")
15+
})
16+
public abstract class UltimateCounterparty {
17+
18+
@JsonIgnore
19+
public abstract Type getType();
20+
21+
@JsonIgnore
22+
public boolean isBusinessClient() {
23+
return this instanceof BusinessClient;
24+
}
25+
26+
@JsonIgnore
27+
public boolean isBusinessDivision() {
28+
return this instanceof BusinessDivision;
29+
}
30+
31+
@JsonIgnore
32+
public BusinessClient asBusinessClient() {
33+
if (!isBusinessClient()) throw new TrueLayerException(buildErrorMessage());
34+
return (BusinessClient) this;
35+
}
36+
37+
@JsonIgnore
38+
public BusinessDivision asBusinessDivision() {
39+
if (!isBusinessDivision()) throw new TrueLayerException(buildErrorMessage());
40+
return (BusinessDivision) this;
41+
}
42+
43+
public static BusinessClient.BusinessClientBuilder businessClient() {
44+
return new BusinessClient.BusinessClientBuilder();
45+
}
46+
47+
public static BusinessDivision.BusinessDivisionBuilder businessDivision() {
48+
return new BusinessDivision.BusinessDivisionBuilder();
49+
}
50+
51+
private String buildErrorMessage() {
52+
return String.format(
53+
"UltimateCounterparty is of type %s.", this.getClass().getSimpleName());
54+
}
55+
56+
@Getter
57+
@RequiredArgsConstructor
58+
public enum Type {
59+
BUSINESS_CLIENT("business_client"),
60+
BUSINESS_DIVISION("business_division");
61+
62+
@JsonValue
63+
private final String type;
64+
}
65+
}

src/test/java/com/truelayer/java/acceptance/PaymentsAcceptanceTests.java

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
import com.truelayer.java.payments.entities.providerselection.ProviderSelection;
3535
import com.truelayer.java.payments.entities.providerselection.UserSelectedProviderSelection;
3636
import com.truelayer.java.payments.entities.schemeselection.preselected.SchemeSelection;
37+
import com.truelayer.java.payments.entities.submerchants.BusinessClient;
38+
import com.truelayer.java.payments.entities.submerchants.BusinessDivision;
39+
import com.truelayer.java.payments.entities.submerchants.SubMerchants;
40+
import com.truelayer.java.payments.entities.submerchants.UltimateCounterparty;
3741
import com.truelayer.java.payments.entities.verification.AutomatedVerification;
3842
import com.truelayer.java.payments.entities.verification.Verification;
3943
import com.truelayer.java.versioninfo.LibraryInfoLoader;
@@ -861,4 +865,109 @@ private static Stream<Arguments> provideAutomatedVerifications() {
861865
.withRemitterDateOfBirth()
862866
.build()));
863867
}
868+
869+
private static Stream<Arguments> provideSubMerchantsScenarios() {
870+
return Stream.of(
871+
Arguments.of(
872+
"BusinessClient",
873+
BusinessClient.builder()
874+
.tradingName("Test Trading Ltd")
875+
.commercialName("Test Commercial Name")
876+
.url("https://example.com")
877+
.mcc("5999")
878+
.registrationNumber("12345678")
879+
.address(Address.builder()
880+
.addressLine1("123 Test Street")
881+
.city("London")
882+
.state("Greater London")
883+
.zip("EC1R 4RB")
884+
.countryCode("GB")
885+
.build())
886+
.build()),
887+
Arguments.of(
888+
"BusinessDivision",
889+
BusinessDivision.builder()
890+
.id("division-123")
891+
.name("Test Division")
892+
.build()));
893+
}
894+
895+
@ParameterizedTest
896+
@MethodSource("provideSubMerchantsScenarios")
897+
@DisplayName("It should create and get a payment with sub-merchants information")
898+
@SneakyThrows
899+
public void shouldCreateAPaymentWithSubMerchants(
900+
String counterpartyType, UltimateCounterparty ultimateCounterparty) {
901+
// create payment with sub-merchants
902+
SubMerchants subMerchants = SubMerchants.builder()
903+
.ultimateCounterparty(ultimateCounterparty)
904+
.build();
905+
906+
CreatePaymentRequest paymentRequest = buildPaymentRequestWithSubMerchants(
907+
buildPreselectedProviderSelection(), CurrencyCode.GBP, subMerchants);
908+
909+
ApiResponse<CreatePaymentResponse> createPaymentResponse =
910+
tlClient.payments().createPayment(paymentRequest).get();
911+
912+
assertNotError(createPaymentResponse);
913+
assertTrue(createPaymentResponse.getData().isAuthorizationRequired());
914+
915+
// get it by id and verify sub-merchants are preserved
916+
ApiResponse<PaymentDetail> getPaymentByIdResponse = tlClient.payments()
917+
.getPayment(createPaymentResponse.getData().getId())
918+
.get();
919+
920+
assertNotError(getPaymentByIdResponse);
921+
922+
// Verify the sub-merchants information is returned correctly
923+
PaymentDetail paymentDetail = getPaymentByIdResponse.getData();
924+
assertNotNull(paymentDetail.getSubMerchants(), "Sub-merchants should not be null");
925+
assertEquals(
926+
subMerchants.getUltimateCounterparty().getType(),
927+
paymentDetail.getSubMerchants().getUltimateCounterparty().getType(),
928+
"Ultimate counterparty type should match");
929+
930+
// Verify type-specific fields
931+
if (ultimateCounterparty instanceof BusinessClient) {
932+
BusinessClient original = (BusinessClient) ultimateCounterparty;
933+
BusinessClient returned =
934+
(BusinessClient) paymentDetail.getSubMerchants().getUltimateCounterparty();
935+
assertEquals(original.getTradingName(), returned.getTradingName(), "Trading name should match");
936+
assertEquals(original.getCommercialName(), returned.getCommercialName(), "Commercial name should match");
937+
assertEquals(original.getMcc(), returned.getMcc(), "MCC should match");
938+
} else if (ultimateCounterparty instanceof BusinessDivision) {
939+
BusinessDivision original = (BusinessDivision) ultimateCounterparty;
940+
BusinessDivision returned =
941+
(BusinessDivision) paymentDetail.getSubMerchants().getUltimateCounterparty();
942+
assertEquals(original.getId(), returned.getId(), "Division ID should match");
943+
assertEquals(original.getName(), returned.getName(), "Division name should match");
944+
}
945+
}
946+
947+
@SneakyThrows
948+
private CreatePaymentRequest buildPaymentRequestWithSubMerchants(
949+
ProviderSelection providerSelection, CurrencyCode currencyCode, SubMerchants subMerchants) {
950+
return CreatePaymentRequest.builder()
951+
.amountInMinor(ThreadLocalRandom.current().nextInt(50, 500))
952+
.currency(currencyCode)
953+
.paymentMethod(PaymentMethod.bankTransfer()
954+
.providerSelection(providerSelection)
955+
.beneficiary(buildBeneficiary(currencyCode))
956+
.build())
957+
.user(User.builder()
958+
.name("John Smith")
959+
960+
.dateOfBirth(LocalDate.now())
961+
.address(Address.builder()
962+
.addressLine1("123 Main Street")
963+
.city("London")
964+
.state("Greater London")
965+
.zip("EC1R 4RB")
966+
.countryCode("GB")
967+
.build())
968+
.build())
969+
.metadata(Collections.singletonMap("a_custom_key", "a-custom-value"))
970+
.subMerchants(subMerchants)
971+
.build();
972+
}
864973
}

0 commit comments

Comments
 (0)