From 2fa012a8b526c1f550926ca5a13a9a9ac0bcd4a0 Mon Sep 17 00:00:00 2001 From: kapcake Date: Mon, 15 May 2023 14:53:59 +0200 Subject: [PATCH] Add Swagger documentation * Split PaymentDTO and PaymentRequest * Updated Converters accordingly * Use LocalDateTime instead of Date --- pom.xml | 6 ++ .../bankingservice/config/AuthConfig.java | 33 +++++++- .../config/BankingServiceConfig.java | 12 +++ .../controllers/AuthController.java | 35 ++++++++- .../controllers/BankingServiceController.java | 77 +++++++++++++++++-- .../controllers/ControllerUtils.java | 21 +++++ .../converters/AbstractConverter.java | 6 +- .../converters/BankAccountConverter.java | 4 +- .../converters/PaymentConverter.java | 11 ++- .../bankingservice/model/domain/Payment.java | 5 +- .../bankingservice/model/dtos/PaymentDTO.java | 4 +- .../model/dtos/PaymentFilter.java | 12 +-- .../model/dtos/PaymentRequest.java | 27 +++++++ ...rUpdateDTO.java => UserUpdateRequest.java} | 2 +- .../services/PaymentService.java | 15 ++-- .../bankingservice/services/UserService.java | 33 ++++---- .../services/PaymentServiceTest.java | 39 ++++++---- 17 files changed, 263 insertions(+), 79 deletions(-) create mode 100644 src/main/java/net/kapcake/bankingservice/model/dtos/PaymentRequest.java rename src/main/java/net/kapcake/bankingservice/model/dtos/{UserUpdateDTO.java => UserUpdateRequest.java} (91%) diff --git a/pom.xml b/pom.xml index d7bdb34..4eb2d2b 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ 17 3.1.1 + 2.1.0 @@ -39,6 +40,11 @@ modelmapper ${modelmapper.version} + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc-openapi.version} + org.springframework.boot diff --git a/src/main/java/net/kapcake/bankingservice/config/AuthConfig.java b/src/main/java/net/kapcake/bankingservice/config/AuthConfig.java index c965e04..20fef59 100644 --- a/src/main/java/net/kapcake/bankingservice/config/AuthConfig.java +++ b/src/main/java/net/kapcake/bankingservice/config/AuthConfig.java @@ -2,8 +2,12 @@ package net.kapcake.bankingservice.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @@ -12,15 +16,42 @@ import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class AuthConfig { + private static final String[] AUTH_WHITELIST = { + "/v3/api-docs/**", + "/swagger-ui.html", + "/swagger-ui/**" + }; + + @Bean + @Order(1) + public SecurityFilterChain loginFilterChain(HttpSecurity http) throws Exception { + return http + .securityMatcher("/login", "/logout") + .csrf().disable() + .httpBasic().and() + .logout(logout -> logout + .clearAuthentication(true) + .invalidateHttpSession(true)).sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .build(); + } + @Bean public SecurityFilterChain authenticationFilterChain(HttpSecurity http) throws Exception { return http .csrf().disable() .authorizeHttpRequests(requests -> requests + .requestMatchers(AUTH_WHITELIST).permitAll() .anyRequest().authenticated() ) - .httpBasic().and() + .httpBasic(AbstractHttpConfigurer::disable) .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) + .exceptionHandling(handler -> handler + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType(MediaType.APPLICATION_JSON.toString()); + response.getWriter().write("{ \"error\": \"You are not authenticated.\" }"); + }) + ) .build(); } diff --git a/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java b/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java index 8236d7e..35964ec 100644 --- a/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java +++ b/src/main/java/net/kapcake/bankingservice/config/BankingServiceConfig.java @@ -1,5 +1,7 @@ package net.kapcake.bankingservice.config; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; import org.modelmapper.ModelMapper; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; @@ -17,4 +19,14 @@ public class BankingServiceConfig { public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { return restTemplateBuilder.build(); } + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Banking REST service") + .description("Simple REST API to perform payments") + .version("0.0.1") + ); + } } diff --git a/src/main/java/net/kapcake/bankingservice/controllers/AuthController.java b/src/main/java/net/kapcake/bankingservice/controllers/AuthController.java index 739f476..08b1e86 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/AuthController.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/AuthController.java @@ -1,11 +1,16 @@ package net.kapcake.bankingservice.controllers; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import net.kapcake.bankingservice.exceptions.ValidationException; -import net.kapcake.bankingservice.model.dtos.UserUpdateDTO; +import net.kapcake.bankingservice.model.dtos.UserUpdateRequest; import net.kapcake.bankingservice.security.UserDetailsImpl; import net.kapcake.bankingservice.services.UserService; import org.springframework.security.core.Authentication; @@ -18,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController; import static net.kapcake.bankingservice.controllers.ControllerUtils.getErrorString; +@Tag(name = "Authentication controller") @RestController @Slf4j public class AuthController { @@ -27,6 +33,12 @@ public class AuthController { this.userService = userService; } + @Operation(summary = "Login using basic authentication to get a session cookie") + @SecurityRequirement(name = "basic") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Successful login"), + @ApiResponse(responseCode = "401", description = "Unsuccessful login") + }) @PostMapping("/login") public void login(HttpServletRequest request) { Authentication auth = (Authentication) request.getUserPrincipal(); @@ -34,13 +46,30 @@ public class AuthController { log.info("User {} logged in.", user.getUsername()); } + @Operation(summary = "Logout and invalidate session") + @ApiResponse(responseCode = "204", description = "Successful logout") + @PostMapping("/logout") + public void logout() { + // Logout is handled by Spring Security + } + + @Operation(summary = "Update user details") + @SecurityRequirement(name = "cookie") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Successful update. If the password was updated, the user has been logged out"), + @ApiResponse(responseCode = "400", description = "Update request is malformed"), + @ApiResponse(responseCode = "401", description = "User not authenticated") + }) @PutMapping("/update-user") - public void updateUser(HttpServletRequest request, @AuthenticationPrincipal UserDetailsImpl authenticatedUser, @RequestBody @Valid UserUpdateDTO userUpdateDTO, BindingResult bindingResult) throws ServletException { + public void updateUser(HttpServletRequest request, + @AuthenticationPrincipal UserDetailsImpl authenticatedUser, + @RequestBody @Valid UserUpdateRequest userUpdateRequest, + BindingResult bindingResult) throws ServletException { if (bindingResult.hasErrors()) { String errorString = getErrorString(bindingResult); throw new ValidationException("User update request invalid: " + errorString); } - boolean needsLogout = userService.updateUser(authenticatedUser, userUpdateDTO); + boolean needsLogout = userService.updateUser(authenticatedUser, userUpdateRequest); if (needsLogout) { request.logout(); } diff --git a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java index 800baeb..6c6d7dc 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java @@ -1,58 +1,119 @@ package net.kapcake.bankingservice.controllers; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.ConstraintViolation; import jakarta.validation.Valid; +import jakarta.validation.Validator; import net.kapcake.bankingservice.exceptions.ValidationException; import net.kapcake.bankingservice.model.dtos.BankAccountDTO; import net.kapcake.bankingservice.model.dtos.PaymentDTO; import net.kapcake.bankingservice.model.dtos.PaymentFilter; +import net.kapcake.bankingservice.model.dtos.PaymentRequest; import net.kapcake.bankingservice.security.UserDetailsImpl; import net.kapcake.bankingservice.services.AccountService; import net.kapcake.bankingservice.services.PaymentService; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; import java.util.List; +import java.util.Set; import static net.kapcake.bankingservice.controllers.ControllerUtils.getErrorString; +@Tag(name = "Banking Service Controller") @RestController public class BankingServiceController { private final AccountService accountService; private final PaymentService paymentService; + private final Validator validator; - public BankingServiceController(AccountService accountService, PaymentService paymentService) { + public BankingServiceController(AccountService accountService, PaymentService paymentService, Validator validator) { this.accountService = accountService; this.paymentService = paymentService; + this.validator = validator; } + @Operation(summary = "List user bank accounts") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "List of accounts owned by the authenticated user"), + @ApiResponse(responseCode = "401", description = "User not authenticated", content = @Content) + }) + @SecurityRequirement(name = "cookie") @GetMapping("/accounts") public List getAccounts(@AuthenticationPrincipal UserDetailsImpl authenticatedUser) { return accountService.getAccounts(authenticatedUser); } + @Operation(summary = "Create payment") + @ApiResponses({ + @ApiResponse(responseCode = "201", description = "The payment was created successfully"), + @ApiResponse(responseCode = "400", description = "The payment request is invalid", content = @Content), + @ApiResponse(responseCode = "401", description = "User not authenticated", content = @Content) + }) + @SecurityRequirement(name = "cookie") @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 PaymentRequest paymentRequest, + BindingResult bindingResult) { if (bindingResult.hasErrors()) { String errorString = getErrorString(bindingResult); throw new ValidationException("Payment request invalid: " + errorString); } - return paymentService.createPayment(authenticatedUser, paymentDTO); + return paymentService.createPayment(authenticatedUser, paymentRequest); } + @Operation(summary = "List payments") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "List of payments by the authenticated user filtered by parameter values"), + @ApiResponse(responseCode = "400", description = "The filter is invalid", content = @Content), + @ApiResponse(responseCode = "401", description = "User not authenticated", content = @Content) + }) + @SecurityRequirement(name = "cookie") @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); + public List getPayments(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, + @RequestParam(name = "beneficiaryAccountNumber", required = false) + @Parameter(description = "The start date to filter payments",example = "LU560303O43349845521") + String beneficiaryAccountNumber, + @RequestParam(name = "startDate", required = false) + @Parameter(description = "The start date to filter payments", example = "2023-05-11T12:00") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + LocalDateTime startDate, + @RequestParam(name = "endDate", required = false) + @Parameter(description = "The end date to filter payments", example = "2023-05-11T12:00") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + LocalDateTime endDate) { + PaymentFilter paymentFilter = new PaymentFilter().setStartDate(startDate).setEndDate(endDate).setBeneficiaryAccountNumber(beneficiaryAccountNumber); + final Set> violations = validator.validate(paymentFilter); + if (!violations.isEmpty()) { + String errorString = getErrorString(violations); + throw new ValidationException("Payment request invalid: " + errorString); } return paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); } + @Operation(summary = "Delete payment") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Payment successfully deleted"), + @ApiResponse(responseCode = "401", description = "User not authenticated"), + @ApiResponse(responseCode = "403", description = "The payment to be deleted does not belong to the authenticated user or has already been executed"), + @ApiResponse(responseCode = "404", description = "A payment with this id was not found") + }) + @SecurityRequirement(name = "cookie") @DeleteMapping("/payment/{id}") - public void deletePayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, @PathVariable("id") Long id) { + public void deletePayment(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, + @Parameter(required = true, description = "Id of the payment to delete", example = "1") + @PathVariable("id") Long id) { paymentService.deletePayment(authenticatedUser, id); } diff --git a/src/main/java/net/kapcake/bankingservice/controllers/ControllerUtils.java b/src/main/java/net/kapcake/bankingservice/controllers/ControllerUtils.java index 6232551..9c7909b 100644 --- a/src/main/java/net/kapcake/bankingservice/controllers/ControllerUtils.java +++ b/src/main/java/net/kapcake/bankingservice/controllers/ControllerUtils.java @@ -1,10 +1,12 @@ package net.kapcake.bankingservice.controllers; +import jakarta.validation.ConstraintViolation; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import java.util.List; +import java.util.Set; public class ControllerUtils { public static String getErrorString(BindingResult bindingResult) { @@ -24,4 +26,23 @@ public class ControllerUtils { builder.append("]"); return builder.toString(); } + + public static String getErrorString(Set> violations) { + StringBuilder builder = new StringBuilder("["); + List> allErrors = violations.stream().toList(); + for (int i = 0; i < allErrors.size(); i++) { + ConstraintViolation error = allErrors.get(i); + if (i != 0) { + builder.append("\n"); + } + if (error.getPropertyPath() != null && !error.getPropertyPath().toString().isBlank()) { + builder.append(error.getPropertyPath()) + .append(": "); + } + builder.append(error.getMessage()); + } + builder.append("]"); + return builder.toString(); + } + } diff --git a/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java b/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java index 4040c1b..3949116 100644 --- a/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java +++ b/src/main/java/net/kapcake/bankingservice/converters/AbstractConverter.java @@ -3,7 +3,7 @@ package net.kapcake.bankingservice.converters; import org.modelmapper.ModelMapper; import org.modelmapper.convention.MatchingStrategies; -public abstract class AbstractConverter { +public abstract class AbstractConverter { protected final ModelMapper modelMapper; public AbstractConverter(ModelMapper modelMapper) { @@ -11,8 +11,4 @@ public abstract class AbstractConverter { this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); } - - public abstract D mapToDTO(E entity); - - public abstract E mapToEntity(D dto); } diff --git a/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java b/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java index 313d4e9..011ec42 100644 --- a/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java +++ b/src/main/java/net/kapcake/bankingservice/converters/BankAccountConverter.java @@ -6,17 +6,15 @@ import org.modelmapper.ModelMapper; import org.springframework.stereotype.Component; @Component -public class BankAccountConverter extends AbstractConverter { +public class BankAccountConverter extends AbstractConverter { public BankAccountConverter(ModelMapper modelMapper) { super(modelMapper); } - @Override public BankAccountDTO mapToDTO(BankAccount bankAccount) { return modelMapper.map(bankAccount, BankAccountDTO.class); } - @Override public BankAccount mapToEntity(BankAccountDTO bankAccountDTO) { return modelMapper.map(bankAccountDTO, BankAccount.class); } diff --git a/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java b/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java index f47a943..1634340 100644 --- a/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java +++ b/src/main/java/net/kapcake/bankingservice/converters/PaymentConverter.java @@ -4,6 +4,7 @@ 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; +import net.kapcake.bankingservice.model.dtos.PaymentRequest; import net.kapcake.bankingservice.repositories.BankAccountRepository; import org.modelmapper.ModelMapper; import org.modelmapper.TypeMap; @@ -12,7 +13,7 @@ import org.springframework.stereotype.Component; import java.util.Optional; @Component -public class PaymentConverter extends AbstractConverter { +public class PaymentConverter extends AbstractConverter { private final BankAccountRepository bankAccountRepository; public PaymentConverter(BankAccountRepository bankAccountRepository, ModelMapper modelMapper) { @@ -20,7 +21,6 @@ public class PaymentConverter extends AbstractConverter { this.bankAccountRepository = bankAccountRepository; } - @Override public PaymentDTO mapToDTO(Payment payment) { TypeMap paymentPaymentDTOTypeMap = modelMapper .typeMap(Payment.class, PaymentDTO.class) @@ -28,10 +28,9 @@ public class PaymentConverter extends AbstractConverter { return paymentPaymentDTOTypeMap.map(payment); } - @Override - public Payment mapToEntity(PaymentDTO paymentDTO) { - Payment payment = modelMapper.map(paymentDTO, Payment.class); - Optional bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount()); + public Payment mapToEntity(PaymentRequest paymentRequest) { + Payment payment = modelMapper.map(paymentRequest, Payment.class); + Optional bankAccountOptional = bankAccountRepository.findById(paymentRequest.getGiverAccount()); if (bankAccountOptional.isEmpty()) { throw new ValidationException("Payment request invalid: Giver account does not exist."); } diff --git a/src/main/java/net/kapcake/bankingservice/model/domain/Payment.java b/src/main/java/net/kapcake/bankingservice/model/domain/Payment.java index 9a17725..f335366 100644 --- a/src/main/java/net/kapcake/bankingservice/model/domain/Payment.java +++ b/src/main/java/net/kapcake/bankingservice/model/domain/Payment.java @@ -7,7 +7,7 @@ import lombok.experimental.Accessors; import org.hibernate.annotations.CreationTimestamp; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; @Entity @Accessors(chain = true) @@ -31,9 +31,8 @@ public class Payment { private String beneficiaryName; private String communication; @CreationTimestamp - @Temporal(TemporalType.TIMESTAMP) @Column(nullable = false, updatable = false) - private Date creationDate; + private LocalDateTime creationDate; @Enumerated(EnumType.STRING) private PaymentStatus status; } 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 494100f..3783566 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentDTO.java @@ -9,7 +9,7 @@ import net.kapcake.bankingservice.model.domain.PaymentStatus; import java.io.Serializable; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; @Data @Accessors(chain = true) @@ -27,6 +27,6 @@ public class PaymentDTO implements Serializable { @NotNull private String beneficiaryName; private String communication; - private Date creationDate; + private LocalDateTime creationDate; private PaymentStatus status; } diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java index 0fb7622..cd4c7c4 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentFilter.java @@ -1,28 +1,28 @@ package net.kapcake.bankingservice.model.dtos; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import lombok.Data; import lombok.experimental.Accessors; -import net.kapcake.bankingservice.validation.AtLeastOneFieldNotEmpty; import java.io.Serializable; -import java.util.Date; +import java.time.LocalDateTime; @Data @Accessors(chain = true) -@AtLeastOneFieldNotEmpty(fieldNames = {"beneficiaryAccountNumber", "startDate", "endDate"}) public class PaymentFilter implements Serializable { private String beneficiaryAccountNumber; - private Date startDate; - private Date endDate; + private LocalDateTime startDate; + private LocalDateTime endDate; + @Schema(hidden = true) @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; + return !startDate.isAfter(endDate); } } } diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentRequest.java b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentRequest.java new file mode 100644 index 0000000..7ade85d --- /dev/null +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/PaymentRequest.java @@ -0,0 +1,27 @@ +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 java.io.Serializable; +import java.math.BigDecimal; + +@Data +@Accessors(chain = true) +public class PaymentRequest implements Serializable { + @NotNull + @DecimalMin(value = "0", inclusive = false) + private BigDecimal amount; + @NotNull + private Currency currency; + @NotNull + private Long giverAccount; + @NotNull + private String beneficiaryAccountNumber; + @NotNull + private String beneficiaryName; + private String communication; +} diff --git a/src/main/java/net/kapcake/bankingservice/model/dtos/UserUpdateDTO.java b/src/main/java/net/kapcake/bankingservice/model/dtos/UserUpdateRequest.java similarity index 91% rename from src/main/java/net/kapcake/bankingservice/model/dtos/UserUpdateDTO.java rename to src/main/java/net/kapcake/bankingservice/model/dtos/UserUpdateRequest.java index e3012ba..956763b 100644 --- a/src/main/java/net/kapcake/bankingservice/model/dtos/UserUpdateDTO.java +++ b/src/main/java/net/kapcake/bankingservice/model/dtos/UserUpdateRequest.java @@ -10,7 +10,7 @@ import java.io.Serializable; @Data @Accessors(chain = true) @AtLeastOneFieldNotEmpty(fieldNames = {"password", "street", "number", "numberExtension", "postalCode", "country"}) -public class UserUpdateDTO implements Serializable { +public class UserUpdateRequest implements Serializable { @Pattern(regexp = "^[^\\s]+$") private String password; private String street; diff --git a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java index de6c316..ab56820 100644 --- a/src/main/java/net/kapcake/bankingservice/services/PaymentService.java +++ b/src/main/java/net/kapcake/bankingservice/services/PaymentService.java @@ -7,6 +7,7 @@ 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.model.dtos.PaymentRequest; import net.kapcake.bankingservice.repositories.BalanceRepository; import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.repositories.PaymentRepository; @@ -16,8 +17,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; +import java.time.LocalDateTime; import java.util.Comparator; -import java.util.Date; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -42,8 +43,8 @@ public class PaymentService { this.transactionTemplate = new TransactionTemplate(transactionManager); } - public PaymentDTO createPayment(UserDetailsImpl authenticatedUser, PaymentDTO paymentDTO) { - Payment payment = paymentConverter.mapToEntity(paymentDTO); + public PaymentDTO createPayment(UserDetailsImpl authenticatedUser, PaymentRequest paymentRequest) { + Payment payment = paymentConverter.mapToEntity(paymentRequest); List userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()); paymentValidator.validate(userBankAccounts, payment); @@ -92,18 +93,18 @@ public class PaymentService { List filteredPayments = userPayments; if (paymentFilter != null) { String beneficiaryAccountNumber = paymentFilter.getBeneficiaryAccountNumber(); - Date startDate = paymentFilter.getStartDate(); - Date endDate = paymentFilter.getEndDate(); + LocalDateTime startDate = paymentFilter.getStartDate(); + LocalDateTime 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; + filter &= !payment.getCreationDate().isBefore(startDate); } if (endDate != null) { - filter &= payment.getCreationDate().compareTo(endDate) <= 0; + filter &= !payment.getCreationDate().isAfter(endDate); } return filter; }).collect(Collectors.toList()); diff --git a/src/main/java/net/kapcake/bankingservice/services/UserService.java b/src/main/java/net/kapcake/bankingservice/services/UserService.java index 4887991..024420f 100644 --- a/src/main/java/net/kapcake/bankingservice/services/UserService.java +++ b/src/main/java/net/kapcake/bankingservice/services/UserService.java @@ -1,47 +1,44 @@ package net.kapcake.bankingservice.services; import net.kapcake.bankingservice.model.domain.User; -import net.kapcake.bankingservice.model.dtos.UserUpdateDTO; +import net.kapcake.bankingservice.model.dtos.UserUpdateRequest; import net.kapcake.bankingservice.repositories.UserRepository; import net.kapcake.bankingservice.security.UserDetailsImpl; -import org.modelmapper.ModelMapper; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepository; - private final ModelMapper modelMapper; private final PasswordEncoder passwordEncoder; - public UserService(UserRepository userRepository, ModelMapper modelMapper, PasswordEncoder passwordEncoder) { + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; - this.modelMapper = modelMapper; this.passwordEncoder = passwordEncoder; } - public boolean updateUser(UserDetailsImpl authenticatedUser, UserUpdateDTO userUpdateDTO) { + public boolean updateUser(UserDetailsImpl authenticatedUser, UserUpdateRequest userUpdateRequest) { boolean needsLogout = false; User user = userRepository.findByUsername(authenticatedUser.getUsername()).orElseThrow(); User.UserBuilder builder = user.toBuilder(); - if (userUpdateDTO.getCountry() != null) { - builder.country(userUpdateDTO.getCountry()); + if (userUpdateRequest.getCountry() != null) { + builder.country(userUpdateRequest.getCountry()); } - if (userUpdateDTO.getStreet() != null) { - builder.street(userUpdateDTO.getStreet()); + if (userUpdateRequest.getStreet() != null) { + builder.street(userUpdateRequest.getStreet()); } - if (userUpdateDTO.getPostalCode() != null) { - builder.postalCode(userUpdateDTO.getPostalCode()); + if (userUpdateRequest.getPostalCode() != null) { + builder.postalCode(userUpdateRequest.getPostalCode()); } - if (userUpdateDTO.getNumber() != null) { - builder.number(userUpdateDTO.getNumber()); + if (userUpdateRequest.getNumber() != null) { + builder.number(userUpdateRequest.getNumber()); } - if (userUpdateDTO.getNumberExtension() != null) { - builder.numberExtension(userUpdateDTO.getNumberExtension()); + if (userUpdateRequest.getNumberExtension() != null) { + builder.numberExtension(userUpdateRequest.getNumberExtension()); } - if (userUpdateDTO.getPassword() != null) { - builder.password(passwordEncoder.encode(userUpdateDTO.getPassword())); + if (userUpdateRequest.getPassword() != null) { + builder.password(passwordEncoder.encode(userUpdateRequest.getPassword())); needsLogout = true; } diff --git a/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java b/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java index 2ca4ac4..45def67 100644 --- a/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java +++ b/src/test/java/net/kapcake/bankingservice/services/PaymentServiceTest.java @@ -4,10 +4,10 @@ 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; import net.kapcake.bankingservice.model.dtos.PaymentFilter; +import net.kapcake.bankingservice.model.dtos.PaymentRequest; import net.kapcake.bankingservice.repositories.BalanceRepository; import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.repositories.PaymentRepository; @@ -21,7 +21,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.transaction.PlatformTransactionManager; import java.math.BigDecimal; -import java.util.*; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.verify; @@ -73,15 +77,16 @@ class PaymentServiceTest { @Test void createPayment() { UserDetailsImpl authenticatedUser = new UserDetailsImpl(user1); + PaymentRequest paymentRequest = new PaymentRequest().setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L); 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(paymentConverter.mapToEntity(paymentRequest)).thenReturn(payment); when(paymentRepository.save(payment)).thenReturn(payment); when(paymentConverter.mapToDTO(payment)).thenReturn(paymentDTO); - paymentService.createPayment(authenticatedUser, paymentDTO); + paymentService.createPayment(authenticatedUser, paymentRequest); assertEquals(balance1.getAmount(), BigDecimal.valueOf(450)); assertEquals(balance2.getAmount(), BigDecimal.valueOf(550)); @@ -91,12 +96,12 @@ class PaymentServiceTest { void getPaymentsForUser() { List bankAccounts = List.of(bankAccount1); 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); + Payment payment1 = new Payment().setId(1L).setCreationDate(LocalDateTime.parse("2023-05-11T12:00")).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1); + PaymentDTO paymentDTO1 = new PaymentDTO().setId(1L).setCreationDate(LocalDateTime.parse("2023-05-11T12:00")).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L); + Payment payment2 = new Payment().setId(2L).setCreationDate(LocalDateTime.parse("2023-05-11T12:01")).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1); + PaymentDTO paymentDTO2 = new PaymentDTO().setId(2L).setCreationDate(LocalDateTime.parse("2023-05-11T12:01")).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L); + Payment payment3 = new Payment().setId(3L).setCreationDate(LocalDateTime.parse("2023-05-11T12:02")).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3).setGiverAccount(bankAccount1); + PaymentDTO paymentDTO3 = new PaymentDTO().setId(3L).setCreationDate(LocalDateTime.parse("2023-05-11T12:02")).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3).setGiverAccount(1L); when(bankAccountRepository.findAllByUsers_username(TEST_USER_1)).thenReturn(bankAccounts); when(paymentRepository.findAllByGiverAccountIn(bankAccounts)).thenReturn(Arrays.asList(payment1, payment2, payment3)); @@ -126,8 +131,8 @@ class PaymentServiceTest { } 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(); + final LocalDateTime startDate = LocalDateTime.parse("2023-05-11T12:01"); + final LocalDateTime endDate = LocalDateTime.parse("2023-05-11T12:02"); PaymentFilter paymentFilter = new PaymentFilter().setStartDate(startDate).setEndDate(endDate).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3); List paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); @@ -136,7 +141,7 @@ class PaymentServiceTest { } private void getPaymentsForUsers_withStartAndEndDateFilter(UserDetailsImpl authenticatedUser) { - Date date = new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime(); + final LocalDateTime date = LocalDateTime.parse("2023-05-11T12:01"); PaymentFilter paymentFilter = new PaymentFilter().setStartDate(date).setEndDate(date); List paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); @@ -145,7 +150,8 @@ class PaymentServiceTest { } private void getPaymentsForUsers_withEndDateFilter(UserDetailsImpl authenticatedUser) { - PaymentFilter paymentFilter = new PaymentFilter().setEndDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime()); + final LocalDateTime endDate = LocalDateTime.parse("2023-05-11T12:01"); + PaymentFilter paymentFilter = new PaymentFilter().setEndDate(endDate); List paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); assertEquals(2, paymentsForUser.size()); @@ -154,7 +160,8 @@ class PaymentServiceTest { } private void getPaymentsForUsers_withStartDateFilter(UserDetailsImpl authenticatedUser) { - PaymentFilter paymentFilter = new PaymentFilter().setStartDate(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime()); + final LocalDateTime startDate = LocalDateTime.parse("2023-05-11T12:01"); + PaymentFilter paymentFilter = new PaymentFilter().setStartDate(startDate); List paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); assertEquals(2, paymentsForUser.size()); @@ -203,4 +210,4 @@ class PaymentServiceTest { verify(paymentRepository).deleteById(1L); } -} \ No newline at end of file +}