From 94fd3a5849b477e934c576af324db27c83458994 Mon Sep 17 00:00:00 2001 From: kapcake Date: Sat, 13 May 2023 20:19:26 +0200 Subject: [PATCH] Add endpoint to delete payments --- .../controllers/BankingServiceController.java | 12 +++-- .../BankingServiceExceptionHandler.java | 15 ++++++- .../exceptions/AccessDeniedException.java | 7 +++ .../exceptions/ResourceNotFoundException.java | 7 +++ .../exceptions/ValidationException.java | 5 +-- .../bankingservice/model/domain/User.java | 2 +- .../services/PaymentService.java | 22 ++++++--- .../services/PaymentServiceTest.java | 45 ++++++++++++++++--- 8 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 src/main/java/net/kapcake/bankingservice/exceptions/AccessDeniedException.java create mode 100644 src/main/java/net/kapcake/bankingservice/exceptions/ResourceNotFoundException.java diff --git a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java index a769dfc..fc5aa30 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java @@ -8,14 +8,12 @@ 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.springframework.http.HttpStatus; 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; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -35,6 +33,7 @@ public class BankingServiceController { } @PostMapping("/payment") + @ResponseStatus(HttpStatus.CREATED) public PaymentDTO createPayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @RequestBody @Valid PaymentDTO paymentDTO, BindingResult bindingResult) { if (bindingResult.hasErrors()) { String errorString = getErrorString(bindingResult); @@ -52,6 +51,11 @@ public class BankingServiceController { 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) { StringBuilder builder = new StringBuilder("["); List allErrors = bindingResult.getAllErrors(); diff --git a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java index 8a82a0e..c43686f 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceExceptionHandler.java @@ -1,5 +1,7 @@ package net.kapcake.bankingservice.controllers; +import net.kapcake.bankingservice.exceptions.AccessDeniedException; +import net.kapcake.bankingservice.exceptions.ResourceNotFoundException; import net.kapcake.bankingservice.exceptions.ValidationException; import org.springframework.http.HttpHeaders; 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.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + @ControllerAdvice public class BankingServiceExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ValidationException.class}) - protected ResponseEntity handlePaymentValidationException(ValidationException exception, WebRequest request) { + protected ResponseEntity handleValidationException(ValidationException exception, WebRequest request) { return this.handleExceptionInternal(exception, exception.getMessage(), new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } + @ExceptionHandler({ResourceNotFoundException.class}) + protected ResponseEntity handleResourceNotFoundException(ResourceNotFoundException exception, WebRequest request) { + return this.handleExceptionInternal(exception, exception.getMessage(), + new HttpHeaders(), HttpStatus.NOT_FOUND, request); + } + @ExceptionHandler({AccessDeniedException.class}) + protected ResponseEntity handleAccessDeniedException(AccessDeniedException exception, WebRequest request) { + return this.handleExceptionInternal(exception, exception.getMessage(), + new HttpHeaders(), HttpStatus.FORBIDDEN, request); + } } diff --git a/src/main/java/net/kapcake/bankingservice/exceptions/AccessDeniedException.java b/src/main/java/net/kapcake/bankingservice/exceptions/AccessDeniedException.java new file mode 100644 index 0000000..bf35161 --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/exceptions/AccessDeniedException.java @@ -0,0 +1,7 @@ +package net.kapcake.bankingservice.exceptions; + +public class AccessDeniedException extends RuntimeException { + public AccessDeniedException(String message) { + super(message); + } +} diff --git a/src/main/java/net/kapcake/bankingservice/exceptions/ResourceNotFoundException.java b/src/main/java/net/kapcake/bankingservice/exceptions/ResourceNotFoundException.java new file mode 100644 index 0000000..0919e2c --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/exceptions/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package net.kapcake.bankingservice.exceptions; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java b/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java index 26b49d5..b9e310f 100644 --- a/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java +++ b/src/main/java/net/kapcake/bankingservice/exceptions/ValidationException.java @@ -1,10 +1,7 @@ package net.kapcake.bankingservice.exceptions; -public class ValidationException extends IllegalArgumentException { +public class ValidationException extends RuntimeException { 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/domain/User.java b/src/main/java/net/kapcake/bankingservice/model/domain/User.java index 1633c5f..f78080f 100644 --- a/src/main/java/net/kapcake/bankingservice/model/domain/User.java +++ b/src/main/java/net/kapcake/bankingservice/model/domain/User.java @@ -16,7 +16,7 @@ public class User { @Id @GeneratedValue private Long id; - @Column(nullable = false) + @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String password; diff --git a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java index 97ddc47..50c7c64 100644 --- a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java +++ b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java @@ -1,10 +1,10 @@ package net.kapcake.bankingservice.services; import net.kapcake.bankingservice.converters.PaymentConverter; -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.exceptions.AccessDeniedException; +import net.kapcake.bankingservice.exceptions.ResourceNotFoundException; +import net.kapcake.bankingservice.exceptions.ValidationException; +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; @@ -79,7 +79,7 @@ public class PaymentService { } public List getPaymentsForUser(UserDetailsImpl authenticatedUser, PaymentFilter paymentFilter) { - List userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()); + List userBankAccounts = authenticatedUser.user().getBankAccounts(); List userPayments = paymentRepository.findAllByGiverAccountIn(userBankAccounts); List filteredPayments = getFilteredPayments(paymentFilter, userPayments); @@ -109,4 +109,16 @@ public class PaymentService { } return filteredPayments; } + + public void deletePayment(UserDetailsImpl authenticatedUser, Long id) { + List 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); + } } diff --git a/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java b/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java index a47d5d4..99b0dba 100644 --- a/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java +++ b/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java @@ -1,6 +1,9 @@ package net.kapcake.bankingservice.services; 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.*; import net.kapcake.bankingservice.model.dtos.PaymentDTO; @@ -20,14 +23,15 @@ import org.springframework.transaction.PlatformTransactionManager; import java.math.BigDecimal; 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; @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 TEST_USER_1 = "test-user-1"; + public static final String TEST_USER_2 = "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"; @@ -58,8 +62,8 @@ class PaymentServiceTest { 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); + user1 = new User().setId(1L).setUsername(TEST_USER_1); + user2 = new User().setId(2L).setUsername(TEST_USER_2); 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)); @@ -85,6 +89,8 @@ class PaymentServiceTest { @Test void getPaymentsForUser() { + List bankAccounts = List.of(bankAccount1); + user1.setBankAccounts(bankAccounts); 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); @@ -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); 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); @@ -172,4 +176,31 @@ class PaymentServiceTest { assertEquals(3, paymentsForUser.size()); } + + @Test + void deletePayment() { + List bankAccounts1 = List.of(bankAccount1); + user1.setBankAccounts(bankAccounts1); + List 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); + } } \ No newline at end of file