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;
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<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;
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<Object> handlePaymentValidationException(PaymentValidationException exception, WebRequest request) {
@ExceptionHandler({ValidationException.class})
protected ResponseEntity<Object> handlePaymentValidationException(ValidationException exception, WebRequest request) {
return this.handleExceptionInternal(exception, exception.getMessage(),
new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}

View File

@@ -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<Payment, PaymentDTO> {
TypeMap<Payment, PaymentDTO> 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, 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.");
throw new ValidationException("Payment request invalid: Giver account does not exist.");
}
payment.setGiverAccount(bankAccountOptional.get());
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.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")

View File

@@ -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;

View File

@@ -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)

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 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;

View File

@@ -7,7 +7,7 @@ import java.util.List;
import java.util.Optional;
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);
}

View File

@@ -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<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 org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
@@ -20,7 +19,7 @@ public class AccountService {
}
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();
}
}

View File

@@ -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<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;
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<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.");
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.");
}
}
}