Payment creation with validation and response
* Move mapping inside service * Extract validation to utility class * Add test for validation
This commit is contained in:
@@ -8,7 +8,6 @@ import org.springframework.web.client.RestTemplate;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class BankingServiceConfig {
|
public class BankingServiceConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ModelMapper modelMapper() {
|
public ModelMapper modelMapper() {
|
||||||
return new ModelMapper();
|
return new ModelMapper();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package net.kapcake.bankingservice.controllers;
|
package net.kapcake.bankingservice.controllers;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
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.exceptions.PaymentValidationException;
|
||||||
import net.kapcake.bankingservice.model.domain.PaymentDTO;
|
import net.kapcake.bankingservice.model.domain.PaymentDTO;
|
||||||
import net.kapcake.bankingservice.model.domain.BankAccountDTO;
|
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.model.entities.Payment;
|
||||||
import net.kapcake.bankingservice.repositories.BankAccountRepository;
|
|
||||||
import net.kapcake.bankingservice.security.UserDetailsImpl;
|
import net.kapcake.bankingservice.security.UserDetailsImpl;
|
||||||
import net.kapcake.bankingservice.services.AccountService;
|
import net.kapcake.bankingservice.services.AccountService;
|
||||||
import net.kapcake.bankingservice.services.PaymentService;
|
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 org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class BankingServiceController {
|
public class BankingServiceController {
|
||||||
private final BankAccountRepository bankAccountRepository;
|
|
||||||
private final AccountService accountService;
|
private final AccountService accountService;
|
||||||
private final PaymentService paymentService;
|
private final PaymentService paymentService;
|
||||||
private final ModelMapper modelMapper;
|
|
||||||
|
|
||||||
public BankingServiceController(BankAccountRepository bankAccountRepository, AccountService accountService, PaymentService paymentService, ModelMapper modelMapper) {
|
public BankingServiceController(AccountService accountService, PaymentService paymentService) {
|
||||||
this.bankAccountRepository = bankAccountRepository;
|
|
||||||
this.accountService = accountService;
|
this.accountService = accountService;
|
||||||
this.paymentService = paymentService;
|
this.paymentService = paymentService;
|
||||||
this.modelMapper = modelMapper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/accounts")
|
@GetMapping("/accounts")
|
||||||
public List<BankAccountDTO> getAccounts(@AuthenticationPrincipal UserDetailsImpl authenticatedUser) {
|
public List<BankAccountDTO> getAccounts(@AuthenticationPrincipal UserDetailsImpl authenticatedUser) {
|
||||||
return accountService.getAccounts(authenticatedUser.user()).stream()
|
return accountService.getAccounts(authenticatedUser);
|
||||||
.map(account -> modelMapper.map(account, BankAccountDTO.class)).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/payment")
|
@PostMapping("/payment")
|
||||||
@@ -46,23 +40,8 @@ public class BankingServiceController {
|
|||||||
if (bindingResult.hasErrors()) {
|
if (bindingResult.hasErrors()) {
|
||||||
throw new PaymentValidationException("Payment request invalid: " + bindingResult.getAllErrors());
|
throw new PaymentValidationException("Payment request invalid: " + bindingResult.getAllErrors());
|
||||||
}
|
}
|
||||||
Payment payment = mapToEntity(paymentDTO);
|
return paymentService.createPayment(authenticatedUser, paymentDTO);
|
||||||
Payment createdPayment = paymentService.createPayment(authenticatedUser.user(), payment);
|
|
||||||
return mapToDTO(createdPayment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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<BankAccount> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<E, D> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -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<BankAccount, BankAccountDTO> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Payment, PaymentDTO> {
|
||||||
|
private final BankAccountRepository bankAccountRepository;
|
||||||
|
|
||||||
|
public PaymentConverter(BankAccountRepository bankAccountRepository, ModelMapper modelMapper) {
|
||||||
|
super(modelMapper);
|
||||||
|
this.bankAccountRepository = bankAccountRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PaymentDTO mapToDTO(Payment payment) {
|
||||||
|
TypeMap<Payment, PaymentDTO> 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<BankAccount> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import java.math.BigDecimal;
|
|||||||
public class BalanceDTO {
|
public class BalanceDTO {
|
||||||
private Long id;
|
private Long id;
|
||||||
@NotNull
|
@NotNull
|
||||||
@DecimalMin(value = "0", inclusive = false)
|
@DecimalMin(value = "0")
|
||||||
private BigDecimal amount;
|
private BigDecimal amount;
|
||||||
@NotNull
|
@NotNull
|
||||||
private Currency currency;
|
private Currency currency;
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package net.kapcake.bankingservice.model.entities;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
import net.kapcake.bankingservice.model.domain.BalanceType;
|
import net.kapcake.bankingservice.model.domain.BalanceType;
|
||||||
import net.kapcake.bankingservice.model.domain.Currency;
|
import net.kapcake.bankingservice.model.domain.Currency;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Accessors(chain = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class Balance {
|
public class Balance {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package net.kapcake.bankingservice.model.entities;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
import net.kapcake.bankingservice.model.domain.AccountStatus;
|
import net.kapcake.bankingservice.model.domain.AccountStatus;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Accessors(chain = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class BankAccount {
|
public class BankAccount {
|
||||||
@@ -25,6 +27,10 @@ public class BankAccount {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String accountName;
|
private String accountName;
|
||||||
@OneToMany
|
@OneToMany
|
||||||
|
@JoinTable(
|
||||||
|
joinColumns = {@JoinColumn(name = "BANK_ACCOUNT_ID", nullable = false)},
|
||||||
|
inverseJoinColumns = {@JoinColumn(name = "BALANCE_ID", nullable = false)}
|
||||||
|
)
|
||||||
private List<Balance> balances;
|
private List<Balance> balances;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private AccountStatus status;
|
private AccountStatus status;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package net.kapcake.bankingservice.model.entities;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
import net.kapcake.bankingservice.model.domain.Currency;
|
import net.kapcake.bankingservice.model.domain.Currency;
|
||||||
import net.kapcake.bankingservice.model.domain.PaymentStatus;
|
import net.kapcake.bankingservice.model.domain.PaymentStatus;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
@@ -11,6 +12,7 @@ import java.math.BigDecimal;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@Accessors(chain = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class Payment {
|
public class Payment {
|
||||||
@@ -32,7 +34,7 @@ public class Payment {
|
|||||||
private String communication;
|
private String communication;
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Temporal(TemporalType.TIMESTAMP)
|
@Temporal(TemporalType.TIMESTAMP)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false, updatable = false)
|
||||||
private Date creationDate;
|
private Date creationDate;
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private PaymentStatus status;
|
private PaymentStatus status;
|
||||||
|
|||||||
@@ -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<Balance, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,4 +8,6 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface BankAccountRepository extends JpaRepository<BankAccount, Long> {
|
public interface BankAccountRepository extends JpaRepository<BankAccount, Long> {
|
||||||
Optional<List<BankAccount>> findAllByUsers_username(String username);
|
Optional<List<BankAccount>> findAllByUsers_username(String username);
|
||||||
|
|
||||||
|
Optional<BankAccount> findByAccountNumber(String accountNumber);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package net.kapcake.bankingservice.services;
|
package net.kapcake.bankingservice.services;
|
||||||
|
|
||||||
import net.kapcake.bankingservice.model.entities.BankAccount;
|
import net.kapcake.bankingservice.converters.BankAccountConverter;
|
||||||
import net.kapcake.bankingservice.model.entities.User;
|
import net.kapcake.bankingservice.model.domain.BankAccountDTO;
|
||||||
import net.kapcake.bankingservice.repositories.BankAccountRepository;
|
import net.kapcake.bankingservice.repositories.BankAccountRepository;
|
||||||
|
import net.kapcake.bankingservice.security.UserDetailsImpl;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -11,12 +12,15 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class AccountService {
|
public class AccountService {
|
||||||
private final BankAccountRepository bankAccountRepository;
|
private final BankAccountRepository bankAccountRepository;
|
||||||
|
private final BankAccountConverter bankAccountConverter;
|
||||||
|
|
||||||
public AccountService(BankAccountRepository bankAccountRepository) {
|
public AccountService(BankAccountRepository bankAccountRepository, BankAccountConverter bankAccountConverter) {
|
||||||
this.bankAccountRepository = bankAccountRepository;
|
this.bankAccountRepository = bankAccountRepository;
|
||||||
|
this.bankAccountConverter = bankAccountConverter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BankAccount> getAccounts(User user) {
|
public List<BankAccountDTO> getAccounts(UserDetailsImpl authenticatedUser) {
|
||||||
return bankAccountRepository.findAllByUsers_username(user.getUsername()).orElse(Collections.emptyList());
|
return bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()).orElse(Collections.emptyList())
|
||||||
|
.stream().map(bankAccountConverter::mapToDTO).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,63 @@
|
|||||||
package net.kapcake.bankingservice.services;
|
package net.kapcake.bankingservice.services;
|
||||||
|
|
||||||
import net.kapcake.bankingservice.exceptions.PaymentValidationException;
|
import net.kapcake.bankingservice.converters.PaymentConverter;
|
||||||
import net.kapcake.bankingservice.model.domain.IbanValidationResponse;
|
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.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.BankAccountRepository;
|
||||||
import net.kapcake.bankingservice.repositories.PaymentRepository;
|
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.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
|
@Service
|
||||||
public class PaymentService {
|
public class PaymentService {
|
||||||
private final PaymentRepository paymentRepository;
|
private final PaymentRepository paymentRepository;
|
||||||
private final BankAccountRepository bankAccountRepository;
|
private final BankAccountRepository bankAccountRepository;
|
||||||
private final RestTemplate restTemplate;
|
private final BalanceRepository balanceRepository;
|
||||||
private final String validationUrl;
|
private final PaymentValidator paymentValidator;
|
||||||
|
private final PaymentConverter paymentConverter;
|
||||||
|
private final TransactionTemplate transactionTemplate;
|
||||||
|
|
||||||
public PaymentService(PaymentRepository paymentRepository,
|
public PaymentService(PaymentRepository paymentRepository, BankAccountRepository bankAccountRepository,
|
||||||
BankAccountRepository bankAccountRepository,
|
BalanceRepository balanceRepository, PaymentValidator paymentValidator, PaymentConverter paymentConverter,
|
||||||
RestTemplate restTemplate,
|
PlatformTransactionManager transactionManager) {
|
||||||
@Value("${banking-service.iban-validation.url}") String validationUrl) {
|
|
||||||
this.paymentRepository = paymentRepository;
|
this.paymentRepository = paymentRepository;
|
||||||
this.bankAccountRepository = bankAccountRepository;
|
this.bankAccountRepository = bankAccountRepository;
|
||||||
this.restTemplate = restTemplate;
|
this.balanceRepository = balanceRepository;
|
||||||
this.validationUrl = validationUrl;
|
this.paymentValidator = paymentValidator;
|
||||||
|
this.paymentConverter = paymentConverter;
|
||||||
|
this.transactionTemplate = new TransactionTemplate(transactionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Payment createPayment(User user, Payment payment) {
|
public PaymentDTO createPayment(UserDetailsImpl authenticatedUser, PaymentDTO paymentDTO) {
|
||||||
validatePayment(user, payment);
|
Payment payment = paymentConverter.mapToEntity(paymentDTO);
|
||||||
|
|
||||||
return null;
|
paymentValidator.validate(authenticatedUser.user(), payment);
|
||||||
}
|
|
||||||
|
|
||||||
private void validatePayment(User user, Payment payment) {
|
BankAccount giverAccount = payment.getGiverAccount();
|
||||||
if (user.getBankAccounts().stream().noneMatch(bankAccount -> bankAccount.getId().equals(payment.getGiverAccount().getId()))) {
|
Optional<BankAccount> beneficiaryAccount = bankAccountRepository.findByAccountNumber(payment.getBeneficiaryAccountNumber());
|
||||||
throw new PaymentValidationException("Giver account not owned by authenticated user.");
|
|
||||||
}
|
Payment persistedPayment = transactionTemplate.execute(status -> {
|
||||||
IbanValidationResponse validationResponse = restTemplate.getForObject(validationUrl + payment.getBeneficiaryAccountNumber(), IbanValidationResponse.class);
|
Payment savedPayment = paymentRepository.save(payment);
|
||||||
if (!validationResponse.valid()) {
|
Balance giverAvailableBalance = giverAccount.getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow();
|
||||||
throw new PaymentValidationException("Beneficiary account not valid: " + validationResponse.messages());
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String> forbiddenAccounts;
|
||||||
|
|
||||||
|
public PaymentValidator(RestTemplate restTemplate,
|
||||||
|
@Value("${banking-service.validation.iban-validator-url:}") String validationUrl,
|
||||||
|
@Value("${banking-service.validation.forbidden-accounts:}") List<String> 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<Balance> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> forbiddenAccounts = Collections.emptyList();
|
||||||
|
private String ibanValidatorUrl = "";
|
||||||
|
}
|
||||||
@@ -9,5 +9,8 @@ spring:
|
|||||||
defer-datasource-initialization: true
|
defer-datasource-initialization: true
|
||||||
|
|
||||||
banking-service:
|
banking-service:
|
||||||
iban-validation:
|
validation:
|
||||||
url: https://openiban.com/validate/
|
forbidden-accounts: >
|
||||||
|
LU280019400644750000,
|
||||||
|
LU120010001234567891
|
||||||
|
iban-validator-url: https://openiban.com/validate
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
-- Users
|
||||||
INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD)
|
INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD)
|
||||||
VALUES (1, 'user1', '{bcrypt}$2a$10$4dPs2u01F/UBJtQyKRCRLevJACUkDzSdD.4EFKkf0T0qllqtkxw5e');
|
VALUES (1, 'user1', '{bcrypt}$2a$10$4dPs2u01F/UBJtQyKRCRLevJACUkDzSdD.4EFKkf0T0qllqtkxw5e');
|
||||||
INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD)
|
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)
|
INSERT INTO BANKING_USER (ID, USERNAME, PASSWORD)
|
||||||
VALUES (8, 'user8', '{bcrypt}$2a$10$L1RWBgjH0L.YJG/uyMhviubSxz8C.PnZJlviF6K/iueeDrWO.FSOy');
|
VALUES (8, 'user8', '{bcrypt}$2a$10$L1RWBgjH0L.YJG/uyMhviubSxz8C.PnZJlviF6K/iueeDrWO.FSOy');
|
||||||
|
|
||||||
|
-- Bank accounts
|
||||||
INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS)
|
INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS)
|
||||||
VALUES (1, 'Personal', 'LU584022594948990503', 'ENABLED');
|
VALUES (1, 'Personal', 'LU584022594948990503', 'ENABLED');
|
||||||
INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS)
|
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)
|
INSERT INTO BANK_ACCOUNT (ID, ACCOUNT_NAME, ACCOUNT_NUMBER, STATUS)
|
||||||
VALUES (20, 'Trading', 'LU081651725326393823', 'ENABLED');
|
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)
|
INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID)
|
||||||
VALUES (1, 1);
|
VALUES (1, 1);
|
||||||
INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID)
|
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);
|
VALUES (19, 2);
|
||||||
INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID)
|
INSERT INTO BANK_ACCOUNT_USERS (BANK_ACCOUNT_ID, USER_ID)
|
||||||
VALUES (20, 8);
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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<String> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user