From 1757546f29286732f2458e5e8be1678c4fcba31f Mon Sep 17 00:00:00 2001 From: kapcake Date: Fri, 12 May 2023 11:42:21 +0200 Subject: [PATCH] Payment creation with validation and response * Move mapping inside service * Extract validation to utility class * Add test for validation --- .../config/BankingServiceConfig.java | 1 - .../controllers/BankingServiceController.java | 31 +-- .../converters/AbstractConverter.java | 20 ++ .../converters/BankAccountConverter.java | 23 +++ .../converters/PaymentConverter.java | 43 +++++ .../model/domain/BalanceDTO.java | 2 +- .../model/entities/Balance.java | 2 + .../model/entities/BankAccount.java | 6 + .../model/entities/Payment.java | 4 +- .../repositories/BalanceRepository.java | 8 + .../repositories/BankAccountRepository.java | 2 + .../services/AccountService.java | 14 +- .../services/PaymentService.java | 68 ++++--- .../validation/PaymentValidator.java | 82 ++++++++ .../validation/ValidationProperties.java | 16 ++ src/main/resources/application.yml | 7 +- src/main/resources/data.sql | 168 ++++++++++++++++ .../validation/PaymentValidatorTest.java | 179 ++++++++++++++++++ 18 files changed, 615 insertions(+), 61 deletions(-) create mode 100644 src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java create mode 100644 src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java create mode 100644 src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java create mode 100644 src/main/java/net/kapcake/bankingservice/repositories/BalanceRepository.java create mode 100644 src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java create mode 100644 src/main/java/net/kapcake/bankingservice/validation/ValidationProperties.java create mode 100644 src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java diff --git a/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java b/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java index 903ccbf..8236d7e 100644 --- a/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java +++ b/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java @@ -8,7 +8,6 @@ import org.springframework.web.client.RestTemplate; @Configuration public class BankingServiceConfig { - @Bean public ModelMapper modelMapper() { return new ModelMapper(); diff --git a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java index effd616..ccc68f1 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java @@ -1,12 +1,12 @@ package net.kapcake.bankingservice.controllers; import jakarta.validation.Valid; +import net.kapcake.bankingservice.converters.BankAccountConverter; +import net.kapcake.bankingservice.converters.PaymentConverter; import net.kapcake.bankingservice.exceptions.PaymentValidationException; import net.kapcake.bankingservice.model.domain.PaymentDTO; import net.kapcake.bankingservice.model.domain.BankAccountDTO; -import net.kapcake.bankingservice.model.entities.BankAccount; import net.kapcake.bankingservice.model.entities.Payment; -import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.security.UserDetailsImpl; import net.kapcake.bankingservice.services.AccountService; import net.kapcake.bankingservice.services.PaymentService; @@ -19,26 +19,20 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.Optional; @RestController public class BankingServiceController { - private final BankAccountRepository bankAccountRepository; private final AccountService accountService; private final PaymentService paymentService; - private final ModelMapper modelMapper; - public BankingServiceController(BankAccountRepository bankAccountRepository, AccountService accountService, PaymentService paymentService, ModelMapper modelMapper) { - this.bankAccountRepository = bankAccountRepository; + public BankingServiceController(AccountService accountService, PaymentService paymentService) { this.accountService = accountService; this.paymentService = paymentService; - this.modelMapper = modelMapper; } @GetMapping("/accounts") public List getAccounts(@AuthenticationPrincipal UserDetailsImpl authenticatedUser) { - return accountService.getAccounts(authenticatedUser.user()).stream() - .map(account -> modelMapper.map(account, BankAccountDTO.class)).toList(); + return accountService.getAccounts(authenticatedUser); } @PostMapping("/payment") @@ -46,23 +40,8 @@ public class BankingServiceController { if (bindingResult.hasErrors()) { throw new PaymentValidationException("Payment request invalid: " + bindingResult.getAllErrors()); } - Payment payment = mapToEntity(paymentDTO); - Payment createdPayment = paymentService.createPayment(authenticatedUser.user(), payment); - return mapToDTO(createdPayment); + return paymentService.createPayment(authenticatedUser, paymentDTO); } - private PaymentDTO mapToDTO(Payment payment) { - PaymentDTO paymentDTO = modelMapper.map(payment, PaymentDTO.class); - return paymentDTO; - } - private Payment mapToEntity(PaymentDTO paymentDTO) { - Payment payment = modelMapper.map(paymentDTO, Payment.class); - Optional bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount()); - if (bankAccountOptional.isEmpty()) { - throw new PaymentValidationException("Payment request invalid: Giver account does not exist."); - } - payment.setGiverAccount(bankAccountOptional.get()); - return payment; - } } diff --git a/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java b/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java new file mode 100644 index 0000000..fef752d --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java @@ -0,0 +1,20 @@ +package net.kapcake.bankingservice.converters; + +import net.kapcake.bankingservice.model.domain.PaymentDTO; +import net.kapcake.bankingservice.model.entities.Payment; +import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; + +public abstract class AbstractConverter { + protected final ModelMapper modelMapper; + + public AbstractConverter(ModelMapper modelMapper) { + this.modelMapper = modelMapper; + + this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); + } + + public abstract D mapToDTO(E entity); + + public abstract E mapToEntity(D dto); +} diff --git a/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java b/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java new file mode 100644 index 0000000..4c5b9cf --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java @@ -0,0 +1,23 @@ +package net.kapcake.bankingservice.converters; + +import net.kapcake.bankingservice.model.domain.BankAccountDTO; +import net.kapcake.bankingservice.model.entities.BankAccount; +import org.modelmapper.ModelMapper; +import org.springframework.stereotype.Component; + +@Component +public class BankAccountConverter extends AbstractConverter { + public BankAccountConverter(ModelMapper modelMapper) { + super(modelMapper); + } + + @Override + public BankAccountDTO mapToDTO(BankAccount bankAccount) { + return modelMapper.map(bankAccount, BankAccountDTO.class); + } + + @Override + public BankAccount mapToEntity(BankAccountDTO bankAccountDTO) { + return modelMapper.map(bankAccountDTO, BankAccount.class); + } +} diff --git a/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java b/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java new file mode 100644 index 0000000..7200bbd --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java @@ -0,0 +1,43 @@ +package net.kapcake.bankingservice.converters; + +import net.kapcake.bankingservice.exceptions.PaymentValidationException; +import net.kapcake.bankingservice.model.domain.PaymentDTO; +import net.kapcake.bankingservice.model.entities.BankAccount; +import net.kapcake.bankingservice.model.entities.Payment; +import net.kapcake.bankingservice.repositories.BankAccountRepository; +import org.modelmapper.ModelMapper; +import org.modelmapper.TypeMap; +import org.modelmapper.convention.MatchingStrategies; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class PaymentConverter extends AbstractConverter { + private final BankAccountRepository bankAccountRepository; + + public PaymentConverter(BankAccountRepository bankAccountRepository, ModelMapper modelMapper) { + super(modelMapper); + this.bankAccountRepository = bankAccountRepository; + } + + @Override + public PaymentDTO mapToDTO(Payment payment) { + TypeMap paymentPaymentDTOTypeMap = modelMapper + .typeMap(Payment.class, PaymentDTO.class) + .addMapping(src -> src.getGiverAccount().getId(), PaymentDTO::setGiverAccount); + PaymentDTO paymentDTO = paymentPaymentDTOTypeMap.map(payment); + return paymentDTO; + } + + @Override + public Payment mapToEntity(PaymentDTO paymentDTO) { + Payment payment = modelMapper.map(paymentDTO, Payment.class); + Optional bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount()); + if (bankAccountOptional.isEmpty()) { + throw new PaymentValidationException("Payment request invalid: Giver account does not exist."); + } + payment.setGiverAccount(bankAccountOptional.get()); + return payment; + } +} diff --git a/src/main/java/net/kapcake/bankingservice/model/domain/BalanceDTO.java b/src/main/java/net/kapcake/bankingservice/model/domain/BalanceDTO.java index 7b5af82..3758615 100644 --- a/src/main/java/net/kapcake/bankingservice/model/domain/BalanceDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/domain/BalanceDTO.java @@ -10,7 +10,7 @@ import java.math.BigDecimal; public class BalanceDTO { private Long id; @NotNull - @DecimalMin(value = "0", inclusive = false) + @DecimalMin(value = "0") private BigDecimal amount; @NotNull private Currency currency; diff --git a/src/main/java/net/kapcake/bankingservice/model/entities/Balance.java b/src/main/java/net/kapcake/bankingservice/model/entities/Balance.java index f98e736..1076af5 100644 --- a/src/main/java/net/kapcake/bankingservice/model/entities/Balance.java +++ b/src/main/java/net/kapcake/bankingservice/model/entities/Balance.java @@ -3,12 +3,14 @@ package net.kapcake.bankingservice.model.entities; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; import net.kapcake.bankingservice.model.domain.BalanceType; import net.kapcake.bankingservice.model.domain.Currency; import java.math.BigDecimal; @Entity +@Accessors(chain = true) @Getter @Setter public class Balance { diff --git a/src/main/java/net/kapcake/bankingservice/model/entities/BankAccount.java b/src/main/java/net/kapcake/bankingservice/model/entities/BankAccount.java index 5f756f4..6eb45d9 100644 --- a/src/main/java/net/kapcake/bankingservice/model/entities/BankAccount.java +++ b/src/main/java/net/kapcake/bankingservice/model/entities/BankAccount.java @@ -3,11 +3,13 @@ package net.kapcake.bankingservice.model.entities; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; import net.kapcake.bankingservice.model.domain.AccountStatus; import java.util.List; @Entity +@Accessors(chain = true) @Getter @Setter public class BankAccount { @@ -25,6 +27,10 @@ public class BankAccount { @Column(nullable = false) private String accountName; @OneToMany + @JoinTable( + joinColumns = {@JoinColumn(name = "BANK_ACCOUNT_ID", nullable = false)}, + inverseJoinColumns = {@JoinColumn(name = "BALANCE_ID", nullable = false)} + ) private List balances; @Enumerated(EnumType.STRING) private AccountStatus status; diff --git a/src/main/java/net/kapcake/bankingservice/model/entities/Payment.java b/src/main/java/net/kapcake/bankingservice/model/entities/Payment.java index b4f25d3..c9c8ccb 100644 --- a/src/main/java/net/kapcake/bankingservice/model/entities/Payment.java +++ b/src/main/java/net/kapcake/bankingservice/model/entities/Payment.java @@ -3,6 +3,7 @@ package net.kapcake.bankingservice.model.entities; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; +import lombok.experimental.Accessors; import net.kapcake.bankingservice.model.domain.Currency; import net.kapcake.bankingservice.model.domain.PaymentStatus; import org.hibernate.annotations.CreationTimestamp; @@ -11,6 +12,7 @@ import java.math.BigDecimal; import java.util.Date; @Entity +@Accessors(chain = true) @Getter @Setter public class Payment { @@ -32,7 +34,7 @@ public class Payment { private String communication; @CreationTimestamp @Temporal(TemporalType.TIMESTAMP) - @Column(nullable = false) + @Column(nullable = false, updatable = false) private Date creationDate; @Enumerated(EnumType.STRING) private PaymentStatus status; diff --git a/src/main/java/net/kapcake/bankingservice/repositories/BalanceRepository.java b/src/main/java/net/kapcake/bankingservice/repositories/BalanceRepository.java new file mode 100644 index 0000000..929f894 --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/repositories/BalanceRepository.java @@ -0,0 +1,8 @@ +package net.kapcake.bankingservice.repositories; + +import net.kapcake.bankingservice.model.entities.Balance; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BalanceRepository extends JpaRepository { + +} diff --git a/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java b/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java index 6d0a11b..4e4ea42 100644 --- a/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java +++ b/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java @@ -8,4 +8,6 @@ import java.util.Optional; public interface BankAccountRepository extends JpaRepository { Optional> findAllByUsers_username(String username); + + Optional findByAccountNumber(String accountNumber); } diff --git a/src/main/java/net/kapcake/bankingservice/services/AccountService.java b/src/main/java/net/kapcake/bankingservice/services/AccountService.java index d67180a..7fd9b94 100644 --- a/src/main/java/net/kapcake/bankingservice/services/AccountService.java +++ b/src/main/java/net/kapcake/bankingservice/services/AccountService.java @@ -1,8 +1,9 @@ package net.kapcake.bankingservice.services; -import net.kapcake.bankingservice.model.entities.BankAccount; -import net.kapcake.bankingservice.model.entities.User; +import net.kapcake.bankingservice.converters.BankAccountConverter; +import net.kapcake.bankingservice.model.domain.BankAccountDTO; import net.kapcake.bankingservice.repositories.BankAccountRepository; +import net.kapcake.bankingservice.security.UserDetailsImpl; import org.springframework.stereotype.Service; import java.util.Collections; @@ -11,12 +12,15 @@ import java.util.List; @Service public class AccountService { private final BankAccountRepository bankAccountRepository; + private final BankAccountConverter bankAccountConverter; - public AccountService(BankAccountRepository bankAccountRepository) { + public AccountService(BankAccountRepository bankAccountRepository, BankAccountConverter bankAccountConverter) { this.bankAccountRepository = bankAccountRepository; + this.bankAccountConverter = bankAccountConverter; } - public List getAccounts(User user) { - return bankAccountRepository.findAllByUsers_username(user.getUsername()).orElse(Collections.emptyList()); + public List getAccounts(UserDetailsImpl authenticatedUser) { + return bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()).orElse(Collections.emptyList()) + .stream().map(bankAccountConverter::mapToDTO).toList(); } } diff --git a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java index 027c236..7e63082 100644 --- a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java +++ b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java @@ -1,45 +1,63 @@ package net.kapcake.bankingservice.services; -import net.kapcake.bankingservice.exceptions.PaymentValidationException; -import net.kapcake.bankingservice.model.domain.IbanValidationResponse; +import net.kapcake.bankingservice.converters.PaymentConverter; +import net.kapcake.bankingservice.model.domain.BalanceType; +import net.kapcake.bankingservice.model.domain.PaymentDTO; +import net.kapcake.bankingservice.model.entities.Balance; +import net.kapcake.bankingservice.model.entities.BankAccount; import net.kapcake.bankingservice.model.entities.Payment; -import net.kapcake.bankingservice.model.entities.User; +import net.kapcake.bankingservice.repositories.BalanceRepository; import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.repositories.PaymentRepository; -import org.springframework.beans.factory.annotation.Value; +import net.kapcake.bankingservice.security.UserDetailsImpl; +import net.kapcake.bankingservice.validation.PaymentValidator; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.Optional; @Service public class PaymentService { private final PaymentRepository paymentRepository; private final BankAccountRepository bankAccountRepository; - private final RestTemplate restTemplate; - private final String validationUrl; + private final BalanceRepository balanceRepository; + private final PaymentValidator paymentValidator; + private final PaymentConverter paymentConverter; + private final TransactionTemplate transactionTemplate; - public PaymentService(PaymentRepository paymentRepository, - BankAccountRepository bankAccountRepository, - RestTemplate restTemplate, - @Value("${banking-service.iban-validation.url}") String validationUrl) { + public PaymentService(PaymentRepository paymentRepository, BankAccountRepository bankAccountRepository, + BalanceRepository balanceRepository, PaymentValidator paymentValidator, PaymentConverter paymentConverter, + PlatformTransactionManager transactionManager) { this.paymentRepository = paymentRepository; this.bankAccountRepository = bankAccountRepository; - this.restTemplate = restTemplate; - this.validationUrl = validationUrl; + this.balanceRepository = balanceRepository; + this.paymentValidator = paymentValidator; + this.paymentConverter = paymentConverter; + this.transactionTemplate = new TransactionTemplate(transactionManager); } - public Payment createPayment(User user, Payment payment) { - validatePayment(user, payment); + public PaymentDTO createPayment(UserDetailsImpl authenticatedUser, PaymentDTO paymentDTO) { + Payment payment = paymentConverter.mapToEntity(paymentDTO); - return null; - } + paymentValidator.validate(authenticatedUser.user(), payment); - private void validatePayment(User user, Payment payment) { - if (user.getBankAccounts().stream().noneMatch(bankAccount -> bankAccount.getId().equals(payment.getGiverAccount().getId()))) { - throw new PaymentValidationException("Giver account not owned by authenticated user."); - } - IbanValidationResponse validationResponse = restTemplate.getForObject(validationUrl + payment.getBeneficiaryAccountNumber(), IbanValidationResponse.class); - if (!validationResponse.valid()) { - throw new PaymentValidationException("Beneficiary account not valid: " + validationResponse.messages()); - } + BankAccount giverAccount = payment.getGiverAccount(); + Optional beneficiaryAccount = bankAccountRepository.findByAccountNumber(payment.getBeneficiaryAccountNumber()); + + Payment persistedPayment = transactionTemplate.execute(status -> { + Payment savedPayment = paymentRepository.save(payment); + Balance giverAvailableBalance = giverAccount.getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow(); + giverAvailableBalance.setAmount(giverAvailableBalance.getAmount().subtract(payment.getAmount())); + balanceRepository.save(giverAvailableBalance); + if (beneficiaryAccount.isPresent()) { + Balance beneficiaryBalance = beneficiaryAccount.get().getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow(); + beneficiaryBalance.setAmount(beneficiaryBalance.getAmount().add(payment.getAmount())); + balanceRepository.save(beneficiaryBalance); + } + return savedPayment; + }); + return paymentConverter.mapToDTO(persistedPayment); } } diff --git a/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java b/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java new file mode 100644 index 0000000..f2a1e4f --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java @@ -0,0 +1,82 @@ +package net.kapcake.bankingservice.validation; + +import net.kapcake.bankingservice.exceptions.PaymentValidationException; +import net.kapcake.bankingservice.model.domain.BalanceType; +import net.kapcake.bankingservice.model.domain.IbanValidationResponse; +import net.kapcake.bankingservice.model.entities.Balance; +import net.kapcake.bankingservice.model.entities.Payment; +import net.kapcake.bankingservice.model.entities.User; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Component +public class PaymentValidator { + + private final RestTemplate restTemplate; + private final String validationUrl; + private final List forbiddenAccounts; + + public PaymentValidator(RestTemplate restTemplate, + @Value("${banking-service.validation.iban-validator-url:}") String validationUrl, + @Value("${banking-service.validation.forbidden-accounts:}") List forbiddenAccounts) { + this.restTemplate = restTemplate; + this.validationUrl = validationUrl; + this.forbiddenAccounts = forbiddenAccounts; + } + + + public void validate(User user, Payment payment) throws PaymentValidationException { + validateGiverAccount(user, payment); + validateBeneficiaryAccount(payment); + validateAccountBalance(payment); + } + + private static void validateGiverAccount(User user, Payment payment) { + boolean userOwnsGiverAccount = user.getBankAccounts().stream().anyMatch(bankAccount -> bankAccount.getId().equals(payment.getGiverAccount().getId())); + if (!userOwnsGiverAccount) { + throw new PaymentValidationException("Giver account not owned by authenticated user."); + } + } + + private void validateBeneficiaryAccount(Payment payment) { + IbanValidationResponse validationResponse = null; + try { + validationResponse = restTemplate.getForObject(validationUrl + "/" + payment.getBeneficiaryAccountNumber(), IbanValidationResponse.class); + } catch (RestClientException e) { + throw new PaymentValidationException("Beneficiary account could not be validated."); + } + if (validationResponse == null) { + throw new PaymentValidationException("Beneficiary account could not be validated."); + } else if (!validationResponse.valid()) { + throw new PaymentValidationException("Beneficiary account not valid: " + validationResponse.messages()); + } + boolean sameGiverAndBeneficiary = payment.getGiverAccount().getAccountNumber().equals(payment.getBeneficiaryAccountNumber()); + if (sameGiverAndBeneficiary) { + throw new PaymentValidationException("Beneficiary and giver account are the same."); + } + boolean isBeneficiaryForbidden = forbiddenAccounts.contains(payment.getBeneficiaryAccountNumber()); + if (isBeneficiaryForbidden) { + throw new PaymentValidationException("Beneficiary account is forbidden."); + } + } + + private void validateAccountBalance(Payment payment) { + List availableBalances = payment.getGiverAccount().getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).toList(); + if (availableBalances.size() != 1) { + throw new PaymentValidationException("Unable to retrieve available account balance."); + } + Balance balance = availableBalances.get(0); + boolean sameCurrencies = balance.getCurrency().equals(payment.getCurrency()); + if (!sameCurrencies) { + throw new PaymentValidationException("Payment and account currency must be the same."); + } + int compareResult = balance.getAmount().compareTo(payment.getAmount()); + if (compareResult < 0) { + throw new PaymentValidationException("Available account balance not sufficient."); + } + } +} diff --git a/src/main/java/net/kapcake/bankingservice/validation/ValidationProperties.java b/src/main/java/net/kapcake/bankingservice/validation/ValidationProperties.java new file mode 100644 index 0000000..8f0fe3c --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/validation/ValidationProperties.java @@ -0,0 +1,16 @@ +package net.kapcake.bankingservice.validation; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; +import java.util.List; + +@Configuration +@ConfigurationProperties(prefix = "banking-service.validation") +@Data +public class ValidationProperties { + private List forbiddenAccounts = Collections.emptyList(); + private String ibanValidatorUrl = ""; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2c88831..f051777 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,5 +9,8 @@ spring: defer-datasource-initialization: true banking-service: - iban-validation: - url: https://openiban.com/validate/ \ No newline at end of file + validation: + forbidden-accounts: > + LU280019400644750000, + LU120010001234567891 + iban-validator-url: https://openiban.com/validate diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index acfed91..1be3b35 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,3 +1,4 @@ +-- Users INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD) VALUES (1, 'user1', '{bcrypt}$2a$10$4dPs2u01F/UBJtQyKRCRLevJACUkDzSdD.4EFKkf0T0qllqtkxw5e'); INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD) @@ -15,6 +16,7 @@ VALUES (7, 'user7', '{bcrypt}$2a$10$w/CSC.FmoFEbGBJ5lPF3oe0gbZmo3n1wOQK9W50DbDHs INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD) VALUES (8, 'user8', '{bcrypt}$2a$10$L1RWBgjH0L.YJG/uyMhviubSxz8C.PnZJlviF6K/iueeDrWO.FSOy'); +-- Bank accounts INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS) VALUES (1, 'Personal', 'LU584022594948990503', 'ENABLED'); INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS) @@ -56,6 +58,89 @@ VALUES (19, 'Savings', 'LU130189044953642517', 'ENABLED'); INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS) VALUES (20, 'Trading', 'LU081651725326393823', 'ENABLED'); +-- Balances +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 1, 578.98, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 2, 578.98, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 3, 4135.14, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 4, 4135.14, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 5, 21545.32, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 6, 21545.32, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 7, 201.00, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 8, 201.00, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 9, 984512.23, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 10, 984512.23, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 11, 382690.16, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 12, 382690.16, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 13, 232663.94, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 14, 232663.94, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 15, 421234.41, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 16, 421234.41, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 17, 158052.05, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 18, 158052.05, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 19, 292888.94, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 20, 292888.94, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 21, 168211.44, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 22, 168211.44, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 23, 223757.44, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 24, 223757.44, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 25, 207268.46, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 26, 207268.46, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 27, 326704.48, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 28, 326704.48, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 29, 98377.59, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 30, 98377.59, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 31, 23818.53, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 32, 23818.53, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 33, 252442.60, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 34, 252442.60, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 35, 412747.20, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 36, 412747.20, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 37, 359691.69, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 38, 359691.69, 'EUR', 'END_OF_DAY' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 39, 278739.76, 'EUR', 'AVAILABLE' ); +INSERT INTO BALANCE (ID, AMOUNT, CURRENCY, TYPE) +VALUES ( 40, 278739.76, 'EUR', 'END_OF_DAY' ); + +-- User/Bank account association INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID) VALUES (1, 1); INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID) @@ -110,3 +195,86 @@ INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID) VALUES (19, 2); INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID) VALUES (20, 8); + +-- Balance / Bank account association +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (1, 1); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (1, 2); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (2, 3); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (2, 4); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (3, 5); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (3, 6); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (4, 7); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (4, 8); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (5, 9); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (5, 10); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (6, 11); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (6, 12); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (7, 13); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (7, 14); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (8, 15); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (8, 16); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (9, 17); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (9, 18); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (10, 19); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (10, 20); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (11, 21); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (11, 22); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (12, 23); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (12, 24); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (13, 25); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (13, 26); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (14, 27); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (14, 28); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (15, 29); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (15, 30); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (16, 31); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (16, 32); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (17, 33); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (17, 34); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (18, 35); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (18, 36); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (19, 37); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (19, 38); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (20, 39); +INSERT INTO BANK_ACCOUNT_BALANCES (BANK_ACCOUNT_ID, BALANCE_ID) +VALUES (20, 40); + diff --git a/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java b/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java new file mode 100644 index 0000000..a9b965a --- /dev/null +++ b/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java @@ -0,0 +1,179 @@ +package net.kapcake.bankingservice.validation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import net.kapcake.bankingservice.config.BankingServiceConfig; +import net.kapcake.bankingservice.exceptions.PaymentValidationException; +import net.kapcake.bankingservice.model.domain.BalanceType; +import net.kapcake.bankingservice.model.domain.Currency; +import net.kapcake.bankingservice.model.domain.IbanValidationResponse; +import net.kapcake.bankingservice.model.entities.Balance; +import net.kapcake.bankingservice.model.entities.BankAccount; +import net.kapcake.bankingservice.model.entities.Payment; +import net.kapcake.bankingservice.model.entities.User; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.client.ExpectedCount; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.response.MockRestResponseCreators; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withServiceUnavailable; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +@RestClientTest +@ContextConfiguration(classes = {BankingServiceConfig.class}) +class PaymentValidatorTest { + + private static final String VALIDATION_URL = "https://openiban.com/validate"; + private static final String FORBIDDEN_ACCOUNT_1 = "LU789665834233588091"; + private static final String FORBIDDEN_ACCOUNT_2 = "LU965552288868196734"; + private static final String VALID_BENEFICIARY = "LU785914975828731987"; + private static final String INVALID_BENEFICIARY = "LU785914975828731947"; + private static final List FORBIDDEN_ACCOUNTS = Arrays.asList(FORBIDDEN_ACCOUNT_1, FORBIDDEN_ACCOUNT_2); + private static final String USERNAME = "username"; + private static final String GIVER_ACCOUNT_NUMBER = "LU038359494013886902"; + private final RestTemplate restTemplate; + private final PaymentValidator paymentValidator; + private final MockRestServiceServer server; + private final ObjectMapper objectMapper; + private BankAccount bankAccount; + private Payment payment; + private User user; + private Balance balance; + + @Autowired + public PaymentValidatorTest(RestTemplate restTemplate, MockRestServiceServer server) { + this.restTemplate = restTemplate; + this.paymentValidator = new PaymentValidator(restTemplate, VALIDATION_URL, FORBIDDEN_ACCOUNTS); + objectMapper = new ObjectMapper(); + this.server = server; + } + + @BeforeEach + void setUp() throws JsonProcessingException { + user = new User().setId(1L).setUsername(USERNAME); + balance = new Balance().setId(1L).setType(BalanceType.AVAILABLE); + bankAccount = new BankAccount().setId(1L).setBalances(Collections.singletonList(balance)); + payment = new Payment(); + + // Set up valid payment + bankAccount.setAccountNumber(GIVER_ACCOUNT_NUMBER).setUsers(Collections.singletonList(user)); + balance.setCurrency(Currency.EUR).setAmount(BigDecimal.valueOf(5.5)); + user.setBankAccounts(Collections.singletonList(bankAccount)); + payment.setGiverAccount(bankAccount).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(VALID_BENEFICIARY).setAmount(BigDecimal.valueOf(4)); + + server + .expect(requestTo(VALIDATION_URL + "/" + VALID_BENEFICIARY)) + .andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(objectMapper.writeValueAsString(new IbanValidationResponse(true, Collections.emptyList(), VALID_BENEFICIARY)), MediaType.APPLICATION_JSON)); + + } + + @Test + void validate_correctPayment() { + paymentValidator.validate(user, payment); + } + + @Test + void validate_invalidGiverAccount() { + user.setBankAccounts(Collections.emptyList()); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Giver account not owned by authenticated user.", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidBeneficiaryAccount_ibanValidationIssue() { + payment.setBeneficiaryAccountNumber(INVALID_BENEFICIARY); + server.reset(); + server + .expect(requestTo(VALIDATION_URL + "/" + INVALID_BENEFICIARY)) + .andExpect(method(HttpMethod.GET)) + .andRespond(withServiceUnavailable()); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account could not be validated.", paymentValidationException.getMessage()); + + server.reset(); + server + .expect(requestTo(VALIDATION_URL + "/" + INVALID_BENEFICIARY)) + .andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess()); + + paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account could not be validated.", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidBeneficiaryAccount_invalidIban() throws JsonProcessingException { + payment.setBeneficiaryAccountNumber(INVALID_BENEFICIARY); + server.reset(); + server + .expect(requestTo(VALIDATION_URL + "/" + INVALID_BENEFICIARY)) + .andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(objectMapper.writeValueAsString(new IbanValidationResponse(false, Collections.singletonList("Invalid"), VALID_BENEFICIARY)), MediaType.APPLICATION_JSON)); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account not valid: [Invalid]", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidBeneficiaryAccount_giverAndBeneficiarySame() { + bankAccount.setAccountNumber(VALID_BENEFICIARY); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary and giver account are the same.", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidBeneficiaryAccount_forbiddenAccount() throws JsonProcessingException { + payment.setBeneficiaryAccountNumber(FORBIDDEN_ACCOUNT_1); + server.reset(); + server + .expect(requestTo(VALIDATION_URL + "/" + FORBIDDEN_ACCOUNT_1)) + .andExpect(method(HttpMethod.GET)) + .andRespond(withSuccess(objectMapper.writeValueAsString(new IbanValidationResponse(true, Collections.emptyList(), FORBIDDEN_ACCOUNT_1)), MediaType.APPLICATION_JSON)); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account is forbidden.", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidAccountBalance_notFound() { + balance.setType(BalanceType.END_OF_DAY); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Unable to retrieve available account balance.", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidAccountBalance_differentCurrencies() { + balance.setCurrency(Currency.USD); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Payment and account currency must be the same.", paymentValidationException.getMessage()); + } + + @Test + void validate_invalidAccountBalance_insufficientBalance() { + balance.setAmount(BigDecimal.valueOf(2)); + + PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Available account balance not sufficient.", paymentValidationException.getMessage()); + } +} \ No newline at end of file