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/README.md b/README.md
new file mode 100644
index 0000000..5aa8a3e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,151 @@
+# ๐ 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
+
+## โก 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
+
+**๐ Telegram notifications**
+- New rentals
+- Overdue rentals
+- Successful payments
+
+## ๐ Database Schema
+
+
+
+
+## ๐ 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:
+
+**๐ [Swagger UI](http://localhost:8080/api/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/auth/registration" \
+-H "Content-Type: application/json" \
+-d '{
+ "email": "user@example.com",
+ "password": "securePassword123",
+ "repeatPassword": "securePassword123",
+ "firstName": "John",
+ "lastName": "Doe"
+}'
+```
+
+**Get available cars:**
+
+```bash
+curl -X GET "http://localhost:8080/api/cars" \
+-H "Authorization: Bearer your.jwt.token"
+```
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..390596f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,34 +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
-
-
-
-
-
-
-
-
-
-
-
-
-
+
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
@@ -42,7 +50,20 @@
org.springframework.boot
spring-boot-starter-web
-
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ ${springdoc.version}
+
+
+ org.liquibase
+ liquibase-core
+
+
+ org.telegram
+ telegrambots-spring-boot-starter
+ ${telegrambots.version}
+
com.h2database
h2
@@ -68,6 +89,60 @@
spring-security-test
test
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+ 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
+ ${stripe.version}
+
+
+ org.testcontainers
+ mysql
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ ${validation.version}
+
@@ -75,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
@@ -96,7 +186,27 @@
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ ${maven.checkstyle.plugin.version}
+
+
+ compile
+
+ check
+
+
+
+
+ ${checkstyle.config.location}
+ 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..6ab552f
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/controller/AuthController.java
@@ -0,0 +1,43 @@
+package mate.academy.carsharing.controller;
+
+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.exception.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 @Valid 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 @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
new file mode 100644
index 0000000..4f6a629
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/controller/CarController.java
@@ -0,0 +1,74 @@
+package mate.academy.carsharing.controller;
+
+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;
+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 @Valid 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 @Valid 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..9019655
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/controller/PaymentController.java
@@ -0,0 +1,64 @@
+package mate.academy.carsharing.controller;
+
+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;
+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
+ @Valid 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..10043ea
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/controller/RentalController.java
@@ -0,0 +1,78 @@
+package mate.academy.carsharing.controller;
+
+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;
+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 @Valid 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 @Valid 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..0436db8
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/controller/UserController.java
@@ -0,0 +1,61 @@
+package mate.academy.carsharing.controller;
+
+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;
+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 @Valid 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 @Valid 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..08b0c5b
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/dto/car/CarRequestDto.java
@@ -0,0 +1,30 @@
+package mate.academy.carsharing.dto.car;
+
+import jakarta.validation.constraints.DecimalMin;
+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;
+import mate.academy.carsharing.model.Car;
+
+@Data
+@Accessors(chain = true)
+public class CarRequestDto {
+ @NotBlank
+ private String model;
+
+ @NotBlank
+ private String brand;
+
+ @NotNull
+ private Car.Type type;
+
+ @Positive
+ private int inventory;
+
+ @NotNull
+ @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..37f1bfe
--- /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.NotNull;
+import jakarta.validation.constraints.Positive;
+import lombok.Data;
+
+@Data
+public class PaymentRequestDto {
+ @NotNull
+ @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..6b5d1bd
--- /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.Future;
+import jakarta.validation.constraints.FutureOrPresent;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import java.time.LocalDate;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+public class RentalRequestDto {
+ @NotNull
+ @JsonFormat(pattern = "yyyy-MM-dd")
+ @FutureOrPresent
+ private LocalDate rentalDate;
+
+ @NotNull
+ @JsonFormat(pattern = "yyyy-MM-dd")
+ @Future
+ private LocalDate returnDate;
+
+ @NotNull
+ @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..db8ec72
--- /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.NotNull;
+import jakarta.validation.constraints.Positive;
+import java.time.LocalDate;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+public class RentalReturnRequestDto {
+ @NotNull
+ @Positive
+ private Long rentalId;
+
+ @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
new file mode 100644
index 0000000..3a62374
--- /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.NotNull;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import mate.academy.carsharing.model.Role;
+
+@Data
+@Accessors(chain = true)
+public class RoleNameRequestDto {
+ @NotNull
+ 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/exception/CarNotAvailableException.java b/src/main/java/mate/academy/carsharing/exception/CarNotAvailableException.java
new file mode 100644
index 0000000..e46633c
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/exception/CarNotAvailableException.java
@@ -0,0 +1,7 @@
+package mate.academy.carsharing.exception;
+
+public class CarNotAvailableException extends RuntimeException {
+ public CarNotAvailableException(String message) {
+ super(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..20ca37a
--- /dev/null
+++ b/src/main/java/mate/academy/carsharing/exception/CustomGlobalExceptionHandler.java
@@ -0,0 +1,91 @@
+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.bind.annotation.ExceptionHandler;
+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