Merge remote-tracking branch 'origin/main'

# Conflicts:
#	src/main/java/net/kapcake/bankingservice/controllers/AuthController.java
#	src/main/java/net/kapcake/bankingservice/controllers/BankingServiceController.java
This commit is contained in:
2023-05-15 18:34:01 +02:00
16 changed files with 252 additions and 82 deletions

View File

@@ -2,8 +2,12 @@ package net.kapcake.bankingservice.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@@ -12,15 +16,42 @@ import org.springframework.security.web.SecurityFilterChain;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
public class AuthConfig { 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 @Bean
public SecurityFilterChain authenticationFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain authenticationFilterChain(HttpSecurity http) throws Exception {
return http return http
.csrf().disable() .csrf().disable()
.authorizeHttpRequests(requests -> requests .authorizeHttpRequests(requests -> requests
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.httpBasic().and() .httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .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(); .build();
} }

View File

@@ -1,5 +1,7 @@
package net.kapcake.bankingservice.config; 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.modelmapper.ModelMapper;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -17,4 +19,14 @@ public class BankingServiceConfig {
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build(); 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")
);
}
} }

View File

@@ -1,13 +1,16 @@
package net.kapcake.bankingservice.controllers; package net.kapcake.bankingservice.controllers;
import io.swagger.v3.oas.annotations.Operation; 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.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kapcake.bankingservice.exceptions.ValidationException; 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.security.UserDetailsImpl;
import net.kapcake.bankingservice.services.UserService; import net.kapcake.bankingservice.services.UserService;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -20,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import static net.kapcake.bankingservice.controllers.ControllerUtils.getErrorString; import static net.kapcake.bankingservice.controllers.ControllerUtils.getErrorString;
@Tag(name = "Authentication controller")
@RestController @RestController
@Slf4j @Slf4j
public class AuthController { public class AuthController {
@@ -29,8 +33,12 @@ public class AuthController {
this.userService = userService; this.userService = userService;
} }
@SecurityRequirement(name = "basic")
@Operation(summary = "Login using basic authentication to get a session cookie") @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") @PostMapping("/login")
public void login(HttpServletRequest request) { public void login(HttpServletRequest request) {
Authentication auth = (Authentication) request.getUserPrincipal(); Authentication auth = (Authentication) request.getUserPrincipal();
@@ -38,13 +46,30 @@ public class AuthController {
log.info("User {} logged in.", user.getUsername()); 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") @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()) { if (bindingResult.hasErrors()) {
String errorString = getErrorString(bindingResult); String errorString = getErrorString(bindingResult);
throw new ValidationException("User update request invalid: " + errorString); throw new ValidationException("User update request invalid: " + errorString);
} }
boolean needsLogout = userService.updateUser(authenticatedUser, userUpdateDTO); boolean needsLogout = userService.updateUser(authenticatedUser, userUpdateRequest);
if (needsLogout) { if (needsLogout) {
request.logout(); request.logout();
} }

View File

@@ -1,62 +1,119 @@
package net.kapcake.bankingservice.controllers; 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.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.Valid;
import jakarta.validation.Validator;
import net.kapcake.bankingservice.exceptions.ValidationException; import net.kapcake.bankingservice.exceptions.ValidationException;
import net.kapcake.bankingservice.model.dtos.BankAccountDTO; import net.kapcake.bankingservice.model.dtos.BankAccountDTO;
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.model.dtos.PaymentRequest;
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.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus; 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.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Set;
import static net.kapcake.bankingservice.controllers.ControllerUtils.getErrorString; import static net.kapcake.bankingservice.controllers.ControllerUtils.getErrorString;
@Tag(name = "Banking Service Controller")
@RestController @RestController
public class BankingServiceController { public class BankingServiceController {
private final AccountService accountService; private final AccountService accountService;
private final PaymentService paymentService; 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.accountService = accountService;
this.paymentService = paymentService; 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") @GetMapping("/accounts")
public List<BankAccountDTO> getAccounts(@AuthenticationPrincipal UserDetailsImpl authenticatedUser) { public List<BankAccountDTO> getAccounts(@AuthenticationPrincipal UserDetailsImpl authenticatedUser) {
return accountService.getAccounts(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") @PostMapping("/payment")
@ResponseStatus(HttpStatus.CREATED) @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()) { if (bindingResult.hasErrors()) {
String errorString = getErrorString(bindingResult); String errorString = getErrorString(bindingResult);
throw new ValidationException("Payment request invalid: " + errorString); 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") @GetMapping("/payments")
public List<PaymentDTO> getPayments(@AuthenticationPrincipal UserDetailsImpl authenticatedUser, public List<PaymentDTO> getPayments(@AuthenticationPrincipal UserDetailsImpl authenticatedUser,
@Parameter(description = "Filter to be applied") @RequestParam(name = "beneficiaryAccountNumber", required = false)
@RequestBody(required = false) @Valid PaymentFilter paymentFilter, @Parameter(description = "The start date to filter payments",example = "LU560303O43349845521")
BindingResult bindingResult) { String beneficiaryAccountNumber,
if (bindingResult.hasErrors()) { @RequestParam(name = "startDate", required = false)
String errorString = getErrorString(bindingResult); @Parameter(description = "The start date to filter payments", example = "2023-05-11T12:00")
throw new ValidationException("Payment filter is invalid: " + errorString); @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<ConstraintViolation<PaymentFilter>> violations = validator.validate(paymentFilter);
if (!violations.isEmpty()) {
String errorString = getErrorString(violations);
throw new ValidationException("Payment request invalid: " + errorString);
} }
return paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); 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}") @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); paymentService.deletePayment(authenticatedUser, id);
} }

View File

@@ -1,10 +1,12 @@
package net.kapcake.bankingservice.controllers; package net.kapcake.bankingservice.controllers;
import jakarta.validation.ConstraintViolation;
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 java.util.List; import java.util.List;
import java.util.Set;
public class ControllerUtils { public class ControllerUtils {
public static String getErrorString(BindingResult bindingResult) { public static String getErrorString(BindingResult bindingResult) {
@@ -24,4 +26,23 @@ public class ControllerUtils {
builder.append("]"); builder.append("]");
return builder.toString(); return builder.toString();
} }
public static <T> String getErrorString(Set<ConstraintViolation<T>> violations) {
StringBuilder builder = new StringBuilder("[");
List<ConstraintViolation<T>> allErrors = violations.stream().toList();
for (int i = 0; i < allErrors.size(); i++) {
ConstraintViolation<T> 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();
}
} }

View File

@@ -3,7 +3,7 @@ package net.kapcake.bankingservice.converters;
import org.modelmapper.ModelMapper; import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies; import org.modelmapper.convention.MatchingStrategies;
public abstract class AbstractConverter<E, D> { public abstract class AbstractConverter {
protected final ModelMapper modelMapper; protected final ModelMapper modelMapper;
public AbstractConverter(ModelMapper modelMapper) { public AbstractConverter(ModelMapper modelMapper) {
@@ -11,8 +11,4 @@ public abstract class AbstractConverter<E, D> {
this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); this.modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
} }
public abstract D mapToDTO(E entity);
public abstract E mapToEntity(D dto);
} }

View File

@@ -6,17 +6,15 @@ import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class BankAccountConverter extends AbstractConverter<BankAccount, BankAccountDTO> { public class BankAccountConverter extends AbstractConverter {
public BankAccountConverter(ModelMapper modelMapper) { public BankAccountConverter(ModelMapper modelMapper) {
super(modelMapper); super(modelMapper);
} }
@Override
public BankAccountDTO mapToDTO(BankAccount bankAccount) { public BankAccountDTO mapToDTO(BankAccount bankAccount) {
return modelMapper.map(bankAccount, BankAccountDTO.class); return modelMapper.map(bankAccount, BankAccountDTO.class);
} }
@Override
public BankAccount mapToEntity(BankAccountDTO bankAccountDTO) { public BankAccount mapToEntity(BankAccountDTO bankAccountDTO) {
return modelMapper.map(bankAccountDTO, BankAccount.class); return modelMapper.map(bankAccountDTO, BankAccount.class);
} }

View File

@@ -4,6 +4,7 @@ import net.kapcake.bankingservice.exceptions.ValidationException;
import net.kapcake.bankingservice.model.dtos.PaymentDTO; import net.kapcake.bankingservice.model.dtos.PaymentDTO;
import net.kapcake.bankingservice.model.domain.BankAccount; import net.kapcake.bankingservice.model.domain.BankAccount;
import net.kapcake.bankingservice.model.domain.Payment; import net.kapcake.bankingservice.model.domain.Payment;
import net.kapcake.bankingservice.model.dtos.PaymentRequest;
import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.repositories.BankAccountRepository;
import org.modelmapper.ModelMapper; import org.modelmapper.ModelMapper;
import org.modelmapper.TypeMap; import org.modelmapper.TypeMap;
@@ -12,7 +13,7 @@ import org.springframework.stereotype.Component;
import java.util.Optional; import java.util.Optional;
@Component @Component
public class PaymentConverter extends AbstractConverter<Payment, PaymentDTO> { public class PaymentConverter extends AbstractConverter {
private final BankAccountRepository bankAccountRepository; private final BankAccountRepository bankAccountRepository;
public PaymentConverter(BankAccountRepository bankAccountRepository, ModelMapper modelMapper) { public PaymentConverter(BankAccountRepository bankAccountRepository, ModelMapper modelMapper) {
@@ -20,7 +21,6 @@ public class PaymentConverter extends AbstractConverter<Payment, PaymentDTO> {
this.bankAccountRepository = bankAccountRepository; this.bankAccountRepository = bankAccountRepository;
} }
@Override
public PaymentDTO mapToDTO(Payment payment) { public PaymentDTO mapToDTO(Payment payment) {
TypeMap<Payment, PaymentDTO> paymentPaymentDTOTypeMap = modelMapper TypeMap<Payment, PaymentDTO> paymentPaymentDTOTypeMap = modelMapper
.typeMap(Payment.class, PaymentDTO.class) .typeMap(Payment.class, PaymentDTO.class)
@@ -28,10 +28,9 @@ public class PaymentConverter extends AbstractConverter<Payment, PaymentDTO> {
return paymentPaymentDTOTypeMap.map(payment); return paymentPaymentDTOTypeMap.map(payment);
} }
@Override public Payment mapToEntity(PaymentRequest paymentRequest) {
public Payment mapToEntity(PaymentDTO paymentDTO) { Payment payment = modelMapper.map(paymentRequest, Payment.class);
Payment payment = modelMapper.map(paymentDTO, Payment.class); Optional<BankAccount> bankAccountOptional = bankAccountRepository.findById(paymentRequest.getGiverAccount());
Optional<BankAccount> bankAccountOptional = bankAccountRepository.findById(paymentDTO.getGiverAccount());
if (bankAccountOptional.isEmpty()) { if (bankAccountOptional.isEmpty()) {
throw new ValidationException("Payment request invalid: Giver account does not exist."); throw new ValidationException("Payment request invalid: Giver account does not exist.");
} }

View File

@@ -7,7 +7,7 @@ import lombok.experimental.Accessors;
import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.CreationTimestamp;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.time.LocalDateTime;
@Entity @Entity
@Accessors(chain = true) @Accessors(chain = true)
@@ -31,9 +31,8 @@ public class Payment {
private String beneficiaryName; private String beneficiaryName;
private String communication; private String communication;
@CreationTimestamp @CreationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(nullable = false, updatable = false) @Column(nullable = false, updatable = false)
private Date creationDate; private LocalDateTime creationDate;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private PaymentStatus status; private PaymentStatus status;
} }

View File

@@ -9,7 +9,7 @@ import net.kapcake.bankingservice.model.domain.PaymentStatus;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.time.LocalDateTime;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@@ -27,6 +27,6 @@ public class PaymentDTO implements Serializable {
@NotNull @NotNull
private String beneficiaryName; private String beneficiaryName;
private String communication; private String communication;
private Date creationDate; private LocalDateTime creationDate;
private PaymentStatus status; private PaymentStatus status;
} }

View File

@@ -1,28 +1,28 @@
package net.kapcake.bankingservice.model.dtos; package net.kapcake.bankingservice.model.dtos;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.AssertTrue;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import net.kapcake.bankingservice.validation.AtLeastOneFieldNotEmpty;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date; import java.time.LocalDateTime;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@AtLeastOneFieldNotEmpty(fieldNames = {"beneficiaryAccountNumber", "startDate", "endDate"})
public class PaymentFilter implements Serializable { public class PaymentFilter implements Serializable {
private String beneficiaryAccountNumber; private String beneficiaryAccountNumber;
private Date startDate; private LocalDateTime startDate;
private Date endDate; private LocalDateTime endDate;
@Schema(hidden = true)
@AssertTrue(message = "Start and end date need to be in order") @AssertTrue(message = "Start and end date need to be in order")
public boolean isValidRange() { public boolean isValidRange() {
if (startDate == null || endDate == null) { if (startDate == null || endDate == null) {
return true; return true;
} else { } else {
return startDate.compareTo(endDate) <= 0; return !startDate.isAfter(endDate);
} }
} }
} }

View File

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

View File

@@ -10,7 +10,7 @@ import java.io.Serializable;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
@AtLeastOneFieldNotEmpty(fieldNames = {"password", "street", "number", "numberExtension", "postalCode", "country"}) @AtLeastOneFieldNotEmpty(fieldNames = {"password", "street", "number", "numberExtension", "postalCode", "country"})
public class UserUpdateDTO implements Serializable { public class UserUpdateRequest implements Serializable {
@Pattern(regexp = "^[^\\s]+$") @Pattern(regexp = "^[^\\s]+$")
private String password; private String password;
private String street; private String street;

View File

@@ -7,6 +7,7 @@ import net.kapcake.bankingservice.exceptions.ValidationException;
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;
import net.kapcake.bankingservice.model.dtos.PaymentFilter; 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.BalanceRepository;
import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.repositories.BankAccountRepository;
import net.kapcake.bankingservice.repositories.PaymentRepository; import net.kapcake.bankingservice.repositories.PaymentRepository;
@@ -16,8 +17,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import java.time.LocalDateTime;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -42,8 +43,8 @@ public class PaymentService {
this.transactionTemplate = new TransactionTemplate(transactionManager); this.transactionTemplate = new TransactionTemplate(transactionManager);
} }
public PaymentDTO createPayment(UserDetailsImpl authenticatedUser, PaymentDTO paymentDTO) { public PaymentDTO createPayment(UserDetailsImpl authenticatedUser, PaymentRequest paymentRequest) {
Payment payment = paymentConverter.mapToEntity(paymentDTO); Payment payment = paymentConverter.mapToEntity(paymentRequest);
List<BankAccount> userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername()); List<BankAccount> userBankAccounts = bankAccountRepository.findAllByUsers_username(authenticatedUser.getUsername());
paymentValidator.validate(userBankAccounts, payment); paymentValidator.validate(userBankAccounts, payment);
@@ -92,18 +93,18 @@ public class PaymentService {
List<Payment> filteredPayments = userPayments; List<Payment> filteredPayments = userPayments;
if (paymentFilter != null) { if (paymentFilter != null) {
String beneficiaryAccountNumber = paymentFilter.getBeneficiaryAccountNumber(); String beneficiaryAccountNumber = paymentFilter.getBeneficiaryAccountNumber();
Date startDate = paymentFilter.getStartDate(); LocalDateTime startDate = paymentFilter.getStartDate();
Date endDate = paymentFilter.getEndDate(); LocalDateTime endDate = paymentFilter.getEndDate();
filteredPayments = userPayments.stream().filter(payment -> { filteredPayments = userPayments.stream().filter(payment -> {
boolean filter = true; boolean filter = true;
if (beneficiaryAccountNumber != null) { if (beneficiaryAccountNumber != null) {
filter &= payment.getBeneficiaryAccountNumber().equals(beneficiaryAccountNumber); filter &= payment.getBeneficiaryAccountNumber().equals(beneficiaryAccountNumber);
} }
if (startDate != null) { if (startDate != null) {
filter &= payment.getCreationDate().compareTo(startDate) >= 0; filter &= !payment.getCreationDate().isBefore(startDate);
} }
if (endDate != null) { if (endDate != null) {
filter &= payment.getCreationDate().compareTo(endDate) <= 0; filter &= !payment.getCreationDate().isAfter(endDate);
} }
return filter; return filter;
}).collect(Collectors.toList()); }).collect(Collectors.toList());

View File

@@ -1,47 +1,44 @@
package net.kapcake.bankingservice.services; package net.kapcake.bankingservice.services;
import net.kapcake.bankingservice.model.domain.User; 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.repositories.UserRepository;
import net.kapcake.bankingservice.security.UserDetailsImpl; import net.kapcake.bankingservice.security.UserDetailsImpl;
import org.modelmapper.ModelMapper;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
public class UserService { public class UserService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final ModelMapper modelMapper;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, ModelMapper modelMapper, PasswordEncoder passwordEncoder) { public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.modelMapper = modelMapper;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
} }
public boolean updateUser(UserDetailsImpl authenticatedUser, UserUpdateDTO userUpdateDTO) { public boolean updateUser(UserDetailsImpl authenticatedUser, UserUpdateRequest userUpdateRequest) {
boolean needsLogout = false; boolean needsLogout = false;
User user = userRepository.findByUsername(authenticatedUser.getUsername()).orElseThrow(); User user = userRepository.findByUsername(authenticatedUser.getUsername()).orElseThrow();
User.UserBuilder builder = user.toBuilder(); User.UserBuilder builder = user.toBuilder();
if (userUpdateDTO.getCountry() != null) { if (userUpdateRequest.getCountry() != null) {
builder.country(userUpdateDTO.getCountry()); builder.country(userUpdateRequest.getCountry());
} }
if (userUpdateDTO.getStreet() != null) { if (userUpdateRequest.getStreet() != null) {
builder.street(userUpdateDTO.getStreet()); builder.street(userUpdateRequest.getStreet());
} }
if (userUpdateDTO.getPostalCode() != null) { if (userUpdateRequest.getPostalCode() != null) {
builder.postalCode(userUpdateDTO.getPostalCode()); builder.postalCode(userUpdateRequest.getPostalCode());
} }
if (userUpdateDTO.getNumber() != null) { if (userUpdateRequest.getNumber() != null) {
builder.number(userUpdateDTO.getNumber()); builder.number(userUpdateRequest.getNumber());
} }
if (userUpdateDTO.getNumberExtension() != null) { if (userUpdateRequest.getNumberExtension() != null) {
builder.numberExtension(userUpdateDTO.getNumberExtension()); builder.numberExtension(userUpdateRequest.getNumberExtension());
} }
if (userUpdateDTO.getPassword() != null) { if (userUpdateRequest.getPassword() != null) {
builder.password(passwordEncoder.encode(userUpdateDTO.getPassword())); builder.password(passwordEncoder.encode(userUpdateRequest.getPassword()));
needsLogout = true; needsLogout = true;
} }

View File

@@ -4,10 +4,10 @@ import net.kapcake.bankingservice.converters.PaymentConverter;
import net.kapcake.bankingservice.exceptions.AccessDeniedException; import net.kapcake.bankingservice.exceptions.AccessDeniedException;
import net.kapcake.bankingservice.exceptions.ResourceNotFoundException; import net.kapcake.bankingservice.exceptions.ResourceNotFoundException;
import net.kapcake.bankingservice.exceptions.ValidationException; import net.kapcake.bankingservice.exceptions.ValidationException;
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;
import net.kapcake.bankingservice.model.dtos.PaymentFilter; 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.BalanceRepository;
import net.kapcake.bankingservice.repositories.BankAccountRepository; import net.kapcake.bankingservice.repositories.BankAccountRepository;
import net.kapcake.bankingservice.repositories.PaymentRepository; import net.kapcake.bankingservice.repositories.PaymentRepository;
@@ -21,7 +21,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import java.math.BigDecimal; 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.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -73,15 +77,16 @@ class PaymentServiceTest {
@Test @Test
void createPayment() { void createPayment() {
UserDetailsImpl authenticatedUser = new UserDetailsImpl(user1); 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); 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); 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(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(paymentRepository.save(payment)).thenReturn(payment);
when(paymentConverter.mapToDTO(payment)).thenReturn(paymentDTO); when(paymentConverter.mapToDTO(payment)).thenReturn(paymentDTO);
paymentService.createPayment(authenticatedUser, paymentDTO); paymentService.createPayment(authenticatedUser, paymentRequest);
assertEquals(balance1.getAmount(), BigDecimal.valueOf(450)); assertEquals(balance1.getAmount(), BigDecimal.valueOf(450));
assertEquals(balance2.getAmount(), BigDecimal.valueOf(550)); assertEquals(balance2.getAmount(), BigDecimal.valueOf(550));
@@ -91,12 +96,12 @@ class PaymentServiceTest {
void getPaymentsForUser() { void getPaymentsForUser() {
List<BankAccount> bankAccounts = List.of(bankAccount1); List<BankAccount> bankAccounts = List.of(bankAccount1);
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(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(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(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(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(bankAccount1); 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(new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime()).setAmount(BigDecimal.valueOf(50)).setCurrency(Currency.EUR).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_2).setGiverAccount(1L); 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(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(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(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(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(bankAccountRepository.findAllByUsers_username(TEST_USER_1)).thenReturn(bankAccounts);
when(paymentRepository.findAllByGiverAccountIn(bankAccounts)).thenReturn(Arrays.asList(payment1, payment2, payment3)); when(paymentRepository.findAllByGiverAccountIn(bankAccounts)).thenReturn(Arrays.asList(payment1, payment2, payment3));
@@ -126,8 +131,8 @@ class PaymentServiceTest {
} }
private void getPaymentsForUsers_withAllFilter(UserDetailsImpl authenticatedUser) { private void getPaymentsForUsers_withAllFilter(UserDetailsImpl authenticatedUser) {
Date startDate = new GregorianCalendar(2023, Calendar.MAY, 11, 12, 1).getTime(); final LocalDateTime startDate = LocalDateTime.parse("2023-05-11T12:01");
Date endDate = new GregorianCalendar(2023, Calendar.MAY, 11, 12, 2).getTime(); final LocalDateTime endDate = LocalDateTime.parse("2023-05-11T12:02");
PaymentFilter paymentFilter = new PaymentFilter().setStartDate(startDate).setEndDate(endDate).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3); PaymentFilter paymentFilter = new PaymentFilter().setStartDate(startDate).setEndDate(endDate).setBeneficiaryAccountNumber(ACCOUNT_NUMBER_3);
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
@@ -136,7 +141,7 @@ class PaymentServiceTest {
} }
private void getPaymentsForUsers_withStartAndEndDateFilter(UserDetailsImpl authenticatedUser) { 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); PaymentFilter paymentFilter = new PaymentFilter().setStartDate(date).setEndDate(date);
List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
@@ -145,7 +150,8 @@ class PaymentServiceTest {
} }
private void getPaymentsForUsers_withEndDateFilter(UserDetailsImpl authenticatedUser) { 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<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(2, paymentsForUser.size()); assertEquals(2, paymentsForUser.size());
@@ -154,7 +160,8 @@ class PaymentServiceTest {
} }
private void getPaymentsForUsers_withStartDateFilter(UserDetailsImpl authenticatedUser) { 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<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter); List<PaymentDTO> paymentsForUser = paymentService.getPaymentsForUser(authenticatedUser, paymentFilter);
assertEquals(2, paymentsForUser.size()); assertEquals(2, paymentsForUser.size());
@@ -203,4 +210,4 @@ class PaymentServiceTest {
verify(paymentRepository).deleteById(1L); verify(paymentRepository).deleteById(1L);
} }
} }