Add endpoint to delete payments

This commit is contained in:
2023-05-13 20:19:26 +02:00
parent 890c030109
commit 94fd3a5849
8 changed files with 93 additions and 22 deletions

View File

@@ -8,14 +8,12 @@ import net.kapcake.bankingservice.model.dtos.PaymentFilter;
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.springframework.http.HttpStatus;
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.FieldError;
import org.springframework.validation.ObjectError; import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@@ -35,6 +33,7 @@ public class BankingServiceController {
} }
@PostMapping("/payment") @PostMapping("/payment")
@ResponseStatus(HttpStatus.CREATED)
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()) {
String errorString = getErrorString(bindingResult); String errorString = getErrorString(bindingResult);
@@ -52,6 +51,11 @@ public class BankingServiceController {
return paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); return paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
} }
@DeleteMapping("/payment/{id}")
public void deletePayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @PathVariable("id") Long id) {
paymentService.deletePayment(authenticatedUser, id);
}
private static String getErrorString(BindingResult bindingResult) { private static String getErrorString(BindingResult bindingResult) {
StringBuilder builder = new StringBuilder("["); StringBuilder builder = new StringBuilder("[");
List<ObjectError> allErrors = bindingResult.getAllErrors(); List<ObjectError> allErrors = bindingResult.getAllErrors();

View File

@@ -1,5 +1,7 @@
package net.kapcake.bankingservice.controllers; package net.kapcake.bankingservice.controllers;
import net.kapcake.bankingservice.exceptions.AccessDeniedException;
import net.kapcake.bankingservice.exceptions.ResourceNotFoundException;
import net.kapcake.bankingservice.exceptions.ValidationException; 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;
@@ -9,11 +11,22 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice @ControllerAdvice
public class BankingServiceExceptionHandler extends ResponseEntityExceptionHandler { public class BankingServiceExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ValidationException.class}) @ExceptionHandler({ValidationException.class})
protected ResponseEntity<Object> handlePaymentValidationException(ValidationException exception, WebRequest request) { protected ResponseEntity<Object> handleValidationException(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);
} }
@ExceptionHandler({ResourceNotFoundException.class})
protected ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException exception, WebRequest request) {
return this.handleExceptionInternal(exception, exception.getMessage(),
new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
@ExceptionHandler({AccessDeniedException.class})
protected ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException exception, WebRequest request) {
return this.handleExceptionInternal(exception, exception.getMessage(),
new HttpHeaders(), HttpStatus.FORBIDDEN, request);
}
} }

View File

@@ -0,0 +1,7 @@
package net.kapcake.bankingservice.exceptions;
public class AccessDeniedException extends RuntimeException {
public AccessDeniedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package net.kapcake.bankingservice.exceptions;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}

View File

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

View File

@@ -16,7 +16,7 @@ public class User {
@Id @Id
@GeneratedValue @GeneratedValue
private Long id; private Long id;
@Column(nullable = false) @Column(nullable = false, unique = true)
private String username; private String username;
@Column(nullable = false) @Column(nullable = false)
private String password; private String password;

View File

@@ -1,10 +1,10 @@
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.Balance; import net.kapcake.bankingservice.exceptions.AccessDeniedException;
import net.kapcake.bankingservice.model.domain.BalanceType; import net.kapcake.bankingservice.exceptions.ResourceNotFoundException;
import net.kapcake.bankingservice.model.domain.BankAccount; import net.kapcake.bankingservice.exceptions.ValidationException;
import net.kapcake.bankingservice.model.domain.Payment; import net.kapcake.bankingservice.model.domain.*;
import net.kapcake.bankingservice.model.dtos.PaymentDTO; import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.model.dtos.PaymentFilter; import net.kapcake.bankingservice.model.dtos.PaymentFilter;
import net.kapcake.bankingservice.repositories.BalanceRepository; import net.kapcake.bankingservice.repositories.BalanceRepository;
@@ -79,7 +79,7 @@ public class PaymentService {
} }
public List<PaymentDTO> getPaymentsForUser(UserDetailsImpl authenticatedUser, PaymentFilter paymentFilter) { public List<PaymentDTO> getPaymentsForUser(UserDetailsImpl authenticatedUser, PaymentFilter paymentFilter) {
List<BankAccount> userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()); List<BankAccount> userBankAccounts = authenticatedUser.user().getBankAccounts();
List<Payment> userPayments = paymentRepository.findAllByGiverAccountIn(userBankAccounts); List<Payment> userPayments = paymentRepository.findAllByGiverAccountIn(userBankAccounts);
List<Payment> filteredPayments = getFilteredPayments(paymentFilter, userPayments); List<Payment> filteredPayments = getFilteredPayments(paymentFilter, userPayments);
@@ -109,4 +109,16 @@ public class PaymentService {
} }
return filteredPayments; return filteredPayments;
} }
public void deletePayment(UserDetailsImpl authenticatedUser, Long id) {
List<BankAccount> userBankAccounts = authenticatedUser.user().getBankAccounts();
Payment payment = paymentRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("A payment with the given id does not exist"));
if (userBankAccounts.stream().noneMatch(account -> account.getId().equals(payment.getGiverAccount().getId()))) {
throw new AccessDeniedException("The payment with id [" + id + "] is not owned by the authenticated user");
}
if (PaymentStatus.EXECUTED.equals(payment.getStatus())) {
throw new ValidationException("The payment to be deleted has already been executed");
}
paymentRepository.deleteById(id);
}
} }

View File

@@ -1,6 +1,9 @@
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.exceptions.AccessDeniedException;
import net.kapcake.bankingservice.exceptions.ResourceNotFoundException;
import net.kapcake.bankingservice.exceptions.ValidationException;
import net.kapcake.bankingservice.model.domain.Currency; import net.kapcake.bankingservice.model.domain.Currency;
import net.kapcake.bankingservice.model.domain.*; import net.kapcake.bankingservice.model.domain.*;
import net.kapcake.bankingservice.model.dtos.PaymentDTO; import net.kapcake.bankingservice.model.dtos.PaymentDTO;
@@ -20,14 +23,15 @@ import org.springframework.transaction.PlatformTransactionManager;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.*; import java.util.*;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class PaymentServiceTest { class PaymentServiceTest {
public static final String TESTUSER1 = "test-user-1"; public static final String TEST_USER_1 = "test-user-1";
public static final String TESTUSER2 = "test-user-2"; public static final String TEST_USER_2 = "test-user-2";
public static final String ACCOUNT_NUMBER_1 = "LU347280737562934669"; public static final String ACCOUNT_NUMBER_1 = "LU347280737562934669";
public static final String ACCOUNT_NUMBER_2 = "LU559999005150266806"; public static final String ACCOUNT_NUMBER_2 = "LU559999005150266806";
public static final String ACCOUNT_NUMBER_3 = "LU293855258657559167"; public static final String ACCOUNT_NUMBER_3 = "LU293855258657559167";
@@ -58,8 +62,8 @@ class PaymentServiceTest {
void beforeEach() { void beforeEach() {
paymentService = new PaymentService(paymentRepository, bankAccountRepository, balanceRepository, paymentValidator, paymentConverter, transactionManager); paymentService = new PaymentService(paymentRepository, bankAccountRepository, balanceRepository, paymentValidator, paymentConverter, transactionManager);
user1 = new User().setId(1L).setUsername(TESTUSER1); user1 = new User().setId(1L).setUsername(TEST_USER_1);
user2 = new User().setId(2L).setUsername(TESTUSER2); user2 = new User().setId(2L).setUsername(TEST_USER_2);
balance1 = new Balance().setId(1L).setType(BalanceType.AVAILABLE).setCurrency(Currency.EUR).setAmount(BigDecimal.valueOf(500)); 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)); 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)); bankAccount1 = new BankAccount().setId(1L).setAccountNumber(ACCOUNT_NUMBER_1).setBalances(Collections.singletonList(balance1)).setUsers(Collections.singletonList(user1));
@@ -85,6 +89,8 @@ class PaymentServiceTest {
@Test @Test
void getPaymentsForUser() { void getPaymentsForUser() {
List<BankAccount> bankAccounts = List.of(bankAccount1);
user1.setBankAccounts(bankAccounts);
UserDetailsImpl authenticatedUser = new UserDetailsImpl(user1); 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); 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); 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);
@@ -93,8 +99,6 @@ class PaymentServiceTest {
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); 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); 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(paymentRepository.findAllByGiverAccountIn(bankAccounts)).thenReturn(Arrays.asList(payment1, payment2, payment3));
when(paymentConverter.mapToDTO(payment1)).thenReturn(paymentDTO1); when(paymentConverter.mapToDTO(payment1)).thenReturn(paymentDTO1);
when(paymentConverter.mapToDTO(payment2)).thenReturn(paymentDTO2); when(paymentConverter.mapToDTO(payment2)).thenReturn(paymentDTO2);
@@ -172,4 +176,31 @@ class PaymentServiceTest {
assertEquals(3, paymentsForUser.size()); assertEquals(3, paymentsForUser.size());
} }
@Test
void deletePayment() {
List<BankAccount> bankAccounts1 = List.of(bankAccount1);
user1.setBankAccounts(bankAccounts1);
List<BankAccount> bankAccounts2 = List.of(bankAccount2);
user2.setBankAccounts(bankAccounts2);
UserDetailsImpl authenticatedUser1 = new UserDetailsImpl(user1);
UserDetailsImpl authenticatedUser2 = new UserDetailsImpl(user2);
Payment payment1 = new Payment().setId(1L).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1);
Payment payment2 = new Payment().setId(2L).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1).setStatus(PaymentStatus.EXECUTED);
when(paymentRepository.findById(1L)).thenReturn(Optional.of(payment1));
when(paymentRepository.findById(2L)).thenReturn(Optional.of(payment2));
when(paymentRepository.findById(3L)).thenReturn(Optional.empty());
AccessDeniedException accessDeniedException = assertThrows(AccessDeniedException.class, () -> paymentService.deletePayment(authenticatedUser2, 1L));
assertEquals("The payment with id [1] is not owned by the authenticated user", accessDeniedException.getMessage());
ResourceNotFoundException resourceNotFoundException = assertThrows(ResourceNotFoundException.class, () -> paymentService.deletePayment(authenticatedUser2, 3L));
assertEquals("A payment with the given id does not exist", resourceNotFoundException.getMessage());
ValidationException validationException = assertThrows(ValidationException.class, () -> paymentService.deletePayment(authenticatedUser1, 2L));
assertEquals("The payment to be deleted has already been executed", validationException.getMessage());
assertDoesNotThrow(() -> paymentService.deletePayment(authenticatedUser1, 1L));
verify(paymentRepository).deleteById(1L);
}
} }