Add endpoint to list payments

This commit is contained in:
2023-05-12 23:32:30 +02:00
parent df46537266
commit 890c030109
17 changed files with 370 additions and 75 deletions

View File

@@ -1,18 +1,17 @@
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.exceptions.ValidationException;
import net.kapcake.bankingservice.converters.PaymentConverter; import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.exceptions.PaymentValidationException; import net.kapcake.bankingservice.model.dtos.BankAccountDTO;
import net.kapcake.bankingservice.model.domain.PaymentDTO; import net.kapcake.bankingservice.model.dtos.PaymentFilter;
import net.kapcake.bankingservice.model.domain.BankAccountDTO;
import net.kapcake.bankingservice.model.entities.Payment;
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;
import org.modelmapper.ModelMapper;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -38,10 +37,35 @@ public class BankingServiceController {
@PostMapping("/payment") @PostMapping("/payment")
public PaymentDTO createPayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @RequestBody @Valid PaymentDTO paymentDTO, BindingResult bindingResult) { public PaymentDTO createPayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @RequestBody @Valid PaymentDTO paymentDTO, BindingResult bindingResult) {
if (bindingResult.hasErrors()) { if (bindingResult.hasErrors()) {
throw new PaymentValidationException("Payment request invalid: " + bindingResult.getAllErrors()); String errorString = getErrorString(bindingResult);
throw new ValidationException("Payment request invalid: " + errorString);
} }
return paymentService.createPayment(authenticatedUser, paymentDTO); return paymentService.createPayment(authenticatedUser, paymentDTO);
} }
@GetMapping("/payments")
public List<PaymentDTO> getPayments(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @RequestBody(required = false) @Valid PaymentFilter paymentFilter, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
String errorString = getErrorString(bindingResult);
throw new ValidationException("Payment filter is invalid: " + errorString);
}
return paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
}
private static String getErrorString(BindingResult bindingResult) {
StringBuilder builder = new StringBuilder("[");
List<ObjectError> allErrors = bindingResult.getAllErrors();
for (int i = 0; i < allErrors.size(); i++){
ObjectError error = allErrors.get(i);
if (i != 0) {
builder.append("\n");
}
if (error instanceof FieldError) {
builder.append(((FieldError) error).getField());
}
builder.append(": ").append(error.getDefaultMessage());
}
builder.append("]");
return builder.toString();
}
} }

View File

@@ -1,6 +1,6 @@
package net.kapcake.bankingservice.controllers; package net.kapcake.bankingservice.controllers;
import net.kapcake.bankingservice.exceptions.PaymentValidationException; import net.kapcake.bankingservice.exceptions.ValidationException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -11,8 +11,8 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep
@ControllerAdvice @ControllerAdvice
public class BankingServiceExceptionHandler extends ResponseEntityExceptionHandler { public class BankingServiceExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({PaymentValidationException.class}) @ExceptionHandler({ValidationException.class})
protected ResponseEntity<Object> handlePaymentValidationException(PaymentValidationException exception, WebRequest request) { protected ResponseEntity<Object> handlePaymentValidationException(ValidationException exception, WebRequest request) {
return this.handleExceptionInternal(exception, exception.getMessage(), return this.handleExceptionInternal(exception, exception.getMessage(),
new HttpHeaders(), HttpStatus.BAD_REQUEST, request); new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
} }

View File

@@ -1,6 +1,6 @@
package net.kapcake.bankingservice.converters; package net.kapcake.bankingservice.converters;
import net.kapcake.bankingservice.exceptions.PaymentValidationException; import net.kapcake.bankingservice.exceptions.ValidationException;
import net.kapcake.bankingservice.model.dtos.PaymentDTO; import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.model.domain.BankAccount; import net.kapcake.bankingservice.model.domain.BankAccount;
import net.kapcake.bankingservice.model.domain.Payment; import net.kapcake.bankingservice.model.domain.Payment;
@@ -25,8 +25,7 @@ public class PaymentConverter extends AbstractConverter<Payment, PaymentDTO> {
TypeMap<Payment, PaymentDTO> paymentPaymentDTOTypeMap = modelMapper TypeMap<Payment, PaymentDTO> paymentPaymentDTOTypeMap = modelMapper
.typeMap(Payment.class, PaymentDTO.class) .typeMap(Payment.class, PaymentDTO.class)
.addMapping(src -> src.getGiverAccount().getId(), PaymentDTO::setGiverAccount); .addMapping(src -> src.getGiverAccount().getId(), PaymentDTO::setGiverAccount);
PaymentDTO paymentDTO = paymentPaymentDTOTypeMap.map(payment); return paymentPaymentDTOTypeMap.map(payment);
return paymentDTO;
} }
@Override @Override
@@ -34,7 +33,7 @@ public class PaymentConverter extends AbstractConverter<Payment, PaymentDTO> {
Payment payment = modelMapper.map(paymentDTO, Payment.class); Payment payment = modelMapper.map(paymentDTO, Payment.class);
Optional<BankAccount> bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount()); Optional<BankAccount> bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount());
if (bankAccountOptional.isEmpty()) { if (bankAccountOptional.isEmpty()) {
throw new PaymentValidationException("Payment request invalid: Giver account does not exist."); throw new ValidationException("Payment request invalid: Giver account does not exist.");
} }
payment.setGiverAccount(bankAccountOptional.get()); payment.setGiverAccount(bankAccountOptional.get());
return payment; return payment;

View File

@@ -1,10 +0,0 @@
package net.kapcake.bankingservice.exceptions;
public class PaymentValidationException extends IllegalArgumentException {
public PaymentValidationException(String message) {
super(message);
}
public PaymentValidationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,10 @@
package net.kapcake.bankingservice.exceptions;
public class ValidationException extends IllegalArgumentException {
public ValidationException(String message) {
super(message);
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -3,13 +3,16 @@ package net.kapcake.bankingservice.model.dtos;
import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
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.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
@Data @Data
public class BalanceDTO { @Accessors(chain = true)
public class BalanceDTO implements Serializable {
private Long id; private Long id;
@NotNull @NotNull
@DecimalMin(value = "0") @DecimalMin(value = "0")

View File

@@ -5,12 +5,15 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
import net.kapcake.bankingservice.model.domain.AccountStatus; import net.kapcake.bankingservice.model.domain.AccountStatus;
import java.io.Serializable;
import java.util.List; import java.util.List;
@Data @Data
public class BankAccountDTO { @Accessors(chain = true)
public class BankAccountDTO implements Serializable {
private Long id; private Long id;
@NotBlank @NotBlank
private String accountNumber; private String accountNumber;

View File

@@ -3,14 +3,17 @@ package net.kapcake.bankingservice.model.dtos;
import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
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 java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
@Data @Data
public class PaymentDTO { @Accessors(chain = true)
public class PaymentDTO implements Serializable {
private Long id; private Long id;
@NotNull @NotNull
@DecimalMin(value = "0", inclusive = false) @DecimalMin(value = "0", inclusive = false)

View File

@@ -0,0 +1,33 @@
package net.kapcake.bankingservice.model.dtos;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
public class PaymentFilter implements Serializable {
private String beneficiaryAccountNumber;
private Date startDate;
private Date endDate;
@AssertTrue(message = "Start and end date need to be in order")
public boolean isValidRange() {
if (startDate == null || endDate == null) {
return true;
} else {
return startDate.compareTo(endDate) <= 0;
}
}
@AssertTrue(message = "At least one field needs to be supplied")
public boolean isAtLeastOneFieldFilled() {
return startDate != null ||
endDate != null ||
(beneficiaryAccountNumber != null && !beneficiaryAccountNumber.isBlank());
}
}

View File

@@ -2,9 +2,13 @@ package net.kapcake.bankingservice.model.dtos;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data @Data
public class UserDTO { @Accessors(chain = true)
public class UserDTO implements Serializable {
private Long id; private Long id;
@NotEmpty @NotEmpty
private String username; private String username;

View File

@@ -7,7 +7,7 @@ import java.util.List;
import java.util.Optional; 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); List<BankAccount> findAllByUsers_username(String username);
Optional<BankAccount> findByAccountNumber(String accountNumber); Optional<BankAccount> findByAccountNumber(String accountNumber);
} }

View File

@@ -1,7 +1,11 @@
package net.kapcake.bankingservice.repositories; package net.kapcake.bankingservice.repositories;
import net.kapcake.bankingservice.model.domain.BankAccount;
import net.kapcake.bankingservice.model.domain.Payment; import net.kapcake.bankingservice.model.domain.Payment;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PaymentRepository extends JpaRepository<Payment, Long> { public interface PaymentRepository extends JpaRepository<Payment, Long> {
List<Payment> findAllByGiverAccountIn(List<BankAccount> bankAccounts);
} }

View File

@@ -6,7 +6,6 @@ import net.kapcake.bankingservice.repositories.BankAccountRepository;
import net.kapcake.bankingservice.security.UserDetailsImpl; import net.kapcake.bankingservice.security.UserDetailsImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List; import java.util.List;
@Service @Service
@@ -20,7 +19,7 @@ public class AccountService {
} }
public List<BankAccountDTO> getAccounts(UserDetailsImpl authenticatedUser) { public List<BankAccountDTO> getAccounts(UserDetailsImpl authenticatedUser) {
return bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()).orElse(Collections.emptyList()) return bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername())
.stream().map(bankAccountConverter::mapToDTO).toList(); .stream().map(bankAccountConverter::mapToDTO).toList();
} }
} }

View File

@@ -1,11 +1,12 @@
package net.kapcake.bankingservice.services; package net.kapcake.bankingservice.services;
import net.kapcake.bankingservice.converters.PaymentConverter; import net.kapcake.bankingservice.converters.PaymentConverter;
import net.kapcake.bankingservice.model.domain.BalanceType;
import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.model.domain.Balance; import net.kapcake.bankingservice.model.domain.Balance;
import net.kapcake.bankingservice.model.domain.BalanceType;
import net.kapcake.bankingservice.model.domain.BankAccount; import net.kapcake.bankingservice.model.domain.BankAccount;
import net.kapcake.bankingservice.model.domain.Payment; import net.kapcake.bankingservice.model.domain.Payment;
import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.model.dtos.PaymentFilter;
import net.kapcake.bankingservice.repositories.BalanceRepository; 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;
@@ -15,7 +16,11 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
@Service @Service
public class PaymentService { public class PaymentService {
@@ -47,16 +52,61 @@ public class PaymentService {
Payment persistedPayment = transactionTemplate.execute(status -> { Payment persistedPayment = transactionTemplate.execute(status -> {
Payment savedPayment = paymentRepository.save(payment); Payment savedPayment = paymentRepository.save(payment);
Balance giverAvailableBalance = giverAccount.getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow(); updateBalances(payment, giverAccount, beneficiaryAccount);
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 savedPayment;
}); });
return paymentConverter.mapToDTO(persistedPayment); return paymentConverter.mapToDTO(persistedPayment);
} }
private void updateBalances(Payment payment, BankAccount giverAccount, Optional<BankAccount> beneficiaryAccount) {
updateGiverBalance(payment, giverAccount);
updateBeneficiaryBalance(payment, beneficiaryAccount);
}
private void updateBeneficiaryBalance(Payment payment, Optional<BankAccount> beneficiaryAccount) {
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);
}
}
private void updateGiverBalance(Payment payment, BankAccount giverAccount) {
Balance giverAvailableBalance = giverAccount.getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow();
giverAvailableBalance.setAmount(giverAvailableBalance.getAmount().subtract(payment.getAmount()));
balanceRepository.save(giverAvailableBalance);
}
public List<PaymentDTO> getPaymentsForUser(UserDetailsImpl authenticatedUser, PaymentFilter paymentFilter) {
List<BankAccount> userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername());
List<Payment> userPayments = paymentRepository.findAllByGiverAccountIn(userBankAccounts);
List<Payment> filteredPayments = getFilteredPayments(paymentFilter, userPayments);
filteredPayments.sort(Comparator.comparing(Payment::getCreationDate));
return filteredPayments.stream().map(paymentConverter::mapToDTO).toList();
}
private static List<Payment> getFilteredPayments(PaymentFilter paymentFilter, List<Payment> userPayments) {
List<Payment> filteredPayments = userPayments;
if (paymentFilter != null) {
String beneficiaryAccountNumber = paymentFilter.getBeneficiaryAccountNumber();
Date startDate = paymentFilter.getStartDate();
Date endDate = paymentFilter.getEndDate();
filteredPayments = userPayments.stream().filter(payment -> {
boolean filter = true;
if (beneficiaryAccountNumber != null) {
filter &= payment.getBeneficiaryAccountNumber().equals(beneficiaryAccountNumber);
}
if (startDate != null) {
filter &= payment.getCreationDate().compareTo(startDate) >= 0;
}
if (endDate != null) {
filter &= payment.getCreationDate().compareTo(endDate) <= 0;
}
return filter;
}).collect(Collectors.toList());
}
return filteredPayments;
}
} }

View File

@@ -1,6 +1,6 @@
package net.kapcake.bankingservice.validation; package net.kapcake.bankingservice.validation;
import net.kapcake.bankingservice.exceptions.PaymentValidationException; import net.kapcake.bankingservice.exceptions.ValidationException;
import net.kapcake.bankingservice.model.domain.BalanceType; import net.kapcake.bankingservice.model.domain.BalanceType;
import net.kapcake.bankingservice.model.domain.IbanValidationResponse; import net.kapcake.bankingservice.model.domain.IbanValidationResponse;
import net.kapcake.bankingservice.model.domain.Balance; import net.kapcake.bankingservice.model.domain.Balance;
@@ -29,7 +29,7 @@ public class PaymentValidator {
} }
public void validate(User user, Payment payment) throws PaymentValidationException { public void validate(User user, Payment payment) throws ValidationException {
validateGiverAccount(user, payment); validateGiverAccount(user, payment);
validateBeneficiaryAccount(payment); validateBeneficiaryAccount(payment);
validateAccountBalance(payment); validateAccountBalance(payment);
@@ -38,45 +38,45 @@ public class PaymentValidator {
private static void validateGiverAccount(User user, Payment payment) { private static void validateGiverAccount(User user, Payment payment) {
boolean userOwnsGiverAccount = user.getBankAccounts().stream().anyMatch(bankAccount -> bankAccount.getId().equals(payment.getGiverAccount().getId())); boolean userOwnsGiverAccount = user.getBankAccounts().stream().anyMatch(bankAccount -> bankAccount.getId().equals(payment.getGiverAccount().getId()));
if (!userOwnsGiverAccount) { if (!userOwnsGiverAccount) {
throw new PaymentValidationException("Giver account not owned by authenticated user."); throw new ValidationException("Giver account not owned by authenticated user.");
} }
} }
private void validateBeneficiaryAccount(Payment payment) { private void validateBeneficiaryAccount(Payment payment) {
IbanValidationResponse validationResponse = null; IbanValidationResponse validationResponse;
try { try {
validationResponse = restTemplate.getForObject(validationUrl + "/" + payment.getBeneficiaryAccountNumber(), IbanValidationResponse.class); validationResponse = restTemplate.getForObject(validationUrl + "/" + payment.getBeneficiaryAccountNumber(), IbanValidationResponse.class);
} catch (RestClientException e) { } catch (RestClientException e) {
throw new PaymentValidationException("Beneficiary account could not be validated."); throw new ValidationException("Beneficiary account could not be validated.");
} }
if (validationResponse == null) { if (validationResponse == null) {
throw new PaymentValidationException("Beneficiary account could not be validated."); throw new ValidationException("Beneficiary account could not be validated.");
} else if (!validationResponse.valid()) { } else if (!validationResponse.valid()) {
throw new PaymentValidationException("Beneficiary account not valid: " + validationResponse.messages()); throw new ValidationException("Beneficiary account not valid: " + validationResponse.messages());
} }
boolean sameGiverAndBeneficiary = payment.getGiverAccount().getAccountNumber().equals(payment.getBeneficiaryAccountNumber()); boolean sameGiverAndBeneficiary = payment.getGiverAccount().getAccountNumber().equals(payment.getBeneficiaryAccountNumber());
if (sameGiverAndBeneficiary) { if (sameGiverAndBeneficiary) {
throw new PaymentValidationException("Beneficiary and giver account are the same."); throw new ValidationException("Beneficiary and giver account are the same.");
} }
boolean isBeneficiaryForbidden = forbiddenAccounts.contains(payment.getBeneficiaryAccountNumber()); boolean isBeneficiaryForbidden = forbiddenAccounts.contains(payment.getBeneficiaryAccountNumber());
if (isBeneficiaryForbidden) { if (isBeneficiaryForbidden) {
throw new PaymentValidationException("Beneficiary account is forbidden."); throw new ValidationException("Beneficiary account is forbidden.");
} }
} }
private void validateAccountBalance(Payment payment) { private void validateAccountBalance(Payment payment) {
List<Balance> availableBalances = payment.getGiverAccount().getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).toList(); List<Balance> availableBalances = payment.getGiverAccount().getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).toList();
if (availableBalances.size() != 1) { if (availableBalances.size() != 1) {
throw new PaymentValidationException("Unable to retrieve available account balance."); throw new ValidationException("Unable to retrieve available account balance.");
} }
Balance balance = availableBalances.get(0); Balance balance = availableBalances.get(0);
boolean sameCurrencies = balance.getCurrency().equals(payment.getCurrency()); boolean sameCurrencies = balance.getCurrency().equals(payment.getCurrency());
if (!sameCurrencies) { if (!sameCurrencies) {
throw new PaymentValidationException("Payment and account currency must be the same."); throw new ValidationException("Payment and account currency must be the same.");
} }
int compareResult = balance.getAmount().compareTo(payment.getAmount()); int compareResult = balance.getAmount().compareTo(payment.getAmount());
if (compareResult < 0) { if (compareResult < 0) {
throw new PaymentValidationException("Available account balance not sufficient."); throw new ValidationException("Available account balance not sufficient.");
} }
} }
} }

View File

@@ -0,0 +1,175 @@
package net.kapcake.bankingservice.services;
import net.kapcake.bankingservice.converters.PaymentConverter;
import net.kapcake.bankingservice.model.domain.Currency;
import net.kapcake.bankingservice.model.domain.*;
import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.model.dtos.PaymentFilter;
import net.kapcake.bankingservice.repositories.BalanceRepository;
import net.kapcake.bankingservice.repositories.BankAccountRepository;
import net.kapcake.bankingservice.repositories.PaymentRepository;
import net.kapcake.bankingservice.security.UserDetailsImpl;
import net.kapcake.bankingservice.validation.PaymentValidator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.transaction.PlatformTransactionManager;
import java.math.BigDecimal;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
public static final String TESTUSER1 = "test-user-1";
public static final String TESTUSER2 = "test-user-2";
public static final String ACCOUNT_NUMBER_1 = "LU347280737562934669";
public static final String ACCOUNT_NUMBER_2 = "LU559999005150266806";
public static final String ACCOUNT_NUMBER_3 = "LU293855258657559167";
@Mock
private PaymentValidator paymentValidator;
@Mock
private PaymentRepository paymentRepository;
@Mock
private BankAccountRepository bankAccountRepository;
@Mock
private BalanceRepository balanceRepository;
@Mock
private PlatformTransactionManager transactionManager;
@Mock
private PaymentConverter paymentConverter;
private PaymentService paymentService;
private User user1;
private User user2;
private BankAccount bankAccount1;
private BankAccount bankAccount2;
private Balance balance1;
private Balance balance2;
@BeforeEach
void beforeEach() {
paymentService = new PaymentService(paymentRepository, bankAccountRepository, balanceRepository, paymentValidator, paymentConverter, transactionManager);
user1 = new User().setId(1L).setUsername(TESTUSER1);
user2 = new User().setId(2L).setUsername(TESTUSER2);
balance1 = new Balance().setId(1L).setType(BalanceType.AVAILABLE).setCurrency(Currency.EUR).setAmount(BigDecimal.valueOf(500));
balance2 = new Balance().setId(2L).setType(BalanceType.AVAILABLE).setCurrency(Currency.EUR).setAmount(BigDecimal.valueOf(500));
bankAccount1 = new BankAccount().setId(1L).setAccountNumber(ACCOUNT_NUMBER_1).setBalances(Collections.singletonList(balance1)).setUsers(Collections.singletonList(user1));
bankAccount2 = new BankAccount().setId(2L).setAccountNumber(ACCOUNT_NUMBER_2).setBalances(Collections.singletonList(balance2)).setUsers(Collections.singletonList(user2));
}
@Test
void createPayment() {
UserDetailsImpl authenticatedUser = new UserDetailsImpl(user1);
PaymentDTO paymentDTO = new PaymentDTO().setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L);
Payment payment = new Payment().setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1);
when(bankAccountRepository.findByAccountNumber(ACCOUNT_NUMBER_2)).thenReturn(Optional.of(bankAccount2));
when(paymentConverter.mapToEntity(paymentDTO)).thenReturn(payment);
when(paymentRepository.save(payment)).thenReturn(payment);
when(paymentConverter.mapToDTO(payment)).thenReturn(paymentDTO);
paymentService.createPayment(authenticatedUser, paymentDTO);
assertEquals(balance1.getAmount(), BigDecimal.valueOf(450));
assertEquals(balance2.getAmount(), BigDecimal.valueOf(550));
}
@Test
void getPaymentsForUser() {
UserDetailsImpl authenticatedUser = new UserDetailsImpl(user1);
Payment payment1 = new Payment().setId(1L).setCreationDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 0).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1);
PaymentDTO paymentDTO1 = new PaymentDTO().setId(1L).setCreationDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 0).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L);
Payment payment2 = new Payment().setId(2L).setCreationDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1);
PaymentDTO paymentDTO2 = new PaymentDTO().setId(2L).setCreationDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L);
Payment payment3 = new Payment().setId(3L).setCreationDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 2).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3).setGiverAccount(bankAccount1);
PaymentDTO paymentDTO3 = new PaymentDTO().setId(3L).setCreationDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 2).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3).setGiverAccount(1L);
List<BankAccount> bankAccounts = List.of(bankAccount1);
when(bankAccountRepository.findAllByUsers_username(TESTUSER1)).thenReturn(bankAccounts);
when(paymentRepository.findAllByGiverAccountIn(bankAccounts)).thenReturn(Arrays.asList(payment1, payment2, payment3));
when(paymentConverter.mapToDTO(payment1)).thenReturn(paymentDTO1);
when(paymentConverter.mapToDTO(payment2)).thenReturn(paymentDTO2);
when(paymentConverter.mapToDTO(payment3)).thenReturn(paymentDTO3);
getPaymentsForUsers_withoutFilter(authenticatedUser);
getPaymentsForUsers_withBeneficiaryAccountFilter(authenticatedUser);
getPaymentsForUsers_withStartDateFilter(authenticatedUser);
getPaymentsForUsers_withEndDateFilter(authenticatedUser);
getPaymentsForUsers_withStartAndEndDateFilter(authenticatedUser);
getPaymentsForUsers_withAllFilter(authenticatedUser);
getPaymentsForUsers_withOtherUser(new UserDetailsImpl(user2));
}
private void getPaymentsForUsers_withOtherUser(UserDetailsImpl authenticatedUser) {
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, null);
assertEquals(0, paymentsForUser.size());
}
private void getPaymentsForUsers_withAllFilter(UserDetailsImpl authenticatedUser) {
Date startDate = new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime();
Date endDate = new GregorianCalendar(2023, Calendar.MAY, 11, 12, 2).getTime();
PaymentFilter paymentFilter = new PaymentFilter().setStartDate(startDate).setEndDate(endDate).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3);
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(1, paymentsForUser.size());
assertEquals(3, paymentsForUser.get(0).getId());
}
private void getPaymentsForUsers_withStartAndEndDateFilter(UserDetailsImpl authenticatedUser) {
Date date = new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime();
PaymentFilter paymentFilter = new PaymentFilter().setStartDate(date).setEndDate(date);
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(1, paymentsForUser.size());
assertEquals(2, paymentsForUser.get(0).getId());
}
private void getPaymentsForUsers_withEndDateFilter(UserDetailsImpl authenticatedUser) {
PaymentFilter paymentFilter = new PaymentFilter().setEndDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime());
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(2, paymentsForUser.size());
assertEquals(1, paymentsForUser.get(0).getId());
assertEquals(2, paymentsForUser.get(1).getId());
}
private void getPaymentsForUsers_withStartDateFilter(UserDetailsImpl authenticatedUser) {
PaymentFilter paymentFilter = new PaymentFilter().setStartDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime());
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(2, paymentsForUser.size());
assertEquals(2, paymentsForUser.get(0).getId());
assertEquals(3, paymentsForUser.get(1).getId());
}
private void getPaymentsForUsers_withBeneficiaryAccountFilter(UserDetailsImpl authenticatedUser) {
PaymentFilter paymentFilter = new PaymentFilter().setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2);
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(2, paymentsForUser.size());
assertEquals(1, paymentsForUser.get(0).getId());
assertEquals(2, paymentsForUser.get(1).getId());
}
private void getPaymentsForUsers_withoutFilter(UserDetailsImpl authenticatedUser) {
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, null);
assertEquals(3, paymentsForUser.size());
}
}

View File

@@ -3,7 +3,7 @@ package net.kapcake.bankingservice.validation;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import net.kapcake.bankingservice.config.BankingServiceConfig; import net.kapcake.bankingservice.config.BankingServiceConfig;
import net.kapcake.bankingservice.exceptions.PaymentValidationException; import net.kapcake.bankingservice.exceptions.ValidationException;
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 net.kapcake.bankingservice.model.domain.IbanValidationResponse; import net.kapcake.bankingservice.model.domain.IbanValidationResponse;
@@ -44,7 +44,6 @@ class PaymentValidatorTest {
private static final List<String> FORBIDDEN_ACCOUNTS = Arrays.asList(FORBIDDEN_ACCOUNT_1, FORBIDDEN_ACCOUNT_2); 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 USERNAME = "username";
private static final String GIVER_ACCOUNT_NUMBER = "LU038359494013886902"; private static final String GIVER_ACCOUNT_NUMBER = "LU038359494013886902";
private final RestTemplate restTemplate;
private final PaymentValidator paymentValidator; private final PaymentValidator paymentValidator;
private final MockRestServiceServer server; private final MockRestServiceServer server;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@@ -55,7 +54,6 @@ class PaymentValidatorTest {
@Autowired @Autowired
public PaymentValidatorTest(RestTemplate restTemplate, MockRestServiceServer server) { public PaymentValidatorTest(RestTemplate restTemplate, MockRestServiceServer server) {
this.restTemplate = restTemplate;
this.paymentValidator = new PaymentValidator(restTemplate, VALIDATION_URL, FORBIDDEN_ACCOUNTS); this.paymentValidator = new PaymentValidator(restTemplate, VALIDATION_URL, FORBIDDEN_ACCOUNTS);
objectMapper = new ObjectMapper(); objectMapper = new ObjectMapper();
this.server = server; this.server = server;
@@ -90,8 +88,8 @@ class PaymentValidatorTest {
void validate_invalidGiverAccount() { void validate_invalidGiverAccount() {
user.setBankAccounts(Collections.emptyList()); user.setBankAccounts(Collections.emptyList());
PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Giver account not owned by authenticated user.", paymentValidationException.getMessage()); Assertions.assertEquals("Giver account not owned by authenticated user.", validationException.getMessage());
} }
@Test @Test
@@ -103,8 +101,8 @@ class PaymentValidatorTest {
.andExpect(method(HttpMethod.GET)) .andExpect(method(HttpMethod.GET))
.andRespond(withServiceUnavailable()); .andRespond(withServiceUnavailable());
PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Beneficiary account could not be validated.", paymentValidationException.getMessage()); Assertions.assertEquals("Beneficiary account could not be validated.", validationException.getMessage());
server.reset(); server.reset();
server server
@@ -112,8 +110,8 @@ class PaymentValidatorTest {
.andExpect(method(HttpMethod.GET)) .andExpect(method(HttpMethod.GET))
.andRespond(withSuccess()); .andRespond(withSuccess());
paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Beneficiary account could not be validated.", paymentValidationException.getMessage()); Assertions.assertEquals("Beneficiary account could not be validated.", validationException.getMessage());
} }
@Test @Test
@@ -125,16 +123,16 @@ class PaymentValidatorTest {
.andExpect(method(HttpMethod.GET)) .andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(objectMapper.writeValueAsString(new IbanValidationResponse(false, Collections.singletonList("Invalid"), VALID_BENEFICIARY)), MediaType.APPLICATION_JSON)); .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)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Beneficiary account not valid: [Invalid]", paymentValidationException.getMessage()); Assertions.assertEquals("Beneficiary account not valid: [Invalid]", validationException.getMessage());
} }
@Test @Test
void validate_invalidBeneficiaryAccount_giverAndBeneficiarySame() { void validate_invalidBeneficiaryAccount_giverAndBeneficiarySame() {
bankAccount.setAccountNumber(VALID_BENEFICIARY); bankAccount.setAccountNumber(VALID_BENEFICIARY);
PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Beneficiary and giver account are the same.", paymentValidationException.getMessage()); Assertions.assertEquals("Beneficiary and giver account are the same.", validationException.getMessage());
} }
@Test @Test
@@ -146,31 +144,31 @@ class PaymentValidatorTest {
.andExpect(method(HttpMethod.GET)) .andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(objectMapper.writeValueAsString(new IbanValidationResponse(true, Collections.emptyList(), FORBIDDEN_ACCOUNT_1)), MediaType.APPLICATION_JSON)); .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)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Beneficiary account is forbidden.", paymentValidationException.getMessage()); Assertions.assertEquals("Beneficiary account is forbidden.", validationException.getMessage());
} }
@Test @Test
void validate_invalidAccountBalance_notFound() { void validate_invalidAccountBalance_notFound() {
balance.setType(BalanceType.END_OF_DAY); balance.setType(BalanceType.END_OF_DAY);
PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Unable to retrieve available account balance.", paymentValidationException.getMessage()); Assertions.assertEquals("Unable to retrieve available account balance.", validationException.getMessage());
} }
@Test @Test
void validate_invalidAccountBalance_differentCurrencies() { void validate_invalidAccountBalance_differentCurrencies() {
balance.setCurrency(Currency.USD); balance.setCurrency(Currency.USD);
PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Payment and account currency must be the same.", paymentValidationException.getMessage()); Assertions.assertEquals("Payment and account currency must be the same.", validationException.getMessage());
} }
@Test @Test
void validate_invalidAccountBalance_insufficientBalance() { void validate_invalidAccountBalance_insufficientBalance() {
balance.setAmount(BigDecimal.valueOf(2)); balance.setAmount(BigDecimal.valueOf(2));
PaymentValidationException paymentValidationException = Assertions.assertThrows(PaymentValidationException.class, () -> paymentValidator.validate(user, payment)); ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment));
Assertions.assertEquals("Available account balance not sufficient.", paymentValidationException.getMessage()); Assertions.assertEquals("Available account balance not sufficient.", validationException.getMessage());
} }
} }