From d0a44929f010b6d40d9284a4df0c23b5f13d02ea Mon Sep 17 00:00:00 2001 From: trokhim03 Date: Mon, 5 May 2025 14:21:52 +0300 Subject: [PATCH 01/14] add car sharing project --- .github/workflows/ci.yml | 18 ++ .gitignore | 3 + Dockerfile | 14 + checkstyle.xml | 250 ++++++++++++++++++ docker-compose.yml | 39 +++ pom.xml | 129 ++++++++- .../carsharing/config/MapperConfig.java | 13 + .../carsharing/config/SecurityConfig.java | 63 +++++ .../carsharing/controller/AuthController.java | 42 +++ .../carsharing/controller/CarController.java | 73 +++++ .../controller/PaymentController.java | 62 +++++ .../controller/RentalController.java | 77 ++++++ .../carsharing/controller/UserController.java | 60 +++++ .../carsharing/dto/car/CarRequestDto.java | 31 +++ .../carsharing/dto/car/CarResponseDto.java | 22 ++ .../dto/payment/PaymentRequestDto.java | 12 + .../dto/payment/PaymentResponseDto.java | 22 ++ .../dto/rental/RentalRequestDto.java | 28 ++ .../dto/rental/RentalResponseDto.java | 19 ++ .../dto/rental/RentalReturnRequestDto.java | 22 ++ .../dto/role/RoleNameRequestDto.java | 13 + .../dto/user/UserLoginRequestDto.java | 18 ++ .../dto/user/UserLoginResponseDto.java | 4 + .../dto/user/UserRegistrationRequestDto.java | 29 ++ .../carsharing/dto/user/UserResponseDto.java | 18 ++ .../exceptions/CarNotAvailableException.java | 7 + .../exceptions/EntityNotFoundException.java | 7 + .../exceptions/PaymentException.java | 7 + .../exceptions/RegistrationException.java | 7 + .../exceptions/StripePaymentException.java | 7 + .../TelegramNotificationException.java | 7 + .../academy/carsharing/mapper/CarMapper.java | 20 ++ .../carsharing/mapper/PaymentMapper.java | 13 + .../carsharing/mapper/RentalMapper.java | 19 ++ .../academy/carsharing/mapper/UserMapper.java | 20 ++ .../mate/academy/carsharing/model/Car.java | 55 ++++ .../academy/carsharing/model/Payment.java | 67 +++++ .../mate/academy/carsharing/model/Rental.java | 48 ++++ .../mate/academy/carsharing/model/Role.java | 37 +++ .../mate/academy/carsharing/model/User.java | 89 +++++++ .../carsharing/repository/CarRepository.java | 7 + .../repository/PaymentRepository.java | 12 + .../repository/RentalRepository.java | 15 ++ .../carsharing/repository/RoleRepository.java | 9 + .../carsharing/repository/UserRepository.java | 16 ++ .../security/AuthenticationService.java | 26 ++ .../security/CustomUserDetailsService.java | 22 ++ .../security/JwtAuthenticationFilter.java | 55 ++++ .../academy/carsharing/security/JwtUtil.java | 60 +++++ .../carsharing/service/car/CarService.java | 18 ++ .../service/car/CarServiceImpl.java | 55 ++++ .../service/payment/PaymentService.java | 14 + .../service/payment/PaymentServiceImpl.java | 128 +++++++++ .../payment/stripe/StripePaymentService.java | 14 + .../stripe/StripePaymentServiceImpl.java | 101 +++++++ .../service/rental/RentalService.java | 17 ++ .../service/rental/RentalServiceImpl.java | 94 +++++++ .../service/telegram/CarSharingBot.java | 60 +++++ .../telegram/NotificationMessageService.java | 110 ++++++++ .../service/telegram/NotificationService.java | 24 ++ .../carsharing/service/user/UserService.java | 17 ++ .../service/user/UserServiceImpl.java | 84 ++++++ .../carsharing/validation/FieldMatch.java | 23 ++ .../validation/FieldMatchValidator.java | 28 ++ src/main/resources/application.properties | 28 ++ .../changes/01-create-users-table.yaml | 42 +++ .../changes/02-create-roles-table.yaml | 37 +++ .../changes/03-create-users-roles-table.yaml | 37 +++ .../changes/04-create-cars-table.yaml | 46 ++++ .../db/changelog/changes/05-insert-users.yaml | 41 +++ .../changes/06-insert-users-roles.yaml | 23 ++ .../changes/07-create-rentals-table.yaml | 66 +++++ .../changes/08-create-payments-table.yaml | 70 +++++ .../db/changelog/db.changelog-master.yaml | 17 ++ .../config/CustomMySqlContainer.java | 32 +++ .../controller/car/CarControllerTest.java | 191 +++++++++++++ .../rental/RentalControllerTest.java | 138 ++++++++++ .../controller/user/UserControllerTest.java | 123 +++++++++ .../service/car/CarServiceImplTest.java | 174 ++++++++++++ .../service/rental/RentalServiceImplTest.java | 188 +++++++++++++ .../service/user/UserServiceImplTest.java | 192 ++++++++++++++ src/test/resources/application.properties | 21 ++ src/test/resources/database/add-tesla-car.sql | 3 + .../database/create-test-for-rental.sql | 16 ++ src/test/resources/database/delete-all.sql | 4 + .../resources/database/remove-tesla-car.sql | 3 + 86 files changed, 3990 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile create mode 100644 checkstyle.xml create mode 100644 docker-compose.yml create mode 100644 src/main/java/mate/academy/carsharing/config/MapperConfig.java create mode 100644 src/main/java/mate/academy/carsharing/config/SecurityConfig.java create mode 100644 src/main/java/mate/academy/carsharing/controller/AuthController.java create mode 100644 src/main/java/mate/academy/carsharing/controller/CarController.java create mode 100644 src/main/java/mate/academy/carsharing/controller/PaymentController.java create mode 100644 src/main/java/mate/academy/carsharing/controller/RentalController.java create mode 100644 src/main/java/mate/academy/carsharing/controller/UserController.java create mode 100644 src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/car/CarResponseDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/payment/PaymentResponseDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/rental/RentalResponseDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/user/UserLoginRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/user/UserLoginResponseDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/user/UserRegistrationRequestDto.java create mode 100644 src/main/java/mate/academy/carsharing/dto/user/UserResponseDto.java create mode 100644 src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java create mode 100644 src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java create mode 100644 src/main/java/mate/academy/carsharing/exceptions/PaymentException.java create mode 100644 src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java create mode 100644 src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java create mode 100644 src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java create mode 100644 src/main/java/mate/academy/carsharing/mapper/CarMapper.java create mode 100644 src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java create mode 100644 src/main/java/mate/academy/carsharing/mapper/RentalMapper.java create mode 100644 src/main/java/mate/academy/carsharing/mapper/UserMapper.java create mode 100644 src/main/java/mate/academy/carsharing/model/Car.java create mode 100644 src/main/java/mate/academy/carsharing/model/Payment.java create mode 100644 src/main/java/mate/academy/carsharing/model/Rental.java create mode 100644 src/main/java/mate/academy/carsharing/model/Role.java create mode 100644 src/main/java/mate/academy/carsharing/model/User.java create mode 100644 src/main/java/mate/academy/carsharing/repository/CarRepository.java create mode 100644 src/main/java/mate/academy/carsharing/repository/PaymentRepository.java create mode 100644 src/main/java/mate/academy/carsharing/repository/RentalRepository.java create mode 100644 src/main/java/mate/academy/carsharing/repository/RoleRepository.java create mode 100644 src/main/java/mate/academy/carsharing/repository/UserRepository.java create mode 100644 src/main/java/mate/academy/carsharing/security/AuthenticationService.java create mode 100644 src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java create mode 100644 src/main/java/mate/academy/carsharing/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/mate/academy/carsharing/security/JwtUtil.java create mode 100644 src/main/java/mate/academy/carsharing/service/car/CarService.java create mode 100644 src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java create mode 100644 src/main/java/mate/academy/carsharing/service/payment/PaymentService.java create mode 100644 src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java create mode 100644 src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentService.java create mode 100644 src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java create mode 100644 src/main/java/mate/academy/carsharing/service/rental/RentalService.java create mode 100644 src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java create mode 100644 src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java create mode 100644 src/main/java/mate/academy/carsharing/service/telegram/NotificationMessageService.java create mode 100644 src/main/java/mate/academy/carsharing/service/telegram/NotificationService.java create mode 100644 src/main/java/mate/academy/carsharing/service/user/UserService.java create mode 100644 src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java create mode 100644 src/main/java/mate/academy/carsharing/validation/FieldMatch.java create mode 100644 src/main/java/mate/academy/carsharing/validation/FieldMatchValidator.java create mode 100644 src/main/resources/db/changelog/changes/01-create-users-table.yaml create mode 100644 src/main/resources/db/changelog/changes/02-create-roles-table.yaml create mode 100644 src/main/resources/db/changelog/changes/03-create-users-roles-table.yaml create mode 100644 src/main/resources/db/changelog/changes/04-create-cars-table.yaml create mode 100644 src/main/resources/db/changelog/changes/05-insert-users.yaml create mode 100644 src/main/resources/db/changelog/changes/06-insert-users-roles.yaml create mode 100644 src/main/resources/db/changelog/changes/07-create-rentals-table.yaml create mode 100644 src/main/resources/db/changelog/changes/08-create-payments-table.yaml create mode 100644 src/main/resources/db/changelog/db.changelog-master.yaml create mode 100644 src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java create mode 100644 src/test/java/mate/academy/carsharing/controller/car/CarControllerTest.java create mode 100644 src/test/java/mate/academy/carsharing/controller/rental/RentalControllerTest.java create mode 100644 src/test/java/mate/academy/carsharing/controller/user/UserControllerTest.java create mode 100644 src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java create mode 100644 src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java create mode 100644 src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java create mode 100644 src/test/resources/application.properties create mode 100644 src/test/resources/database/add-tesla-car.sql create mode 100644 src/test/resources/database/create-test-for-rental.sql create mode 100644 src/test/resources/database/delete-all.sql create mode 100644 src/test/resources/database/remove-tesla-car.sql diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..01e304b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: Java CI + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + cache: maven + - name: Build with Maven + run: mvn --batch-mode --update-snapshots verify diff --git a/.gitignore b/.gitignore index 549e00a..13ba091 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ target/ !**/src/main/**/target/ !**/src/test/**/target/ +.env +*.env +.env* ### STS ### .apt_generated .classpath diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7955f97 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM openjdk:17-slim AS builder +WORKDIR application +ARG JAR_FILE=target/*.jar +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract + +FROM openjdk:17-slim +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/snapshot-dependencies/ ./ +COPY --from=builder application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] +EXPOSE 8080 diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..4548f16 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..58fdcec --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.8" + +services: + mysqldb: + image: mysql:8 + platform: linux/amd64 + restart: unless-stopped + env_file: ./.env + environment: + MYSQL_ROOT_PASSWORD: "${MYSQLDB_ROOT_PASSWORD}" + MYSQL_DATABASE: "${MYSQLDB_DATABASE}" + MYSQL_USER: "${MYSQLDB_USER}" + MYSQL_PASSWORD: "${MYSQLDB_PASSWORD}" + ports: + - "${MYSQLDB_LOCAL_PORT}:3306" + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + interval: 30s + timeout: 30s + retries: 3 + + carsharing-service: + build: + context: . + dockerfile: Dockerfile + env_file: ./.env + restart: on-failure + ports: + - "${SPRING_LOCAL_PORT}:${SPRING_DOCKER_PORT}" + - "${DEBUG_PORT}:${DEBUG_PORT}" + environment: + SPRING_DATASOURCE_URL: "jdbc:mysql://mysqldb:3306/${MYSQLDB_DATABASE}" + SPRING_DATASOURCE_USERNAME: "${MYSQLDB_USER}" + SPRING_DATASOURCE_PASSWORD: "${MYSQLDB_PASSWORD}" + SPRING_JPA_PROPERTIES_HIBERNATE_DIALECT: "org.hibernate.dialect.MySQL8Dialect" + JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:${DEBUG_PORT}" + depends_on: + mysqldb: + condition: service_healthy \ No newline at end of file diff --git a/pom.xml b/pom.xml index 40a5a5a..154d758 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,9 @@ + checkstyle.xml 17 + 0.12.6 @@ -42,7 +44,20 @@ org.springframework.boot spring-boot-starter-web - + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + org.liquibase + liquibase-core + + + org.telegram + telegrambots-spring-boot-starter + 6.9.7.1 + com.h2database h2 @@ -68,6 +83,54 @@ spring-security-test test + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.mapstruct + mapstruct + 1.6.3 + + + org.mapstruct + mapstruct-processor + 1.6.3 + provided + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + com.stripe + stripe-java + 28.0.0 + + + org.testcontainers + mysql + 1.20.6 + test + + + org.testcontainers + junit-jupiter + test + @@ -96,7 +159,69 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.2.0 + + + verify + + check + + + + + checkstyle.xml + true + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + 1.18.30 + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + + compile + + check + + + + + ${maven.checkstyle.plugin.configLocation} + true + true + false + src + + - diff --git a/src/main/java/mate/academy/carsharing/config/MapperConfig.java b/src/main/java/mate/academy/carsharing/config/MapperConfig.java new file mode 100644 index 0000000..da4bd16 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/config/MapperConfig.java @@ -0,0 +1,13 @@ +package mate.academy.carsharing.config; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.NullValueCheckStrategy; + +@org.mapstruct.MapperConfig( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + implementationPackage = ".impl" +) +public class MapperConfig { +} diff --git a/src/main/java/mate/academy/carsharing/config/SecurityConfig.java b/src/main/java/mate/academy/carsharing/config/SecurityConfig.java new file mode 100644 index 0000000..6ed2650 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/config/SecurityConfig.java @@ -0,0 +1,63 @@ +package mate.academy.carsharing.config; + +import static org.springframework.security.config.Customizer.withDefaults; + +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.security.JwtAuthenticationFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableMethodSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final UserDetailsService service; + + @Bean + public PasswordEncoder getPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain( + HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) + throws Exception { + return http + .cors(AbstractHttpConfigurer::disable) + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests( + auth -> auth + .requestMatchers("/auth/**", "/error", + "/swagger-ui/**", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated() + ) + .httpBasic(withDefaults()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class) + .userDetailsService(service) + .build(); + } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } +} diff --git a/src/main/java/mate/academy/carsharing/controller/AuthController.java b/src/main/java/mate/academy/carsharing/controller/AuthController.java new file mode 100644 index 0000000..f85c0c1 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/controller/AuthController.java @@ -0,0 +1,42 @@ +package mate.academy.carsharing.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.user.UserLoginRequestDto; +import mate.academy.carsharing.dto.user.UserLoginResponseDto; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.security.AuthenticationService; +import mate.academy.carsharing.service.user.UserService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Authentication management", + description = "Endpoints for user registration and login") +@RestController +@RequiredArgsConstructor +@RequestMapping("/auth") +public class AuthController { + private final UserService userService; + private final AuthenticationService authenticationService; + + @Operation(summary = "Register a new user", + description = "Register a new user with email, password and other details") + @PostMapping("/registration") + public UserResponseDto registration( + @RequestBody UserRegistrationRequestDto userRegistrationRequestDto) + throws RegistrationException { + return userService.register(userRegistrationRequestDto); + } + + @Operation(summary = "Login user", + description = "Authenticate user and return JWT token") + @PostMapping("/login") + public UserLoginResponseDto login(@RequestBody UserLoginRequestDto userLoginRequestDto) { + return authenticationService.authenticate(userLoginRequestDto); + } +} diff --git a/src/main/java/mate/academy/carsharing/controller/CarController.java b/src/main/java/mate/academy/carsharing/controller/CarController.java new file mode 100644 index 0000000..d276ca9 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/controller/CarController.java @@ -0,0 +1,73 @@ +package mate.academy.carsharing.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.car.CarRequestDto; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.service.car.CarService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Car manager", + description = "Endpoints for managing cars") +@RestController +@RequiredArgsConstructor +@RequestMapping("/cars") +public class CarController { + private final CarService carService; + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Get all cars", + description = "Get a paginated list of all available cars") + @GetMapping + public Page getCars(Pageable pageable) { + return carService.getCars(pageable); + } + + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "Create a new car", + description = "Create a new car with the provided details") + @PostMapping + public CarResponseDto createCar(@RequestBody CarRequestDto carRequestDto) { + return carService.createCar(carRequestDto); + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Get car by ID", + description = "Get a single car by its unique identifier") + @GetMapping("/{carId}") + public CarResponseDto findById(@PathVariable Long carId) { + return carService.findById(carId); + } + + @PreAuthorize("hasRole('MANAGER')") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Update car by ID", + description = "Update an existing car with new data") + @PutMapping("/{carId}") + public CarResponseDto updateById(@PathVariable Long carId, + @RequestBody CarRequestDto carRequestDto) { + return carService.updateById(carId, carRequestDto); + } + + @PreAuthorize("hasRole('MANAGER')") + @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete car by ID", + description = "Delete a car by its unique identifier") + @DeleteMapping("/{carId}") + public void deleteById(@PathVariable Long carId) { + carService.deleteById(carId); + } +} diff --git a/src/main/java/mate/academy/carsharing/controller/PaymentController.java b/src/main/java/mate/academy/carsharing/controller/PaymentController.java new file mode 100644 index 0000000..9e38437 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/controller/PaymentController.java @@ -0,0 +1,62 @@ +package mate.academy.carsharing.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.payment.PaymentRequestDto; +import mate.academy.carsharing.dto.payment.PaymentResponseDto; +import mate.academy.carsharing.service.payment.PaymentService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Payment Management", + description = "Endpoints for managing payments and payment processing") +@RestController +@RequiredArgsConstructor +@RequestMapping("/payments") +public class PaymentController { + private final PaymentService paymentService; + + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "Get user payments", + description = "Retrieve paginated list of payments for specific user") + @GetMapping + public Page getWithUserId(Pageable pageable, + @RequestParam(name = "user_id") Long userId) { + return paymentService.getWithUserId(pageable, userId); + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Create payment", + description = "Create a new payment session for rental") + @PostMapping + public PaymentResponseDto createPayment(@RequestBody PaymentRequestDto paymentRequestDto) { + return paymentService.createPayment(paymentRequestDto); + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Payment success callback", + description = "Endpoint for payment provider to redirect after successful payment") + @GetMapping("/success/{paymentId}") + public String paymentSuccessRedirect(@PathVariable(required = false) Long paymentId) { + paymentService.checkSuccessfulPayment(paymentId); + return "Success"; + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Payment cancellation callback", + description = "Endpoint for payment provider to redirect after cancelled payment") + @GetMapping("/cancel/{paymentId}") + public String paymentCancelRedirect(@PathVariable(required = false) Long paymentId) { + return "Payment with id " + paymentId + + " was canceled."; + } +} diff --git a/src/main/java/mate/academy/carsharing/controller/RentalController.java b/src/main/java/mate/academy/carsharing/controller/RentalController.java new file mode 100644 index 0000000..bc70dbc --- /dev/null +++ b/src/main/java/mate/academy/carsharing/controller/RentalController.java @@ -0,0 +1,77 @@ +package mate.academy.carsharing.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.rental.RentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.service.rental.RentalService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Rental Management", + description = "Endpoints for managing car rentals") +@RestController +@RequiredArgsConstructor +@RequestMapping("/rentals") +public class RentalController { + private final RentalService rentalService; + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Create new rental", + description = "Create a new car rental for authenticated user") + @ResponseStatus(HttpStatus.CREATED) + @PostMapping + public RentalResponseDto createRental(Authentication authentication, + @RequestBody RentalRequestDto rentalRequestDto) { + Long userId = getAuthenticationUserId(authentication); + return rentalService.createRental(userId, rentalRequestDto); + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Get rental by ID", + description = "Get specific rental details by ID for authenticated user") + @GetMapping("/{rentalId}") + public RentalResponseDto getById(Authentication authentication, @PathVariable Long rentalId) { + Long userId = getAuthenticationUserId(authentication); + return rentalService.getById(userId, rentalId); + } + + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "Get user rentals", + description = "Get paginated list of rentals for" + + " specific user with active status filter") + @GetMapping + public Page getAllByUserId( + Pageable pageable, + @RequestParam(name = "user_id") Long userId, + @RequestParam(name = "is_active") boolean isActive) { + return rentalService.getAllByUserId(pageable, userId, isActive); + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Return rental car", + description = "Set actual return date and process rental completion") + @PostMapping("/return") + public RentalResponseDto setDataReturn( + @RequestBody RentalReturnRequestDto rentalReturnRequestDto) { + return rentalService.setDataReturn(rentalReturnRequestDto); + } + + private Long getAuthenticationUserId(Authentication authentication) { + return ((User) authentication.getPrincipal()).getId(); + } +} diff --git a/src/main/java/mate/academy/carsharing/controller/UserController.java b/src/main/java/mate/academy/carsharing/controller/UserController.java new file mode 100644 index 0000000..353996d --- /dev/null +++ b/src/main/java/mate/academy/carsharing/controller/UserController.java @@ -0,0 +1,60 @@ +package mate.academy.carsharing.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.role.RoleNameRequestDto; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.service.user.UserService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "User Management", + description = "Endpoints for managing user accounts and roles") +@RestController +@RequiredArgsConstructor +@RequestMapping("/users") +public class UserController { + private final UserService userService; + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Get current user info", + description = "Retrieve authenticated user's information") + @GetMapping("/me") + public UserResponseDto getUser(Authentication authentication) { + Long authenticationUserId = getAuthenticationUserId(authentication); + return userService.getUser(authenticationUserId); + } + + @PreAuthorize("hasRole('MANAGER')") + @Operation(summary = "Update user role", + description = "Update user's role (Admin only)") + @PutMapping("/update/{userId}/role") + public UserResponseDto updateRole(@PathVariable Long userId, + @RequestBody RoleNameRequestDto roleNameRequestDto) { + return userService.updateUserRole(userId, roleNameRequestDto); + } + + @PreAuthorize("hasRole('CUSTOMER')") + @Operation(summary = "Update current user info", + description = "Update authenticated user's personal information") + @PutMapping("/me") + public UserResponseDto updateUserInfo( + Authentication authentication, + @RequestBody UserRegistrationRequestDto userRegistrationRequestDto) { + Long authenticationUserId = getAuthenticationUserId(authentication); + return userService.updateMe(authenticationUserId, userRegistrationRequestDto); + } + + private Long getAuthenticationUserId(Authentication authentication) { + return ((User) authentication.getPrincipal()).getId(); + } +} diff --git a/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java b/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java new file mode 100644 index 0000000..c930b9a --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java @@ -0,0 +1,31 @@ +package mate.academy.carsharing.dto.car; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.math.BigDecimal; +import lombok.Data; +import lombok.experimental.Accessors; +import mate.academy.carsharing.model.Car; + +@Data +@Accessors(chain = true) +public class CarRequestDto { + @NotBlank + private String model; + + @NotBlank + + private String brand; + + @NotBlank + private Car.Type type; + + @NotBlank + @Min(0) + private int inventory; + + @NotBlank + @DecimalMin(value = "0.0", inclusive = false) + private BigDecimal dailyFee; +} diff --git a/src/main/java/mate/academy/carsharing/dto/car/CarResponseDto.java b/src/main/java/mate/academy/carsharing/dto/car/CarResponseDto.java new file mode 100644 index 0000000..e5b5333 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/car/CarResponseDto.java @@ -0,0 +1,22 @@ +package mate.academy.carsharing.dto.car; + +import java.math.BigDecimal; +import lombok.Data; +import lombok.experimental.Accessors; +import mate.academy.carsharing.model.Car; + +@Data +@Accessors(chain = true) +public class CarResponseDto { + private Long id; + + private String model; + + private String brand; + + private Car.Type type; + + private int inventory; + + private BigDecimal dailyFee; +} diff --git a/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java b/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java new file mode 100644 index 0000000..178ed9a --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java @@ -0,0 +1,12 @@ +package mate.academy.carsharing.dto.payment; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +@Data +public class PaymentRequestDto { + @NotBlank + @Positive + private Long rentalId; +} diff --git a/src/main/java/mate/academy/carsharing/dto/payment/PaymentResponseDto.java b/src/main/java/mate/academy/carsharing/dto/payment/PaymentResponseDto.java new file mode 100644 index 0000000..eab3f46 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/payment/PaymentResponseDto.java @@ -0,0 +1,22 @@ +package mate.academy.carsharing.dto.payment; + +import java.math.BigDecimal; +import lombok.Data; +import mate.academy.carsharing.model.Payment; + +@Data +public class PaymentResponseDto { + private Long id; + + private Payment.Status status; + + private Payment.Type type; + + private Long rentalId; + + private String sessionUrl; + + private String sessionId; + + private BigDecimal amountToPay; +} diff --git a/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java b/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java new file mode 100644 index 0000000..9295399 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java @@ -0,0 +1,28 @@ +package mate.academy.carsharing.dto.rental; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.Positive; +import java.time.LocalDate; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class RentalRequestDto { + @NotBlank + @JsonFormat(pattern = "yyyy-MM-dd") + @FutureOrPresent + private LocalDate rentalDate; + + @NotBlank + @JsonFormat(pattern = "yyyy-MM-dd") + @PastOrPresent + private LocalDate returnDate; + + @NotBlank + @Positive + private Long carId; +} diff --git a/src/main/java/mate/academy/carsharing/dto/rental/RentalResponseDto.java b/src/main/java/mate/academy/carsharing/dto/rental/RentalResponseDto.java new file mode 100644 index 0000000..5ba8e27 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/rental/RentalResponseDto.java @@ -0,0 +1,19 @@ +package mate.academy.carsharing.dto.rental; + +import java.time.LocalDate; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class RentalResponseDto { + private LocalDate rentalDate; + + private LocalDate returnDate; + + private LocalDate actualReturnDate; + + private Long carId; + + private Long userId; +} diff --git a/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java b/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java new file mode 100644 index 0000000..1a2ccec --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java @@ -0,0 +1,22 @@ +package mate.academy.carsharing.dto.rental; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; +import java.time.LocalDate; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class RentalReturnRequestDto { + @NotBlank + @Positive + private Long rentalId; + + @NotBlank + @FutureOrPresent + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate actualReturnDate; +} diff --git a/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java b/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java new file mode 100644 index 0000000..1bd0a52 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java @@ -0,0 +1,13 @@ +package mate.academy.carsharing.dto.role; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.experimental.Accessors; +import mate.academy.carsharing.model.Role; + +@Data +@Accessors(chain = true) +public class RoleNameRequestDto { + @NotBlank + private Role.RoleName roleName; +} diff --git a/src/main/java/mate/academy/carsharing/dto/user/UserLoginRequestDto.java b/src/main/java/mate/academy/carsharing/dto/user/UserLoginRequestDto.java new file mode 100644 index 0000000..a803119 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/user/UserLoginRequestDto.java @@ -0,0 +1,18 @@ +package mate.academy.carsharing.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class UserLoginRequestDto { + @NotBlank + @Email + @Size(min = 8, max = 30) + private String email; + + @NotBlank + @Size(min = 8, max = 20) + private String password; +} diff --git a/src/main/java/mate/academy/carsharing/dto/user/UserLoginResponseDto.java b/src/main/java/mate/academy/carsharing/dto/user/UserLoginResponseDto.java new file mode 100644 index 0000000..8578504 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/user/UserLoginResponseDto.java @@ -0,0 +1,4 @@ +package mate.academy.carsharing.dto.user; + +public record UserLoginResponseDto(String token) { +} diff --git a/src/main/java/mate/academy/carsharing/dto/user/UserRegistrationRequestDto.java b/src/main/java/mate/academy/carsharing/dto/user/UserRegistrationRequestDto.java new file mode 100644 index 0000000..8fa421a --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/user/UserRegistrationRequestDto.java @@ -0,0 +1,29 @@ +package mate.academy.carsharing.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import mate.academy.carsharing.validation.FieldMatch; + +@Data +@FieldMatch(first = "password", second = "repeatPassword") +public class UserRegistrationRequestDto { + @Email + @NotBlank + private String email; + + @NotBlank + @Size(min = 8, max = 20) + private String password; + + @NotBlank + @Size(min = 8, max = 20) + private String repeatPassword; + + @NotBlank + private String firstName; + + @NotBlank + private String lastName; +} diff --git a/src/main/java/mate/academy/carsharing/dto/user/UserResponseDto.java b/src/main/java/mate/academy/carsharing/dto/user/UserResponseDto.java new file mode 100644 index 0000000..a3c5b7d --- /dev/null +++ b/src/main/java/mate/academy/carsharing/dto/user/UserResponseDto.java @@ -0,0 +1,18 @@ +package mate.academy.carsharing.dto.user; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class UserResponseDto { + private Long id; + + private String email; + + private String firstName; + + private String lastName; + + private String password; +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java b/src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java new file mode 100644 index 0000000..5b800de --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exceptions; + +public class CarNotAvailableException extends RuntimeException { + public CarNotAvailableException(String message) { + super(message); + } +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java b/src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java new file mode 100644 index 0000000..4165aa6 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exceptions; + +public class EntityNotFoundException extends RuntimeException { + public EntityNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/PaymentException.java b/src/main/java/mate/academy/carsharing/exceptions/PaymentException.java new file mode 100644 index 0000000..3560fc6 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exceptions/PaymentException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exceptions; + +public class PaymentException extends RuntimeException { + public PaymentException(String message) { + super(message); + } +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java b/src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java new file mode 100644 index 0000000..d93b368 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exceptions; + +public class RegistrationException extends Exception { + public RegistrationException(String message) { + super(message); + } +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java b/src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java new file mode 100644 index 0000000..64c9138 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exceptions; + +public class StripePaymentException extends RuntimeException { + public StripePaymentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java b/src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java new file mode 100644 index 0000000..c3c4c14 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.exceptions; + +public class TelegramNotificationException extends RuntimeException { + public TelegramNotificationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/mate/academy/carsharing/mapper/CarMapper.java b/src/main/java/mate/academy/carsharing/mapper/CarMapper.java new file mode 100644 index 0000000..fb3f647 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/mapper/CarMapper.java @@ -0,0 +1,20 @@ +package mate.academy.carsharing.mapper; + +import mate.academy.carsharing.config.MapperConfig; +import mate.academy.carsharing.dto.car.CarRequestDto; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.model.Car; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +@Mapper(config = MapperConfig.class) +public interface CarMapper { + CarResponseDto toDto(Car car); + + Car toModel(CarRequestDto carRequestDto); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateModelFromDto(CarRequestDto requestDto, @MappingTarget Car car); +} diff --git a/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java b/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java new file mode 100644 index 0000000..37b2614 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/mapper/PaymentMapper.java @@ -0,0 +1,13 @@ +package mate.academy.carsharing.mapper; + +import mate.academy.carsharing.config.MapperConfig; +import mate.academy.carsharing.dto.payment.PaymentResponseDto; +import mate.academy.carsharing.model.Payment; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface PaymentMapper { + @Mapping(target = "rentalId", source = "rental.id") + PaymentResponseDto toDto(Payment payment); +} diff --git a/src/main/java/mate/academy/carsharing/mapper/RentalMapper.java b/src/main/java/mate/academy/carsharing/mapper/RentalMapper.java new file mode 100644 index 0000000..7335d61 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/mapper/RentalMapper.java @@ -0,0 +1,19 @@ +package mate.academy.carsharing.mapper; + +import mate.academy.carsharing.config.MapperConfig; +import mate.academy.carsharing.dto.rental.RentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.model.Rental; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface RentalMapper { + @Mapping(target = "user.id", source = "userId") + @Mapping(target = "car.id", source = "carId") + Rental toModelWithCarIdAndUserId(RentalRequestDto rentalRequestDto, Long carId, Long userId); + + @Mapping(target = "carId", source = "car.id") + @Mapping(target = "userId", source = "user.id") + RentalResponseDto toDto(Rental rental); +} diff --git a/src/main/java/mate/academy/carsharing/mapper/UserMapper.java b/src/main/java/mate/academy/carsharing/mapper/UserMapper.java new file mode 100644 index 0000000..88be139 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/mapper/UserMapper.java @@ -0,0 +1,20 @@ +package mate.academy.carsharing.mapper; + +import mate.academy.carsharing.config.MapperConfig; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.model.User; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; + +@Mapper(config = MapperConfig.class) +public interface UserMapper { + UserResponseDto toUserResponse(User user); + + User toModel(UserRegistrationRequestDto userRegistrationDto); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + void updateModelFromDto(UserRegistrationRequestDto requestDto, @MappingTarget User user); +} diff --git a/src/main/java/mate/academy/carsharing/model/Car.java b/src/main/java/mate/academy/carsharing/model/Car.java new file mode 100644 index 0000000..f7adbc8 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/model/Car.java @@ -0,0 +1,55 @@ +package mate.academy.carsharing.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Getter +@Setter +@SQLDelete(sql = "UPDATE cars SET is_deleted = true WHERE id=?") +@SQLRestriction(value = "is_deleted = false") +@Accessors(chain = true) +@Table(name = "cars") +public class Car { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String model; + + @Column(nullable = false) + private String brand; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Type type; + + @Column(nullable = false) + private int inventory; + + @Column(name = "daily_fee", nullable = false) + private BigDecimal dailyFee; + + @Column(nullable = false) + private boolean isDeleted = false; + + public enum Type { + SEDAN, + SUV, + HATCHBACK, + UNIVERSAL + } +} diff --git a/src/main/java/mate/academy/carsharing/model/Payment.java b/src/main/java/mate/academy/carsharing/model/Payment.java new file mode 100644 index 0000000..eb637b9 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/model/Payment.java @@ -0,0 +1,67 @@ +package mate.academy.carsharing.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Setter +@Getter +@SQLDelete(sql = "UPDATE payments SET is_deleted = true WHERE id =?") +@SQLRestriction(value = "is_deleted = false") +@Table(name = "payments") +public class Payment { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Status status; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Type type; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "rental_id", nullable = false) + private Rental rental; + + @Lob + @Column(name = "session_url", nullable = false, columnDefinition = "TEXT") + private String sessionUrl; + + @Lob + @Column(name = "session_id", nullable = false, unique = true, columnDefinition = "TEXT") + private String sessionId; + + @Column(name = "amount_to_pay", nullable = false) + private BigDecimal amountToPay; + + @Column(nullable = false) + private boolean isDeleted = false; + + public enum Status { + PENDING, + PAID + } + + public enum Type { + PAYMENT, + FINE + } +} diff --git a/src/main/java/mate/academy/carsharing/model/Rental.java b/src/main/java/mate/academy/carsharing/model/Rental.java new file mode 100644 index 0000000..d9041a8 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/model/Rental.java @@ -0,0 +1,48 @@ +package mate.academy.carsharing.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Setter +@Getter +@SQLDelete(sql = "UPDATE rentals SET is_deleted = true WHERE id=?") +@SQLRestriction(value = "is_deleted = false") +@Table(name = "rentals") +public class Rental { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "rental_date", nullable = false) + private LocalDate rentalDate; + + @Column(name = "return_date", nullable = false) + private LocalDate returnDate; + + @Column(name = "actual_return_date") + private LocalDate actualReturnDate; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "car_id", nullable = false) + private Car car; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private boolean isDeleted = false; +} diff --git a/src/main/java/mate/academy/carsharing/model/Role.java b/src/main/java/mate/academy/carsharing/model/Role.java new file mode 100644 index 0000000..52e34a6 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/model/Role.java @@ -0,0 +1,37 @@ +package mate.academy.carsharing.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; + +@Entity +@Setter +@Getter +@Table(name = "roles") +public class Role implements GrantedAuthority { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(value = EnumType.STRING) + @Column(nullable = false, unique = true) + private RoleName name; + + @Override + public String getAuthority() { + return "ROLE_" + name.name(); + } + + public enum RoleName { + MANAGER, + CUSTOMER + } +} diff --git a/src/main/java/mate/academy/carsharing/model/User.java b/src/main/java/mate/academy/carsharing/model/User.java new file mode 100644 index 0000000..23d6fbf --- /dev/null +++ b/src/main/java/mate/academy/carsharing/model/User.java @@ -0,0 +1,89 @@ +package mate.academy.carsharing.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.Table; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +@Entity +@Setter +@Getter +@SQLDelete(sql = "UPDATE users SET is_deleted = true Where id=?") +@SQLRestriction(value = "is_deleted = false") +@Table(name = "users") +public class User implements UserDetails { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String email; + + @Column(name = "first_name", nullable = false) + private String firstName; + + @Column(name = "last_name", nullable = false) + private String lastName; + + @Column(nullable = false) + private String password; + + @ManyToMany + @JoinTable(name = "users_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + private Set roles = new HashSet<>(); + + @Column(nullable = false) + private boolean isDeleted = false; + + @Override + public Collection getAuthorities() { + return roles; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return !isDeleted; + } +} diff --git a/src/main/java/mate/academy/carsharing/repository/CarRepository.java b/src/main/java/mate/academy/carsharing/repository/CarRepository.java new file mode 100644 index 0000000..f15c2ee --- /dev/null +++ b/src/main/java/mate/academy/carsharing/repository/CarRepository.java @@ -0,0 +1,7 @@ +package mate.academy.carsharing.repository; + +import mate.academy.carsharing.model.Car; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CarRepository extends JpaRepository { +} diff --git a/src/main/java/mate/academy/carsharing/repository/PaymentRepository.java b/src/main/java/mate/academy/carsharing/repository/PaymentRepository.java new file mode 100644 index 0000000..33e9a1e --- /dev/null +++ b/src/main/java/mate/academy/carsharing/repository/PaymentRepository.java @@ -0,0 +1,12 @@ +package mate.academy.carsharing.repository; + +import mate.academy.carsharing.model.Payment; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PaymentRepository extends JpaRepository { + Page findAllByRental_UserId(Pageable pageable, Long userId); + + boolean existsByRentalIdAndStatus(Long id, Payment.Status status); +} diff --git a/src/main/java/mate/academy/carsharing/repository/RentalRepository.java b/src/main/java/mate/academy/carsharing/repository/RentalRepository.java new file mode 100644 index 0000000..b9b6f19 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/repository/RentalRepository.java @@ -0,0 +1,15 @@ +package mate.academy.carsharing.repository; + +import java.util.Optional; +import mate.academy.carsharing.model.Rental; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RentalRepository extends JpaRepository { + Page findAllByUserIdAndActualReturnDateIsNotNull(Pageable pageable, Long userId); + + Page findAllByUserIdAndActualReturnDateIsNull(Pageable pageable, Long userId); + + Optional findByIdAndUserId(Long rentalId, Long userId); +} diff --git a/src/main/java/mate/academy/carsharing/repository/RoleRepository.java b/src/main/java/mate/academy/carsharing/repository/RoleRepository.java new file mode 100644 index 0000000..afd8ffd --- /dev/null +++ b/src/main/java/mate/academy/carsharing/repository/RoleRepository.java @@ -0,0 +1,9 @@ +package mate.academy.carsharing.repository; + +import java.util.Optional; +import mate.academy.carsharing.model.Role; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RoleRepository extends JpaRepository { + Optional findByName(Role.RoleName roleName); +} diff --git a/src/main/java/mate/academy/carsharing/repository/UserRepository.java b/src/main/java/mate/academy/carsharing/repository/UserRepository.java new file mode 100644 index 0000000..b18de8c --- /dev/null +++ b/src/main/java/mate/academy/carsharing/repository/UserRepository.java @@ -0,0 +1,16 @@ +package mate.academy.carsharing.repository; + +import java.util.Optional; +import mate.academy.carsharing.model.User; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + Boolean existsByEmail(String email); + + @EntityGraph(attributePaths = "roles") + Optional findByEmail(String email); + + @EntityGraph(attributePaths = "roles") + Optional findById(Long id); +} diff --git a/src/main/java/mate/academy/carsharing/security/AuthenticationService.java b/src/main/java/mate/academy/carsharing/security/AuthenticationService.java new file mode 100644 index 0000000..a062d55 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/security/AuthenticationService.java @@ -0,0 +1,26 @@ +package mate.academy.carsharing.security; + +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.user.UserLoginRequestDto; +import mate.academy.carsharing.dto.user.UserLoginResponseDto; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + private final JwtUtil jwtUtil; + private final AuthenticationManager authenticationManager; + + public UserLoginResponseDto authenticate(UserLoginRequestDto request) { + final Authentication authentication = authenticationManager + .authenticate(new UsernamePasswordAuthenticationToken( + request.getEmail(), + request.getPassword() + )); + String token = jwtUtil.generateToken(authentication.getName()); + return new UserLoginResponseDto(token); + } +} diff --git a/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java b/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java new file mode 100644 index 0000000..3c5faad --- /dev/null +++ b/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java @@ -0,0 +1,22 @@ +package mate.academy.carsharing.security; + +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.repository.UserRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + return userRepository.findByEmail(email) + .orElseThrow(() -> new EntityNotFoundException("Can't find user " + + "by email:" + email)); + } +} diff --git a/src/main/java/mate/academy/carsharing/security/JwtAuthenticationFilter.java b/src/main/java/mate/academy/carsharing/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..7f596f8 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/security/JwtAuthenticationFilter.java @@ -0,0 +1,55 @@ +package mate.academy.carsharing.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private static final String BEARER_PREFIX = "Bearer "; + private final JwtUtil jwtUtil; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + String token = getToken(request); + + if (token != null && jwtUtil.isValidToken(token)) { + String username = jwtUtil.getUsername(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request, response); + } + + private String getToken(HttpServletRequest request) { + String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { + return bearerToken.substring(BEARER_PREFIX.length()); + } + return null; + } +} diff --git a/src/main/java/mate/academy/carsharing/security/JwtUtil.java b/src/main/java/mate/academy/carsharing/security/JwtUtil.java new file mode 100644 index 0000000..fc9494d --- /dev/null +++ b/src/main/java/mate/academy/carsharing/security/JwtUtil.java @@ -0,0 +1,60 @@ +package mate.academy.carsharing.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.function.Function; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + private final SecretKey secret; + + @Value("${jwt.expiration}") + private long expiration; + + public JwtUtil(@Value("${jwt.secret}") String secretString) { + secret = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String username) { + return Jwts.builder() + .subject(username) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(secret) + .compact(); + } + + public boolean isValidToken(String token) { + try { + Jws claimsJws = Jwts.parser() + .verifyWith(secret) + .build() + .parseSignedClaims(token); + return claimsJws.getPayload().getExpiration().after(new Date()); + } catch (JwtException | IllegalArgumentException e) { + throw new JwtException("Expired or invalid JWT token"); + } + } + + public String getUsername(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + private T getClaimFromToken(String token, Function claimsResolver) { + Claims claims = Jwts + .parser() + .verifyWith(secret) + .build() + .parseSignedClaims(token) + .getPayload(); + return claimsResolver.apply(claims); + } +} diff --git a/src/main/java/mate/academy/carsharing/service/car/CarService.java b/src/main/java/mate/academy/carsharing/service/car/CarService.java new file mode 100644 index 0000000..a03dedd --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/car/CarService.java @@ -0,0 +1,18 @@ +package mate.academy.carsharing.service.car; + +import mate.academy.carsharing.dto.car.CarRequestDto; +import mate.academy.carsharing.dto.car.CarResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CarService { + CarResponseDto createCar(CarRequestDto carRequestDto); + + Page getCars(Pageable pageable); + + CarResponseDto findById(Long carId); + + CarResponseDto updateById(Long carId, CarRequestDto carRequestDto); + + void deleteById(Long carId); +} diff --git a/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java b/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java new file mode 100644 index 0000000..d05ca4b --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java @@ -0,0 +1,55 @@ +package mate.academy.carsharing.service.car; + +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.car.CarRequestDto; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.mapper.CarMapper; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.repository.CarRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CarServiceImpl implements CarService { + private final CarMapper carMapper; + private final CarRepository carRepository; + + @Override + public CarResponseDto createCar(CarRequestDto carRequestDto) { + Car car = carMapper.toModel(carRequestDto); + carRepository.save(car); + return carMapper.toDto(car); + } + + @Override + public Page getCars(Pageable pageable) { + return carRepository.findAll(pageable) + .map(carMapper::toDto); + } + + @Override + public CarResponseDto findById(Long carId) { + Car car = carRepository.findById(carId).orElseThrow( + () -> new EntityNotFoundException("Can't find car with id: " + carId) + ); + return carMapper.toDto(car); + } + + @Override + public CarResponseDto updateById(Long carId, CarRequestDto carRequestDto) { + Car car = carRepository.findById(carId).orElseThrow( + () -> new EntityNotFoundException("Can't find car with id: " + carId) + ); + carMapper.updateModelFromDto(carRequestDto, car); + carRepository.save(car); + return carMapper.toDto(car); + } + + @Override + public void deleteById(Long carId) { + carRepository.deleteById(carId); + } +} diff --git a/src/main/java/mate/academy/carsharing/service/payment/PaymentService.java b/src/main/java/mate/academy/carsharing/service/payment/PaymentService.java new file mode 100644 index 0000000..07604b7 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/payment/PaymentService.java @@ -0,0 +1,14 @@ +package mate.academy.carsharing.service.payment; + +import mate.academy.carsharing.dto.payment.PaymentRequestDto; +import mate.academy.carsharing.dto.payment.PaymentResponseDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface PaymentService { + Page getWithUserId(Pageable pageable, Long userId); + + PaymentResponseDto createPayment(PaymentRequestDto paymentRequestDto); + + void checkSuccessfulPayment(Long paymentId); +} diff --git a/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java new file mode 100644 index 0000000..0f47df5 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java @@ -0,0 +1,128 @@ +package mate.academy.carsharing.service.payment; + +import com.stripe.model.checkout.Session; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.time.temporal.ChronoUnit; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.payment.PaymentRequestDto; +import mate.academy.carsharing.dto.payment.PaymentResponseDto; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exceptions.PaymentException; +import mate.academy.carsharing.mapper.PaymentMapper; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; +import mate.academy.carsharing.repository.PaymentRepository; +import mate.academy.carsharing.repository.RentalRepository; +import mate.academy.carsharing.service.payment.stripe.StripePaymentService; +import mate.academy.carsharing.service.telegram.NotificationMessageService; +import mate.academy.carsharing.service.telegram.NotificationService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PaymentServiceImpl implements PaymentService { + private static final BigDecimal DAILY_FINE_MULTIPLIER = BigDecimal.valueOf(1.4); + private final PaymentRepository paymentRepository; + private final StripePaymentService stripePaymentService; + private final PaymentMapper paymentMapper; + private final RentalRepository rentalRepository; + private final NotificationService notificationService; + private final NotificationMessageService notificationMessageService; + + @Override + public Page getWithUserId(Pageable pageable, Long userId) { + return paymentRepository.findAllByRental_UserId(pageable, userId) + .map(paymentMapper::toDto); + + } + + @Override + @Transactional + public PaymentResponseDto createPayment(PaymentRequestDto paymentRequestDto) { + Rental rental = rentalRepository.findById(paymentRequestDto.getRentalId()) + .orElseThrow(() -> new EntityNotFoundException("Can't find rental by id:" + + paymentRequestDto.getRentalId())); + + Payment.Type type = determinePaymentType(rental); + + if (paymentRepository.existsByRentalIdAndStatus(rental.getId(), Payment.Status.PENDING)) { + throw new PaymentException("Active payment already exists for this rental"); + } + + BigDecimal amountToPay = calculateAmountToPay(rental, type); + + Payment payment = new Payment(); + payment.setRental(rental); + payment.setType(type); + payment.setStatus(Payment.Status.PENDING); + payment.setAmountToPay(amountToPay); + payment.setSessionId("none"); + payment.setSessionUrl("http://none.none"); + + payment = paymentRepository.save(payment); + + Session paymentSession = stripePaymentService + .createPaymentSession(payment, rental, amountToPay); + + stripePaymentService.setPaymentSessionUrl(payment, paymentSession); + payment.setSessionId(paymentSession.getId()); + String message = notificationMessageService + .createPaymentNotification(payment, rental, rental.getCar()); + notificationService.sendNotification(message); + return paymentMapper.toDto(paymentRepository.save(payment)); + } + + @Override + @Transactional + public void checkSuccessfulPayment(Long paymentId) { + Payment payment = paymentRepository.findById(paymentId) + .orElseThrow(() -> new EntityNotFoundException( + "Can't find payment by id: " + paymentId)); + + if (payment.getStatus() == Payment.Status.PAID) { + return; + } + + Payment.Status status = stripePaymentService + .checkPaymentStatus(payment.getSessionId()); + payment.setStatus(status); + String message = notificationMessageService + .createSuccessfulPaymentNotification(payment); + notificationService.sendNotification(message); + paymentRepository.save(payment); + } + + private Payment.Type determinePaymentType(Rental rental) { + if (rental.getActualReturnDate() == null) { + throw new PaymentException("Cannot create payment - car is not returned yet"); + } + + if (rental.getActualReturnDate().isAfter(rental.getReturnDate())) { + return Payment.Type.FINE; + } + + return Payment.Type.PAYMENT; + } + + private BigDecimal calculateAmountToPay(Rental rental, Payment.Type type) { + long basicRentalDays = Math.max(1, rental.getRentalDate() + .until(rental.getReturnDate(), ChronoUnit.DAYS)); + BigDecimal basicCost = rental.getCar().getDailyFee() + .multiply(BigDecimal.valueOf(basicRentalDays)); + + if (type == Payment.Type.PAYMENT) { + return basicCost; + } else { + long overdueDays = Math.max(1, rental.getReturnDate() + .until(rental.getActualReturnDate(), ChronoUnit.DAYS)); + BigDecimal fine = rental.getCar().getDailyFee() + .multiply(BigDecimal.valueOf(overdueDays)) + .multiply(DAILY_FINE_MULTIPLIER); + + return basicCost.add(fine); + } + } +} diff --git a/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentService.java b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentService.java new file mode 100644 index 0000000..11a5091 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentService.java @@ -0,0 +1,14 @@ +package mate.academy.carsharing.service.payment.stripe; + +import com.stripe.model.checkout.Session; +import java.math.BigDecimal; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; + +public interface StripePaymentService { + Session createPaymentSession(Payment payment, Rental rental, BigDecimal amount); + + void setPaymentSessionUrl(Payment payment, Session session); + + Payment.Status checkPaymentStatus(String sessionId); +} diff --git a/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java new file mode 100644 index 0000000..755132c --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java @@ -0,0 +1,101 @@ +package mate.academy.carsharing.service.payment.stripe; + +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.checkout.Session; +import com.stripe.param.checkout.SessionCreateParams; +import jakarta.annotation.PostConstruct; +import java.math.BigDecimal; +import java.time.Instant; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.exceptions.StripePaymentException; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class StripePaymentServiceImpl implements StripePaymentService { + private static final String CURRENCY = "usd"; + private static final long SESSION_EXPIRATION_HOURS = 24; + + @Value("${stripe.secret.key}") + private String stripeSecretKey; + + @Value("${stripe.success.url}") + private String successUrl; + + @Value("${stripe.cancel.url}") + private String cancelUrl; + + @PostConstruct + public void init() { + Stripe.apiKey = stripeSecretKey; + } + + @Override + public Session createPaymentSession(Payment payment, Rental rental, BigDecimal amount) { + try { + SessionCreateParams params = SessionCreateParams.builder() + .setMode(SessionCreateParams.Mode.PAYMENT) + .setSuccessUrl(successUrl + payment.getId()) + .setCancelUrl(cancelUrl + payment.getId()) + .addLineItem(createLineItem(rental, amount)) + .setExpiresAt(calculateExpirationTime()) + .build(); + + return Session.create(params); + } catch (StripeException e) { + throw new StripePaymentException("Failed to create Stripe session", e); + } + } + + private Long calculateExpirationTime() { + return Instant.now() + .plusSeconds(SESSION_EXPIRATION_HOURS * 60 * 60) + .getEpochSecond(); + } + + private SessionCreateParams.LineItem createLineItem(Rental rental, BigDecimal amount) { + return SessionCreateParams.LineItem.builder() + .setQuantity(1L) + .setPriceData( + SessionCreateParams.LineItem.PriceData.builder() + .setCurrency(CURRENCY) + .setUnitAmount(convertToCents(amount)) + .setProductData( + SessionCreateParams.LineItem.PriceData.ProductData.builder() + .setName("Rental #" + rental.getId()) + .build()) + .build()) + .build(); + } + + private Long convertToCents(BigDecimal amount) { + return amount.multiply(BigDecimal.valueOf(100)).longValue(); + } + + @Override + public void setPaymentSessionUrl(Payment payment, Session session) { + payment.setSessionUrl(session.getUrl()); + } + + @Override + public Payment.Status checkPaymentStatus(String sessionId) { + try { + Session session = Session.retrieve(sessionId); + return mapStripeStatusToPaymentStatus(session.getStatus()); + } catch (StripeException e) { + throw new StripePaymentException("Failed to check payment status", e); + } + } + + private Payment.Status mapStripeStatusToPaymentStatus(String status) { + return switch (status) { + case "complete", "succeeded", "paid" -> Payment.Status.PAID; + case "pending", "canceled", "failed" -> Payment.Status.PENDING; + default -> throw new IllegalStateException("Unexpected value: " + status); + }; + } +} diff --git a/src/main/java/mate/academy/carsharing/service/rental/RentalService.java b/src/main/java/mate/academy/carsharing/service/rental/RentalService.java new file mode 100644 index 0000000..89dad1b --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/rental/RentalService.java @@ -0,0 +1,17 @@ +package mate.academy.carsharing.service.rental; + +import mate.academy.carsharing.dto.rental.RentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface RentalService { + RentalResponseDto createRental(Long userId, RentalRequestDto rentalRequestDto); + + Page getAllByUserId(Pageable pageable, Long userId, boolean isActive); + + RentalResponseDto getById(Long userId, Long rentalId); + + RentalResponseDto setDataReturn(RentalReturnRequestDto rentalReturnRequestDto); +} diff --git a/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java b/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java new file mode 100644 index 0000000..a838a43 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java @@ -0,0 +1,94 @@ +package mate.academy.carsharing.service.rental; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.rental.RentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; +import mate.academy.carsharing.exceptions.CarNotAvailableException; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.mapper.RentalMapper; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.model.Rental; +import mate.academy.carsharing.repository.CarRepository; +import mate.academy.carsharing.repository.RentalRepository; +import mate.academy.carsharing.service.telegram.NotificationMessageService; +import mate.academy.carsharing.service.telegram.NotificationService; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class RentalServiceImpl implements RentalService { + private final RentalRepository rentalRepository; + private final RentalMapper rentalMapper; + private final CarRepository carRepository; + private final NotificationService notificationService; + private final NotificationMessageService notificationMessageService; + + @Override + public RentalResponseDto createRental(Long userId, RentalRequestDto rentalRequestDto) { + Car car = carRepository + .findById(rentalRequestDto.getCarId()).orElseThrow(() -> + new EntityNotFoundException("Can't find car by id: " + + rentalRequestDto.getCarId())); + if (car.getInventory() < 1) { + throw new CarNotAvailableException("Car with id " + + rentalRequestDto.getCarId() + " is not available"); + } + + car.setInventory(car.getInventory() - 1); + + Rental rental = rentalMapper.toModelWithCarIdAndUserId(rentalRequestDto, + car.getId(), userId); + carRepository.save(car); + rentalRepository.save(rental); + String message = notificationMessageService.createRentalNotification(rental, car); + notificationService.sendNotification(message); + return rentalMapper.toDto(rental); + } + + @Override + public Page getAllByUserId(Pageable pageable, + Long userId, boolean isActive) { + Page rentalsPage; + if (isActive) { + rentalsPage = rentalRepository + .findAllByUserIdAndActualReturnDateIsNull(pageable, userId); + + } else { + rentalsPage = rentalRepository + .findAllByUserIdAndActualReturnDateIsNotNull(pageable, userId); + } + return rentalsPage.map(rentalMapper::toDto); + } + + @Override + public RentalResponseDto getById(Long userId, Long rentalId) { + Rental rental = rentalRepository.findByIdAndUserId(rentalId, userId) + .orElseThrow(() -> new EntityNotFoundException("Can't find " + + "rental by id:" + rentalId)); + return rentalMapper.toDto(rental); + } + + @Override + public RentalResponseDto setDataReturn(RentalReturnRequestDto rentalReturnRequestDto) { + Rental rental = rentalRepository.findById(rentalReturnRequestDto.getRentalId()) + .orElseThrow(() -> new EntityNotFoundException("Can't find rental by id:" + + rentalReturnRequestDto.getRentalId())); + + Car car = carRepository.findById(rental.getCar().getId()) + .orElseThrow(() -> new EntityNotFoundException("Can't find car by id:" + + rental.getCar().getId())); + + rental.setActualReturnDate(rentalReturnRequestDto.getActualReturnDate()); + car.setInventory(car.getInventory() + 1); + rentalRepository.save(rental); + carRepository.save(car); + String message = notificationMessageService.createReturnNotification(rental, car); + notificationService.sendNotification(message); + return rentalMapper.toDto(rental); + } +} diff --git a/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java b/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java new file mode 100644 index 0000000..f9c8873 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java @@ -0,0 +1,60 @@ +package mate.academy.carsharing.service.telegram; + +import mate.academy.carsharing.exceptions.TelegramNotificationException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +@Component +public class CarSharingBot extends TelegramLongPollingBot { + + @Value("${telegram.bot.name}") + private String botName; + + @Value("${telegram.bot.token}") + private String botToken; + + @Override + public String getBotUsername() { + return botName; + } + + @Override + public String getBotToken() { + return botToken; + } + + @Override + public void onUpdateReceived(Update update) { + if (update.hasMessage() && update.getMessage().hasText()) { + String text = update.getMessage().getText(); + Long chatId = update.getMessage().getChatId(); + + SendMessage message = new SendMessage(); + message.setChatId(chatId); + message.setText(text); + + try { + execute(message); + } catch (TelegramApiException e) { + throw new TelegramNotificationException("Failed to send" + + " Telegram message to chat:", e); + } + } + } + + public void sendMessage(String text, Long chatId) { + SendMessage message = new SendMessage(); + message.setChatId(chatId); + message.setText(text); + + try { + execute(message); + } catch (TelegramApiException e) { + throw new TelegramNotificationException("Failed to send Telegram message to chat:", e); + } + } +} diff --git a/src/main/java/mate/academy/carsharing/service/telegram/NotificationMessageService.java b/src/main/java/mate/academy/carsharing/service/telegram/NotificationMessageService.java new file mode 100644 index 0000000..f0a0062 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/telegram/NotificationMessageService.java @@ -0,0 +1,110 @@ +package mate.academy.carsharing.service.telegram; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.model.Payment; +import mate.academy.carsharing.model.Rental; +import org.springframework.stereotype.Service; + +@Service +public class NotificationMessageService { + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("dd.MM.yyyy"); + + public String createRentalNotification(Rental rental, Car car) { + return String.format( + """ + 🚗 New rental created! + ---------------------------- + Car: %s %s + Type: %s + Available cars: %d + Rental dates: %s - %s + Rental ID: %d + ---------------------------- + Thank you for choosing our service! + """, + car.getBrand(), + car.getModel(), + car.getType().name(), + car.getInventory(), + rental.getRentalDate().format(DATE_FORMATTER), + rental.getReturnDate().format(DATE_FORMATTER), + rental.getId() + ); + } + + public String createReturnNotification(Rental rental, Car car) { + return String.format( + """ + 🔙 Car returned! + ---------------------------- + Car: %s %s + Rental ID: %d + Planned return date: %s + Actual return date: %s + Delay: %s + Available cars of this model: %d + ---------------------------- + Thank you for using our service! + """, + car.getBrand(), + car.getModel(), + rental.getId(), + rental.getReturnDate().format(DATE_FORMATTER), + rental.getActualReturnDate().format(DATE_FORMATTER), + calculateDelay(rental.getReturnDate(), rental.getActualReturnDate()), + car.getInventory() + ); + } + + public String createPaymentNotification(Payment payment, Rental rental, Car car) { + return String.format( + """ + 💳 New payment created! + ---------------------------- + Car: %s %s + Rental ID: %d + Payment type: %s + Amount to pay: %.2f $ + Status: %s + ---------------------------- + """, + car.getBrand(), + car.getModel(), + rental.getId(), + payment.getType().name(), + payment.getAmountToPay(), + payment.getStatus().name() + ); + } + + public String createSuccessfulPaymentNotification(Payment payment) { + return String.format( + """ + ✅ Payment successful! + ---------------------------- + Rental ID: %d + Payment type: %s + Amount paid: %.2f $ + Payment date: %s + ---------------------------- + Thank you for your payment! Receipt has been sent to your email. + """, + payment.getRental().getId(), + payment.getType().name(), + payment.getAmountToPay(), + LocalDate.now().format(DATE_FORMATTER) + ); + } + + private String calculateDelay(LocalDate returnDate, LocalDate actualReturnDate) { + if (actualReturnDate.isAfter(returnDate)) { + long days = ChronoUnit.DAYS.between(returnDate, actualReturnDate); + return days + " day(s) delay"; + } + return "no delay"; + } +} diff --git a/src/main/java/mate/academy/carsharing/service/telegram/NotificationService.java b/src/main/java/mate/academy/carsharing/service/telegram/NotificationService.java new file mode 100644 index 0000000..1a7db27 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/telegram/NotificationService.java @@ -0,0 +1,24 @@ +package mate.academy.carsharing.service.telegram; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +public class NotificationService { + private final CarSharingBot carSharingBot; + + @Value("${telegram.bot.chat.id}") + private String chatId; + + @Autowired + public NotificationService(CarSharingBot carSharingBot) { + this.carSharingBot = carSharingBot; + } + + @Async + public void sendNotification(String message) { + carSharingBot.sendMessage(message, Long.valueOf(chatId)); + } +} diff --git a/src/main/java/mate/academy/carsharing/service/user/UserService.java b/src/main/java/mate/academy/carsharing/service/user/UserService.java new file mode 100644 index 0000000..72bd3d1 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/user/UserService.java @@ -0,0 +1,17 @@ +package mate.academy.carsharing.service.user; + +import mate.academy.carsharing.dto.role.RoleNameRequestDto; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.exceptions.RegistrationException; + +public interface UserService { + UserResponseDto register(UserRegistrationRequestDto userRegistrationRequestDto) + throws RegistrationException; + + UserResponseDto updateUserRole(Long userId, RoleNameRequestDto roleNameRequestDto); + + UserResponseDto getUser(Long userId); + + UserResponseDto updateMe(Long id, UserRegistrationRequestDto userRegistrationRequestDto); +} diff --git a/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java b/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java new file mode 100644 index 0000000..a42fe4f --- /dev/null +++ b/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java @@ -0,0 +1,84 @@ +package mate.academy.carsharing.service.user; + +import jakarta.transaction.Transactional; +import java.util.HashSet; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import mate.academy.carsharing.dto.role.RoleNameRequestDto; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.mapper.UserMapper; +import mate.academy.carsharing.model.Role; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.RoleRepository; +import mate.academy.carsharing.repository.UserRepository; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + private final UserRepository userRepository; + private final UserMapper userMapper; + private final PasswordEncoder passwordEncoder; + private final RoleRepository roleRepository; + + @Override + public UserResponseDto register(UserRegistrationRequestDto userRegistrationRequestDto) + throws RegistrationException { + if (userRepository.existsByEmail(userRegistrationRequestDto.getEmail())) { + throw new RegistrationException("Can't registration user " + + "with existing email: " + userRegistrationRequestDto.getEmail()); + + } + User user = userMapper.toModel(userRegistrationRequestDto); + user.setPassword(passwordEncoder.encode(userRegistrationRequestDto.getPassword())); + + Role defaultRole = roleRepository.findByName(Role.RoleName.CUSTOMER) + .orElseThrow(() -> new EntityNotFoundException("Default role not found")); + user.setRoles(Set.of(defaultRole)); + userRepository.save(user); + return userMapper.toUserResponse(user); + } + + @Override + public UserResponseDto updateUserRole(Long userId, RoleNameRequestDto roleNameRequestDto) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("Can't find user by id: " + userId)); + Role role = roleRepository + .findByName(roleNameRequestDto.getRoleName()) + .orElseThrow(() -> new EntityNotFoundException("Can't find role by role name: " + + roleNameRequestDto.getRoleName())); + Set roles = new HashSet<>(); + roles.add(role); + user.setRoles(roles); + return userMapper.toUserResponse(userRepository.save(user)); + } + + @Override + public UserResponseDto getUser(Long userId) { + User user = userRepository + .findById(userId).orElseThrow( + () -> new EntityNotFoundException("Can't find user by id: " + userId)); + return userMapper.toUserResponse(user); + } + + @Override + public UserResponseDto updateMe(Long id, + UserRegistrationRequestDto userRegistrationRequestDto) { + User user = userRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Can't find user email: " + + userRegistrationRequestDto + .getEmail())); + userMapper.updateModelFromDto(userRegistrationRequestDto, user); + + if (!userRegistrationRequestDto.getPassword().isEmpty()) { + user.setPassword(passwordEncoder.encode(userRegistrationRequestDto.getPassword())); + } + userRepository.save(user); + return userMapper.toUserResponse(user); + } +} diff --git a/src/main/java/mate/academy/carsharing/validation/FieldMatch.java b/src/main/java/mate/academy/carsharing/validation/FieldMatch.java new file mode 100644 index 0000000..4ae5f0b --- /dev/null +++ b/src/main/java/mate/academy/carsharing/validation/FieldMatch.java @@ -0,0 +1,23 @@ +package mate.academy.carsharing.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = FieldMatchValidator.class) +@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface FieldMatch { + String first(); + + String second(); + + String message() default "Fields must match"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/mate/academy/carsharing/validation/FieldMatchValidator.java b/src/main/java/mate/academy/carsharing/validation/FieldMatchValidator.java new file mode 100644 index 0000000..08c4d61 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/validation/FieldMatchValidator.java @@ -0,0 +1,28 @@ +package mate.academy.carsharing.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.Objects; +import org.springframework.beans.BeanWrapperImpl; + +public class FieldMatchValidator implements ConstraintValidator { + private String firstField; + private String secondField; + + @Override + public void initialize(FieldMatch constraintAnnotation) { + this.firstField = constraintAnnotation.first(); + this.secondField = constraintAnnotation.second(); + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) { + if (value == null) { + return true; + } + Object firstValue = new BeanWrapperImpl(value).getPropertyValue(firstField); + Object secondValue = new BeanWrapperImpl(value).getPropertyValue(secondField); + return Objects.equals(firstValue, secondValue); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 50ca5ac..1fe9dbd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,29 @@ +# Application spring.application.name=car-sharing +server.servlet.context-path=/api + +# Database +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect + +# JWT +jwt.secret=${JWT_SECRET} +jwt.expiration=${JWT_EXPIRATION} + +# Stripe +stripe.secret.key=${STRIPE_SECRET_KEY} +stripe.public.key=${STRIPE_PUBLIC_KEY} +stripe.success.url=${STRIPE_SUCCESS_URL} +stripe.cancel.url=${STRIPE_CANCEL_URL} + +# Telegram +telegram.bot.name=${TELEGRAM_BOT_NAME} +telegram.bot.token=${TELEGRAM_BOT_TOKEN} +telegram.bot.chat.id=${TELEGRAM_CHAT_ID} diff --git a/src/main/resources/db/changelog/changes/01-create-users-table.yaml b/src/main/resources/db/changelog/changes/01-create-users-table.yaml new file mode 100644 index 0000000..f8e0b62 --- /dev/null +++ b/src/main/resources/db/changelog/changes/01-create-users-table.yaml @@ -0,0 +1,42 @@ +databaseChangeLog: + - changeSet: + id: create-users-table + author: trokhim + changes: + - createTable: + tableName: users + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: email + type: varchar(320) + constraints: + nullable: false + unique: true + - column: + name: first_name + type: varchar(50) + constraints: + nullable: false + - column: + name: last_name + type: varchar(50) + constraints: + nullable: false + - column: + name: password + type: varchar(255) + constraints: + nullable: false + - column: + name: is_deleted + type: boolean + defaultValueBoolean: false + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/02-create-roles-table.yaml b/src/main/resources/db/changelog/changes/02-create-roles-table.yaml new file mode 100644 index 0000000..b982bc8 --- /dev/null +++ b/src/main/resources/db/changelog/changes/02-create-roles-table.yaml @@ -0,0 +1,37 @@ +databaseChangeLog: + - changeSet: + id: create-roles-table + author: trokhim + changes: + - createTable: + tableName: roles + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: varchar(255) + constraints: + nullable: false + unique: true + - changeSet: + id: add-role-customer + author: trokhim + changes: + - insert: + tableName: roles + columns: + - column: + name: name + value: 'CUSTOMER' + - insert: + tableName: roles + columns: + - column: + name: name + value: 'MANAGER' diff --git a/src/main/resources/db/changelog/changes/03-create-users-roles-table.yaml b/src/main/resources/db/changelog/changes/03-create-users-roles-table.yaml new file mode 100644 index 0000000..a0dea1d --- /dev/null +++ b/src/main/resources/db/changelog/changes/03-create-users-roles-table.yaml @@ -0,0 +1,37 @@ +databaseChangeLog: + - changeSet: + id: create-users-roles-table + author: trokhim + preConditions: + - onFail: MARK_RAN + - not: + tableExists: + tableName: users_roles + changes: + - createTable: + tableName: users_roles + columns: + - column: + name: user_id + type: bigint + constraints: + nullable: false + - column: + name: role_id + type: bigint + constraints: + nullable: false + - addForeignKeyConstraint: + baseTableName: users_roles + baseColumnNames: user_id + constraintName: fk_user_roles_user_id + referencedTableName: users + referencedColumnNames: id + onDelete: CASCADE + - addForeignKeyConstraint: + baseTableName: users_roles + baseColumnNames: role_id + constraintName: fk_user_roles_role_id + referencedTableName: roles + referencedColumnNames: id + onDelete: CASCADE diff --git a/src/main/resources/db/changelog/changes/04-create-cars-table.yaml b/src/main/resources/db/changelog/changes/04-create-cars-table.yaml new file mode 100644 index 0000000..87a6d42 --- /dev/null +++ b/src/main/resources/db/changelog/changes/04-create-cars-table.yaml @@ -0,0 +1,46 @@ +databaseChangeLog: + - changeSet: + id: create-cars-table + author: trokhim + changes: + - createTable: + tableName: cars + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: model + type: varchar(100) + constraints: + nullable: false + - column: + name: brand + type: varchar(50) + constraints: + nullable: false + - column: + name: type + type: varchar(20) + constraints: + nullable: false + - column: + name: inventory + type: int + constraints: + nullable: false + - column: + name: daily_fee + type: decimal(19,2) + constraints: + nullable: false + - column: + name: is_deleted + type: boolean + defaultValueBoolean: false + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/05-insert-users.yaml b/src/main/resources/db/changelog/changes/05-insert-users.yaml new file mode 100644 index 0000000..7d0ff73 --- /dev/null +++ b/src/main/resources/db/changelog/changes/05-insert-users.yaml @@ -0,0 +1,41 @@ +databaseChangeLog: + - changeSet: + id: insert-users + author: trokhim + changes: + - insert: + tableName: users + columns: + - column: + name: email + value: "customer@example.com" + - column: + name: first_name + value: "John" + - column: + name: last_name + value: "Doe" + - column: + name: password + value: "$2a$10$P/QlrPsxZ9AKvvl24zQ5W.WCPL.LmI//508AJnbn4nMZmbr9IWIJG" # password = "12345678" + - column: + name: is_deleted + valueBoolean: false + - insert: + tableName: users + columns: + - column: + name: email + value: "manager@example.com" + - column: + name: first_name + value: "Jane" + - column: + name: last_name + value: "Smith" + - column: + name: password + value: "$2a$10$P/QlrPsxZ9AKvvl24zQ5W.WCPL.LmI//508AJnbn4nMZmbr9IWIJG" # password = "12345678" + - column: + name: is_deleted + valueBoolean: false \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/06-insert-users-roles.yaml b/src/main/resources/db/changelog/changes/06-insert-users-roles.yaml new file mode 100644 index 0000000..10b5715 --- /dev/null +++ b/src/main/resources/db/changelog/changes/06-insert-users-roles.yaml @@ -0,0 +1,23 @@ +databaseChangeLog: + - changeSet: + id: insert-users-roles + author: trokhim + changes: + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueComputed: (SELECT id FROM users WHERE email = 'manager@example.com') + - column: + name: role_id + value: 2 + - insert: + tableName: users_roles + columns: + - column: + name: user_id + valueComputed: (SELECT id FROM users WHERE email = 'customer@example.com') + - column: + name: role_id + value: 1 \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/07-create-rentals-table.yaml b/src/main/resources/db/changelog/changes/07-create-rentals-table.yaml new file mode 100644 index 0000000..82d43f4 --- /dev/null +++ b/src/main/resources/db/changelog/changes/07-create-rentals-table.yaml @@ -0,0 +1,66 @@ +databaseChangeLog: + - changeSet: + id: create-rental-table + author: trokhim + changes: + - createTable: + tableName: rentals + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: rental_date + type: date + constraints: + nullable: false + - column: + name: return_date + type: date + constraints: + nullable: false + - column: + name: actual_return_date + type: date + constraints: + nullable: true + - column: + name: car_id + type: bigint + constraints: + nullable: false + - column: + name: user_id + type: bigint + constraints: + nullable: false + - column: + name: is_deleted + type: boolean + defaultValueBoolean: false + constraints: + nullable: false + + - changeSet: + id: add-foreign-keys-rentals + author: trokhim + changes: + - addForeignKeyConstraint: + constraintName: fk_rentals_car + baseColumnNames: car_id + baseTableName: rentals + referencedTableName: cars + referencedColumnNames: id + onDelete: CASCADE + + - addForeignKeyConstraint: + constraintName: fk_rentals_user + baseColumnNames: user_id + baseTableName: rentals + referencedTableName: users + referencedColumnNames: id + onDelete: CASCADE \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/08-create-payments-table.yaml b/src/main/resources/db/changelog/changes/08-create-payments-table.yaml new file mode 100644 index 0000000..a5755ac --- /dev/null +++ b/src/main/resources/db/changelog/changes/08-create-payments-table.yaml @@ -0,0 +1,70 @@ + +databaseChangeLog: + - changeSet: + id: create-payments-table + author: trokhim + runOnChange: true + changes: + - createTable: + tableName: payments + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: status + type: varchar(20) + constraints: + nullable: false + - column: + name: type + type: varchar(20) + constraints: + nullable: false + - column: + name: rental_id + type: bigint + constraints: + nullable: false + unique: true + - column: + name: session_url + type: TEXT + constraints: + nullable: false + - column: + name: session_id + type: TEXT + constraints: + nullable: false + - column: + name: amount_to_pay + type: decimal(19,2) + constraints: + nullable: false + - column: + name: is_deleted + type: boolean + constraints: + nullable: false + defaultValueBoolean: false + + - changeSet: + id: add-foreign-keys-payments + author: trokhim + changes: + - addForeignKeyConstraint: + constraintName: fk_payments_rental + baseColumnNames: rental_id + baseTableName: payments + referencedTableName: rentals + referencedColumnNames: id + onDelete: cascade + - addUniqueConstraint: + tableName: payments + columnNames: rental_id + constraintName: uc_payments_rental_id \ No newline at end of file diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 0000000..1e72e6d --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,17 @@ +databaseChangeLog: + - include: + file: db/changelog/changes/01-create-users-table.yaml + - include: + file: db/changelog/changes/02-create-roles-table.yaml + - include: + file: db/changelog/changes/03-create-users-roles-table.yaml + - include: + file: db/changelog/changes/04-create-cars-table.yaml + - include: + file: db/changelog/changes/05-insert-users.yaml + - include: + file: db/changelog/changes/06-insert-users-roles.yaml + - include: + file: db/changelog/changes/07-create-rentals-table.yaml + - include: + file: db/changelog/changes/08-create-payments-table.yaml \ No newline at end of file diff --git a/src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java b/src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java new file mode 100644 index 0000000..5d32364 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/config/CustomMySqlContainer.java @@ -0,0 +1,32 @@ +package mate.academy.carsharing.config; + +import org.testcontainers.containers.MySQLContainer; + +public class CustomMySqlContainer extends MySQLContainer { + private static final String IMAGE_VERSION = "mysql:8.0"; + private static CustomMySqlContainer container; + + private CustomMySqlContainer() { + super(IMAGE_VERSION); + } + + public static CustomMySqlContainer getInstance() { + if (container == null) { + container = new CustomMySqlContainer(); + } + return container; + } + + @Override + public void start() { + super.start(); + System.setProperty("TEST_DB_URL", container.getJdbcUrl()); + System.setProperty("TEST_DB_USERNAME", container.getUsername()); + System.setProperty("TEST_DB_PASSWORD", container.getPassword()); + } + + @Override + public void stop() { + super.stop(); + } +} diff --git a/src/test/java/mate/academy/carsharing/controller/car/CarControllerTest.java b/src/test/java/mate/academy/carsharing/controller/car/CarControllerTest.java new file mode 100644 index 0000000..f085c20 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/controller/car/CarControllerTest.java @@ -0,0 +1,191 @@ +package mate.academy.carsharing.controller.car; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import mate.academy.carsharing.dto.car.CarRequestDto; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.model.Car; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CarControllerTest { + protected static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeAll + static void beforeAll( + @Autowired WebApplicationContext webApplicationContext, + @Autowired DataSource dataSource) throws SQLException { + mockMvc = MockMvcBuilders + .webAppContextSetup(webApplicationContext) + .apply(springSecurity()) + .build(); + teardown(dataSource); + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("database/create-test-for-rental.sql") + ); + } + } + + @AfterAll + static void afterAll(@Autowired DataSource dataSource) { + teardown(dataSource); + } + + @SneakyThrows + static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("database/delete-all.sql") + ); + } + } + + @Test + @WithMockUser(roles = "CUSTOMER") + @DisplayName("Get all cars - returns list of cars with status 200") + void getAll_ShouldReturnListOfCars_WhenRequested() throws Exception { + MvcResult result = mockMvc.perform(get("/cars") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + String content = result.getResponse().getContentAsString(); + assertNotNull(content); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + @DisplayName("Get car by ID - returns car details with status 200 for valid ID") + void getById_ShouldReturnCar_WhenValidIdProvided() throws Exception { + Long validCarId = 1L; + + MvcResult result = mockMvc.perform(get("/cars/{id}", validCarId) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + CarResponseDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + CarResponseDto.class); + assertNotNull(actual); + assertEquals(validCarId, actual.getId()); + } + + @Test + @WithMockUser(roles = "MANAGER") + @DisplayName("Create car - creates new car and returns 200 for valid request") + void create_ShouldCreateCar_WhenValidRequest() throws Exception { + CarRequestDto requestDto = new CarRequestDto() + .setModel("Tesla Model S") + .setBrand("Tesla") + .setType(Car.Type.SEDAN) + .setDailyFee(new BigDecimal("100.00")) + .setInventory(5); + + MvcResult result = mockMvc.perform(post("/cars") + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + CarResponseDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + CarResponseDto.class); + assertNotNull(actual); + assertNotNull(actual.getId()); + assertEquals(requestDto.getModel(), actual.getModel()); + } + + @Test + @WithMockUser(roles = "MANAGER") + @Sql(scripts = "classpath:database/add-tesla-car.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = "classpath:database/remove-tesla-car.sql", + executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + @DisplayName("Update car - updates existing car and returns 200 for valid ID") + void update_ShouldUpdateCar_WhenValidIdProvided() throws Exception { + Long existingCarId = 4L; + CarRequestDto updateDto = new CarRequestDto() + .setModel("Tesla Model X") + .setBrand("Tesla") + .setType(Car.Type.SUV) + .setDailyFee(new BigDecimal("120.00")) + .setInventory(3); + + MvcResult result = mockMvc.perform(put("/cars/{id}", existingCarId) + .content(objectMapper.writeValueAsString(updateDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + CarResponseDto actual = objectMapper.readValue( + result.getResponse().getContentAsString(), + CarResponseDto.class); + assertNotNull(actual); + assertEquals(existingCarId, actual.getId()); + assertEquals(updateDto.getModel(), actual.getModel()); + } + + @Test + @WithMockUser(roles = "MANAGER") + @Sql(scripts = "classpath:database/add-tesla-car.sql", + executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @DisplayName("Delete car - removes car and returns 204 for valid ID") + void delete_ShouldRemoveCar_WhenValidIdProvided() throws Exception { + Long validCarId = 4L; + mockMvc.perform(delete("/cars/{id}", validCarId)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + @DisplayName("Create car - returns 403 when unauthorized user tries to create") + void create_ShouldReturnForbidden_WhenUnauthorized() throws Exception { + CarRequestDto requestDto = new CarRequestDto() + .setModel("Tesla Model S") + .setBrand("Tesla") + .setType(Car.Type.SEDAN) + .setDailyFee(new BigDecimal("100.00")) + .setInventory(5); + + mockMvc.perform(post("/cars") + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } +} diff --git a/src/test/java/mate/academy/carsharing/controller/rental/RentalControllerTest.java b/src/test/java/mate/academy/carsharing/controller/rental/RentalControllerTest.java new file mode 100644 index 0000000..d895449 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/controller/rental/RentalControllerTest.java @@ -0,0 +1,138 @@ +package mate.academy.carsharing.controller.rental; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import mate.academy.carsharing.dto.rental.RentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class RentalControllerTest { + private static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeAll + static void beforeAll( + @Autowired WebApplicationContext applicationContext, + @Autowired DataSource dataSource) throws SQLException { + mockMvc = MockMvcBuilders + .webAppContextSetup(applicationContext) + .apply(springSecurity()) + .build(); + teardown(dataSource); + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("database/create-test-for-rental.sql") + ); + } + } + + @AfterAll + static void afterAll(@Autowired DataSource dataSource) { + teardown(dataSource); + } + + @SneakyThrows + static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("database/delete-all.sql") + ); + } + } + + @Test + @WithUserDetails(value = "user@example.com", + userDetailsServiceBeanName = "customUserDetailsService") + @DisplayName("Create rental - returns created rental with status 201") + void createRental_ShouldReturnCreatedRental_WhenValidRequest() throws Exception { + RentalRequestDto requestDto = new RentalRequestDto() + .setRentalDate(LocalDate.now()) + .setReturnDate(LocalDate.now().plusDays(7)) + .setCarId(1L); + + MvcResult result = mockMvc.perform(post("/rentals") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isCreated()) + .andReturn(); + + RentalResponseDto response = objectMapper.readValue( + result.getResponse().getContentAsString(), + RentalResponseDto.class); + assertNotNull(response); + assertEquals(requestDto.getCarId(), response.getCarId()); + assertEquals(requestDto.getRentalDate(), response.getRentalDate()); + } + + @Test + @WithMockUser(username = "user@example.com", roles = "CUSTOMER") + @DisplayName("Complete rental - returns updated rental with status 200") + void completeRental_ShouldReturnUpdatedRental_WhenValidRequest() throws Exception { + RentalReturnRequestDto requestDto = new RentalReturnRequestDto() + .setRentalId(1L) + .setActualReturnDate(LocalDate.now()); + + mockMvc.perform(post("/rentals/return") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(roles = "MANAGER") + @DisplayName("Create rental - returns 403 when manager tries to create") + void createRental_ShouldReturnForbidden_WhenManagerRole() throws Exception { + RentalRequestDto requestDto = new RentalRequestDto() + .setCarId(1L) + .setRentalDate(LocalDate.now().plusDays(1)) + .setReturnDate(LocalDate.now().plusDays(5)); + + mockMvc.perform(post("/rentals") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + @DisplayName("Get rentals - returns 403 when customer tries to get all") + void getAllByUserId_ShouldReturnForbidden_WhenCustomerRole() throws Exception { + mockMvc.perform(get("/rentals") + .param("user_id", "1") + .param("is_active", "true")) + .andExpect(status().isForbidden()); + } +} diff --git a/src/test/java/mate/academy/carsharing/controller/user/UserControllerTest.java b/src/test/java/mate/academy/carsharing/controller/user/UserControllerTest.java new file mode 100644 index 0000000..a1965db --- /dev/null +++ b/src/test/java/mate/academy/carsharing/controller/user/UserControllerTest.java @@ -0,0 +1,123 @@ +package mate.academy.carsharing.controller.user; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; +import lombok.SneakyThrows; +import mate.academy.carsharing.dto.role.RoleNameRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.model.Role; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.datasource.init.ScriptUtils; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class UserControllerTest { + private static MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @BeforeAll + static void beforeAll( + @Autowired WebApplicationContext applicationContext, + @Autowired DataSource dataSource) throws SQLException { + mockMvc = MockMvcBuilders + .webAppContextSetup(applicationContext) + .apply(springSecurity()) + .build(); + teardown(dataSource); + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("database/create-test-for-rental.sql") + ); + } + } + + @AfterAll + static void afterAll(@Autowired DataSource dataSource) { + teardown(dataSource); + } + + @SneakyThrows + static void teardown(DataSource dataSource) { + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(true); + ScriptUtils.executeSqlScript( + connection, + new ClassPathResource("database/delete-all.sql") + ); + } + } + + @Test + @WithUserDetails(value = "user@example.com", + userDetailsServiceBeanName = "customUserDetailsService") + @DisplayName("Get current user info - returns user data with status 200") + void getCurrentUserInfo_ShouldReturnUserData_WhenAuthenticated() throws Exception { + MvcResult result = mockMvc.perform(get("/users/me") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + UserResponseDto response = objectMapper.readValue( + result.getResponse().getContentAsString(), + UserResponseDto.class); + assertNotNull(response); + assertEquals("user@example.com", response.getEmail()); + } + + @Test + @WithMockUser(username = "admin@example.com", roles = "MANAGER") + @DisplayName("Update user role - updates role successfully with status 200") + void updateRole_ShouldUpdateRole_WhenManagerRequest() throws Exception { + RoleNameRequestDto requestDto = new RoleNameRequestDto() + .setRoleName(Role.RoleName.MANAGER); + + MvcResult result = mockMvc.perform(put("/users/update/{id}/role", 2L) + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + UserResponseDto response = objectMapper.readValue( + result.getResponse().getContentAsString(), + UserResponseDto.class); + assertNotNull(response); + } + + @Test + @WithMockUser(roles = "CUSTOMER") + @DisplayName("Update user role - returns 403 when customer tries to update") + void updateRole_ShouldReturnForbidden_WhenCustomerRequest() throws Exception { + RoleNameRequestDto requestDto = new RoleNameRequestDto() + .setRoleName(Role.RoleName.MANAGER); + + mockMvc.perform(put("/users/update/{id}/role", 3L) + .content(objectMapper.writeValueAsString(requestDto)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java new file mode 100644 index 0000000..f1e8c57 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java @@ -0,0 +1,174 @@ +package mate.academy.carsharing.service.car; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import mate.academy.carsharing.dto.car.CarRequestDto; +import mate.academy.carsharing.dto.car.CarResponseDto; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.mapper.CarMapper; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.repository.CarRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +class CarServiceImplTest { + @Mock + private CarRepository carRepository; + + @Mock + private CarMapper carMapper; + + @InjectMocks + private CarServiceImpl carService; + + private Car car; + private Car oldCar; + private CarRequestDto carRequestDto; + private CarResponseDto carResponseDto; + + @BeforeEach + void setUp() { + car = new Car() + .setId(1L) + .setModel("M5 F90") + .setBrand("BMW") + .setType(Car.Type.SEDAN) + .setInventory(10) + .setDailyFee(BigDecimal.valueOf(250)); + + oldCar = new Car() + .setId(1L) + .setModel("M5 F90") + .setBrand("BMW") + .setType(Car.Type.SEDAN) + .setInventory(5) + .setDailyFee(BigDecimal.valueOf(150)); + + carRequestDto = new CarRequestDto() + .setModel(car.getModel()) + .setBrand(car.getBrand()) + .setType(car.getType()) + .setInventory(car.getInventory()) + .setDailyFee(car.getDailyFee()); + + carResponseDto = new CarResponseDto() + .setId(car.getId()) + .setModel(car.getModel()) + .setBrand(car.getBrand()) + .setType(car.getType()) + .setInventory(car.getInventory()) + .setDailyFee(car.getDailyFee()); + } + + @Test + @DisplayName("Create car - returns car DTO when valid request") + void createCar_ShouldReturnCarDto_WhenValidRequest() { + when(carMapper.toModel(carRequestDto)).thenReturn(car); + when(carRepository.save(car)).thenReturn(car); + when(carMapper.toDto(car)).thenReturn(carResponseDto); + + CarResponseDto result = carService.createCar(carRequestDto); + + assertThat(result).isEqualTo(carResponseDto); + verify(carMapper).toModel(carRequestDto); + verify(carRepository).save(car); + verify(carMapper).toDto(car); + } + + @Test + @DisplayName("Get cars - returns page of cars when valid pageable") + void getCars_ShouldReturnPageOfCars_WhenValidPageable() { + Pageable pageable = PageRequest.of(0, 10); + Page carPage = new PageImpl<>(List.of(car), pageable, 1); + + when(carRepository.findAll(pageable)).thenReturn(carPage); + when(carMapper.toDto(car)).thenReturn(carResponseDto); + + Page result = carService.getCars(pageable); + + assertThat(result.getContent()).hasSize(1).contains(carResponseDto); + verify(carRepository).findAll(pageable); + verify(carMapper).toDto(car); + } + + @Test + @DisplayName("Find by ID - returns car DTO when valid ID") + void findById_ShouldReturnCarDto_WhenValidId() { + when(carRepository.findById(1L)).thenReturn(Optional.of(car)); + when(carMapper.toDto(car)).thenReturn(carResponseDto); + + CarResponseDto result = carService.findById(1L); + + assertThat(result).isEqualTo(carResponseDto); + verify(carRepository).findById(1L); + verify(carMapper).toDto(car); + } + + @Test + @DisplayName("Update by ID - returns updated car DTO when valid ID and request") + void updateById_ShouldReturnUpdatedCarDto_WhenValidIdAndRequest() { + when(carRepository.findById(1L)).thenReturn(Optional.of(oldCar)); + when(carMapper.toDto(oldCar)).thenReturn(carResponseDto); + + CarResponseDto result = carService.updateById(1L, carRequestDto); + + assertThat(result).isEqualTo(carResponseDto); + verify(carRepository).findById(1L); + verify(carMapper).updateModelFromDto(carRequestDto, oldCar); + verify(carRepository).save(oldCar); + verify(carMapper).toDto(oldCar); + } + + @Test + @DisplayName("Delete by ID - successfully deletes car when valid ID") + void deleteById_ShouldDeleteCar_WhenValidId() { + carService.deleteById(1L); + verify(carRepository).deleteById(1L); + } + + @Test + @DisplayName("Find by ID - throws exception when invalid ID") + void findById_ShouldThrowException_WhenInvalidId() { + Long invalidId = 999L; + when(carRepository.findById(invalidId)).thenReturn(Optional.empty()); + + EntityNotFoundException exception = assertThrows( + EntityNotFoundException.class, + () -> carService.findById(invalidId) + ); + + assertThat(exception.getMessage()).isEqualTo("Can't find car with id: " + invalidId); + verify(carRepository).findById(invalidId); + } + + @Test + @DisplayName("Update by ID - throws exception when invalid ID") + void updateById_ShouldThrowException_WhenInvalidId() { + Long invalidId = 999L; + when(carRepository.findById(invalidId)).thenReturn(Optional.empty()); + + EntityNotFoundException exception = assertThrows( + EntityNotFoundException.class, + () -> carService.updateById(invalidId, carRequestDto) + ); + + assertThat(exception.getMessage()).isEqualTo("Can't find car with id: " + invalidId); + verify(carRepository).findById(invalidId); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java new file mode 100644 index 0000000..9ae2be7 --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java @@ -0,0 +1,188 @@ +package mate.academy.carsharing.service.rental; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.LocalDate; +import java.util.Optional; +import mate.academy.carsharing.dto.rental.RentalRequestDto; +import mate.academy.carsharing.dto.rental.RentalResponseDto; +import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; +import mate.academy.carsharing.exceptions.CarNotAvailableException; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.mapper.RentalMapper; +import mate.academy.carsharing.model.Car; +import mate.academy.carsharing.model.Rental; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.CarRepository; +import mate.academy.carsharing.repository.RentalRepository; +import mate.academy.carsharing.service.telegram.NotificationMessageService; +import mate.academy.carsharing.service.telegram.NotificationService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +@ExtendWith(MockitoExtension.class) +class RentalServiceImplTest { + @InjectMocks + private RentalServiceImpl rentalService; + + @Mock + private RentalRepository rentalRepository; + @Mock + private RentalMapper rentalMapper; + @Mock + private CarRepository carRepository; + @Mock + private NotificationService notificationService; + @Mock + private NotificationMessageService notificationMessageService; + + private Car car; + private User user; + private Rental rental; + private RentalRequestDto rentalRequestDto; + + @BeforeEach + void setUp() { + car = new Car(); + car.setId(1L); + car.setInventory(10); + + user = new User(); + user.setId(1L); + + rental = new Rental(); + rental.setId(1L); + rental.setCar(car); + + rentalRequestDto = new RentalRequestDto() + .setCarId(car.getId()) + .setRentalDate(LocalDate.now()) + .setReturnDate(LocalDate.now().plusDays(7)); + } + + @Test + @DisplayName("Create rental - returns rental DTO when valid request") + void createRental_ShouldReturnRentalDto_WhenValidRequest() { + when(carRepository.findById(car.getId())).thenReturn(Optional.of(car)); + when(rentalMapper.toModelWithCarIdAndUserId(any(), any(), any())).thenReturn(rental); + when(rentalRepository.save(rental)).thenReturn(rental); + when(rentalMapper.toDto(rental)).thenReturn(new RentalResponseDto()); + when(notificationMessageService.createRentalNotification(any(), + any())).thenReturn("Message"); + + RentalResponseDto result = rentalService.createRental(user.getId(), rentalRequestDto); + + assertNotNull(result); + verify(carRepository).save(car); + verify(notificationService).sendNotification(any()); + } + + @Test + @DisplayName("Get all rentals - returns active rentals when isActive=true") + void getAllByUserId_ShouldReturnActiveRentals_WhenIsActiveTrue() { + Pageable pageable = Pageable.ofSize(10); + when(rentalRepository.findAllByUserIdAndActualReturnDateIsNull(pageable, user.getId())) + .thenReturn(Page.empty()); + + Page result = rentalService.getAllByUserId(pageable, user.getId(), true); + + assertNotNull(result); + } + + @Test + @DisplayName("Get by ID - returns rental DTO when valid ID") + void getById_ShouldReturnRentalDto_WhenValidId() { + when(rentalRepository.findByIdAndUserId(rental.getId(), user.getId())) + .thenReturn(Optional.of(rental)); + when(rentalMapper.toDto(rental)).thenReturn(new RentalResponseDto()); + + RentalResponseDto result = rentalService.getById(rental.getId(), user.getId()); + + assertNotNull(result); + } + + @Test + @DisplayName("Set return date - returns updated rental when valid request") + void setDataReturn_ShouldReturnUpdatedRental_WhenValidRequest() { + RentalReturnRequestDto returnRequestDto = new RentalReturnRequestDto() + .setRentalId(rental.getId()) + .setActualReturnDate(LocalDate.now()); + + when(rentalRepository.findById(rental.getId())).thenReturn(Optional.of(rental)); + when(carRepository.findById(car.getId())).thenReturn(Optional.of(car)); + when(rentalRepository.save(rental)).thenReturn(rental); + when(notificationMessageService.createReturnNotification(any(), + any())).thenReturn("Message"); + when(rentalMapper.toDto(rental)).thenReturn(new RentalResponseDto()); + + RentalResponseDto result = rentalService.setDataReturn(returnRequestDto); + + assertNotNull(result); + assertEquals(returnRequestDto.getActualReturnDate(), rental.getActualReturnDate()); + verify(carRepository).save(car); + verify(notificationService).sendNotification(any()); + } + + @Test + @DisplayName("Create rental - throws exception when car not available") + void createRental_ShouldThrowException_WhenCarNotAvailable() { + car.setInventory(0); + when(carRepository.findById(car.getId())).thenReturn(Optional.of(car)); + + Exception exception = assertThrows(CarNotAvailableException.class, + () -> rentalService.createRental(user.getId(), rentalRequestDto)); + + assertEquals("Car with id " + car.getId() + " is not available", exception.getMessage()); + } + + @Test + @DisplayName("Create rental - throws exception when car not found") + void createRental_ShouldThrowException_WhenCarNotFound() { + Long invalidId = 999L; + when(carRepository.findById(invalidId)).thenReturn(Optional.empty()); + + Exception exception = assertThrows(EntityNotFoundException.class, + () -> rentalService.createRental(user.getId(), + rentalRequestDto.setCarId(invalidId))); + + assertEquals("Can't find car by id: " + invalidId, exception.getMessage()); + } + + @Test + @DisplayName("Set return date - throws exception when rental not found") + void setDataReturn_ShouldThrowException_WhenRentalNotFound() { + Long invalidId = 999L; + when(rentalRepository.findById(invalidId)).thenReturn(Optional.empty()); + + Exception exception = assertThrows(EntityNotFoundException.class, + () -> rentalService.setDataReturn(new RentalReturnRequestDto() + .setRentalId(invalidId))); + + assertEquals("Can't find rental by id:" + invalidId, exception.getMessage()); + } + + @Test + @DisplayName("Set return date - throws exception when car not found") + void setDataReturn_ShouldThrowException_WhenCarNotFound() { + when(rentalRepository.findById(rental.getId())).thenReturn(Optional.of(rental)); + when(carRepository.findById(car.getId())).thenReturn(Optional.empty()); + + Exception exception = assertThrows(EntityNotFoundException.class, + () -> rentalService.setDataReturn(new RentalReturnRequestDto() + .setRentalId(rental.getId()))); + + assertEquals("Can't find car by id:" + car.getId(), exception.getMessage()); + } +} diff --git a/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java new file mode 100644 index 0000000..1891b3b --- /dev/null +++ b/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java @@ -0,0 +1,192 @@ +package mate.academy.carsharing.service.user; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.Set; +import mate.academy.carsharing.dto.role.RoleNameRequestDto; +import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; +import mate.academy.carsharing.dto.user.UserResponseDto; +import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.mapper.UserMapper; +import mate.academy.carsharing.model.Role; +import mate.academy.carsharing.model.User; +import mate.academy.carsharing.repository.RoleRepository; +import mate.academy.carsharing.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class UserServiceImplTest { + @Mock + private UserRepository userRepository; + @Mock + private UserMapper userMapper; + @Mock + private RoleRepository roleRepository; + @Mock + private PasswordEncoder passwordEncoder; + @InjectMocks + private UserServiceImpl userService; + + private Role customerRole; + private Role managerRole; + private User user; + private UserRegistrationRequestDto userRequestDto; + private UserResponseDto userResponseDto; + private RoleNameRequestDto roleNameRequestDto; + + @BeforeEach + void setUp() { + customerRole = new Role(); + customerRole.setName(Role.RoleName.CUSTOMER); + + managerRole = new Role(); + managerRole.setName(Role.RoleName.MANAGER); + + user = new User(); + user.setId(1L); + user.setEmail("user@example.com"); + user.setPassword("encodedPassword"); + user.setFirstName("John"); + user.setLastName("Doe"); + user.setRoles(Set.of()); + + userRequestDto = new UserRegistrationRequestDto(); + userRequestDto.setEmail(user.getEmail()); + userRequestDto.setPassword("password123"); + userRequestDto.setRepeatPassword("password123"); + userRequestDto.setFirstName(user.getFirstName()); + userRequestDto.setLastName(user.getLastName()); + + userResponseDto = new UserResponseDto(); + userResponseDto.setId(user.getId()); + userResponseDto.setEmail(user.getEmail()); + userResponseDto.setFirstName(user.getFirstName()); + userResponseDto.setLastName(user.getLastName()); + + roleNameRequestDto = new RoleNameRequestDto(); + roleNameRequestDto.setRoleName(managerRole.getName()); + } + + @Test + @DisplayName("Register user - returns user DTO when valid request") + void register_ShouldReturnUserDto_WhenValidRequest() throws RegistrationException { + when(userRepository.existsByEmail(userRequestDto.getEmail())).thenReturn(false); + when(userMapper.toModel(userRequestDto)).thenReturn(user); + when(passwordEncoder.encode(userRequestDto.getPassword())).thenReturn("encodedPassword"); + when(roleRepository.findByName(Role.RoleName.CUSTOMER)) + .thenReturn(Optional.of(customerRole)); + when(userMapper.toUserResponse(user)).thenReturn(userResponseDto); + + UserResponseDto result = userService.register(userRequestDto); + + assertThat(result).isEqualTo(userResponseDto); + verify(userRepository).save(user); + } + + @Test + @DisplayName("Register user - throws exception when email exists") + void register_ShouldThrowException_WhenEmailExists() { + when(userRepository.existsByEmail(userRequestDto.getEmail())).thenReturn(true); + + RegistrationException exception = assertThrows(RegistrationException.class, + () -> userService.register(userRequestDto)); + + assertEquals("Can't registration user with existing email: " + userRequestDto.getEmail(), + exception.getMessage()); + } + + @Test + @DisplayName("Update role - returns updated user DTO when valid request") + void updateUserRole_ShouldReturnUpdatedUser_WhenValidRequest() { + User updateRoleUser = new User(); + updateRoleUser.setId(user.getId()); + updateRoleUser.setEmail(user.getEmail()); + updateRoleUser.setPassword(user.getPassword()); + updateRoleUser.setFirstName(user.getFirstName()); + updateRoleUser.setLastName(user.getLastName()); + updateRoleUser.setRoles(Set.of(managerRole)); + + Long validId = 1L; + + when(userRepository.findById(validId)).thenReturn(Optional.of(user)); + when(roleRepository.findByName(roleNameRequestDto.getRoleName())) + .thenReturn(Optional.of(managerRole)); + when(userRepository.save(Mockito.any(User.class))).thenReturn(updateRoleUser); + when(userMapper.toUserResponse(updateRoleUser)).thenReturn(userResponseDto); + + UserResponseDto expected = userResponseDto; + UserResponseDto actual = userService.updateUserRole(validId, roleNameRequestDto); + + assertNotNull(actual); + assertThat(actual).isEqualTo(expected); + + verify(userRepository).findById(validId); + verify(roleRepository).findByName(roleNameRequestDto.getRoleName()); + verify(userRepository).save(user); + verify(userMapper).toUserResponse(updateRoleUser); + } + + @Test + @DisplayName("Update role - throws exception when user not found") + void updateUserRole_ShouldThrowException_WhenUserNotFound() { + Long invalidId = 999L; + when(userRepository.findById(invalidId)).thenReturn(Optional.empty()); + + EntityNotFoundException exception = assertThrows(EntityNotFoundException.class, + () -> userService.updateUserRole(invalidId, roleNameRequestDto)); + + assertEquals("Can't find user by id: " + invalidId, exception.getMessage()); + } + + @Test + @DisplayName("Get user - returns user DTO when valid ID") + void getUser_ShouldReturnUserDto_WhenValidId() { + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + when(userMapper.toUserResponse(user)).thenReturn(userResponseDto); + + UserResponseDto result = userService.getUser(user.getId()); + + assertThat(result).isEqualTo(userResponseDto); + } + + @Test + @DisplayName("Get user - throws exception when user not found") + void getUser_ShouldThrowException_WhenUserNotFound() { + Long invalidId = 999L; + when(userRepository.findById(invalidId)).thenReturn(Optional.empty()); + + EntityNotFoundException exception = assertThrows(EntityNotFoundException.class, + () -> userService.getUser(invalidId)); + + assertEquals("Can't find user by id: " + invalidId, exception.getMessage()); + } + + @Test + @DisplayName("Update me - returns updated user DTO when valid request") + void updateMe_ShouldReturnUpdatedUser_WhenValidRequest() { + when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); + when(passwordEncoder.encode(userRequestDto.getPassword())).thenReturn("newEncodedPassword"); + when(userRepository.save(user)).thenReturn(user); + when(userMapper.toUserResponse(user)).thenReturn(userResponseDto); + + UserResponseDto result = userService.updateMe(user.getId(), userRequestDto); + + assertThat(result).isEqualTo(userResponseDto); + verify(userRepository).save(user); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..fc47d26 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,21 @@ +spring.datasource.url=jdbc:tc:mysql:8.0.37:///car_sharing_test +spring.datasource.username=test +spring.datasource.password=test + +# JWT +jwt.secret=my-very-long-secret-key-that-i-should-store-safely +jwt.expiration=30000000 + +# Stripe +stripe.secret.key=sk_test_51RABxWGgalaRGHX7SrESLaQMZMUuGdwEimFknl\ + UElNq3tFF4UpqFU7L61hmNxj1wZMY1xQVi1aRpykQRBRmeEmzh00QIMOwb9s +stripe.public.key=pk_test_51RABxWGgalaRGHX7qZOH8HwIl648YUsFNZKxKh\ + lxX5VffvuacA1YEi2LHhwUbviUmZu3MtgCSGICTESOhS2Sr7Ax00x3IIdwuv +stripe.success.url=http://localhost:8080/payments/success/ +stripe.cancel.url=http://localhost:8080/payments/cancel/ + +# Telegram +telegram.bot.name=CarSharingTrokhimBot +telegram.bot.token=7837073313:AAGtmb1ej345p_h1Lnmb8OYlRXDTc5sMAYs +telegram.bot.chat.id=581117817 + diff --git a/src/test/resources/database/add-tesla-car.sql b/src/test/resources/database/add-tesla-car.sql new file mode 100644 index 0000000..5f34b6d --- /dev/null +++ b/src/test/resources/database/add-tesla-car.sql @@ -0,0 +1,3 @@ +DELETE FROM cars WHERE brand = 'Tesla'; +INSERT INTO cars (id,model, brand, type, inventory, daily_fee) +VALUES (4,'Plaid', 'Tesla', 'SEDAN', 9, 180); \ No newline at end of file diff --git a/src/test/resources/database/create-test-for-rental.sql b/src/test/resources/database/create-test-for-rental.sql new file mode 100644 index 0000000..6c5ff48 --- /dev/null +++ b/src/test/resources/database/create-test-for-rental.sql @@ -0,0 +1,16 @@ +INSERT INTO users (id, email, password, first_name, last_name) +VALUES (3, 'user@example.com', 'password', 'Test', 'User'); + +INSERT INTO users_roles(user_id, role_id) +VALUES (3, 1); + +INSERT INTO cars (id,model, brand, type, inventory, daily_fee) +VALUES (1,'I4', 'BMW', 'SEDAN', 5, 120), + (2,'XM', 'BMW', 'SUV', 3, 230), + (3,'Golf', 'Volkswagen', 'HATCHBACK', 10, 60); + +INSERT INTO rentals (id,rental_date, return_date, actual_return_date, car_id, user_id) +VALUES + (1,'2025-03-16', '2025-03-20', NULL, 1, 3), + (2,'2025-03-16', '2025-03-16', '2025-03-16', 2, 3), + (3,'2025-03-16', '2025-03-27', NULL, 3, 3); \ No newline at end of file diff --git a/src/test/resources/database/delete-all.sql b/src/test/resources/database/delete-all.sql new file mode 100644 index 0000000..2f3000b --- /dev/null +++ b/src/test/resources/database/delete-all.sql @@ -0,0 +1,4 @@ +DELETE FROM rentals WHERE user_id = 3; +DELETE FROM cars; +DELETE FROM users_roles WHERE user_id = 3; +DELETE FROM users WHERE id = 3; \ No newline at end of file diff --git a/src/test/resources/database/remove-tesla-car.sql b/src/test/resources/database/remove-tesla-car.sql new file mode 100644 index 0000000..4b11c61 --- /dev/null +++ b/src/test/resources/database/remove-tesla-car.sql @@ -0,0 +1,3 @@ +DELETE +FROM cars +WHERE brand = 'Tesla'; \ No newline at end of file From 203c5f72f3d3a60ef974da72c8ac311774d20f29 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 11:07:53 +0300 Subject: [PATCH 02/14] Create README.md --- README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..f00fb8b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +FF From 15b56d1ab0a4a4b240534a370233d7b5f0914756 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 11:17:47 +0300 Subject: [PATCH 03/14] Update README.md --- README.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f00fb8b..639deb6 100644 --- a/README.md +++ b/README.md @@ -1 +1,63 @@ -FF +# 🚗 Car Sharing Service + +[![CI](https://github.com/your-org/car-sharing-app/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/car-sharing-app/actions/workflows/ci.yml) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +Modern car sharing platform that automates rental processes with secure payments and real-time notifications. + +## 🌟 Features + +- **User Management** + - JWT authentication & role-based access (MANAGER | CUSTOMER) + - Profile management + +- **Car Inventory** + - CRUD operations for cars (MANAGER only) + - Public catalog with availability tracking + +- **Rental System** + - Book cars with automatic inventory adjustment + - Rental history with filters (active/returned) + - Secure return process + +- **Payments** + - Stripe integration for credit card payments + - Automatic fee calculation (rentals & fines) + - Payment session tracking + +- **Notifications** + - Telegram bot for real-time alerts + - Daily overdue rental checks + +## 🛠 Tech Stack + +**Backend:** +![Java](https://img.shields.io/badge/Java-21-red?logo=java) +![Spring Boot](https://img.shields.io/badge/Spring_Boot-3.4.1-green?logo=spring) + +**Database:** +![MySQL](https://img.shields.io/badge/MySQL-8.0.33-blue?logo=mysql) + +**Security:** +![JWT](https://img.shields.io/badge/JWT-0.12.6-black?logo=jsonwebtokens) + +**Payments:** +![Stripe](https://img.shields.io/badge/Stripe-API-v28.3.0-blueviolet?logo=stripe) + +**Other:** +![Liquibase](https://img.shields.io/badge/Liquibase-4.29.2-lightgrey) +![Docker](https://img.shields.io/badge/Docker-✓-blue?logo=docker) + +## 🚀 Quick Start + +### Prerequisites +- Java 21 +- MySQL 8.0+ +- Maven 3.10+ +- Docker (optional) + +### Local Setup +1. Clone the repo: + ```sh + git clone https://github.com/your-org/car-sharing-app.git + cd car-sharing-app From a0c614f27859aee85e778020585574a1054ba905 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 11:20:11 +0300 Subject: [PATCH 04/14] Update README.md --- README.md | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/README.md b/README.md index 639deb6..57df3ab 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # 🚗 Car Sharing Service -[![CI](https://github.com/your-org/car-sharing-app/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/car-sharing-app/actions/workflows/ci.yml) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) - Modern car sharing platform that automates rental processes with secure payments and real-time notifications. ## 🌟 Features @@ -30,34 +27,3 @@ Modern car sharing platform that automates rental processes with secure payments - Daily overdue rental checks ## 🛠 Tech Stack - -**Backend:** -![Java](https://img.shields.io/badge/Java-21-red?logo=java) -![Spring Boot](https://img.shields.io/badge/Spring_Boot-3.4.1-green?logo=spring) - -**Database:** -![MySQL](https://img.shields.io/badge/MySQL-8.0.33-blue?logo=mysql) - -**Security:** -![JWT](https://img.shields.io/badge/JWT-0.12.6-black?logo=jsonwebtokens) - -**Payments:** -![Stripe](https://img.shields.io/badge/Stripe-API-v28.3.0-blueviolet?logo=stripe) - -**Other:** -![Liquibase](https://img.shields.io/badge/Liquibase-4.29.2-lightgrey) -![Docker](https://img.shields.io/badge/Docker-✓-blue?logo=docker) - -## 🚀 Quick Start - -### Prerequisites -- Java 21 -- MySQL 8.0+ -- Maven 3.10+ -- Docker (optional) - -### Local Setup -1. Clone the repo: - ```sh - git clone https://github.com/your-org/car-sharing-app.git - cd car-sharing-app From c35bb5c572f5c5314e5a91831e2002d8281b3219 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 13:22:26 +0300 Subject: [PATCH 05/14] Update README.md --- README.md | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 57df3ab..0d6e390 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,9 @@ -# 🚗 Car Sharing Service - -Modern car sharing platform that automates rental processes with secure payments and real-time notifications. - -## 🌟 Features - -- **User Management** - - JWT authentication & role-based access (MANAGER | CUSTOMER) - - Profile management - -- **Car Inventory** - - CRUD operations for cars (MANAGER only) - - Public catalog with availability tracking - -- **Rental System** - - Book cars with automatic inventory adjustment - - Rental history with filters (active/returned) - - Secure return process - -- **Payments** - - Stripe integration for credit card payments - - Automatic fee calculation (rentals & fines) - - Payment session tracking - -- **Notifications** - - Telegram bot for real-time alerts - - Daily overdue rental checks - -## 🛠 Tech Stack +# 🚗 Car Sharing API + +This API provides a modern car sharing platform that automates vehicle rental processes with secure payments and real-time notifications. Designed as a complete digital solution for car rental businesses, it replaces manual operations with an efficient web-based system featuring automated inventory management, rental tracking, and integrated payment processing. +## 📌 Technologies & Tools +## ⚡ Functionality +## 📊 Database Schema +## 🛠️ Setting up a project +## 📖 API Documentation +## 📌 Example API Request From ee66c807782d4b3cdcbdccd2d31d14034e7aa6b3 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 13:28:58 +0300 Subject: [PATCH 06/14] Update README.md --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0d6e390..21b6c87 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,56 @@ -# 🚗 Car Sharing API +# 🚗 Car-Sharing API + +This API is designed to manage a car-sharing service, offering features such as car inventory management, rental processing, user registration, JWT authentication, payment integration with Stripe, and Telegram notifications. It provides a complete solution for building a modern car-sharing platform with role-based access control for users and administrators. + +## 🛠️ Technologies & Tools +- **Core Language**: Java 17 +- **Framework**: Spring Boot 3.4.4 (with Spring Web, Spring Data JPA, Spring Security) +- **Database**: MySQL 8.0 (with Liquibase for schema migrations) +- **Payment Processing**: Stripe API +- **Notifications**: Telegram Bot API +- **Testing**: JUnit 5, MockMvc, Testcontainers +- **API Documentation**: Swagger/OpenAPI +- **Dependency Management**: Maven +- **Containerization**: Docker, Docker Compose +- **Object Mapping**: MapStruct 1.6.3 +- **Validation**: Jakarta Validation 3.4.5 -This API provides a modern car sharing platform that automates vehicle rental processes with secure payments and real-time notifications. Designed as a complete digital solution for car rental businesses, it replaces manual operations with an efficient web-based system featuring automated inventory management, rental tracking, and integrated payment processing. -## 📌 Technologies & Tools ## ⚡ Functionality +The project provides a comprehensive set of features for managing cars, users, rentals, and payments: + +**👤 User Management** (`AuthController`, `UsersController`) +- User registration and authentication with JWT +- Role-based access control (MANAGER/CUSTOMER) +- Profile management + +**🚗 Car Management** (`CarsController`) +- CRUD operations for car inventory +- Car type classification (SEDAN, SUV, HATCHBACK, UNIVERSAL) +- Inventory tracking +- Daily fee management + +**📅 Rental Management** (`RentalsController`) +- Create new rentals with inventory checks +- Return management with inventory updates +- Rental status tracking (active/returned) +- Filtering by user and status + +**💳 Payment Processing** (`PaymentsController`) +- Integration with Stripe payment system +- Payment session management +- Success/cancel payment handlers +- Fine calculation for overdue rentals + +**🔔 Notification System** +- Telegram notifications for: + - New rentals + - Overdue rentals + - Successful payments +- Scheduled daily checks for overdue rentals + ## 📊 Database Schema -## 🛠️ Setting up a project -## 📖 API Documentation -## 📌 Example API Request + +![image](https://github.com/user-attachments/assets/7ca5d4c0-4331-419a-bd03-fd238edc5cff) + + +## 🚀 Getting Started From b592645de72482a1ed57989b9b0abdffc8d47326 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 13:35:26 +0300 Subject: [PATCH 07/14] Update README.md --- README.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 21b6c87..6fc1566 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,10 @@ The project provides a comprehensive set of features for managing cars, users, r - Success/cancel payment handlers - Fine calculation for overdue rentals -**🔔 Notification System** -- Telegram notifications for: - - New rentals - - Overdue rentals - - Successful payments -- Scheduled daily checks for overdue rentals +**🔔 Telegram notifications** +- New rentals +- Overdue rentals +- Successful payments ## 📊 Database Schema @@ -54,3 +52,30 @@ The project provides a comprehensive set of features for managing cars, users, r ## 🚀 Getting Started +## 📖 API Documentation + +Explore the API endpoints with Swagger UI: + +**🔗 [Swagger UI](http://localhost:8080/swagger-ui/index.html)** + +## 📌 Example API Requests +**Register a new user:** + +```bash +curl -X POST "http://localhost:8080/api/register" \ +-H "Content-Type: application/json" \ +-d '{ + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe", + "password": "securePassword123", + "repeatPassword": "securePassword123" +}' +``` + +**Get available cars:** + +```bash +curl -X GET "http://localhost:8080/api/cars" \ +-H "Authorization: Bearer your.jwt.token" +``` From 86fe8901f0e8553abc97a4b274f4ac0b4ac0d346 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 13:45:14 +0300 Subject: [PATCH 08/14] Update README.md --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fc1566..7c08927 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,22 @@ Explore the API endpoints with Swagger UI: **🔗 [Swagger UI](http://localhost:8080/swagger-ui/index.html)** +## 🔒 Security +- JWT authentication for all endpoints + +- Role-based authorization + +- Password encryption + +- Secure payment processing with Stripe + +- All sensitive data stored in environment variables + ## 📌 Example API Requests **Register a new user:** ```bash -curl -X POST "http://localhost:8080/api/register" \ +curl -X POST "http://localhost:8080/api/auth/registration" \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", From 86f44f269fb5eeb02a0f33c56e68fbe60e55200f Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 14:13:23 +0300 Subject: [PATCH 09/14] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c08927..7d7bc8d 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ The project provides a comprehensive set of features for managing cars, users, r Explore the API endpoints with Swagger UI: -**🔗 [Swagger UI](http://localhost:8080/swagger-ui/index.html)** +**🔗 [Swagger UI](http://localhost:8080/api/swagger-ui/index.html)** + +Знімок екрана 2025-05-08 о 14 10 15 ## 🔒 Security - JWT authentication for all endpoints From 9e0015cecab05fcd65a65b3f12aca14452605dc0 Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 14:31:06 +0300 Subject: [PATCH 10/14] Update README.md --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/README.md b/README.md index 7d7bc8d..f1b75af 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,63 @@ The project provides a comprehensive set of features for managing cars, users, r ## 🚀 Getting Started + +1️⃣ **Setup** + +Clone the repository: + +```bash +git clone https://github.com/trokhim03/CarSharing-API.git +``` +2️⃣ **Сreate an environment of variables** + +Create file .env by copying the content from file .env.sample and fill in the fields. + +3️⃣ **Build the project:** +```bash +docker build -t name_image_your_app +``` +4️⃣ **Start the application using Docker Compose:** +```bash +docker-compose up +``` + +## Connecting to a Custom Database 🗄 +Configure your database connection and application settings by editing the src/main/resources/application.properties file. + +```bash +# Application +spring.application.name=car-sharing +server.servlet.context-path=/api + +# Database +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect + +# JWT +jwt.secret=${JWT_SECRET} +jwt.expiration=${JWT_EXPIRATION} + +# Stripe +stripe.secret.key=${STRIPE_SECRET_KEY} +stripe.public.key=${STRIPE_PUBLIC_KEY} +stripe.success.url=${STRIPE_SUCCESS_URL} +stripe.cancel.url=${STRIPE_CANCEL_URL} + +# Telegram +telegram.bot.name=${TELEGRAM_BOT_NAME} +telegram.bot.token=${TELEGRAM_BOT_TOKEN} +telegram.bot.chat.id=${TELEGRAM_CHAT_ID} +``` +The application will be available at http://localhost:8080 (default port) + ## 📖 API Documentation Explore the API endpoints with Swagger UI: From 9bfd1421c4955db5e147572c202e3882d042e3aa Mon Sep 17 00:00:00 2001 From: Dmytro Trokhymchuk <91589000+trokhim03@users.noreply.github.com> Date: Thu, 8 May 2025 14:32:30 +0300 Subject: [PATCH 11/14] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f1b75af..5aa8a3e 100644 --- a/README.md +++ b/README.md @@ -136,10 +136,10 @@ curl -X POST "http://localhost:8080/api/auth/registration" \ -H "Content-Type: application/json" \ -d '{ "email": "user@example.com", - "firstName": "John", - "lastName": "Doe", "password": "securePassword123", - "repeatPassword": "securePassword123" + "repeatPassword": "securePassword123", + "firstName": "John", + "lastName": "Doe" }' ``` From 5abf1a9db0a458a0e6eb0e1cd921e471d43dedf2 Mon Sep 17 00:00:00 2001 From: trokhim03 Date: Thu, 8 May 2025 14:39:52 +0300 Subject: [PATCH 12/14] fix after review --- pom.xml | 121 ++++++++---------- .../carsharing/controller/AuthController.java | 7 +- .../carsharing/controller/CarController.java | 5 +- .../controller/PaymentController.java | 4 +- .../controller/RentalController.java | 5 +- .../carsharing/controller/UserController.java | 5 +- .../carsharing/dto/car/CarRequestDto.java | 11 +- .../dto/payment/PaymentRequestDto.java | 4 +- .../dto/rental/RentalRequestDto.java | 12 +- .../dto/rental/RentalReturnRequestDto.java | 6 +- .../dto/role/RoleNameRequestDto.java | 4 +- .../CarNotAvailableException.java | 2 +- .../CustomGlobalExceptionHandler.java | 50 ++++++++ .../EntityNotFoundException.java | 2 +- .../PaymentException.java | 2 +- .../RegistrationException.java | 2 +- .../StripePaymentException.java | 2 +- .../TelegramNotificationException.java | 2 +- .../mate/academy/carsharing/model/Car.java | 1 - .../academy/carsharing/model/Payment.java | 6 +- .../mate/academy/carsharing/model/Rental.java | 5 +- .../mate/academy/carsharing/model/Role.java | 2 +- .../mate/academy/carsharing/model/User.java | 6 +- .../security/CustomUserDetailsService.java | 2 +- .../service/car/CarServiceImpl.java | 2 +- .../service/payment/PaymentServiceImpl.java | 28 ++-- .../stripe/StripePaymentServiceImpl.java | 2 +- .../service/rental/RentalServiceImpl.java | 4 +- .../service/telegram/CarSharingBot.java | 2 +- .../carsharing/service/user/UserService.java | 2 +- .../service/user/UserServiceImpl.java | 7 +- .../service/car/CarServiceImplTest.java | 2 +- .../service/rental/RentalServiceImplTest.java | 4 +- .../service/user/UserServiceImplTest.java | 4 +- 34 files changed, 182 insertions(+), 143 deletions(-) rename src/main/java/mate/academy/carsharing/{exceptions => exception}/CarNotAvailableException.java (77%) create mode 100644 src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java rename src/main/java/mate/academy/carsharing/{exceptions => exception}/EntityNotFoundException.java (77%) rename src/main/java/mate/academy/carsharing/{exceptions => exception}/PaymentException.java (75%) rename src/main/java/mate/academy/carsharing/{exceptions => exception}/RegistrationException.java (75%) rename src/main/java/mate/academy/carsharing/{exceptions => exception}/StripePaymentException.java (79%) rename src/main/java/mate/academy/carsharing/{exceptions => exception}/TelegramNotificationException.java (80%) diff --git a/pom.xml b/pom.xml index 154d758..390596f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,36 +1,42 @@ - + + 4.0.0 + org.springframework.boot spring-boot-starter-parent 3.4.4 - + + mate.academy car-sharing 0.0.1-SNAPSHOT car-sharing car-sharing - - - - - - - - - - - - - + - checkstyle.xml 17 0.12.6 + 2.8.6 + 6.9.7.1 + 1.6.3 + 1.18.30 + 0.2.0 + 3.12.1 + 3.3.0 + checkstyle.xml + 28.0.0 + 1.20.6 + 3.4.5 + 6.2.5 + 6.4.4 + org.springframework.boot @@ -47,7 +53,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.3.0 + ${springdoc.version} org.liquibase @@ -56,7 +62,7 @@ org.telegram telegrambots-spring-boot-starter - 6.9.7.1 + ${telegrambots.version} com.h2database @@ -90,12 +96,12 @@ org.mapstruct mapstruct - 1.6.3 + ${mapstruct.version} org.mapstruct mapstruct-processor - 1.6.3 + ${mapstruct.version} provided @@ -118,19 +124,25 @@ com.stripe stripe-java - 28.0.0 + ${stripe.version} org.testcontainers mysql - 1.20.6 + ${testcontainers.version} test org.testcontainers junit-jupiter + ${testcontainers.version} test + + org.springframework.boot + spring-boot-starter-validation + ${validation.version} + @@ -138,15 +150,30 @@ org.apache.maven.plugins maven-compiler-plugin + ${maven.compiler.plugin.version} + ${java.version} + ${java.version} org.projectlombok lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruct.binding.version} + org.springframework.boot spring-boot-maven-plugin @@ -159,53 +186,11 @@ + org.apache.maven.plugins maven-checkstyle-plugin - 3.2.0 - - - verify - - check - - - - - checkstyle.xml - true - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.12.1 - - ${java.version} - ${java.version} - - - org.projectlombok - lombok - 1.18.30 - - - org.mapstruct - mapstruct-processor - 1.6.3 - - - org.projectlombok - lombok-mapstruct-binding - 0.2.0 - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 + ${maven.checkstyle.plugin.version} compile @@ -215,7 +200,7 @@ - ${maven.checkstyle.plugin.configLocation} + ${checkstyle.config.location} true true false diff --git a/src/main/java/mate/academy/carsharing/controller/AuthController.java b/src/main/java/mate/academy/carsharing/controller/AuthController.java index f85c0c1..6ab552f 100644 --- a/src/main/java/mate/academy/carsharing/controller/AuthController.java +++ b/src/main/java/mate/academy/carsharing/controller/AuthController.java @@ -2,12 +2,13 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.user.UserLoginRequestDto; import mate.academy.carsharing.dto.user.UserLoginResponseDto; import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; import mate.academy.carsharing.dto.user.UserResponseDto; -import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.exception.RegistrationException; import mate.academy.carsharing.security.AuthenticationService; import mate.academy.carsharing.service.user.UserService; import org.springframework.web.bind.annotation.PostMapping; @@ -28,7 +29,7 @@ public class AuthController { description = "Register a new user with email, password and other details") @PostMapping("/registration") public UserResponseDto registration( - @RequestBody UserRegistrationRequestDto userRegistrationRequestDto) + @RequestBody @Valid UserRegistrationRequestDto userRegistrationRequestDto) throws RegistrationException { return userService.register(userRegistrationRequestDto); } @@ -36,7 +37,7 @@ public UserResponseDto registration( @Operation(summary = "Login user", description = "Authenticate user and return JWT token") @PostMapping("/login") - public UserLoginResponseDto login(@RequestBody UserLoginRequestDto userLoginRequestDto) { + public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto userLoginRequestDto) { return authenticationService.authenticate(userLoginRequestDto); } } diff --git a/src/main/java/mate/academy/carsharing/controller/CarController.java b/src/main/java/mate/academy/carsharing/controller/CarController.java index d276ca9..4f6a629 100644 --- a/src/main/java/mate/academy/carsharing/controller/CarController.java +++ b/src/main/java/mate/academy/carsharing/controller/CarController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.car.CarRequestDto; import mate.academy.carsharing.dto.car.CarResponseDto; @@ -40,7 +41,7 @@ public Page getCars(Pageable pageable) { @Operation(summary = "Create a new car", description = "Create a new car with the provided details") @PostMapping - public CarResponseDto createCar(@RequestBody CarRequestDto carRequestDto) { + public CarResponseDto createCar(@RequestBody @Valid CarRequestDto carRequestDto) { return carService.createCar(carRequestDto); } @@ -58,7 +59,7 @@ public CarResponseDto findById(@PathVariable Long carId) { description = "Update an existing car with new data") @PutMapping("/{carId}") public CarResponseDto updateById(@PathVariable Long carId, - @RequestBody CarRequestDto carRequestDto) { + @RequestBody @Valid CarRequestDto carRequestDto) { return carService.updateById(carId, carRequestDto); } diff --git a/src/main/java/mate/academy/carsharing/controller/PaymentController.java b/src/main/java/mate/academy/carsharing/controller/PaymentController.java index 9e38437..9019655 100644 --- a/src/main/java/mate/academy/carsharing/controller/PaymentController.java +++ b/src/main/java/mate/academy/carsharing/controller/PaymentController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.payment.PaymentRequestDto; import mate.academy.carsharing.dto.payment.PaymentResponseDto; @@ -38,7 +39,8 @@ public Page getWithUserId(Pageable pageable, @Operation(summary = "Create payment", description = "Create a new payment session for rental") @PostMapping - public PaymentResponseDto createPayment(@RequestBody PaymentRequestDto paymentRequestDto) { + public PaymentResponseDto createPayment(@RequestBody + @Valid PaymentRequestDto paymentRequestDto) { return paymentService.createPayment(paymentRequestDto); } diff --git a/src/main/java/mate/academy/carsharing/controller/RentalController.java b/src/main/java/mate/academy/carsharing/controller/RentalController.java index bc70dbc..10043ea 100644 --- a/src/main/java/mate/academy/carsharing/controller/RentalController.java +++ b/src/main/java/mate/academy/carsharing/controller/RentalController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.rental.RentalRequestDto; import mate.academy.carsharing.dto.rental.RentalResponseDto; @@ -36,7 +37,7 @@ public class RentalController { @ResponseStatus(HttpStatus.CREATED) @PostMapping public RentalResponseDto createRental(Authentication authentication, - @RequestBody RentalRequestDto rentalRequestDto) { + @RequestBody @Valid RentalRequestDto rentalRequestDto) { Long userId = getAuthenticationUserId(authentication); return rentalService.createRental(userId, rentalRequestDto); } @@ -67,7 +68,7 @@ public Page getAllByUserId( description = "Set actual return date and process rental completion") @PostMapping("/return") public RentalResponseDto setDataReturn( - @RequestBody RentalReturnRequestDto rentalReturnRequestDto) { + @RequestBody @Valid RentalReturnRequestDto rentalReturnRequestDto) { return rentalService.setDataReturn(rentalReturnRequestDto); } diff --git a/src/main/java/mate/academy/carsharing/controller/UserController.java b/src/main/java/mate/academy/carsharing/controller/UserController.java index 353996d..0436db8 100644 --- a/src/main/java/mate/academy/carsharing/controller/UserController.java +++ b/src/main/java/mate/academy/carsharing/controller/UserController.java @@ -2,6 +2,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.role.RoleNameRequestDto; import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; @@ -39,7 +40,7 @@ public UserResponseDto getUser(Authentication authentication) { description = "Update user's role (Admin only)") @PutMapping("/update/{userId}/role") public UserResponseDto updateRole(@PathVariable Long userId, - @RequestBody RoleNameRequestDto roleNameRequestDto) { + @RequestBody @Valid RoleNameRequestDto roleNameRequestDto) { return userService.updateUserRole(userId, roleNameRequestDto); } @@ -49,7 +50,7 @@ public UserResponseDto updateRole(@PathVariable Long userId, @PutMapping("/me") public UserResponseDto updateUserInfo( Authentication authentication, - @RequestBody UserRegistrationRequestDto userRegistrationRequestDto) { + @RequestBody @Valid UserRegistrationRequestDto userRegistrationRequestDto) { Long authenticationUserId = getAuthenticationUserId(authentication); return userService.updateMe(authenticationUserId, userRegistrationRequestDto); } diff --git a/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java b/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java index c930b9a..08b0c5b 100644 --- a/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java +++ b/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java @@ -1,8 +1,9 @@ package mate.academy.carsharing.dto.car; import jakarta.validation.constraints.DecimalMin; -import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import java.math.BigDecimal; import lombok.Data; import lombok.experimental.Accessors; @@ -15,17 +16,15 @@ public class CarRequestDto { private String model; @NotBlank - private String brand; - @NotBlank + @NotNull private Car.Type type; - @NotBlank - @Min(0) + @Positive private int inventory; - @NotBlank + @NotNull @DecimalMin(value = "0.0", inclusive = false) private BigDecimal dailyFee; } diff --git a/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java b/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java index 178ed9a..37f1bfe 100644 --- a/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java +++ b/src/main/java/mate/academy/carsharing/dto/payment/PaymentRequestDto.java @@ -1,12 +1,12 @@ package mate.academy.carsharing.dto.payment; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import lombok.Data; @Data public class PaymentRequestDto { - @NotBlank + @NotNull @Positive private Long rentalId; } diff --git a/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java b/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java index 9295399..6b5d1bd 100644 --- a/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java +++ b/src/main/java/mate/academy/carsharing/dto/rental/RentalRequestDto.java @@ -1,9 +1,9 @@ package mate.academy.carsharing.dto.rental; import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.Future; import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.PastOrPresent; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; import lombok.Data; @@ -12,17 +12,17 @@ @Data @Accessors(chain = true) public class RentalRequestDto { - @NotBlank + @NotNull @JsonFormat(pattern = "yyyy-MM-dd") @FutureOrPresent private LocalDate rentalDate; - @NotBlank + @NotNull @JsonFormat(pattern = "yyyy-MM-dd") - @PastOrPresent + @Future private LocalDate returnDate; - @NotBlank + @NotNull @Positive private Long carId; } diff --git a/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java b/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java index 1a2ccec..db8ec72 100644 --- a/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java +++ b/src/main/java/mate/academy/carsharing/dto/rental/RentalReturnRequestDto.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.FutureOrPresent; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import java.time.LocalDate; import lombok.Data; @@ -11,11 +11,11 @@ @Data @Accessors(chain = true) public class RentalReturnRequestDto { - @NotBlank + @NotNull @Positive private Long rentalId; - @NotBlank + @NotNull @FutureOrPresent @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate actualReturnDate; diff --git a/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java b/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java index 1bd0a52..3a62374 100644 --- a/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java +++ b/src/main/java/mate/academy/carsharing/dto/role/RoleNameRequestDto.java @@ -1,6 +1,6 @@ package mate.academy.carsharing.dto.role; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.experimental.Accessors; import mate.academy.carsharing.model.Role; @@ -8,6 +8,6 @@ @Data @Accessors(chain = true) public class RoleNameRequestDto { - @NotBlank + @NotNull private Role.RoleName roleName; } diff --git a/src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java b/src/main/java/mate/academy/carsharing/exception/CarNotAvailableException.java similarity index 77% rename from src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java rename to src/main/java/mate/academy/carsharing/exception/CarNotAvailableException.java index 5b800de..e46633c 100644 --- a/src/main/java/mate/academy/carsharing/exceptions/CarNotAvailableException.java +++ b/src/main/java/mate/academy/carsharing/exception/CarNotAvailableException.java @@ -1,4 +1,4 @@ -package mate.academy.carsharing.exceptions; +package mate.academy.carsharing.exception; public class CarNotAvailableException extends RuntimeException { public CarNotAvailableException(String message) { diff --git a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java new file mode 100644 index 0000000..e0af1b5 --- /dev/null +++ b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java @@ -0,0 +1,50 @@ +package mate.academy.carsharing.exception; + +import java.time.LocalDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { + private static final String TIME_LABEL = "time"; + private static final String STATUS_LABEL = "status"; + private static final String ERRORS_LABEL = "errors"; + + @Override + protected ResponseEntity handleMethodArgumentNotValid( + MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request + ) { + Map body = new LinkedHashMap<>(); + body.put(TIME_LABEL, LocalDateTime.now()); + body.put(STATUS_LABEL, HttpStatus.BAD_REQUEST); + List errors = ex.getBindingResult() + .getAllErrors() + .stream() + .map(this::getErrorMessage) + .toList(); + body.put(ERRORS_LABEL, errors); + + return new ResponseEntity<>(body, headers, status); + } + + private Object getErrorMessage(ObjectError e) { + if (e instanceof FieldError fieldError) { + return fieldError.getField() + " " + fieldError.getDefaultMessage(); + } + return e.getDefaultMessage(); + } +} diff --git a/src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java b/src/main/java/mate/academy/carsharing/exception/EntityNotFoundException.java similarity index 77% rename from src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java rename to src/main/java/mate/academy/carsharing/exception/EntityNotFoundException.java index 4165aa6..e52d30e 100644 --- a/src/main/java/mate/academy/carsharing/exceptions/EntityNotFoundException.java +++ b/src/main/java/mate/academy/carsharing/exception/EntityNotFoundException.java @@ -1,4 +1,4 @@ -package mate.academy.carsharing.exceptions; +package mate.academy.carsharing.exception; public class EntityNotFoundException extends RuntimeException { public EntityNotFoundException(String message) { diff --git a/src/main/java/mate/academy/carsharing/exceptions/PaymentException.java b/src/main/java/mate/academy/carsharing/exception/PaymentException.java similarity index 75% rename from src/main/java/mate/academy/carsharing/exceptions/PaymentException.java rename to src/main/java/mate/academy/carsharing/exception/PaymentException.java index 3560fc6..498f4be 100644 --- a/src/main/java/mate/academy/carsharing/exceptions/PaymentException.java +++ b/src/main/java/mate/academy/carsharing/exception/PaymentException.java @@ -1,4 +1,4 @@ -package mate.academy.carsharing.exceptions; +package mate.academy.carsharing.exception; public class PaymentException extends RuntimeException { public PaymentException(String message) { diff --git a/src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java b/src/main/java/mate/academy/carsharing/exception/RegistrationException.java similarity index 75% rename from src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java rename to src/main/java/mate/academy/carsharing/exception/RegistrationException.java index d93b368..60aed32 100644 --- a/src/main/java/mate/academy/carsharing/exceptions/RegistrationException.java +++ b/src/main/java/mate/academy/carsharing/exception/RegistrationException.java @@ -1,4 +1,4 @@ -package mate.academy.carsharing.exceptions; +package mate.academy.carsharing.exception; public class RegistrationException extends Exception { public RegistrationException(String message) { diff --git a/src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java b/src/main/java/mate/academy/carsharing/exception/StripePaymentException.java similarity index 79% rename from src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java rename to src/main/java/mate/academy/carsharing/exception/StripePaymentException.java index 64c9138..24f5c98 100644 --- a/src/main/java/mate/academy/carsharing/exceptions/StripePaymentException.java +++ b/src/main/java/mate/academy/carsharing/exception/StripePaymentException.java @@ -1,4 +1,4 @@ -package mate.academy.carsharing.exceptions; +package mate.academy.carsharing.exception; public class StripePaymentException extends RuntimeException { public StripePaymentException(String message, Throwable cause) { diff --git a/src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java b/src/main/java/mate/academy/carsharing/exception/TelegramNotificationException.java similarity index 80% rename from src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java rename to src/main/java/mate/academy/carsharing/exception/TelegramNotificationException.java index c3c4c14..18bec5d 100644 --- a/src/main/java/mate/academy/carsharing/exceptions/TelegramNotificationException.java +++ b/src/main/java/mate/academy/carsharing/exception/TelegramNotificationException.java @@ -1,4 +1,4 @@ -package mate.academy.carsharing.exceptions; +package mate.academy.carsharing.exception; public class TelegramNotificationException extends RuntimeException { public TelegramNotificationException(String message, Throwable cause) { diff --git a/src/main/java/mate/academy/carsharing/model/Car.java b/src/main/java/mate/academy/carsharing/model/Car.java index f7adbc8..1b3a9a4 100644 --- a/src/main/java/mate/academy/carsharing/model/Car.java +++ b/src/main/java/mate/academy/carsharing/model/Car.java @@ -37,7 +37,6 @@ public class Car { @Column(nullable = false) private Type type; - @Column(nullable = false) private int inventory; @Column(name = "daily_fee", nullable = false) diff --git a/src/main/java/mate/academy/carsharing/model/Payment.java b/src/main/java/mate/academy/carsharing/model/Payment.java index eb637b9..f1ed407 100644 --- a/src/main/java/mate/academy/carsharing/model/Payment.java +++ b/src/main/java/mate/academy/carsharing/model/Payment.java @@ -42,14 +42,14 @@ public class Payment { private Rental rental; @Lob - @Column(name = "session_url", nullable = false, columnDefinition = "TEXT") + @Column(nullable = false, columnDefinition = "TEXT") private String sessionUrl; @Lob - @Column(name = "session_id", nullable = false, unique = true, columnDefinition = "TEXT") + @Column(nullable = false, unique = true, columnDefinition = "TEXT") private String sessionId; - @Column(name = "amount_to_pay", nullable = false) + @Column(nullable = false) private BigDecimal amountToPay; @Column(nullable = false) diff --git a/src/main/java/mate/academy/carsharing/model/Rental.java b/src/main/java/mate/academy/carsharing/model/Rental.java index d9041a8..0a94a19 100644 --- a/src/main/java/mate/academy/carsharing/model/Rental.java +++ b/src/main/java/mate/academy/carsharing/model/Rental.java @@ -26,13 +26,12 @@ public class Rental { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "rental_date", nullable = false) + @Column(nullable = false) private LocalDate rentalDate; - @Column(name = "return_date", nullable = false) + @Column(nullable = false) private LocalDate returnDate; - @Column(name = "actual_return_date") private LocalDate actualReturnDate; @ManyToOne(fetch = FetchType.LAZY) diff --git a/src/main/java/mate/academy/carsharing/model/Role.java b/src/main/java/mate/academy/carsharing/model/Role.java index 52e34a6..bbb2f3b 100644 --- a/src/main/java/mate/academy/carsharing/model/Role.java +++ b/src/main/java/mate/academy/carsharing/model/Role.java @@ -21,7 +21,7 @@ public class Role implements GrantedAuthority { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Enumerated(value = EnumType.STRING) + @Enumerated(EnumType.STRING) @Column(nullable = false, unique = true) private RoleName name; diff --git a/src/main/java/mate/academy/carsharing/model/User.java b/src/main/java/mate/academy/carsharing/model/User.java index 23d6fbf..42a1752 100644 --- a/src/main/java/mate/academy/carsharing/model/User.java +++ b/src/main/java/mate/academy/carsharing/model/User.java @@ -30,13 +30,13 @@ public class User implements UserDetails { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false) + @Column(nullable = false, unique = true) private String email; - @Column(name = "first_name", nullable = false) + @Column(nullable = false) private String firstName; - @Column(name = "last_name", nullable = false) + @Column(nullable = false) private String lastName; @Column(nullable = false) diff --git a/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java b/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java index 3c5faad..5f5755d 100644 --- a/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java +++ b/src/main/java/mate/academy/carsharing/security/CustomUserDetailsService.java @@ -1,7 +1,7 @@ package mate.academy.carsharing.security; import lombok.RequiredArgsConstructor; -import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exception.EntityNotFoundException; import mate.academy.carsharing.repository.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; diff --git a/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java b/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java index d05ca4b..4dbe0fd 100644 --- a/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/car/CarServiceImpl.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.car.CarRequestDto; import mate.academy.carsharing.dto.car.CarResponseDto; -import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exception.EntityNotFoundException; import mate.academy.carsharing.mapper.CarMapper; import mate.academy.carsharing.model.Car; import mate.academy.carsharing.repository.CarRepository; diff --git a/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java index 0f47df5..15e4cbd 100644 --- a/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java @@ -7,8 +7,8 @@ import lombok.RequiredArgsConstructor; import mate.academy.carsharing.dto.payment.PaymentRequestDto; import mate.academy.carsharing.dto.payment.PaymentResponseDto; -import mate.academy.carsharing.exceptions.EntityNotFoundException; -import mate.academy.carsharing.exceptions.PaymentException; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.exception.PaymentException; import mate.academy.carsharing.mapper.PaymentMapper; import mate.academy.carsharing.model.Payment; import mate.academy.carsharing.model.Rental; @@ -36,7 +36,6 @@ public class PaymentServiceImpl implements PaymentService { public Page getWithUserId(Pageable pageable, Long userId) { return paymentRepository.findAllByRental_UserId(pageable, userId) .map(paymentMapper::toDto); - } @Override @@ -62,7 +61,7 @@ public PaymentResponseDto createPayment(PaymentRequestDto paymentRequestDto) { payment.setSessionId("none"); payment.setSessionUrl("http://none.none"); - payment = paymentRepository.save(payment); + paymentRepository.save(payment); Session paymentSession = stripePaymentService .createPaymentSession(payment, rental, amountToPay); @@ -82,17 +81,18 @@ public void checkSuccessfulPayment(Long paymentId) { .orElseThrow(() -> new EntityNotFoundException( "Can't find payment by id: " + paymentId)); - if (payment.getStatus() == Payment.Status.PAID) { - return; - } + Payment.Status oldStatus = payment.getStatus(); + Payment.Status newStatus = stripePaymentService.checkPaymentStatus(payment.getSessionId()); - Payment.Status status = stripePaymentService - .checkPaymentStatus(payment.getSessionId()); - payment.setStatus(status); - String message = notificationMessageService - .createSuccessfulPaymentNotification(payment); - notificationService.sendNotification(message); - paymentRepository.save(payment); + if (oldStatus != newStatus) { + payment.setStatus(newStatus); + paymentRepository.save(payment); + } + if (newStatus == Payment.Status.PAID) { + String message = notificationMessageService + .createSuccessfulPaymentNotification(payment); + notificationService.sendNotification(message); + } } private Payment.Type determinePaymentType(Rental rental) { diff --git a/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java index 755132c..2a44e36 100644 --- a/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java @@ -8,7 +8,7 @@ import java.math.BigDecimal; import java.time.Instant; import lombok.RequiredArgsConstructor; -import mate.academy.carsharing.exceptions.StripePaymentException; +import mate.academy.carsharing.exception.StripePaymentException; import mate.academy.carsharing.model.Payment; import mate.academy.carsharing.model.Rental; import org.springframework.beans.factory.annotation.Value; diff --git a/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java b/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java index a838a43..b2a139f 100644 --- a/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/rental/RentalServiceImpl.java @@ -5,8 +5,8 @@ import mate.academy.carsharing.dto.rental.RentalRequestDto; import mate.academy.carsharing.dto.rental.RentalResponseDto; import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; -import mate.academy.carsharing.exceptions.CarNotAvailableException; -import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exception.CarNotAvailableException; +import mate.academy.carsharing.exception.EntityNotFoundException; import mate.academy.carsharing.mapper.RentalMapper; import mate.academy.carsharing.model.Car; import mate.academy.carsharing.model.Rental; diff --git a/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java b/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java index f9c8873..1071080 100644 --- a/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java +++ b/src/main/java/mate/academy/carsharing/service/telegram/CarSharingBot.java @@ -1,6 +1,6 @@ package mate.academy.carsharing.service.telegram; -import mate.academy.carsharing.exceptions.TelegramNotificationException; +import mate.academy.carsharing.exception.TelegramNotificationException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.telegram.telegrambots.bots.TelegramLongPollingBot; diff --git a/src/main/java/mate/academy/carsharing/service/user/UserService.java b/src/main/java/mate/academy/carsharing/service/user/UserService.java index 72bd3d1..6d0b986 100644 --- a/src/main/java/mate/academy/carsharing/service/user/UserService.java +++ b/src/main/java/mate/academy/carsharing/service/user/UserService.java @@ -3,7 +3,7 @@ import mate.academy.carsharing.dto.role.RoleNameRequestDto; import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; import mate.academy.carsharing.dto.user.UserResponseDto; -import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.exception.RegistrationException; public interface UserService { UserResponseDto register(UserRegistrationRequestDto userRegistrationRequestDto) diff --git a/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java b/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java index a42fe4f..22c2a90 100644 --- a/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java @@ -7,8 +7,8 @@ import mate.academy.carsharing.dto.role.RoleNameRequestDto; import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; import mate.academy.carsharing.dto.user.UserResponseDto; -import mate.academy.carsharing.exceptions.EntityNotFoundException; -import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.exception.RegistrationException; import mate.academy.carsharing.mapper.UserMapper; import mate.academy.carsharing.model.Role; import mate.academy.carsharing.model.User; @@ -38,7 +38,8 @@ public UserResponseDto register(UserRegistrationRequestDto userRegistrationReque user.setPassword(passwordEncoder.encode(userRegistrationRequestDto.getPassword())); Role defaultRole = roleRepository.findByName(Role.RoleName.CUSTOMER) - .orElseThrow(() -> new EntityNotFoundException("Default role not found")); + .orElseThrow(() -> new EntityNotFoundException("Role " + + Role.RoleName.CUSTOMER.name() + " not found")); user.setRoles(Set.of(defaultRole)); userRepository.save(user); return userMapper.toUserResponse(user); diff --git a/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java index f1e8c57..4222acf 100644 --- a/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java +++ b/src/test/java/mate/academy/carsharing/service/car/CarServiceImplTest.java @@ -10,7 +10,7 @@ import java.util.Optional; import mate.academy.carsharing.dto.car.CarRequestDto; import mate.academy.carsharing.dto.car.CarResponseDto; -import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exception.EntityNotFoundException; import mate.academy.carsharing.mapper.CarMapper; import mate.academy.carsharing.model.Car; import mate.academy.carsharing.repository.CarRepository; diff --git a/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java index 9ae2be7..bb583e0 100644 --- a/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java +++ b/src/test/java/mate/academy/carsharing/service/rental/RentalServiceImplTest.java @@ -12,8 +12,8 @@ import mate.academy.carsharing.dto.rental.RentalRequestDto; import mate.academy.carsharing.dto.rental.RentalResponseDto; import mate.academy.carsharing.dto.rental.RentalReturnRequestDto; -import mate.academy.carsharing.exceptions.CarNotAvailableException; -import mate.academy.carsharing.exceptions.EntityNotFoundException; +import mate.academy.carsharing.exception.CarNotAvailableException; +import mate.academy.carsharing.exception.EntityNotFoundException; import mate.academy.carsharing.mapper.RentalMapper; import mate.academy.carsharing.model.Car; import mate.academy.carsharing.model.Rental; diff --git a/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java index 1891b3b..d634100 100644 --- a/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java +++ b/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java @@ -12,8 +12,8 @@ import mate.academy.carsharing.dto.role.RoleNameRequestDto; import mate.academy.carsharing.dto.user.UserRegistrationRequestDto; import mate.academy.carsharing.dto.user.UserResponseDto; -import mate.academy.carsharing.exceptions.EntityNotFoundException; -import mate.academy.carsharing.exceptions.RegistrationException; +import mate.academy.carsharing.exception.EntityNotFoundException; +import mate.academy.carsharing.exception.RegistrationException; import mate.academy.carsharing.mapper.UserMapper; import mate.academy.carsharing.model.Role; import mate.academy.carsharing.model.User; From e0c20134167f32146e0526eb4ce0dac284ca275e Mon Sep 17 00:00:00 2001 From: trokhim03 Date: Tue, 13 May 2025 10:39:39 +0300 Subject: [PATCH 13/14] fix after review --- .../CustomGlobalExceptionHandler.java | 41 ++++++++++ .../mate/academy/carsharing/model/Car.java | 2 +- .../service/payment/PaymentServiceImpl.java | 78 ++++++++++++------- .../stripe/StripePaymentServiceImpl.java | 46 +++++------ src/main/resources/application.properties | 4 + src/test/resources/application.properties | 3 + 6 files changed, 123 insertions(+), 51 deletions(-) diff --git a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java index e0af1b5..b55b3e8 100644 --- a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java +++ b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java @@ -12,6 +12,7 @@ import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -41,6 +42,46 @@ protected ResponseEntity handleMethodArgumentNotValid( return new ResponseEntity<>(body, headers, status); } + @ExceptionHandler(CarNotAvailableException.class) + public ResponseEntity handleCarNotAvailableException(CarNotAvailableException ex) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException ex) { + return buildResponse(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(PaymentException.class) + public ResponseEntity handlePaymentException(PaymentException ex) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(RegistrationException.class) + public ResponseEntity handleRegistrationException(RegistrationException ex) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(StripePaymentException.class) + public ResponseEntity handleStripePaymentException(StripePaymentException ex) { + return buildResponse(HttpStatus.SERVICE_UNAVAILABLE, ex.getMessage()); + } + + @ExceptionHandler(TelegramNotificationException.class) + public ResponseEntity handleTelegramNotificationException( + TelegramNotificationException ex + ) { + return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + } + + private ResponseEntity buildResponse(HttpStatus status, String message) { + Map body = new LinkedHashMap<>(); + body.put(TIME_LABEL, LocalDateTime.now()); + body.put(STATUS_LABEL, status); + body.put(ERRORS_LABEL, List.of(message)); + return new ResponseEntity<>(body, status); + } + private Object getErrorMessage(ObjectError e) { if (e instanceof FieldError fieldError) { return fieldError.getField() + " " + fieldError.getDefaultMessage(); diff --git a/src/main/java/mate/academy/carsharing/model/Car.java b/src/main/java/mate/academy/carsharing/model/Car.java index 1b3a9a4..511087b 100644 --- a/src/main/java/mate/academy/carsharing/model/Car.java +++ b/src/main/java/mate/academy/carsharing/model/Car.java @@ -39,7 +39,7 @@ public class Car { private int inventory; - @Column(name = "daily_fee", nullable = false) + @Column(nullable = false) private BigDecimal dailyFee; @Column(nullable = false) diff --git a/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java index 15e4cbd..febf004 100644 --- a/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/payment/PaymentServiceImpl.java @@ -17,14 +17,23 @@ import mate.academy.carsharing.service.payment.stripe.StripePaymentService; import mate.academy.carsharing.service.telegram.NotificationMessageService; import mate.academy.carsharing.service.telegram.NotificationService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @Service +@Transactional @RequiredArgsConstructor public class PaymentServiceImpl implements PaymentService { private static final BigDecimal DAILY_FINE_MULTIPLIER = BigDecimal.valueOf(1.4); + + @Value("${payment.session-id}") + private String defaultSessionId; + + @Value("${payment.session-url}") + private String defaultSessionUrl; + private final PaymentRepository paymentRepository; private final StripePaymentService stripePaymentService; private final PaymentMapper paymentMapper; @@ -39,43 +48,25 @@ public Page getWithUserId(Pageable pageable, Long userId) { } @Override - @Transactional public PaymentResponseDto createPayment(PaymentRequestDto paymentRequestDto) { - Rental rental = rentalRepository.findById(paymentRequestDto.getRentalId()) - .orElseThrow(() -> new EntityNotFoundException("Can't find rental by id:" - + paymentRequestDto.getRentalId())); - + Rental rental = getRentalById(paymentRequestDto.getRentalId()); Payment.Type type = determinePaymentType(rental); - - if (paymentRepository.existsByRentalIdAndStatus(rental.getId(), Payment.Status.PENDING)) { - throw new PaymentException("Active payment already exists for this rental"); - } + checkActivePaymentExists(rental.getId()); BigDecimal amountToPay = calculateAmountToPay(rental, type); - - Payment payment = new Payment(); - payment.setRental(rental); - payment.setType(type); - payment.setStatus(Payment.Status.PENDING); - payment.setAmountToPay(amountToPay); - payment.setSessionId("none"); - payment.setSessionUrl("http://none.none"); - - paymentRepository.save(payment); + Payment payment = createPendingPayment(rental, type, amountToPay); Session paymentSession = stripePaymentService .createPaymentSession(payment, rental, amountToPay); - stripePaymentService.setPaymentSessionUrl(payment, paymentSession); + payment.setSessionId(paymentSession.getId()); - String message = notificationMessageService - .createPaymentNotification(payment, rental, rental.getCar()); - notificationService.sendNotification(message); + notifyAboutPaymentCreation(payment, rental); + return paymentMapper.toDto(paymentRepository.save(payment)); } @Override - @Transactional public void checkSuccessfulPayment(Long paymentId) { Payment payment = paymentRepository.findById(paymentId) .orElseThrow(() -> new EntityNotFoundException( @@ -89,12 +80,45 @@ public void checkSuccessfulPayment(Long paymentId) { paymentRepository.save(payment); } if (newStatus == Payment.Status.PAID) { - String message = notificationMessageService - .createSuccessfulPaymentNotification(payment); - notificationService.sendNotification(message); + notifyAboutSuccessfulPayment(payment); + } + } + + private Rental getRentalById(Long rentalId) { + return rentalRepository.findById(rentalId) + .orElseThrow(() -> new EntityNotFoundException("Can't find rental" + + " by id:" + rentalId)); + } + + private void checkActivePaymentExists(Long rentalId) { + if (paymentRepository.existsByRentalIdAndStatus(rentalId, Payment.Status.PENDING)) { + throw new PaymentException("Active payment already exists for this rental"); } } + private Payment createPendingPayment(Rental rental, Payment.Type type, BigDecimal amountToPay) { + Payment payment = new Payment(); + payment.setRental(rental); + payment.setType(type); + payment.setStatus(Payment.Status.PENDING); + payment.setAmountToPay(amountToPay); + payment.setSessionId(defaultSessionId); + payment.setSessionUrl(defaultSessionUrl); + return paymentRepository.save(payment); + } + + private void notifyAboutPaymentCreation(Payment payment, Rental rental) { + String message = notificationMessageService + .createPaymentNotification(payment, rental, rental.getCar()); + notificationService.sendNotification(message); + } + + private void notifyAboutSuccessfulPayment(Payment payment) { + String message = notificationMessageService + .createSuccessfulPaymentNotification(payment); + notificationService.sendNotification(message); + } + private Payment.Type determinePaymentType(Rental rental) { if (rental.getActualReturnDate() == null) { throw new PaymentException("Cannot create payment - car is not returned yet"); diff --git a/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java index 2a44e36..78c987e 100644 --- a/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/payment/stripe/StripePaymentServiceImpl.java @@ -51,6 +51,29 @@ public Session createPaymentSession(Payment payment, Rental rental, BigDecimal a } } + @Override + public void setPaymentSessionUrl(Payment payment, Session session) { + payment.setSessionUrl(session.getUrl()); + } + + @Override + public Payment.Status checkPaymentStatus(String sessionId) { + try { + Session session = Session.retrieve(sessionId); + return mapStripeStatusToPaymentStatus(session.getStatus()); + } catch (StripeException e) { + throw new StripePaymentException("Failed to check payment status", e); + } + } + + private Payment.Status mapStripeStatusToPaymentStatus(String status) { + return switch (status) { + case "complete", "succeeded", "paid" -> Payment.Status.PAID; + case "pending", "canceled", "failed" -> Payment.Status.PENDING; + default -> throw new IllegalStateException("Unexpected value: " + status); + }; + } + private Long calculateExpirationTime() { return Instant.now() .plusSeconds(SESSION_EXPIRATION_HOURS * 60 * 60) @@ -75,27 +98,4 @@ private SessionCreateParams.LineItem createLineItem(Rental rental, BigDecimal am private Long convertToCents(BigDecimal amount) { return amount.multiply(BigDecimal.valueOf(100)).longValue(); } - - @Override - public void setPaymentSessionUrl(Payment payment, Session session) { - payment.setSessionUrl(session.getUrl()); - } - - @Override - public Payment.Status checkPaymentStatus(String sessionId) { - try { - Session session = Session.retrieve(sessionId); - return mapStripeStatusToPaymentStatus(session.getStatus()); - } catch (StripeException e) { - throw new StripePaymentException("Failed to check payment status", e); - } - } - - private Payment.Status mapStripeStatusToPaymentStatus(String status) { - return switch (status) { - case "complete", "succeeded", "paid" -> Payment.Status.PAID; - case "pending", "canceled", "failed" -> Payment.Status.PENDING; - default -> throw new IllegalStateException("Unexpected value: " + status); - }; - } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1fe9dbd..37880e8 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -27,3 +27,7 @@ stripe.cancel.url=${STRIPE_CANCEL_URL} telegram.bot.name=${TELEGRAM_BOT_NAME} telegram.bot.token=${TELEGRAM_BOT_TOKEN} telegram.bot.chat.id=${TELEGRAM_CHAT_ID} + +# Payment +payment.session-id=none +payment.session-url=http://none.none diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index fc47d26..a39a18d 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -19,3 +19,6 @@ telegram.bot.name=CarSharingTrokhimBot telegram.bot.token=7837073313:AAGtmb1ej345p_h1Lnmb8OYlRXDTc5sMAYs telegram.bot.chat.id=581117817 +# Payment +payment.session-id=none +payment.session-url=http://none.none From a42daff2b5fdbcc7948d5007296d6acf1e6406c3 Mon Sep 17 00:00:00 2001 From: trokhim03 Date: Thu, 15 May 2025 11:36:50 +0300 Subject: [PATCH 14/14] fix after review --- .../carsharing/exception/CustomGlobalExceptionHandler.java | 2 +- .../java/mate/academy/carsharing/repository/UserRepository.java | 2 +- .../mate/academy/carsharing/service/user/UserServiceImpl.java | 2 +- .../academy/carsharing/service/user/UserServiceImplTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java index b55b3e8..20ca37a 100644 --- a/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java +++ b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java @@ -59,7 +59,7 @@ public ResponseEntity handlePaymentException(PaymentException ex) { @ExceptionHandler(RegistrationException.class) public ResponseEntity handleRegistrationException(RegistrationException ex) { - return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + return buildResponse(HttpStatus.CONFLICT, ex.getMessage()); } @ExceptionHandler(StripePaymentException.class) diff --git a/src/main/java/mate/academy/carsharing/repository/UserRepository.java b/src/main/java/mate/academy/carsharing/repository/UserRepository.java index b18de8c..28b78b5 100644 --- a/src/main/java/mate/academy/carsharing/repository/UserRepository.java +++ b/src/main/java/mate/academy/carsharing/repository/UserRepository.java @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository { - Boolean existsByEmail(String email); + boolean existsByEmail(String email); @EntityGraph(attributePaths = "roles") Optional findByEmail(String email); diff --git a/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java b/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java index 22c2a90..7fb9e00 100644 --- a/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java +++ b/src/main/java/mate/academy/carsharing/service/user/UserServiceImpl.java @@ -30,7 +30,7 @@ public class UserServiceImpl implements UserService { public UserResponseDto register(UserRegistrationRequestDto userRegistrationRequestDto) throws RegistrationException { if (userRepository.existsByEmail(userRegistrationRequestDto.getEmail())) { - throw new RegistrationException("Can't registration user " + throw new RegistrationException("Can't register user " + "with existing email: " + userRegistrationRequestDto.getEmail()); } diff --git a/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java b/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java index d634100..3939626 100644 --- a/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java +++ b/src/test/java/mate/academy/carsharing/service/user/UserServiceImplTest.java @@ -106,7 +106,7 @@ void register_ShouldThrowException_WhenEmailExists() { RegistrationException exception = assertThrows(RegistrationException.class, () -> userService.register(userRequestDto)); - assertEquals("Can't registration user with existing email: " + userRequestDto.getEmail(), + assertEquals("Can't register user with existing email: " + userRequestDto.getEmail(), exception.getMessage()); }