diff --git a/Book-Store.pdf b/Book-Store.pdf
new file mode 100644
index 0000000..b86a7da
Binary files /dev/null and b/Book-Store.pdf differ
diff --git a/Book-Store.png b/Book-Store.png
new file mode 100644
index 0000000..28b4105
Binary files /dev/null and b/Book-Store.png differ
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dff39e4
--- /dev/null
+++ b/README.md
@@ -0,0 +1,63 @@
+# π― Project Overview
+This application provides functionalities for managing users, books, categories, orders, and shopping carts. The system ensures secure access with role-based authorization (USER and ADMIN). The app is designed for book sales, allowing users to:
+
+Create accounts and log in.
+Purchase books conveniently and quickly from home.
+Track the status of orders and know when they arrive at the post office.
+This is my first large-scale application, and I faced numerous challenges during its development. Each part was difficult but immensely educational, and I gained a lot of knowledge that will be invaluable for my future projects.
+
+# π Technologies Used
+- Spring Boot
+- Spring Security
+- Spring Data JPA
+- JWT (JSON Web Token)
+- Swagger
+- Liquibase
+- MapStruct
+- Lombok
+- Hibernate Validator
+- MySQL
+
+# π API Endpoints
+1. AuthenticationController
+- ```POST /auth/registration``` β Register a new user.
+- ```POST /auth/login``` β User authentication (login).
+2. BookController
+- ```GET /books``` β Retrieve all books with pagination (ADMIN access only).
+- ```GET /books/{id}``` β Retrieve a book by its ID.
+- ```POST /books``` β Create a new book (ADMIN access only).
+- ```PUT /books/{id}``` β Update a book by its ID (ADMIN access only).
+- ```DELETE /books/{id}``` β Delete a book by its ID (ADMIN access only).
+- ```GET /books/search``` β Search books by parameters.
+3. CategoryController
+- ```POST /categories``` β Create a new category (ADMIN access only).
+- ```GET /categories``` β Retrieve all categories.
+- ```GET /categories/{id}``` β Retrieve a category by its ID.
+- ```PUT /categories/{id}``` β Update a category by its ID (ADMIN access only).
+- ```DELETE /categories/{id}``` β Delete a category by its ID (ADMIN access only).
+- ```GET /categories/{id}/books``` β Retrieve all books in a category by its ID.
+4. OrderController
+- ```POST /orders``` β Create a new order (USER access only).
+- ```GET /orders``` β Retrieve all orders of the logged-in user (USER access only).
+- ```PATCH /orders/{id}``` β Update the status of an order (ADMIN access only).
+- ```GET /orders/{orderId}/items``` β Retrieve all items from a specific order (USER access only).
+- ```GET /orders/{orderId}/items/{itemId}``` β Retrieve a specific item from an order (USER access only).
+5. ShoppingCartController
+- ```GET /cart``` β Retrieve the current user's shopping cart.
+- ```POST /cart``` β Add a book to the shopping cart.
+- ```PUT /cart/items/{cartItemId}``` β Update the quantity of items in the cart.
+- ```DELETE /cart/items/{cartItemId}``` β Remove an item from the cart.
+
+# Models and relations
+
+
+
+# π Getting Started
+I have always loved books, especially those that help understand the meaning of human existence and provide fuel for thought. Inspired by this passion, I decided to create my own application, where users can purchase books from the comfort of their homes.
+
+# πΉVideo Overview of Program Functionality
+You can also find a video of the program at this link: https://www.loom.com/share/0c519d24efc04b64acf9a3e9096b40cb?sid=e3750a8b-c044-4b13-b382-535497228e21
+
+# Contacts
+- Email: greqit.work@gmail.com
+- [LinkedIn](https://www.linkedin.com/in/ivan-prystaia-7099a22b1/)
diff --git a/pom.xml b/pom.xml
index d17cb27..521749c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
3.3.1
- mare.academy
+ mate.academy
Spring-Boot-web
0.0.1-SNAPSHOT
Spring-Boot-web
@@ -39,6 +39,10 @@
8.0.1.Final
3.3.2
0.11.5
+ 3.1.1
+
+ https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml
+
@@ -60,6 +64,17 @@
1.6.14
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.1.0
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ 2.2.25
+
io.jsonwebtoken
@@ -121,11 +136,10 @@
test
-
- mysql
- mysql-connector-java
- 8.0.33
+ com.mysql
+ mysql-connector-j
+ 9.1.0
@@ -224,6 +238,27 @@
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ ${maven.checkstyle.plugin.version}
+
+
+ compile
+
+ check
+
+
+
+
+ **/src/main/java/**
+ **/target/generated-sources/**
+ ${maven.checkstyle.plugin.configLocation}
+ true
+ true
+ false
+
+
diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java
index b689b75..da1d6fc 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/controller/AuthenticationController.java
@@ -1,5 +1,9 @@
package mate.academy.springbootwebgreqit.controller;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.user.UserLoginRequestDto;
@@ -13,6 +17,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+@Tag(name = "Auth managemant", description = "Endpoint to auth")
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@@ -20,15 +25,23 @@ public class AuthenticationController {
private final UserService userService;
private final AuthenticationService authenticationService;
+ @Operation(summary = "Register a new user")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "User registered successfully"),
+ @ApiResponse(responseCode = "400", description = "Invalid input data")
+ })
@PostMapping("/registration")
- public UserResponseDto register(@RequestBody
- @Valid UserRegistrationRequestDto requestBody) {
+ public UserResponseDto register(@RequestBody @Valid UserRegistrationRequestDto requestBody) {
return userService.register(requestBody);
}
+ @Operation(summary = "Authenticate a user")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "User authenticated successfully"),
+ @ApiResponse(responseCode = "401", description = "Invalid credentials")
+ })
@PostMapping("/login")
- public UserLoginResponseDto login(@RequestBody
- @Valid UserLoginRequestDto response) {
+ public UserLoginResponseDto login(@RequestBody @Valid UserLoginRequestDto response) {
return authenticationService.authenticate(response);
}
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java
index ca81229..b859ffa 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/controller/BookController.java
@@ -1,6 +1,8 @@
package mate.academy.springbootwebgreqit.controller;
-import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.BookDto;
@@ -27,40 +29,66 @@ public class BookController {
private final BookService bookService;
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Get all books with pagination")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
+ @ApiResponse(responseCode = "403", description = "Access denied")
+ })
@GetMapping
- @ApiOperation(value = "get all books with pagination")
public Page getAll(Pageable pageable) {
return bookService.findAll(pageable);
}
+ @Operation(summary = "Get book by id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Book retrieved successfully"),
+ @ApiResponse(responseCode = "404", description = "Book not found")
+ })
@GetMapping("/{id}")
- @ApiOperation(value = "Get book by id")
public BookDto getBookById(@Valid @PathVariable Long id) {
return bookService.findById(id);
}
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Create a book")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "Book created successfully"),
+ @ApiResponse(responseCode = "400", description = "Invalid input data")
+ })
@PostMapping
- @ApiOperation(value = "create a book")
public BookDto createBook(@Valid @RequestBody CreateBookRequestDto requestDto) {
return bookService.save(requestDto);
}
- @PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
- public BookDto updateBook(@PathVariable Long id, @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) {
+ @Operation(summary = "Update a book")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Book updated successfully"),
+ @ApiResponse(responseCode = "404", description = "Book not found")
+ })
+ @PutMapping("/{id}")
+ public BookDto updateBook(@PathVariable Long id,
+ @RequestBody @Valid UpdateBookRequestDto updateBookRequestDto) {
return bookService.update(updateBookRequestDto);
}
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Delete a book")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "Book deleted successfully"),
+ @ApiResponse(responseCode = "404", description = "Book not found")
+ })
@DeleteMapping("/{id}")
- @ApiOperation(value = "delete a book")
public void deleteBook(@PathVariable Long id) {
bookService.deleteById(id);
}
+ @Operation(summary = "Search a book")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
+ @ApiResponse(responseCode = "400", description = "Invalid search parameters")
+ })
@GetMapping("/search")
- @ApiOperation(value = "search a book")
public Page searchBooks(BookSearchParameters searchParameters, Pageable pageable) {
return bookService.search(searchParameters, pageable);
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java
index 8f184af..408ce00 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/controller/CategoryController.java
@@ -1,5 +1,8 @@
package mate.academy.springbootwebgreqit.controller;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.BookDtoWithoutCategotyIds;
@@ -26,34 +29,65 @@ public class CategoryController {
private final CategoryService categoryService;
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Create a new category")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "Category created successfully"),
+ @ApiResponse(responseCode = "400", description = "Invalid input data")
+ })
@PostMapping
public CategoryDto createCategory(@Valid @RequestBody CreateCategoryRequestDto categoryDto) {
return categoryService.save(categoryDto);
}
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Get all categories")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Categories retrieved successfully"),
+ @ApiResponse(responseCode = "403", description = "Access denied")
+ })
@GetMapping
public List getAll() {
return categoryService.findAll();
}
+ @Operation(summary = "Get category by id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Category retrieved successfully"),
+ @ApiResponse(responseCode = "404", description = "Category not found")
+ })
@GetMapping("/{id}")
public CategoryDto getCategoryById(@Valid @PathVariable Long id) {
return categoryService.getById(id);
}
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Update a category")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Category updated successfully"),
+ @ApiResponse(responseCode = "404", description = "Category not found")
+ })
@PutMapping("/{id}")
- public CategoryDto updateCategory(@PathVariable Long id, @RequestBody @Valid UpdateCategoryRequestDto categoryDto) {
+ public CategoryDto updateCategory(@PathVariable Long id,
+ @RequestBody @Valid UpdateCategoryRequestDto categoryDto) {
return categoryService.update(id, categoryDto);
}
@PreAuthorize("hasRole('ADMIN')")
+ @Operation(summary = "Delete a category")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "Category deleted successfully"),
+ @ApiResponse(responseCode = "404", description = "Category not found")
+ })
@DeleteMapping("/{id}")
public void deleteCategory(@PathVariable Long id) {
categoryService.deleteById(id);
}
+ @Operation(summary = "Get books by category id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Books retrieved successfully"),
+ @ApiResponse(responseCode = "404", description = "Category not found")
+ })
@GetMapping("/{id}/books")
public List getBooksByCategoryId(@PathVariable Long id) {
return categoryService.findBooksByCategoryId(id);
diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java
index 35fbf29..fb3c1c6 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/controller/OrderController.java
@@ -1,6 +1,8 @@
package mate.academy.springbootwebgreqit.controller;
-import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.order.CreateOrderRequestDto;
@@ -28,14 +30,22 @@ public class OrderController {
private final OrderService orderService;
@PostMapping
- @ApiOperation("Make order")
+ @Operation(summary = "Make order")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Order created successfully"),
+ @ApiResponse(responseCode = "400", description = "Invalid input data")
+ })
@PreAuthorize("hasRole('USER')")
public OrderResponseDto addOrder(@RequestParam Long userId,
@RequestBody CreateOrderRequestDto createOrderRequestDto) {
return orderService.addOrder(userId, createOrderRequestDto);
}
- @ApiOperation("Get all orders")
+ @Operation(summary = "Get all orders")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Orders retrieved successfully"),
+ @ApiResponse(responseCode = "403", description = "Access denied")
+ })
@GetMapping
@PreAuthorize("hasRole('USER')")
public List getAllOrders(@RequestParam Long userId) {
@@ -44,26 +54,37 @@ public List getAllOrders(@RequestParam Long userId) {
@PreAuthorize("hasRole('ADMIN')")
@PatchMapping("/{id}")
- @ApiOperation("Update order status by id")
+ @Operation(summary = "Update order status by id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Order status updated successfully"),
+ @ApiResponse(responseCode = "404", description = "Order not found")
+ })
public OrderResponseDto updateOrderStatus(@PathVariable Long id,
@RequestBody @Valid OrderRequestDto requestDto) {
return orderService.updateOrderStatus(id, requestDto);
}
+ @Operation(summary = "Get all items from order by order id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Order items retrieved successfully"),
+ @ApiResponse(responseCode = "404", description = "Order not found")
+ })
@GetMapping("/{orderId}/items")
- @ApiOperation("Get all items from order by order id")
@PreAuthorize("hasRole('USER')")
public List getAllItemsFromOrder(@PathVariable Long orderId,
Authentication authentication) {
return orderService.getAllItemsFromOrder(orderId);
}
+ @Operation(summary = "Get item from order by order id and item id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Order item retrieved successfully"),
+ @ApiResponse(responseCode = "404", description = "Order or item not found")
+ })
@GetMapping("{orderId}/items/{itemId}")
- @ApiOperation("Get item from order by order id and item id")
@PreAuthorize("hasRole('USER')")
public OrderItemResponseDto getItemFromOrderById(@PathVariable Long orderId,
- @PathVariable Long itemId,
- Authentication authentication) {
+ @PathVariable Long itemId) {
return orderService.getItemFromOrderById(orderId, itemId);
}
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java
index b9c4c97..a7b1a24 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/controller/ShoppingCartController.java
@@ -1,9 +1,15 @@
package mate.academy.springbootwebgreqit.controller;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto;
+import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto;
import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto;
import mate.academy.springbootwebgreqit.service.ShoppingCartService;
+import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -11,7 +17,6 @@
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -20,26 +25,46 @@
public class ShoppingCartController {
private final ShoppingCartService shoppingCartService;
- @GetMapping
- public ShoppingCartDto getShoppingCartForCurrentUser(@RequestParam Long userId) {
- return shoppingCartService.getShoppingCartForCurrentUser(userId);
+ @Operation(summary = "Get shopping cart for current user")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Shopping cart retrieved successfully"),
+ @ApiResponse(responseCode = "403", description = "Access denied")
+ })
+ @GetMapping("/{shoppingCartId}")
+ public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication,
+ @PathVariable Long shoppingCartId) {
+ return shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCartId);
}
+ @Operation(summary = "Add book to shopping cart")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Book added to shopping cart"),
+ @ApiResponse(responseCode = "400", description = "Invalid input data")
+ })
@PostMapping
public ShoppingCartDto addBookToShoppingCart(@RequestBody CartItemRequestDto cartItem,
- @RequestParam Long userId) {
- return shoppingCartService.addBookToShoppingCart(cartItem, userId);
+ Authentication authentication) {
+ return shoppingCartService.addBookToShoppingCart(cartItem, authentication);
}
+ @Operation(summary = "Update cart item quantity")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "Cart item quantity updated successfully"),
+ @ApiResponse(responseCode = "404", description = "Cart item not found")
+ })
@PutMapping("/items/{cartItemId}")
public ShoppingCartDto updateCartItemQuantity(@PathVariable Long cartItemId,
- @RequestParam int quantity,
- @RequestParam Long userId) {
- return shoppingCartService.updateCartItemQuantity(cartItemId, quantity, userId);
+ @RequestBody @Valid UpdateCartItemDto quantity) {
+ return shoppingCartService.updateCartItemQuantity(cartItemId, quantity);
}
+ @Operation(summary = "Remove cart item")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "Cart item removed successfully"),
+ @ApiResponse(responseCode = "404", description = "Cart item not found")
+ })
@DeleteMapping("/items/{cartItemId}")
- public void removeCartItem(@PathVariable Long cartItemId, @RequestParam Long userId) {
- shoppingCartService.removeCartItem(cartItemId, userId);
+ public void removeCartItem(@PathVariable Long cartItemId, Authentication authentication) {
+ shoppingCartService.removeCartItem(cartItemId, authentication);
}
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java
index 69f177b..43b6dd9 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/dto/category/CreateCategoryRequestDto.java
@@ -1,7 +1,6 @@
package mate.academy.springbootwebgreqit.dto.category;
import jakarta.validation.constraints.NotBlank;
-import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
diff --git a/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java
new file mode 100644
index 0000000..b4ecbfd
--- /dev/null
+++ b/src/main/java/mate/academy/springbootwebgreqit/dto/shoppingcart/UpdateCartItemDto.java
@@ -0,0 +1,10 @@
+package mate.academy.springbootwebgreqit.dto.shoppingcart;
+
+import jakarta.validation.constraints.Positive;
+import lombok.Data;
+
+@Data
+public class UpdateCartItemDto {
+ @Positive
+ private int quantity;
+}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/Book.java b/src/main/java/mate/academy/springbootwebgreqit/model/Book.java
index f2994c6..05635c2 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/model/Book.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/model/Book.java
@@ -63,6 +63,7 @@ public class Book {
)
@ToString.Exclude
@EqualsAndHashCode.Exclude
+ @JsonIgnore
private Set categories = new HashSet<>();
private boolean isDeleted = false;
diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java b/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java
index 11aa5e4..88c053b 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/model/CartItem.java
@@ -28,6 +28,7 @@ public class CartItem {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id", nullable = false)
+ @JsonIgnore
private Book book;
private int quantity;
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/model/User.java b/src/main/java/mate/academy/springbootwebgreqit/model/User.java
index fe41696..ce07cf5 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/model/User.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/model/User.java
@@ -1,7 +1,6 @@
package mate.academy.springbootwebgreqit.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
@@ -11,7 +10,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
-import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@@ -46,10 +44,6 @@ public class User implements UserDetails {
@Column(name = "is_deleted")
private boolean isDeleted = false;
- @JoinColumn(name = "shopping_card_id")
- @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
- private ShoppingCart shoppingCart;
-
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java
index 884389a..944044c 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderItemReposirory.java
@@ -2,8 +2,11 @@
import mate.academy.springbootwebgreqit.model.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
import java.util.Optional;
+@Repository
public interface OrderItemReposirory extends JpaRepository {
Optional findByOrderId(Long orderId);
diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java
index dbc4a49..bbe80a2 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/repository/OrderRepository.java
@@ -3,8 +3,11 @@
import mate.academy.springbootwebgreqit.model.Order;
import mate.academy.springbootwebgreqit.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
import java.util.List;
+@Repository
public interface OrderRepository extends JpaRepository {
List findByUser(User user);
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java
index 2c2f319..5163ae3 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/repository/ShoppingCartRepository.java
@@ -1,12 +1,18 @@
package mate.academy.springbootwebgreqit.repository;
+import mate.academy.springbootwebgreqit.model.CartItem;
import mate.academy.springbootwebgreqit.model.ShoppingCart;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
import java.util.Optional;
+import java.util.Set;
+@Repository
public interface ShoppingCartRepository extends JpaRepository {
@EntityGraph(attributePaths = {"cartItems", "cartItems.book"})
Optional findByUserId(Long userId);
+
+ Optional findByCartItems(Set cartItems);
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java b/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java
index ddc3f33..a24ee83 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/security/CustomUserDetailService.java
@@ -5,6 +5,7 @@
import mate.academy.springbootwebgreqit.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
@@ -15,6 +16,6 @@ public class CustomUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String email) throws EntityNotFoundException {
return userRepository.findByEmail(email)
- .orElseThrow(() -> new EntityNotFoundException("Can't find user by email"));
+ .orElseThrow(() -> new UsernameNotFoundException("Can't find user by email"));
}
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java
index ee79370..3345ae4 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/service/ShoppingCartService.java
@@ -1,18 +1,24 @@
package mate.academy.springbootwebgreqit.service;
import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto;
+import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto;
import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto;
import mate.academy.springbootwebgreqit.model.ShoppingCart;
import mate.academy.springbootwebgreqit.model.User;
+import org.springframework.security.core.Authentication;
public interface ShoppingCartService {
- ShoppingCartDto getShoppingCartForCurrentUser(Long userId);
+ ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication,
+ Long shoppingCartId);
- ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem, Long userId);
+ ShoppingCartDto addBookToShoppingCart(CartItemRequestDto cartItem,
+ Authentication authentication);
- ShoppingCartDto updateCartItemQuantity(Long cartItemId, int quantity, Long userId);
+ ShoppingCartDto updateCartItemQuantity(Long cartItemId,
+ UpdateCartItemDto quantity);
- ShoppingCartDto removeCartItem(Long cartItemId, Long userId);
+ ShoppingCartDto removeCartItem(Long cartItemId,
+ Authentication authentication);
ShoppingCart createShoppingCart(User user);
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java
index 64a0119..7e5d9e8 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/ShoppingCartServiceImpl.java
@@ -3,6 +3,7 @@
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto;
+import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto;
import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto;
import mate.academy.springbootwebgreqit.exception.EntityNotFoundException;
import mate.academy.springbootwebgreqit.mapper.CartItemMapper;
@@ -16,8 +17,10 @@
import mate.academy.springbootwebgreqit.repository.UserRepository;
import mate.academy.springbootwebgreqit.service.ShoppingCartService;
import org.hibernate.Hibernate;
+import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.Optional;
+import java.util.Set;
@Service
@RequiredArgsConstructor
@@ -31,20 +34,18 @@ public class ShoppingCartServiceImpl implements ShoppingCartService {
@Transactional
@Override
- public ShoppingCartDto getShoppingCartForCurrentUser(Long userId) {
- User user = userRepository.findById(userId)
- .orElseThrow(() -> new IllegalArgumentException("User not found"));
- ShoppingCart shoppingCart = user.getShoppingCart();
+ public ShoppingCartDto getShoppingCartForCurrentUser(Authentication authentication,
+ Long shoppingCartId) {
+ User user = userRepository.findByEmail(authentication.getName())
+ .orElseThrow(() -> new IllegalArgumentException("User not found with id: "
+ + authentication.getName()));
+ ShoppingCart shoppingCart = shoppingCartRepository.findById(shoppingCartId)
+ .orElseThrow(() -> new EntityNotFoundException("Shopping cart not "
+ + "found for user with id: " + shoppingCartId));
if (shoppingCart == null) {
- throw new EntityNotFoundException("Shopping cart not found for user with ID "
- + userId);
+ throw new EntityNotFoundException("Shopping cart not found for user with email: "
+ + user.getEmail());
}
- Hibernate.initialize(shoppingCart.getCartItems());
- Hibernate.initialize(user.getRoles());
- shoppingCart.getCartItems().forEach(cartItem ->
- Hibernate.initialize(cartItem.getBook().getCategories()));
- shoppingCart.getCartItems().forEach(cartItem ->
- Hibernate.initialize(cartItem.getBook().getCartItems()));
return shoppingCartMapper.toDto(shoppingCart);
}
@@ -52,8 +53,11 @@ public ShoppingCartDto getShoppingCartForCurrentUser(Long userId) {
@Override
public ShoppingCartDto addBookToShoppingCart(
CartItemRequestDto cartItemDto,
- Long userId) {
- ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(userId)
+ Authentication authentication) {
+ User user = userRepository.findByEmail(authentication.getName())
+ .orElseThrow(() -> new IllegalArgumentException("User not found with id: "
+ + authentication.getName()));
+ ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId())
.orElseThrow(() -> new EntityNotFoundException("Shopping cart not found"));
Optional existingItemOpt = shoppingCart.getCartItems().stream()
@@ -71,67 +75,54 @@ public ShoppingCartDto addBookToShoppingCart(
newCartItem.setShoppingCart(shoppingCart);
newCartItem.setBook(bookRepository.findById(cartItemDto.getBookId())
.orElseThrow(() -> new EntityNotFoundException("Book not found")));
- Hibernate.initialize(shoppingCart.getUser());
- Hibernate.initialize(shoppingCart.getUser().getRoles());
newCartItem.setQuantity(cartItemDto.getQuantity());
newCartItem.setShoppingCart(shoppingCart);
shoppingCart.getCartItems().add(newCartItem);
}
);
- ShoppingCart savedshoppingCart = shoppingCartRepository.save(shoppingCart);
- Hibernate.initialize(savedshoppingCart.getCartItems());
- savedshoppingCart.getCartItems().forEach(cartItem ->
- Hibernate.initialize(cartItem.getBook().getCategories()));
- savedshoppingCart.getCartItems().forEach(cartItem ->
- Hibernate.initialize(cartItem.getBook().getCartItems()));
-
- return shoppingCartMapper.toDto(savedshoppingCart);
+ shoppingCartRepository.save(shoppingCart);
+ return shoppingCartMapper.toDto(shoppingCart);
}
@Transactional
@Override
public ShoppingCartDto updateCartItemQuantity(Long cartItemId,
- int quantity, Long userId) {
- if (quantity <= 0) {
+ UpdateCartItemDto quantity) {
+ if (quantity.getQuantity() <= 0) {
throw new EntityNotFoundException("Quantity must be a positive integer");
}
- User user = userRepository.findById(userId)
- .orElseThrow(() -> new EntityNotFoundException("User not found"));
- ShoppingCart shoppingCart = user.getShoppingCart();
- if (shoppingCart == null) {
- throw new EntityNotFoundException("Shopping cart not found for user with ID "
- + userId);
- }
+ CartItem cartItemToFindShoppingCart = cartItemRepository.findById(cartItemId)
+ .orElseThrow(() -> new EntityNotFoundException("Cart item not found with id: "
+ + cartItemId));
+
+ ShoppingCart shoppingCart = shoppingCartRepository
+ .findByCartItems(Set.of(cartItemToFindShoppingCart))
+ .orElseThrow(() -> new EntityNotFoundException("Shopping cart "
+ + "not found by cart item with id: " + cartItemToFindShoppingCart));
CartItem cartItem = shoppingCart.getCartItems().stream()
.filter(item -> item.getId().equals(cartItemId))
.findFirst()
.orElseThrow(() -> new EntityNotFoundException("CartItem with ID "
+ cartItemId + " not found in the user's shopping cart"));
- cartItem.setQuantity(quantity);
- Hibernate.initialize(shoppingCart.getCartItems());
- Hibernate.initialize(user.getRoles());
-
- shoppingCart.getCartItems().forEach(ci ->
- Hibernate.initialize(cartItem.getBook().getCategories()));
- shoppingCart.getCartItems().forEach(ci ->
- Hibernate.initialize(cartItem.getBook().getCartItems()));
-
+ cartItem.setQuantity(quantity.getQuantity());
cartItemRepository.save(cartItem);
- ShoppingCart savedShoppingCart = shoppingCartRepository.save(shoppingCart);
+ shoppingCartRepository.save(shoppingCart);
- return shoppingCartMapper.toDto(savedShoppingCart);
+ return shoppingCartMapper.toDto(shoppingCart);
}
@Transactional
@Override
- public ShoppingCartDto removeCartItem(Long cartItemId, Long userId) {
- ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(userId)
+ public ShoppingCartDto removeCartItem(Long cartItemId, Authentication authentication) {
+ User user = userRepository.findByEmail(authentication.getName())
+ .orElseThrow(() -> new IllegalArgumentException("User not found with id: "
+ + authentication.getName()));
+
+ ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(user.getId())
.orElseThrow(() -> new EntityNotFoundException("Shopping cart not found"));
- User user = userRepository.findById(userId)
- .orElseThrow(() -> new EntityNotFoundException("User not found"));
CartItem cartItem = shoppingCart.getCartItems().stream()
.filter(item -> item.getId().equals(cartItemId))
@@ -142,14 +133,6 @@ public ShoppingCartDto removeCartItem(Long cartItemId, Long userId) {
shoppingCart.getCartItems().remove(cartItem);
ShoppingCart savedshoppingCart = shoppingCartRepository.save(shoppingCart);
- Hibernate.initialize(shoppingCart.getCartItems());
- Hibernate.initialize(user.getRoles());
-
- shoppingCart.getCartItems().forEach(ci ->
- Hibernate.initialize(cartItem.getBook().getCategories()));
- shoppingCart.getCartItems().forEach(ci ->
- Hibernate.initialize(cartItem.getBook().getCartItems()));
-
return shoppingCartMapper.toDto(savedshoppingCart);
}
diff --git a/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java b/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java
index 6ae5234..41312a4 100644
--- a/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java
+++ b/src/main/java/mate/academy/springbootwebgreqit/service/impl/UserServiceImpl.java
@@ -48,7 +48,7 @@ public UserResponseDto register(UserRegistrationRequestDto requestDto) {
user.setRoles(roles);
userRepository.save(user);
ShoppingCart shoppingCart = shoppingCartService.createShoppingCart(user);
- user.setShoppingCart(shoppingCart);
+ shoppingCart.setUser(user);
return userMapper.toDto(user);
}
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a6d1391..64ba9e0 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,4 +1,3 @@
-spring.application.name=Spring-boot-web-greqit
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/book-store
spring.datasource.username=root
@@ -9,6 +8,10 @@ spring.jpa.show-sql=true
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
+spring.liquibase.change-log=/db/changelog/db.changelog-master.yaml
+
+springdoc.api-docs.enabled=true
+springdoc.swagger-ui.enabled=true
jwt.expiration=1800000
jwt.secret=haveAGoodDayIfYouReadIt243509349584398534025
diff --git a/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml b/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml
index 1e36e67..9c3c24f 100644
--- a/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml
+++ b/src/main/resources/db/changelog/changes/02-create-table-users-roles-shoppingcarts.yaml
@@ -41,9 +41,6 @@ databaseChangeLog:
name: is_deleted
type: boolean
defaultValue: false
- - column:
- name: shopping_card_id
- type: bigint
- createTable:
tableName: roles
diff --git a/src/main/resources/liquibase.properties b/src/main/resources/liquibase.properties
index e83e494..2b801bc 100644
--- a/src/main/resources/liquibase.properties
+++ b/src/main/resources/liquibase.properties
@@ -1,4 +1,4 @@
url=jdbc:mysql://localhost:3306/book-store
username=root
password=1234567
-changelog-file=/db/chandelog/db.changelog-master.yaml
+changelog-file=/db/changelog/db.changelog-master.yaml
diff --git a/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java
new file mode 100644
index 0000000..74896cb
--- /dev/null
+++ b/src/test/java/mate/academy/springbootwebgreqit/controller/ShoppingCartControllerTest.java
@@ -0,0 +1,123 @@
+package mate.academy.springbootwebgreqit.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto;
+import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto;
+import mate.academy.springbootwebgreqit.security.CustomUserDetailService;
+import mate.academy.springbootwebgreqit.service.ShoppingCartService;
+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.http.MediaType;
+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;
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class ShoppingCartControllerTest {
+ protected static MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Autowired
+ private ShoppingCartService shoppingCartService;
+ @Autowired
+ private CustomUserDetailService customUserDetailService;
+
+ @BeforeAll
+ static void beforeAll(@Autowired WebApplicationContext webApplicationContext) {
+ mockMvc = MockMvcBuilders
+ .webAppContextSetup(webApplicationContext)
+ .apply(springSecurity())
+ .build();
+ }
+
+
+ @Test
+ @Sql(scripts = {"/data-sql/create-books.sql",
+ "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"},
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase =
+ Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @WithMockUser(username = "john.doe@example.com", roles = {"USER"})
+ @DisplayName("Get shopping cart for the current user")
+ void getShoppingCart_ShouldReturnCartForUser() throws Exception {
+ Long shoppingCartId = 1L;
+ mockMvc.perform(get("/cart/{shoppingCartId}", shoppingCartId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.user.id").value(1L))
+ .andReturn();
+ }
+
+ @Test
+ @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql"},
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase =
+ Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @WithMockUser(username = "john.doe@example.com", roles = {"USER"})
+ @DisplayName("Add book to shopping cart")
+ void addBookToShoppingCart_ShouldReturnUpdatedCart() throws Exception {
+ CartItemRequestDto cartItemRequestDto = new CartItemRequestDto();
+ cartItemRequestDto.setBookId(1L);
+ cartItemRequestDto.setQuantity(2);
+
+ String jsonRequest = objectMapper.writeValueAsString(cartItemRequestDto);
+
+ MvcResult result = mockMvc.perform(post("/cart")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonRequest))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.cartItems[0].quantity")
+ .value(cartItemRequestDto.getQuantity()))
+ .andReturn();
+ }
+
+
+ @Test
+ @Sql(scripts = {"/data-sql/create-books.sql",
+ "/data-sql/create-users.sql", "/data-sql/create-cart-item.sql"},
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @WithMockUser(username = "john.doe@example.com", roles = {"USER"})
+ @DisplayName("Update cart item quantity")
+ void updateCartItemQuantity_ShouldReturnUpdatedQuantity() throws Exception {
+ Long cartItemId = 1L;
+ UpdateCartItemDto quantityDto = new UpdateCartItemDto();
+ quantityDto.setQuantity(3);
+
+ String jsonRequest = objectMapper.writeValueAsString(quantityDto);
+
+ mockMvc.perform(put("/cart/items/{cartItemId}", cartItemId)
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(jsonRequest))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.cartItems[0].quantity").value(quantityDto.getQuantity()))
+ .andReturn();
+ }
+
+ @Test
+ @Sql(scripts = {"/data-sql/create-books.sql", "/data-sql/create-users.sql",
+ "/data-sql/create-cart-item.sql"},
+ executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+ @Sql(scripts = "/data-sql/clear-tables-for-sh.sql", executionPhase =
+ Sql.ExecutionPhase.AFTER_TEST_METHOD)
+ @WithMockUser(username = "john.doe@example.com", roles = {"USER"})
+ @DisplayName("Remove item from cart")
+ void removeItemFromCart_ShouldReturnEmptyCart() throws Exception {
+ Long cartItemId = 1L;
+
+ mockMvc.perform(delete("/cart/items/{cartItemId}", cartItemId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk());
+ }
+}
diff --git a/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java
new file mode 100644
index 0000000..1e23d89
--- /dev/null
+++ b/src/test/java/mate/academy/springbootwebgreqit/service/ShoppingCartServiceTest.java
@@ -0,0 +1,180 @@
+package mate.academy.springbootwebgreqit.service;
+
+import mate.academy.springbootwebgreqit.dto.cartitem.CartItemRequestDto;
+import mate.academy.springbootwebgreqit.dto.shoppingcart.UpdateCartItemDto;
+import mate.academy.springbootwebgreqit.dto.shoppingcart.ShoppingCartDto;
+import mate.academy.springbootwebgreqit.exception.EntityNotFoundException;
+import mate.academy.springbootwebgreqit.mapper.CartItemMapper;
+import mate.academy.springbootwebgreqit.mapper.ShoppingCartMapper;
+import mate.academy.springbootwebgreqit.model.Book;
+import mate.academy.springbootwebgreqit.model.CartItem;
+import mate.academy.springbootwebgreqit.model.Category;
+import mate.academy.springbootwebgreqit.model.ShoppingCart;
+import mate.academy.springbootwebgreqit.model.User;
+import mate.academy.springbootwebgreqit.repository.BookRepository;
+import mate.academy.springbootwebgreqit.repository.CartItemRepository;
+import mate.academy.springbootwebgreqit.repository.ShoppingCartRepository;
+import mate.academy.springbootwebgreqit.repository.UserRepository;
+import mate.academy.springbootwebgreqit.service.impl.ShoppingCartServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+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.security.core.Authentication;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class ShoppingCartServiceTest {
+
+ @Mock
+ private CartItemRepository cartItemRepository;
+ @Mock
+ private ShoppingCartRepository shoppingCartRepository;
+ @Mock
+ private ShoppingCartMapper shoppingCartMapper;
+ @Mock
+ private CartItemMapper cartItemMapper;
+ @Mock
+ private BookRepository bookRepository;
+ @Mock
+ private UserRepository userRepository;
+
+ @InjectMocks
+ private ShoppingCartServiceImpl shoppingCartService;
+
+ private User user;
+ private ShoppingCart shoppingCart;
+ private ShoppingCartDto shoppingCartDto;
+ private CartItemRequestDto cartItemRequestDto;
+ private Book book;
+ private CartItem cartItem;
+ private Authentication authentication;
+ private UpdateCartItemDto requestUpdateQuantityDto;
+ private Category category;
+
+ @BeforeEach
+ void setUp() {
+ user = new User();
+ user.setId(1L);
+ user.setEmail("test@example.com");
+
+ shoppingCart = new ShoppingCart();
+ shoppingCart.setId(1L);
+ shoppingCart.setUser(user);
+
+ shoppingCartDto = new ShoppingCartDto();
+
+
+ cartItemRequestDto = new CartItemRequestDto();
+ cartItemRequestDto.setBookId(1L);
+ cartItemRequestDto.setQuantity(2);
+ cartItemRequestDto.setShoppingCartId(shoppingCart.getId());
+
+ book = new Book();
+ book.setId(1L);
+ book.setTitle("wallet");
+ book.setAuthor("John");
+ book.setIsbn("9283234577892");
+ book.setPrice(BigDecimal.valueOf(60.99));
+ book.setDescription("Updated description");
+ book.setCoverImage("https://example.com/updated-cover-.jpg");
+ book.setCategories(Collections.singleton(category));
+
+ category = new Category();
+ category.setId(1L);
+ category.setName("Roman");
+
+ cartItem = new CartItem();
+ cartItem.setId(1L);
+ cartItem.setBook(book);
+ cartItem.setQuantity(2);
+ cartItem.setShoppingCart(shoppingCart);
+ shoppingCart.getCartItems().add(cartItem);
+
+ authentication = mock(Authentication.class);
+
+ requestUpdateQuantityDto = new UpdateCartItemDto();
+ requestUpdateQuantityDto.setQuantity(3);
+ }
+
+ @Test
+ void getShoppingCartForCurrentUser_ShouldReturnShoppingCartDto() {
+ when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user));
+ when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.of(shoppingCart));
+ when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto);
+
+ ShoppingCartDto result = shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId());
+
+ assertNotNull(result);
+ verify(shoppingCartRepository).findById(shoppingCart.getId());
+ verify(shoppingCartMapper).toDto(shoppingCart);
+ }
+
+ @Test
+ void getShoppingCartForCurrentUser_ShouldThrowEntityNotFoundException_WhenShoppingCartNotFound() {
+ when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user));
+ when(shoppingCartRepository.findById(shoppingCart.getId())).thenReturn(Optional.empty());
+
+ assertThrows(EntityNotFoundException.class,
+ () -> shoppingCartService.getShoppingCartForCurrentUser(authentication, shoppingCart.getId()));
+ }
+
+ @Test
+ void updateCartItemQuantity_ShouldUpdateQuantityAndReturnShoppingCartDto() {
+ int newQuantity = requestUpdateQuantityDto.getQuantity();
+ when(cartItemRepository.findById(cartItem.getId())).thenReturn(Optional.of(cartItem));
+ when(cartItemRepository.save(cartItem)).thenReturn(cartItem);
+ when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto);
+ when(shoppingCartRepository.findByCartItems(Set.of(cartItem))).thenReturn(Optional.of(shoppingCart));
+
+ ShoppingCartDto result = shoppingCartService.updateCartItemQuantity(cartItem.getId(), requestUpdateQuantityDto);
+
+ assertNotNull(result);
+ assertEquals(newQuantity, cartItem.getQuantity(), "Quantity should be updated in the cart item");
+ verify(cartItemRepository).save(cartItem);
+ verify(shoppingCartRepository).save(shoppingCart);
+ verify(shoppingCartMapper).toDto(shoppingCart);
+ verifyNoMoreInteractions(cartItemRepository, shoppingCartRepository, shoppingCartMapper);
+ }
+
+
+ @Test
+ void removeCartItem_ShouldRemoveItemAndReturnShoppingCartDto() {
+ shoppingCart.setCartItems(new HashSet<>(Set.of(cartItem)));
+
+ when(userRepository.findByEmail(authentication.getName())).thenReturn(Optional.of(user));
+ when(shoppingCartRepository.findByUserId(user.getId())).thenReturn(Optional.of(shoppingCart));
+ when(shoppingCartRepository.save(shoppingCart)).thenReturn(shoppingCart);
+ when(shoppingCartMapper.toDto(shoppingCart)).thenReturn(shoppingCartDto);
+
+ ShoppingCartDto result = shoppingCartService.removeCartItem(cartItem.getId(), authentication);
+
+ assertNotNull(result);
+ assertFalse(shoppingCart.getCartItems().contains(cartItem), "Cart item should be removed from the cart");
+ verify(shoppingCartRepository).findByUserId(user.getId());
+ verify(shoppingCartRepository).save(shoppingCart);
+ verify(shoppingCartMapper).toDto(shoppingCart);
+ }
+
+
+ @Test
+ void createShoppingCart_ShouldCreateAndReturnShoppingCart() {
+ when(shoppingCartRepository.save(any(ShoppingCart.class))).thenReturn(shoppingCart);
+
+ ShoppingCart result = shoppingCartService.createShoppingCart(user);
+
+ assertNotNull(result);
+ assertEquals(user, result.getUser());
+ verify(shoppingCartRepository).save(result);
+ }
+}
diff --git a/src/test/resources/data-sql/clear-tables-for-sh.sql b/src/test/resources/data-sql/clear-tables-for-sh.sql
new file mode 100644
index 0000000..63582c0
--- /dev/null
+++ b/src/test/resources/data-sql/clear-tables-for-sh.sql
@@ -0,0 +1,11 @@
+-- Delete from cart_items
+DELETE FROM cart_items;
+
+-- Delete from shopping_carts
+DELETE FROM shopping_carts;
+
+-- Delete from books
+DELETE FROM books;
+
+-- Delete from users;
+DELETE FROM users;
diff --git a/src/test/resources/data-sql/clear-tables.sql b/src/test/resources/data-sql/clear-tables.sql
index 2b2a583..106cba8 100644
--- a/src/test/resources/data-sql/clear-tables.sql
+++ b/src/test/resources/data-sql/clear-tables.sql
@@ -1,3 +1,5 @@
DELETE FROM books_categories;
DELETE FROM books;
-DELETE FROM categories;
\ No newline at end of file
+DELETE FROM categories;
+DELETE FROM shopping_carts;
+DELETE FROM users;
diff --git a/src/test/resources/data-sql/create-book.sql b/src/test/resources/data-sql/create-book.sql
index 811b040..e5cd4b1 100644
--- a/src/test/resources/data-sql/create-book.sql
+++ b/src/test/resources/data-sql/create-book.sql
@@ -2,4 +2,4 @@ INSERT INTO books (id, title, author, isbn, price, description, cover_image)
VALUES (1, 'wallet', 'John', '9283234577892', 60.99, 'Updated description', 'https://example.com/updated-cover-.jpg');
INSERT INTO books_categories (book_id, category_id)
-VALUES (1, 1);
\ No newline at end of file
+VALUES (1, 1);
diff --git a/src/test/resources/data-sql/create-books.sql b/src/test/resources/data-sql/create-books.sql
new file mode 100644
index 0000000..67b9368
--- /dev/null
+++ b/src/test/resources/data-sql/create-books.sql
@@ -0,0 +1,2 @@
+INSERT INTO books (id, title, author, isbn, price, description, cover_image)
+VALUES (1, 'Sample Book', 'John Author', '1234567890', 10.99, 'A sample book description.', 'https://example.com/book-cover.jpg');
diff --git a/src/test/resources/data-sql/create-cart-item.sql b/src/test/resources/data-sql/create-cart-item.sql
new file mode 100644
index 0000000..cba9555
--- /dev/null
+++ b/src/test/resources/data-sql/create-cart-item.sql
@@ -0,0 +1 @@
+INSERT INTO cart_items (id, shopping_cart_id, book_id, quantity) VALUES (1, 1, 1, 2);
diff --git a/src/test/resources/data-sql/create-category.sql b/src/test/resources/data-sql/create-category.sql
index afd0770..b3d6282 100644
--- a/src/test/resources/data-sql/create-category.sql
+++ b/src/test/resources/data-sql/create-category.sql
@@ -1 +1 @@
-INSERT INTO categories (id, name, description) VALUES (1, 'Roman', 'Some description');
\ No newline at end of file
+INSERT INTO categories (id, name, description) VALUES (1, 'Roman', 'Some description');
diff --git a/src/test/resources/data-sql/create-users.sql b/src/test/resources/data-sql/create-users.sql
new file mode 100644
index 0000000..a34cfee
--- /dev/null
+++ b/src/test/resources/data-sql/create-users.sql
@@ -0,0 +1,6 @@
+-- Insert user
+INSERT INTO users (id, first_name, last_name, email, password, shipping_address)
+VALUES (1, 'John', 'Doe', 'john.doe@example.com', 'password123', '123 Main St, Springfield');
+
+-- Insert shopping cart
+INSERT INTO shopping_carts (id, user_id) VALUES (1, 1);