From ede7ff0b972edad25f2c050979d1ddaf2ba8315b Mon Sep 17 00:00:00 2001 From: Andrea Di Lisio Date: Fri, 13 Jan 2023 18:21:27 +0100 Subject: [PATCH] [EWT-85] fixes existing items returned by the `merchant-accounts/{id}/transactions` endpoint and add support for refunds (#180) --- gradle.properties | 2 +- .../transactions/ExternalPayment.java | 3 +- .../transactions/MerchantAccountPayment.java | 2 +- .../entities/transactions/PaymentSource.java | 2 - .../entities/transactions/Payout.java | 21 ++- .../entities/transactions/Refund.java | 42 ++++++ .../entities/transactions/Remitter.java | 20 +++ .../entities/transactions/Transaction.java | 18 ++- .../accountidentifier/AccountIdentifier.java | 72 ++++++++++ .../IbanAccountIdentifier.java | 12 ++ ...ortCodeAccountNumberAccountIdentifier.java | 14 ++ .../transactions/beneficiary/Beneficiary.java | 86 ++++++++++++ .../beneficiary/BusinessAccount.java | 20 +++ .../beneficiary/ExternalAccount.java | 20 +++ .../beneficiary/PaymentSource.java | 24 ++++ .../MerchantAccountsAcceptanceTests.java | 6 +- .../transactions/TransactionTests.java | 79 +++++++---- .../AccountIdentifierTests.java | 71 ++++++++++ .../beneficiary/BeneficiaryTests.java | 125 ++++++++++++++++++ .../200.get_transactions.json | 107 ++++++++++++++- 20 files changed, 703 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Refund.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Remitter.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifier.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/IbanAccountIdentifier.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/SortCodeAccountNumberAccountIdentifier.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/Beneficiary.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BusinessAccount.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/ExternalAccount.java create mode 100644 src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/PaymentSource.java create mode 100644 src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifierTests.java create mode 100644 src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BeneficiaryTests.java diff --git a/gradle.properties b/gradle.properties index f52dce0c..c829aaca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # Main properties group=com.truelayer archivesBaseName=truelayer-java -version=5.0.0 +version=6.0.0 # Artifacts properties sonatype_repository_url=https://s01.oss.sonatype.org/service/local/ diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/ExternalPayment.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/ExternalPayment.java index 19330298..926a8774 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/ExternalPayment.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/ExternalPayment.java @@ -1,7 +1,6 @@ package com.truelayer.java.merchantaccounts.entities.transactions; import com.truelayer.java.entities.CurrencyCode; -import com.truelayer.java.entities.Remitter; import java.time.ZonedDateTime; import lombok.EqualsAndHashCode; import lombok.Value; @@ -18,7 +17,7 @@ public class ExternalPayment extends Transaction { int amountInMinor; - Transaction.Status status; + Transaction.Status status = Status.SETTLED; ZonedDateTime settledAt; diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/MerchantAccountPayment.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/MerchantAccountPayment.java index 3cf1b08e..d91b9b77 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/MerchantAccountPayment.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/MerchantAccountPayment.java @@ -16,7 +16,7 @@ public class MerchantAccountPayment extends Transaction { int amountInMinor; - Transaction.Status status; + Transaction.Status status = Status.SETTLED; ZonedDateTime settledAt; diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/PaymentSource.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/PaymentSource.java index 78168391..c1dde4e5 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/PaymentSource.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/PaymentSource.java @@ -11,6 +11,4 @@ public class PaymentSource { List accountIdentifiers; String accountHolderName; - - String userId; } diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Payout.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Payout.java index 3273bd37..14fe2769 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Payout.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Payout.java @@ -1,8 +1,9 @@ package com.truelayer.java.merchantaccounts.entities.transactions; +import com.fasterxml.jackson.annotation.JsonGetter; import com.fasterxml.jackson.annotation.JsonValue; import com.truelayer.java.entities.CurrencyCode; -import com.truelayer.java.payments.entities.beneficiary.Beneficiary; +import com.truelayer.java.merchantaccounts.entities.transactions.beneficiary.Beneficiary; import java.time.ZonedDateTime; import java.util.Optional; import lombok.EqualsAndHashCode; @@ -10,6 +11,13 @@ import lombok.RequiredArgsConstructor; import lombok.Value; +/** + * DTO class that represents both pending and executed payouts. + * We're not defining explicit variants in this case based on the status field because + * it seems that the library used for deserialization is not capable of handling more than one deserialization hints, + * which would be required in this case to first select a DTO based on the type and then on the + * status fields. + */ @Value @EqualsAndHashCode(callSuper = false) public class Payout extends Transaction { @@ -22,11 +30,15 @@ public class Payout extends Transaction { int amountInMinor; + /** + * Represents the status of the payout. + * Either pending or executed. + */ Transaction.Status status; ZonedDateTime createdAt; - ZonedDateTime settledAt; + ZonedDateTime executedAt; Beneficiary beneficiary; @@ -34,8 +46,9 @@ public class Payout extends Transaction { String payoutId; - public Optional getSettledAt() { - return Optional.ofNullable(settledAt); + @JsonGetter + public Optional getExecutedAt() { + return Optional.ofNullable(executedAt); } @RequiredArgsConstructor diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Refund.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Refund.java new file mode 100644 index 00000000..01f48a8a --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Refund.java @@ -0,0 +1,42 @@ +package com.truelayer.java.merchantaccounts.entities.transactions; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.truelayer.java.entities.CurrencyCode; +import java.time.ZonedDateTime; +import java.util.Optional; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class Refund extends Transaction { + Type type = Type.REFUND; + + String id; + + CurrencyCode currency; + + int amountInMinor; + + /** + * Represents the refund status, either pending or executed. + */ + Transaction.Status status; + + ZonedDateTime createdAt; + + ZonedDateTime executedAt; + + com.truelayer.java.merchantaccounts.entities.transactions.beneficiary.PaymentSource beneficiary; + + Payout.ContextCode contextCode; + + String refundId; + + String paymentId; + + @JsonGetter + public Optional getExecutedAt() { + return Optional.ofNullable(executedAt); + } +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Remitter.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Remitter.java new file mode 100644 index 00000000..853ab3d4 --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Remitter.java @@ -0,0 +1,20 @@ +package com.truelayer.java.merchantaccounts.entities.transactions; + +import com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier.AccountIdentifier; +import java.util.List; +import lombok.Value; + +/** + * A transaction item specific DTO for external payments + * representing account identifiers of either IBAN or SCAN type. + * This is deliberately different from the more generic + * {@link com.truelayer.java.entities.Remitter} class. + */ +@Value +public class Remitter { + private List accountIdentifiers; + + private String accountHolderName; + + private String reference; +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Transaction.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Transaction.java index 872bc161..d85bfd9f 100644 --- a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Transaction.java +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/Transaction.java @@ -13,6 +13,7 @@ @JsonSubTypes.Type(value = MerchantAccountPayment.class, name = "merchant_account_payment"), @JsonSubTypes.Type(value = ExternalPayment.class, name = "external_payment"), @JsonSubTypes.Type(value = Payout.class, name = "payout"), + @JsonSubTypes.Type(value = Refund.class, name = "refund"), }) @Getter public abstract class Transaction { @@ -24,7 +25,8 @@ public abstract class Transaction { public enum Type { MERCHANT_ACCOUNT_PAYMENT("merchant_account_payment"), EXTERNAL_PAYMENT("external_payment"), - PAYOUT("payout"); + PAYOUT("payout"), + REFUND("refund"); @JsonValue private final String type; @@ -33,6 +35,7 @@ public enum Type { @RequiredArgsConstructor @Getter public enum Status { + EXECUTED("executed"), SETTLED("settled"), PENDING("pending"); @@ -55,6 +58,11 @@ public boolean isPayout() { return this instanceof Payout; } + @JsonIgnore + public boolean isRefund() { + return this instanceof Refund; + } + @JsonIgnore public MerchantAccountPayment asMerchantAccountPayment() { if (!isMerchantAccountPayment()) { @@ -79,6 +87,14 @@ public Payout asPayout() { return (Payout) this; } + @JsonIgnore + public Refund asRefund() { + if (!isRefund()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (Refund) this; + } + private String buildErrorMessage() { return String.format("Payment is of type %s.", this.getClass().getSimpleName()); } diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifier.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifier.java new file mode 100644 index 00000000..f5078bbc --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifier.java @@ -0,0 +1,72 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier; + +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; + +/** + * A transaction item specific DTO for external payments + * representing account identifiers of either IBAN or SCAN type only. + * This is deliberately different from the more generic {@link com.truelayer.java.entities.Remitter Remitter} class. + */ +@JsonTypeInfo( + include = JsonTypeInfo.As.EXISTING_PROPERTY, + use = JsonTypeInfo.Id.NAME, + property = "type", + defaultImpl = SortCodeAccountNumberAccountIdentifier.class) +@JsonSubTypes({ + @JsonSubTypes.Type(value = SortCodeAccountNumberAccountIdentifier.class, name = "sort_code_account_number"), + @JsonSubTypes.Type(value = IbanAccountIdentifier.class, name = "iban"), +}) +@ToString +@EqualsAndHashCode +@Getter +public abstract class AccountIdentifier { + public abstract Type getType(); + + @JsonIgnore + public boolean isSortCodeAccountNumberIdentifier() { + return this instanceof SortCodeAccountNumberAccountIdentifier; + } + + @JsonIgnore + public boolean isIbanIdentifier() { + return this instanceof IbanAccountIdentifier; + } + + @JsonIgnore + public SortCodeAccountNumberAccountIdentifier asSortCodeAccountNumber() { + if (!isSortCodeAccountNumberIdentifier()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (SortCodeAccountNumberAccountIdentifier) this; + } + + @JsonIgnore + public IbanAccountIdentifier asIban() { + if (!isIbanIdentifier()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (IbanAccountIdentifier) this; + } + + private String buildErrorMessage() { + return String.format("Identifier is of type %s.", this.getClass().getSimpleName()); + } + + @Getter + @RequiredArgsConstructor + public enum Type { + SORT_CODE_ACCOUNT_NUMBER("sort_code_account_number"), + IBAN("iban"); + + @JsonValue + private final String type; + } +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/IbanAccountIdentifier.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/IbanAccountIdentifier.java new file mode 100644 index 00000000..8201c885 --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/IbanAccountIdentifier.java @@ -0,0 +1,12 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class IbanAccountIdentifier extends AccountIdentifier { + private final Type type = Type.IBAN; + + private String iban; +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/SortCodeAccountNumberAccountIdentifier.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/SortCodeAccountNumberAccountIdentifier.java new file mode 100644 index 00000000..3b7eeed8 --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/SortCodeAccountNumberAccountIdentifier.java @@ -0,0 +1,14 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class SortCodeAccountNumberAccountIdentifier extends AccountIdentifier { + private final Type type = Type.SORT_CODE_ACCOUNT_NUMBER; + + private String sortCode; + + private String accountNumber; +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/Beneficiary.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/Beneficiary.java new file mode 100644 index 00000000..5e1cc506 --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/Beneficiary.java @@ -0,0 +1,86 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.beneficiary; + +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; + +/** + * A transaction item specific DTO for payouts beneficiaries. Unlike the shared + * {@link com.truelayer.java.payments.entities.beneficiary.Beneficiary Beneficiary} type, this abstract class is + * subclassed by concrete variants that represent one of external_account, business_account or + * payment_source. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = ExternalAccount.class) +@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") +}) +@ToString +@EqualsAndHashCode +@Getter +public abstract class Beneficiary { + + @JsonIgnore + public abstract Type getType(); + + @JsonIgnore + public boolean isExternalAccount() { + return this instanceof ExternalAccount; + } + + @JsonIgnore + public boolean isPaymentSource() { + return this instanceof PaymentSource; + } + + @JsonIgnore + public boolean isBusinessAccount() { + return this instanceof BusinessAccount; + } + + @JsonIgnore + public BusinessAccount asBusinessAccount() { + if (!isBusinessAccount()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (BusinessAccount) this; + } + + @JsonIgnore + public ExternalAccount asExternalAccount() { + if (!isExternalAccount()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (ExternalAccount) this; + } + + @JsonIgnore + public PaymentSource asPaymentSource() { + if (!isPaymentSource()) { + throw new TrueLayerException(buildErrorMessage()); + } + return (PaymentSource) this; + } + + private String buildErrorMessage() { + return String.format("Beneficiary is of type %s.", this.getClass().getSimpleName()); + } + + @Getter + @RequiredArgsConstructor + public enum Type { + EXTERNAL_ACCOUNT("external_account"), + PAYMENT_SOURCE("payment_source"), + BUSINESS_ACCOUNT("business_account"); + + @JsonValue + private final String type; + } +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BusinessAccount.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BusinessAccount.java new file mode 100644 index 00000000..7034a7cf --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BusinessAccount.java @@ -0,0 +1,20 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.beneficiary; + +import static com.truelayer.java.merchantaccounts.entities.transactions.beneficiary.Beneficiary.Type.BUSINESS_ACCOUNT; + +import com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier.AccountIdentifier; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class BusinessAccount extends Beneficiary { + private final Type type = BUSINESS_ACCOUNT; + + private String reference; + + private String accountHolderName; + + private List accountIdentifiers; +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/ExternalAccount.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/ExternalAccount.java new file mode 100644 index 00000000..9aa8fdcd --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/ExternalAccount.java @@ -0,0 +1,20 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.beneficiary; + +import static com.truelayer.java.merchantaccounts.entities.transactions.beneficiary.Beneficiary.Type.EXTERNAL_ACCOUNT; + +import com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier.AccountIdentifier; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class ExternalAccount extends Beneficiary { + private final Type type = EXTERNAL_ACCOUNT; + + private String reference; + + private String accountHolderName; + + private List accountIdentifiers; +} diff --git a/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/PaymentSource.java b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/PaymentSource.java new file mode 100644 index 00000000..8fbd183e --- /dev/null +++ b/src/main/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/PaymentSource.java @@ -0,0 +1,24 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.beneficiary; + +import static com.truelayer.java.merchantaccounts.entities.transactions.beneficiary.Beneficiary.Type.PAYMENT_SOURCE; + +import com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier.AccountIdentifier; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper = false) +public class PaymentSource extends Beneficiary { + private final Type type = PAYMENT_SOURCE; + + private String paymentSourceId; + + private String userId; + + private String reference; + + private String accountHolderName; + + private List accountIdentifiers; +} diff --git a/src/test/java/com/truelayer/java/acceptance/MerchantAccountsAcceptanceTests.java b/src/test/java/com/truelayer/java/acceptance/MerchantAccountsAcceptanceTests.java index 4f0e8294..fc790620 100644 --- a/src/test/java/com/truelayer/java/acceptance/MerchantAccountsAcceptanceTests.java +++ b/src/test/java/com/truelayer/java/acceptance/MerchantAccountsAcceptanceTests.java @@ -44,7 +44,9 @@ public void itShouldGetAMerchantAccountById() { @Test @DisplayName("It should get the list of transactions for a given merchant account") public void itShouldGetTheListOfTransactions() { - assertNotError(getTransactions()); + ApiResponse transactionList = getTransactions(); + + assertNotError(transactionList); } @SneakyThrows @@ -114,7 +116,7 @@ public void itShouldGetPaymentSources() { ListPaymentSourcesQuery.builder() .userId(merchantAccountPayment .getPaymentSource() - .getUserId()) + .getId()) .build()) .get(); diff --git a/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/TransactionTests.java b/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/TransactionTests.java index 2c3b3403..e59bb519 100644 --- a/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/TransactionTests.java +++ b/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/TransactionTests.java @@ -20,9 +20,8 @@ public void shouldYieldTrueIfMerchantAccountPayment() { UUID.randomUUID().toString(), CurrencyCode.GBP, 100, - Transaction.Status.PENDING, ZonedDateTime.now(Clock.systemUTC()), - new PaymentSource(UUID.randomUUID().toString(), null, null, null), + new PaymentSource(UUID.randomUUID().toString(), null, null), UUID.randomUUID().toString()); assertTrue(sut.isMerchantAccountPayment()); @@ -36,7 +35,6 @@ public void shouldConvertToMerchantAccountPayment() { UUID.randomUUID().toString(), CurrencyCode.GBP, 100, - Transaction.Status.PENDING, ZonedDateTime.now(Clock.systemUTC()), null, UUID.randomUUID().toString()); @@ -48,12 +46,7 @@ public void shouldConvertToMerchantAccountPayment() { @DisplayName("It should throw an error when converting to MerchantAccountPayment") public void shouldNotConvertToSortCodeAccNumber() { Transaction sut = new ExternalPayment( - UUID.randomUUID().toString(), - CurrencyCode.GBP, - 100, - Transaction.Status.PENDING, - ZonedDateTime.now(Clock.systemUTC()), - null); + UUID.randomUUID().toString(), CurrencyCode.GBP, 100, ZonedDateTime.now(Clock.systemUTC()), null); Throwable thrown = assertThrows(TrueLayerException.class, sut::asMerchantAccountPayment); @@ -64,12 +57,7 @@ public void shouldNotConvertToSortCodeAccNumber() { @DisplayName("It should yield true if instance is of type ExternalPayment") public void shouldYieldTrueIfExternalPayment() { Transaction sut = new ExternalPayment( - UUID.randomUUID().toString(), - CurrencyCode.GBP, - 100, - Transaction.Status.PENDING, - ZonedDateTime.now(Clock.systemUTC()), - null); + UUID.randomUUID().toString(), CurrencyCode.GBP, 100, ZonedDateTime.now(Clock.systemUTC()), null); assertTrue(sut.isExternalPayment()); } @@ -78,12 +66,7 @@ public void shouldYieldTrueIfExternalPayment() { @DisplayName("It should convert to an instance of class ExternalPayment") public void shouldConvertToExternalPayment() { Transaction sut = new ExternalPayment( - UUID.randomUUID().toString(), - CurrencyCode.GBP, - 100, - Transaction.Status.PENDING, - ZonedDateTime.now(Clock.systemUTC()), - null); + UUID.randomUUID().toString(), CurrencyCode.GBP, 100, ZonedDateTime.now(Clock.systemUTC()), null); assertDoesNotThrow(sut::asExternalPayment); } @@ -95,7 +78,6 @@ public void shouldNotConvertToExternalPayment() { UUID.randomUUID().toString(), CurrencyCode.GBP, 100, - Transaction.Status.PENDING, ZonedDateTime.now(Clock.systemUTC()), null, UUID.randomUUID().toString()); @@ -146,7 +128,6 @@ public void shouldNotConvertToPayout() { UUID.randomUUID().toString(), CurrencyCode.GBP, 100, - Transaction.Status.PENDING, ZonedDateTime.now(Clock.systemUTC()), null, UUID.randomUUID().toString()); @@ -155,4 +136,56 @@ public void shouldNotConvertToPayout() { assertEquals(String.format("Payment is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); } + + @Test + @DisplayName("It should yield true if instance is of type Refund") + public void shouldYieldTrueIfRefund() { + Transaction sut = new Refund( + UUID.randomUUID().toString(), + CurrencyCode.GBP, + 100, + Transaction.Status.EXECUTED, + ZonedDateTime.now(Clock.systemUTC()), + ZonedDateTime.now(Clock.systemUTC()), + null, + Payout.ContextCode.INTERNAL, + UUID.randomUUID().toString(), + UUID.randomUUID().toString()); + + assertTrue(sut.isRefund()); + } + + @Test + @DisplayName("It should convert to an instance of class Refund") + public void shouldConvertToRefund() { + Transaction sut = new Refund( + UUID.randomUUID().toString(), + CurrencyCode.GBP, + 100, + Transaction.Status.EXECUTED, + ZonedDateTime.now(Clock.systemUTC()), + ZonedDateTime.now(Clock.systemUTC()), + null, + Payout.ContextCode.INTERNAL, + UUID.randomUUID().toString(), + UUID.randomUUID().toString()); + + assertDoesNotThrow(sut::asRefund); + } + + @Test + @DisplayName("It should throw an error when converting to Refund") + public void shouldNotConvertToRefund() { + Transaction sut = new MerchantAccountPayment( + UUID.randomUUID().toString(), + CurrencyCode.GBP, + 100, + ZonedDateTime.now(Clock.systemUTC()), + null, + UUID.randomUUID().toString()); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asRefund); + + assertEquals(String.format("Payment is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } } diff --git a/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifierTests.java b/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifierTests.java new file mode 100644 index 00000000..9c63b028 --- /dev/null +++ b/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/accountidentifier/AccountIdentifierTests.java @@ -0,0 +1,71 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier; + +import static com.truelayer.java.Utils.getObjectMapper; +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.TrueLayerException; +import lombok.SneakyThrows; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class AccountIdentifierTests { + + @Test + @DisplayName("It should yield true if instance is of type SortCodeAccountNumberAccountIdentifier") + public void shouldYieldTrueIfSortCodeAccNumber() { + AccountIdentifier sut = new SortCodeAccountNumberAccountIdentifier("123456", "12345678"); + + assertTrue(sut.isSortCodeAccountNumberIdentifier()); + } + + @SneakyThrows + @Test + @DisplayName("It should convert to an instance of class SortCodeAccountNumberAccountIdentifier") + public void shouldConvertToSortCodeAccNumber() { + AccountIdentifier sut = new SortCodeAccountNumberAccountIdentifier("123456", "12345678"); + + assertDoesNotThrow(sut::asSortCodeAccountNumber); + assertEquals( + "{\"sort_code\":\"123456\",\"account_number\":\"12345678\",\"type\":\"sort_code_account_number\"}", + getObjectMapper().writeValueAsString(sut)); + } + + @Test + @DisplayName("It should throw an error when converting to SortCodeAccountNumberAccountIdentifier") + public void shouldNotConvertToSortCodeAccNumber() { + AccountIdentifier sut = new IbanAccountIdentifier("12345678"); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asSortCodeAccountNumber); + + assertEquals(String.format("Identifier is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type IbanAccountIdentifier") + public void shouldYieldTrueIfIban() { + AccountIdentifier sut = new IbanAccountIdentifier("12345678"); + + assertTrue(sut.isIbanIdentifier()); + } + + @SneakyThrows + @Test + @DisplayName("It should convert to an instance of class IbanAccountIdentifier") + public void shouldConvertToIban() { + AccountIdentifier sut = new IbanAccountIdentifier("12345678"); + + assertDoesNotThrow(sut::asIban); + assertEquals( + "{\"iban\":\"12345678\",\"type\":\"iban\"}", getObjectMapper().writeValueAsString(sut)); + } + + @Test + @DisplayName("It should throw an error when converting to IbanAccountIdentifier") + public void shouldNotConvertToIbanAccountIdentifier() { + AccountIdentifier sut = new SortCodeAccountNumberAccountIdentifier("123456", "12345678"); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asIban); + + assertEquals(String.format("Identifier is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } +} diff --git a/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BeneficiaryTests.java b/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BeneficiaryTests.java new file mode 100644 index 00000000..ec2b0fb6 --- /dev/null +++ b/src/test/java/com/truelayer/java/merchantaccounts/entities/transactions/beneficiary/BeneficiaryTests.java @@ -0,0 +1,125 @@ +package com.truelayer.java.merchantaccounts.entities.transactions.beneficiary; + +import static org.junit.jupiter.api.Assertions.*; + +import com.truelayer.java.TrueLayerException; +import com.truelayer.java.merchantaccounts.entities.transactions.accountidentifier.IbanAccountIdentifier; +import java.util.Arrays; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BeneficiaryTests { + + @Test + @DisplayName("It should yield true if instance is of type ExternalAccount") + public void shouldYieldTrueIfExternalAccount() { + Beneficiary sut = new ExternalAccount( + "a-reference", + "a-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + assertTrue(sut.isExternalAccount()); + } + + @Test + @DisplayName("It should convert to an instance of class ExternalAccount") + public void shouldConvertToExternalAccount() { + Beneficiary sut = new ExternalAccount( + "a-reference", + "a-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + assertDoesNotThrow(sut::asExternalAccount); + } + + @Test + @DisplayName("It should throw an error when converting to ExternalAccount") + public void shouldNotConvertToExternalAccount() { + Beneficiary sut = new PaymentSource( + "an-id", + "a-user-id", + "a-reference", + "an-account-holder-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asExternalAccount); + + assertEquals(String.format("Beneficiary is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type BusinessAccount") + public void shouldYieldTrueIfBusinessAccount() { + Beneficiary sut = new BusinessAccount( + "a-reference", + "a-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + assertTrue(sut.isBusinessAccount()); + } + + @Test + @DisplayName("It should convert to an instance of class BusinessAccount") + public void shouldConvertToBusinessAccount() { + Beneficiary sut = new BusinessAccount( + "a-reference", + "a-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + assertDoesNotThrow(sut::asBusinessAccount); + } + + @Test + @DisplayName("It should throw an error when converting to BusinessAccount") + public void shouldNotConvertToBusinessAccount() { + Beneficiary sut = new PaymentSource( + "an-id", + "a-user-id", + "a-reference", + "an-account-holder-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asBusinessAccount); + + assertEquals(String.format("Beneficiary is of type %s.", sut.getClass().getSimpleName()), thrown.getMessage()); + } + + @Test + @DisplayName("It should yield true if instance is of type PaymentSource") + public void shouldYieldTrueIfPaymentSource() { + Beneficiary sut = new PaymentSource( + "an-id", + "a-user-id", + "a-reference", + "an-account-holder-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + assertTrue(sut.isPaymentSource()); + } + + @Test + @DisplayName("It should convert to an instance of class PaymentSource") + public void shouldConvertToPaymentSource() { + Beneficiary sut = new PaymentSource( + "an-id", + "a-user-id", + "a-reference", + "an-account-holder-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + assertDoesNotThrow(sut::asPaymentSource); + } + + @Test + @DisplayName("It should throw an error when converting to PaymentSource") + public void shouldNotConvertToPaymentSource() { + Beneficiary sut = new BusinessAccount( + "a-reference", + "a-name", + Arrays.asList(IbanAccountIdentifier.builder().build())); + + Throwable thrown = assertThrows(TrueLayerException.class, sut::asPaymentSource); + + assertEquals(String.format("Beneficiary 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 9bb99a93..2603ddef 100644 --- a/src/test/resources/__files/merchant_accounts/200.get_transactions.json +++ b/src/test/resources/__files/merchant_accounts/200.get_transactions.json @@ -37,7 +37,18 @@ "account_identifier": { "type": "sort_code_account_number", "sort_code": "040662", - "account_number": "00002775" + "account_number": "00002775", + "account_identifiers":[ + { + "type":"sort_code_account_number", + "sort_code":"040662", + "account_number":"00002723" + }, + { + "type":"iban", + "iban":"GB53CLRB04066200002723" + } + ] } } }, @@ -49,9 +60,49 @@ "status":"pending", "created_at":"2022-02-10T15:47:17.403Z", "beneficiary":{ - "type":"merchant_account", + "type":"external_account", "merchant_account_id":"e83c4c20-b2ad-4b73-8a32-***", - "account_holder_name":"john smith" + "account_holder_name":"john smith", + "account_identifiers":[ + { + "type":"sort_code_account_number", + "sort_code":"040662", + "account_number":"00002723" + }, + { + "type":"iban", + "iban":"GB53CLRB04066200002723" + } + ] + }, + "context_code":"withdrawal", + "payout_id":"string" + }, + { + "type":"payout", + "id":"string", + "currency":"GBP", + "amount_in_minor":1, + "status":"pending", + "created_at":"2022-02-10T15:47:17.403Z", + "beneficiary":{ + "type":"payment_source", + "payment_source_id": "an-id", + "account_holder_name":"john smith", + "reference": "a-ref", + "account_identifiers":[ + { + "type":"sort_code_account_number", + "sort_code":"040662", + "account_number":"00002723" + }, + { + "type":"iban", + "iban":"GB53CLRB04066200002723" + } + ], + "context_code": "internal", + "payout_id": "an-id" }, "context_code":"withdrawal", "payout_id":"string" @@ -61,16 +112,58 @@ "id":"string", "currency":"GBP", "amount_in_minor":1, - "status":"settled", + "status":"executed", "created_at":"2022-02-10T15:47:17.403Z", - "settled_at":"2022-02-10T15:47:17.403Z", + "executed_at":"2022-02-10T15:47:17.403Z", "beneficiary":{ - "type":"merchant_account", + "type":"business_account", "merchant_account_id":"e83c4c20-b2ad-4b73-8a32-***", - "account_holder_name":"john smith" + "account_holder_name":"john smith", + "account_identifiers":[ + { + "type":"sort_code_account_number", + "sort_code":"040662", + "account_number":"00002723" + }, + { + "type":"iban", + "iban":"GB53CLRB04066200002723" + } + ] }, "context_code":"withdrawal", "payout_id":"string" + }, + { + "type":"refund", + "id":"a-transaction-id", + "currency":"GBP", + "amount_in_minor":1, + "status":"executed", + "created_at":"2022-02-10T15:47:17.403Z", + "executed_at":"2022-02-10T15:47:17.403Z", + "beneficiary":{ + "type":"payment_source", + "payment_source_id": "an-id", + "account_holder_name":"john smith", + "reference": "a-ref", + "account_identifiers":[ + { + "type":"sort_code_account_number", + "sort_code":"040662", + "account_number":"00002723" + }, + { + "type":"iban", + "iban":"GB53CLRB04066200002723" + } + ], + "context_code": "internal", + "payout_id": "an-id" + }, + "context_code":"withdrawal", + "refund_id":"an-id", + "payment_id":"another-id" } ] } \ No newline at end of file