Add endpoint to list payments
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user