Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion .gitignore
Comment thread
dili91 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ build
.idea

#OSX
.DS_Store
.DS_Store

#Claude Code
.claude/
CLAUDE.local.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.truelayer.java.entities.RelatedProducts;
import com.truelayer.java.entities.User;
import com.truelayer.java.payments.entities.paymentmethod.PaymentMethod;
import com.truelayer.java.payments.entities.submerchants.SubMerchants;
import java.util.Map;
import lombok.Builder;
import lombok.EqualsAndHashCode;
Expand Down Expand Up @@ -34,4 +35,9 @@ public class CreatePaymentRequest {
* Optional field for configuring risk assessment and the payment_creditable webhook
*/
private RiskAssessment riskAssessment;

/**
* Optional field for sub-merchant details
*/
private SubMerchants subMerchants;
Comment thread
dili91 marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.truelayer.java.commonapi.entities.UserDetail;
import com.truelayer.java.entities.CurrencyCode;
import com.truelayer.java.payments.entities.paymentmethod.PaymentMethod;
import com.truelayer.java.payments.entities.submerchants.SubMerchants;
import java.time.ZonedDateTime;
import java.util.Map;
import lombok.*;
Expand Down Expand Up @@ -37,6 +38,8 @@ public abstract class PaymentDetail {

private Map<String, String> metadata;

private SubMerchants subMerchants;

@JsonIgnore
public abstract Status getStatus();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.truelayer.java.payments.entities.submerchants;

import com.truelayer.java.entities.Address;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;

@Builder
@Getter
@ToString
@EqualsAndHashCode(callSuper = false)
public class BusinessClient extends UltimateCounterparty {
private final Type type = Type.BUSINESS_CLIENT;
private String tradingName;
private String commercialName;
private String url;
private String mcc;
private String registrationNumber;
private Address address;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.truelayer.java.payments.entities.submerchants;

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

@Builder
@Getter
@ToString
@EqualsAndHashCode(callSuper = false)
public class BusinessDivision extends UltimateCounterparty {
private final Type type = Type.BUSINESS_DIVISION;
private String id;
private String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.truelayer.java.payments.entities.submerchants;

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

@Builder
@Getter
@ToString
@EqualsAndHashCode
public class SubMerchants {
private UltimateCounterparty ultimateCounterparty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.truelayer.java.payments.entities.submerchants;

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.Getter;
import lombok.RequiredArgsConstructor;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = BusinessClient.class)
@JsonSubTypes({
@JsonSubTypes.Type(value = BusinessClient.class, name = "business_client"),
@JsonSubTypes.Type(value = BusinessDivision.class, name = "business_division")
})
public abstract class UltimateCounterparty {

@JsonIgnore
public abstract Type getType();

@JsonIgnore
public boolean isBusinessClient() {
return this instanceof BusinessClient;
}

@JsonIgnore
public boolean isBusinessDivision() {
return this instanceof BusinessDivision;
}

@JsonIgnore
public BusinessClient asBusinessClient() {
if (!isBusinessClient()) throw new TrueLayerException(buildErrorMessage());
return (BusinessClient) this;
}

@JsonIgnore
public BusinessDivision asBusinessDivision() {
if (!isBusinessDivision()) throw new TrueLayerException(buildErrorMessage());
return (BusinessDivision) this;
}

public static BusinessClient.BusinessClientBuilder businessClient() {
return new BusinessClient.BusinessClientBuilder();
}

public static BusinessDivision.BusinessDivisionBuilder businessDivision() {
return new BusinessDivision.BusinessDivisionBuilder();
}

private String buildErrorMessage() {
return String.format(
"UltimateCounterparty is of type %s.", this.getClass().getSimpleName());
}

@Getter
@RequiredArgsConstructor
public enum Type {
BUSINESS_CLIENT("business_client"),
BUSINESS_DIVISION("business_division");

@JsonValue
private final String type;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.truelayer.java.integration;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.truelayer.java.Constants.Scopes.PAYMENTS;
import static com.truelayer.java.TestUtils.assertNotError;
import static org.junit.jupiter.api.Assertions.*;

import com.truelayer.java.TestUtils.RequestStub;
import com.truelayer.java.http.entities.ApiResponse;
import com.truelayer.java.payments.entities.paymentdetail.ExecutedPaymentDetail;
import com.truelayer.java.payments.entities.paymentdetail.PaymentDetail;
import com.truelayer.java.payments.entities.submerchants.BusinessClient;
import com.truelayer.java.payments.entities.submerchants.UltimateCounterparty;
import java.util.Collections;
import lombok.SneakyThrows;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class PaymentSubMerchantsIntegrationTest extends IntegrationTests {

@Test
@DisplayName("It should deserialize payment response with sub_merchants")
@SneakyThrows
void shouldDeserializePaymentWithSubMerchants() {
String paymentId = "payment-id";

RequestStub.New()
.method("post")
.path(urlPathEqualTo("/connect/token"))
.status(200)
.bodyFile("auth/200.access_token.json")
.build();

RequestStub.New()
.method("get")
.path(urlPathEqualTo(String.format("/payments/%s", paymentId)))
.status(200)
.bodyFile("payments/200.get_payment_by_id.with_submerchants.json")
.build();

ApiResponse<PaymentDetail> response =
tlClient.payments().getPayment(paymentId).get();

verifyGeneratedToken(Collections.singletonList(PAYMENTS));
assertNotError(response);

PaymentDetail result = response.getData();
assertNotNull(result);
assertTrue(result.isExecuted());

ExecutedPaymentDetail executedPayment = result.asExecuted();
assertNotNull(executedPayment.getSubMerchants());
assertNotNull(executedPayment.getSubMerchants().getUltimateCounterparty());

UltimateCounterparty ultimateCounterparty =
executedPayment.getSubMerchants().getUltimateCounterparty();
assertTrue(ultimateCounterparty.isBusinessClient());

BusinessClient businessClient = ultimateCounterparty.asBusinessClient();
assertEquals("Example Trading Ltd", businessClient.getTradingName());
assertEquals("Example Commercial Name", businessClient.getCommercialName());
assertEquals("https://example.com", businessClient.getUrl());
assertEquals("1234", businessClient.getMcc());
assertEquals("12345678", businessClient.getRegistrationNumber());
assertNotNull(businessClient.getAddress());
assertEquals("London", businessClient.getAddress().getCity());
}

@Test
@DisplayName("It should handle payment response without sub_merchants (backward compatibility)")
@SneakyThrows
void shouldHandlePaymentWithoutSubMerchants() {
String paymentId = "payment-id";

RequestStub.New()
.method("post")
.path(urlPathEqualTo("/connect/token"))
.status(200)
.bodyFile("auth/200.access_token.json")
.build();

RequestStub.New()
.method("get")
.path(urlPathEqualTo(String.format("/payments/%s", paymentId)))
.status(200)
.bodyFile("payments/200.get_payment_by_id.bank_transfer.executed.json")
.build();

ApiResponse<PaymentDetail> response =
tlClient.payments().getPayment(paymentId).get();

verifyGeneratedToken(Collections.singletonList(PAYMENTS));
assertNotError(response);

PaymentDetail result = response.getData();
assertNotNull(result);
assertTrue(result.isExecuted());

ExecutedPaymentDetail executedPayment = result.asExecuted();
// subMerchants should be null for backward compatibility
assertNull(executedPayment.getSubMerchants());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.truelayer.java.payments.entities;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.truelayer.java.entities.CurrencyCode;
import com.truelayer.java.entities.User;
import com.truelayer.java.payments.entities.paymentmethod.BankTransfer;
import com.truelayer.java.payments.entities.submerchants.BusinessClient;
import com.truelayer.java.payments.entities.submerchants.SubMerchants;
import com.truelayer.java.payments.entities.submerchants.UltimateCounterparty;
import org.junit.jupiter.api.Test;

class CreatePaymentRequestSubMerchantsTest {

@Test
void canCreatePaymentRequestWithSubMerchants() {
BusinessClient businessClient = UltimateCounterparty.businessClient()
.tradingName("Test Trading Name")
.build();

SubMerchants subMerchants =
SubMerchants.builder().ultimateCounterparty(businessClient).build();

CreatePaymentRequest request = CreatePaymentRequest.builder()
.amountInMinor(1000)
.currency(CurrencyCode.GBP)
.paymentMethod(BankTransfer.builder().build())
.user(User.builder().id("user-123").build())
.subMerchants(subMerchants)
.build();

assertNotNull(request);
assertNotNull(request.getSubMerchants());
assertEquals(
UltimateCounterparty.Type.BUSINESS_CLIENT,
request.getSubMerchants().getUltimateCounterparty().getType());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.truelayer.java.payments.entities.submerchants;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import com.truelayer.java.entities.Address;
import org.junit.jupiter.api.Test;

class SubMerchantsTest {

@Test
void canCreateBusinessClient() {
Address address = Address.builder()
.addressLine1("123 Test Street")
.city("London")
.countryCode("GB")
.build();

BusinessClient businessClient = UltimateCounterparty.businessClient()
.tradingName("Test Trading Name")
.commercialName("Test Commercial Name")
.url("https://example.com")
.mcc("1234")
.registrationNumber("REG123")
.address(address)
.build();

assertNotNull(businessClient);
assertEquals(UltimateCounterparty.Type.BUSINESS_CLIENT, businessClient.getType());
assertEquals("Test Trading Name", businessClient.getTradingName());
}

@Test
void canCreateBusinessDivision() {
BusinessDivision businessDivision = UltimateCounterparty.businessDivision()
.id("division-123")
.name("Test Division")
.build();

assertNotNull(businessDivision);
assertEquals(UltimateCounterparty.Type.BUSINESS_DIVISION, businessDivision.getType());
assertEquals("division-123", businessDivision.getId());
assertEquals("Test Division", businessDivision.getName());
}

@Test
void canCreateSubMerchantsWithBusinessClient() {
BusinessClient businessClient = UltimateCounterparty.businessClient()
.tradingName("Test Trading Name")
.build();

SubMerchants subMerchants =
SubMerchants.builder().ultimateCounterparty(businessClient).build();

assertNotNull(subMerchants);
assertNotNull(subMerchants.getUltimateCounterparty());
assertEquals(
UltimateCounterparty.Type.BUSINESS_CLIENT,
subMerchants.getUltimateCounterparty().getType());
}
}
Loading