diff --git a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java index ccc68f1..a769dfc 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java @@ -1,18 +1,17 @@ package net.kapcake.bankingservice.controllers; import jakarta.validation.Valid; -import net.kapcake.bankingservice.converters.BankAccountConverter; -import net.kapcake.bankingservice.converters.PaymentConverter; -import net.kapcake.bankingservice.exceptions.PaymentValidationException; -import net.kapcake.bankingservice.model.domain.PaymentDTO; -import net.kapcake.bankingservice.model.domain.BankAccountDTO; -import net.kapcake.bankingservice.model.entities.Payment; +import net.kapcake.bankingservice.exceptions.ValidationException; +import net.kapcake.bankingservice.model.dtos.PaymentDTO; +import net.kapcake.bankingservice.model.dtos.BankAccountDTO; +import net.kapcake.bankingservice.model.dtos.PaymentFilter; import net.kapcake.bankingservice.security.UserDetailsImpl; import net.kapcake.bankingservice.services.AccountService; import net.kapcake.bankingservice.services.PaymentService; -import org.modelmapper.ModelMapper; import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -38,10 +37,35 @@ public class BankingServiceController { @PostMapping("/payment") public PaymentDTO createPayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @RequestBody @Valid PaymentDTO paymentDTO, BindingResult bindingResult) { 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); } + @GetMapping("/payments") + public List 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 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(); + } } diff --git a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java index ad24d25..8a82a0e 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java @@ -1,6 +1,6 @@ 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.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,8 +11,8 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep @ControllerAdvice public class BankingServiceExceptionHandler extends ResponseEntityExceptionHandler { - @ExceptionHandler({PaymentValidationException.class}) - protected ResponseEntity handlePaymentValidationException(PaymentValidationException exception, WebRequest request) { + @ExceptionHandler({ValidationException.class}) + protected ResponseEntity handlePaymentValidationException(ValidationException exception, WebRequest request) { return this.handleExceptionInternal(exception, exception.getMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } diff --git a/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java b/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java index 7562f6f..f47a943 100644 --- a/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java +++ b/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java @@ -1,6 +1,6 @@ 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.domain.BankAccount; import net.kapcake.bankingservice.model.domain.Payment; @@ -25,8 +25,7 @@ public class PaymentConverter extends AbstractConverter { TypeMap paymentPaymentDTOTypeMap = modelMapper .typeMap(Payment.class, PaymentDTO.class) .addMapping(src -> src.getGiverAccount().getId(), PaymentDTO::setGiverAccount); - PaymentDTO paymentDTO = paymentPaymentDTOTypeMap.map(payment); - return paymentDTO; + return paymentPaymentDTOTypeMap.map(payment); } @Override @@ -34,7 +33,7 @@ public class PaymentConverter extends AbstractConverter { Payment payment = modelMapper.map(paymentDTO, Payment.class); Optional bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount()); if (bankAccountOptional.isEmpty()) { - throw new PaymentValidationException("Payment request invalid: Giver account does not exist."); + throw new ValidationException("Payment request invalid: Giver account does not exist."); } payment.setGiverAccount(bankAccountOptional.get()); return payment; diff --git a/src/main/java/net/kapcake/bankingservice/exceptions/PaymentValidationException.java b/src/main/java/net/kapcake/bankingservice/exceptions/PaymentValidationException.java deleted file mode 100644 index ef0636b..0000000 --- a/src/main/java/net/kapcake/bankingservice/exceptions/PaymentValidationException.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java b/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java new file mode 100644 index 0000000..26b49d5 --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java @@ -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); + } +} diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/BalanceDTO.java b/src/main/java/net/kapcake/bankingservice/model/dtos/BalanceDTO.java index 5850087..10585f5 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/BalanceDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/BalanceDTO.java @@ -3,13 +3,16 @@ package net.kapcake.bankingservice.model.dtos; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotNull; import lombok.Data; +import lombok.experimental.Accessors; import net.kapcake.bankingservice.model.domain.BalanceType; import net.kapcake.bankingservice.model.domain.Currency; +import java.io.Serializable; import java.math.BigDecimal; @Data -public class BalanceDTO { +@Accessors(chain = true) +public class BalanceDTO implements Serializable { private Long id; @NotNull @DecimalMin(value = "0") diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/BankAccountDTO.java b/src/main/java/net/kapcake/bankingservice/model/dtos/BankAccountDTO.java index d8b22b8..ffa7e92 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/BankAccountDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/BankAccountDTO.java @@ -5,12 +5,15 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import lombok.experimental.Accessors; import net.kapcake.bankingservice.model.domain.AccountStatus; +import java.io.Serializable; import java.util.List; @Data -public class BankAccountDTO { +@Accessors(chain = true) +public class BankAccountDTO implements Serializable { private Long id; @NotBlank private String accountNumber; diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentDTO.java b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentDTO.java index e3d37dd..494100f 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentDTO.java @@ -3,14 +3,17 @@ package net.kapcake.bankingservice.model.dtos; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotNull; import lombok.Data; +import lombok.experimental.Accessors; import net.kapcake.bankingservice.model.domain.Currency; import net.kapcake.bankingservice.model.domain.PaymentStatus; +import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; @Data -public class PaymentDTO { +@Accessors(chain = true) +public class PaymentDTO implements Serializable { private Long id; @NotNull @DecimalMin(value = "0", inclusive = false) diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java new file mode 100644 index 0000000..f947be8 --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java @@ -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()); + } +} diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/UserDTO.java b/src/main/java/net/kapcake/bankingservice/model/dtos/UserDTO.java index bddef35..73a198c 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/UserDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/UserDTO.java @@ -2,9 +2,13 @@ package net.kapcake.bankingservice.model.dtos; import jakarta.validation.constraints.NotEmpty; import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; @Data -public class UserDTO { +@Accessors(chain = true) +public class UserDTO implements Serializable { private Long id; @NotEmpty private String username; diff --git a/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java b/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java index 0556f89..0ee1d99 100644 --- a/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java +++ b/src/main/java/net/kapcake/bankingservice/repositories/BankAccountRepository.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Optional; public interface BankAccountRepository extends JpaRepository { - Optional> findAllByUsers_username(String username); + List findAllByUsers_username(String username); Optional findByAccountNumber(String accountNumber); } diff --git a/src/main/java/net/kapcake/bankingservice/repositories/PaymentRepository.java b/src/main/java/net/kapcake/bankingservice/repositories/PaymentRepository.java index 087fa44..464b2a2 100644 --- a/src/main/java/net/kapcake/bankingservice/repositories/PaymentRepository.java +++ b/src/main/java/net/kapcake/bankingservice/repositories/PaymentRepository.java @@ -1,7 +1,11 @@ package net.kapcake.bankingservice.repositories; +import net.kapcake.bankingservice.model.domain.BankAccount; import net.kapcake.bankingservice.model.domain.Payment; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface PaymentRepository extends JpaRepository { + List findAllByGiverAccountIn(List bankAccounts); } diff --git a/src/main/java/net/kapcake/bankingservice/services/AccountService.java b/src/main/java/net/kapcake/bankingservice/services/AccountService.java index f9c956d..54dd182 100644 --- a/src/main/java/net/kapcake/bankingservice/services/AccountService.java +++ b/src/main/java/net/kapcake/bankingservice/services/AccountService.java @@ -6,7 +6,6 @@ import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.security.UserDetailsImpl; import org.springframework.stereotype.Service; -import java.util.Collections; import java.util.List; @Service @@ -20,7 +19,7 @@ public class AccountService { } public List getAccounts(UserDetailsImpl authenticatedUser) { - return bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()).orElse(Collections.emptyList()) + return bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()) .stream().map(bankAccountConverter::mapToDTO).toList(); } } diff --git a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java index ba18738..97ddc47 100644 --- a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java +++ b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java @@ -1,11 +1,12 @@ package net.kapcake.bankingservice.services; 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.BalanceType; import net.kapcake.bankingservice.model.domain.BankAccount; 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.BankAccountRepository; import net.kapcake.bankingservice.repositories.PaymentRepository; @@ -15,7 +16,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; 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.stream.Collectors; @Service public class PaymentService { @@ -47,16 +52,61 @@ public class PaymentService { Payment persistedPayment = transactionTemplate.execute(status -> { Payment savedPayment = paymentRepository.save(payment); - Balance giverAvailableBalance = giverAccount.getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow(); - giverAvailableBalance.setAmount(giverAvailableBalance.getAmount().subtract(payment.getAmount())); - balanceRepository.save(giverAvailableBalance); - if (beneficiaryAccount.isPresent()) { - Balance beneficiaryBalance = beneficiaryAccount.get().getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).findFirst().orElseThrow(); - beneficiaryBalance.setAmount(beneficiaryBalance.getAmount().add(payment.getAmount())); - balanceRepository.save(beneficiaryBalance); - } + updateBalances(payment, giverAccount, beneficiaryAccount); return savedPayment; }); + return paymentConverter.mapToDTO(persistedPayment); } + + private void updateBalances(Payment payment, BankAccount giverAccount, Optional beneficiaryAccount) { + updateGiverBalance(payment, giverAccount); + updateBeneficiaryBalance(payment, beneficiaryAccount); + } + + private void updateBeneficiaryBalance(Payment payment, Optional 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 getPaymentsForUser(UserDetailsImpl authenticatedUser, PaymentFilter paymentFilter) { + List userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()); + List userPayments = paymentRepository.findAllByGiverAccountIn(userBankAccounts); + List filteredPayments = getFilteredPayments(paymentFilter, userPayments); + + filteredPayments.sort(Comparator.comparing(Payment::getCreationDate)); + return filteredPayments.stream().map(paymentConverter::mapToDTO).toList(); + } + + private static List getFilteredPayments(PaymentFilter paymentFilter, List userPayments) { + List 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; + } } diff --git a/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java b/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java index 22133e8..1c69d6e 100644 --- a/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java +++ b/src/main/java/net/kapcake/bankingservice/validation/PaymentValidator.java @@ -1,6 +1,6 @@ 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.IbanValidationResponse; 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); validateBeneficiaryAccount(payment); validateAccountBalance(payment); @@ -38,45 +38,45 @@ public class PaymentValidator { 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."); + throw new ValidationException("Giver account not owned by authenticated user."); } } private void validateBeneficiaryAccount(Payment payment) { - IbanValidationResponse validationResponse = null; + IbanValidationResponse validationResponse; try { validationResponse = restTemplate.getForObject(validationUrl + "/" + payment.getBeneficiaryAccountNumber(), IbanValidationResponse.class); } catch (RestClientException e) { - throw new PaymentValidationException("Beneficiary account could not be validated."); + throw new ValidationException("Beneficiary account could not be validated."); } 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()) { - 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()); 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()); if (isBeneficiaryForbidden) { - throw new PaymentValidationException("Beneficiary account is forbidden."); + throw new ValidationException("Beneficiary account is forbidden."); } } private void validateAccountBalance(Payment payment) { List availableBalances = payment.getGiverAccount().getBalances().stream().filter(balance -> BalanceType.AVAILABLE.equals(balance.getType())).toList(); if (availableBalances.size() != 1) { - throw new PaymentValidationException("Unable to retrieve available account balance."); + throw new ValidationException("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."); + throw new ValidationException("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."); + throw new ValidationException("Available account balance not sufficient."); } } } diff --git a/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java b/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java new file mode 100644 index 0000000..a47d5d4 --- /dev/null +++ b/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java @@ -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 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 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 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 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 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 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 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 paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, null); + + assertEquals(3, paymentsForUser.size()); + } +} \ No newline at end of file diff --git a/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java b/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java index d6575c7..23747e0 100644 --- a/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java +++ b/src/test/java/net/kapcake/bankingservice/validation/PaymentValidatorTest.java @@ -3,7 +3,7 @@ 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.exceptions.ValidationException; import net.kapcake.bankingservice.model.domain.BalanceType; import net.kapcake.bankingservice.model.domain.Currency; import net.kapcake.bankingservice.model.domain.IbanValidationResponse; @@ -44,7 +44,6 @@ class PaymentValidatorTest { private static final List FORBIDDEN_ACCOUNTS = Arrays.asList(FORBIDDEN_ACCOUNT_1, FORBIDDEN_ACCOUNT_2); private static final String USERNAME = "username"; private static final String GIVER_ACCOUNT_NUMBER = "LU038359494013886902"; - private final RestTemplate restTemplate; private final PaymentValidator paymentValidator; private final MockRestServiceServer server; private final ObjectMapper objectMapper; @@ -55,7 +54,6 @@ class PaymentValidatorTest { @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; @@ -90,8 +88,8 @@ class PaymentValidatorTest { 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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Giver account not owned by authenticated user.", validationException.getMessage()); } @Test @@ -103,8 +101,8 @@ class PaymentValidatorTest { .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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account could not be validated.", validationException.getMessage()); server.reset(); server @@ -112,8 +110,8 @@ class PaymentValidatorTest { .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()); + validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account could not be validated.", validationException.getMessage()); } @Test @@ -125,16 +123,16 @@ class PaymentValidatorTest { .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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account not valid: [Invalid]", validationException.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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary and giver account are the same.", validationException.getMessage()); } @Test @@ -146,31 +144,31 @@ class PaymentValidatorTest { .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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Beneficiary account is forbidden.", validationException.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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Unable to retrieve available account balance.", validationException.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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Payment and account currency must be the same.", validationException.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()); + ValidationException validationException = Assertions.assertThrows(ValidationException.class, () -> paymentValidator.validate(user, payment)); + Assertions.assertEquals("Available account balance not sufficient.", validationException.getMessage()); } } \ No newline at end of file