diff --git a/.gitignore b/.gitignore
index 5eac309..159e300 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,4 +30,7 @@ build/
!**/src/test/**/build/
### VS Code ###
-.vscode/
\ No newline at end of file
+.vscode/
+
+### Docker ###
+/.env
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..9a70779
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+# Builder stage
+FROM openjdk:21-jdk-slim as builder
+WORKDIR application
+ARG JAR_FILE=target/*.jar
+COPY ${JAR_FILE} application.jar
+RUN java -Djarmode=layertools -jar application.jar extract
+
+# Final stage
+FROM openjdk:21-jdk-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/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..03c7308
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,34 @@
+version: "3.8"
+
+services:
+ mysqldb:
+ image: mysql:8
+ restart: always
+ environment:
+ MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
+ MYSQL_DATABASE: ${MYSQL_DATABASE}
+ ports:
+ - "${MYSQL_LOCAL_PORT}:${MYSQL_DOCKER_PORT}"
+ healthcheck:
+ test: ["CMD-SHELL", "mysqladmin ping -h localhost -P ${MYSQL_DOCKER_PORT} -u ${MYSQL_USER} -p${MYSQL_ROOT_PASSWORD}"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
+ app:
+ depends_on:
+ mysqldb:
+ condition: service_healthy
+ image: book-service
+ build: .
+ ports:
+ - "${SPRING_LOCAL_PORT}:${SPRING_DOCKER_PORT}"
+ - "${DEBUG_PORT}:${DEBUG_PORT}"
+ environment:
+ SPRING_APPLICATION_JSON: '{
+ "spring.datasource.url": "jdbc:mysql://mysqldb:${MYSQL_DOCKER_PORT}/${MYSQL_DATABASE}?serverTimezone=UTC",
+ "spring.datasource.username": "${MYSQL_USER}",
+ "spring.datasource.password": "${MYSQL_ROOT_PASSWORD}",
+ "jwt.secret": "${JWT_SECRET}"
+ }'
+ JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
diff --git a/pom.xml b/pom.xml
index a4ef937..7cc1a71 100644
--- a/pom.xml
+++ b/pom.xml
@@ -109,8 +109,11 @@
jjwt-jackson
0.12.5
+
+ org.springframework.boot
+ spring-boot-docker-compose
+
-
diff --git a/src/main/java/mate/academy/bookstore/controller/OrderController.java b/src/main/java/mate/academy/bookstore/controller/OrderController.java
new file mode 100644
index 0000000..cad68d5
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/controller/OrderController.java
@@ -0,0 +1,81 @@
+package mate.academy.bookstore.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import mate.academy.bookstore.dto.order.OrderItemResponseDto;
+import mate.academy.bookstore.dto.order.OrderRequestDto;
+import mate.academy.bookstore.dto.order.OrderResponseDto;
+import mate.academy.bookstore.dto.order.OrderStatusDto;
+import mate.academy.bookstore.model.User;
+import mate.academy.bookstore.service.OrderService;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
+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.RestController;
+
+@Tag(name = "Order Management", description = "Endpoints for managing the order")
+@RestController
+@RequestMapping("/orders")
+@RequiredArgsConstructor
+public class OrderController {
+ private final OrderService orderService;
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
+ @Operation(summary = "Create an order",
+ description = "Creates a new order with the items in the current user cart")
+ public void createOrder(
+ @Valid @RequestBody OrderRequestDto orderDto,
+ @AuthenticationPrincipal User user) {
+ orderService.createOrder(orderDto, user);
+ }
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
+ @Operation(summary = "Get orders history",
+ description = "Retrieves the order history for the current user")
+ public List getOrdersHistory(
+ @AuthenticationPrincipal User currentUser) {
+ return orderService.getAllOrders(currentUser);
+ }
+
+ @PatchMapping("/{orderId}")
+ @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
+ @Operation(summary = "Update an order status",
+ description = "Updates the status of an existing order identified by its id")
+ public void updateOrderStatus(
+ @Valid @RequestBody OrderStatusDto statusDto,
+ @PathVariable Long orderId,
+ @AuthenticationPrincipal User user) {
+ orderService.updateStatus(statusDto, orderId, user.getId());
+ }
+
+ @GetMapping("/{orderId}/items")
+ @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
+ @Operation(summary = "Get all order items from order",
+ description = "Retrieves all items associated for authenticated user")
+ public List getAllOrderItems(
+ @PathVariable Long orderId,
+ @AuthenticationPrincipal User user) {
+ return orderService.getAllOrderItems(orderId, user);
+ }
+
+ @GetMapping("/{orderId}/items/{itemId}")
+ @PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
+ @Operation(summary = "Get item from order",
+ description = "Retrieves a specific item from an order identified by its id")
+ public OrderItemResponseDto getOrderItem(
+ @PathVariable Long orderId,
+ @PathVariable Long itemId,
+ @AuthenticationPrincipal User user) {
+ return orderService.getOrderItem(orderId, itemId, user);
+ }
+}
diff --git a/src/main/java/mate/academy/bookstore/dto/book/CreateBookRequestDto.java b/src/main/java/mate/academy/bookstore/dto/book/CreateBookRequestDto.java
index d123fe1..0e48311 100644
--- a/src/main/java/mate/academy/bookstore/dto/book/CreateBookRequestDto.java
+++ b/src/main/java/mate/academy/bookstore/dto/book/CreateBookRequestDto.java
@@ -2,8 +2,10 @@
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
+import java.util.Set;
import lombok.Data;
import org.hibernate.validator.constraints.ISBN;
@@ -16,6 +18,8 @@ public class CreateBookRequestDto {
@NotNull
@ISBN(type = ISBN.Type.ANY)
private String isbn;
+ @NotEmpty
+ private Set categoryIds;
@NotNull
@Min(0)
private BigDecimal price;
diff --git a/src/main/java/mate/academy/bookstore/dto/category/CategoryDto.java b/src/main/java/mate/academy/bookstore/dto/category/CategoryDto.java
index 55a44d5..bd353d4 100644
--- a/src/main/java/mate/academy/bookstore/dto/category/CategoryDto.java
+++ b/src/main/java/mate/academy/bookstore/dto/category/CategoryDto.java
@@ -2,12 +2,13 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
+import lombok.Data;
-public record CategoryDto(
- Long id,
- @NotBlank
- @Size(min = 4, max = 24, message = "Category name must be 4 to 24 characters long")
- String name,
- String description
-) {
+@Data
+public class CategoryDto {
+ private Long id;
+ @NotBlank
+ @Size(min = 4, max = 24, message = "length should be 4 to 24 characters long")
+ private String name;
+ private String description;
}
diff --git a/src/main/java/mate/academy/bookstore/dto/order/OrderItemResponseDto.java b/src/main/java/mate/academy/bookstore/dto/order/OrderItemResponseDto.java
new file mode 100644
index 0000000..be84356
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/dto/order/OrderItemResponseDto.java
@@ -0,0 +1,10 @@
+package mate.academy.bookstore.dto.order;
+
+import lombok.Data;
+
+@Data
+public class OrderItemResponseDto {
+ private Long id;
+ private Long bookId;
+ private int quantity;
+}
diff --git a/src/main/java/mate/academy/bookstore/dto/order/OrderRequestDto.java b/src/main/java/mate/academy/bookstore/dto/order/OrderRequestDto.java
new file mode 100644
index 0000000..9e97c69
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/dto/order/OrderRequestDto.java
@@ -0,0 +1,10 @@
+package mate.academy.bookstore.dto.order;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+@Data
+public class OrderRequestDto {
+ @NotBlank(message = "Shipping address can not be empty")
+ private String shippingAddress;
+}
diff --git a/src/main/java/mate/academy/bookstore/dto/order/OrderResponseDto.java b/src/main/java/mate/academy/bookstore/dto/order/OrderResponseDto.java
new file mode 100644
index 0000000..5986a9d
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/dto/order/OrderResponseDto.java
@@ -0,0 +1,16 @@
+package mate.academy.bookstore.dto.order;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Set;
+import lombok.Data;
+
+@Data
+public class OrderResponseDto {
+ private Long id;
+ private Long userId;
+ private Set orderItems;
+ private LocalDateTime orderDate;
+ private BigDecimal total;
+ private String status;
+}
diff --git a/src/main/java/mate/academy/bookstore/dto/order/OrderStatusDto.java b/src/main/java/mate/academy/bookstore/dto/order/OrderStatusDto.java
new file mode 100644
index 0000000..755c702
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/dto/order/OrderStatusDto.java
@@ -0,0 +1,11 @@
+package mate.academy.bookstore.dto.order;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import mate.academy.bookstore.model.order.Status;
+
+@Data
+public class OrderStatusDto {
+ @NotNull
+ private Status status;
+}
diff --git a/src/main/java/mate/academy/bookstore/mapper/BookMapper.java b/src/main/java/mate/academy/bookstore/mapper/BookMapper.java
index 6ff1a92..976f1d4 100644
--- a/src/main/java/mate/academy/bookstore/mapper/BookMapper.java
+++ b/src/main/java/mate/academy/bookstore/mapper/BookMapper.java
@@ -1,6 +1,7 @@
package mate.academy.bookstore.mapper;
import java.util.Collections;
+import java.util.Set;
import java.util.stream.Collectors;
import mate.academy.bookstore.config.MapperConfig;
import mate.academy.bookstore.dto.book.BookDto;
@@ -20,6 +21,7 @@ public interface BookMapper {
@Mapping(target = "id", ignore = true)
@Mapping(target = "deleted", ignore = true)
+ @Mapping(target = "categories", ignore = true)
Book toEntity(CreateBookRequestDto requestDto);
BookDtoWithoutCategoryIds toDtoWithoutCategories(Book book);
@@ -28,9 +30,26 @@ public interface BookMapper {
default void setCategoryIds(@MappingTarget BookDto bookDto, Book book) {
if (book.getCategories() == null) {
bookDto.setCategories(Collections.emptySet());
+ } else {
+ bookDto.setCategories(book.getCategories().stream()
+ .map(Category::getId)
+ .collect(Collectors.toSet()));
+ }
+ }
+
+ @AfterMapping
+ default void setCategories(@MappingTarget Book book, CreateBookRequestDto requestDto) {
+ if (requestDto.getCategoryIds() == null) {
+ book.setCategories(Collections.emptySet());
+ } else {
+ Set categories = requestDto.getCategoryIds().stream()
+ .map(id -> {
+ Category category = new Category();
+ category.setId(id);
+ return category;
+ })
+ .collect(Collectors.toSet());
+ book.setCategories(categories);
}
- bookDto.setCategories(book.getCategories().stream()
- .map(Category::getId)
- .collect(Collectors.toSet()));
}
}
diff --git a/src/main/java/mate/academy/bookstore/mapper/OrderItemMapper.java b/src/main/java/mate/academy/bookstore/mapper/OrderItemMapper.java
new file mode 100644
index 0000000..9eb5f19
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/mapper/OrderItemMapper.java
@@ -0,0 +1,13 @@
+package mate.academy.bookstore.mapper;
+
+import mate.academy.bookstore.config.MapperConfig;
+import mate.academy.bookstore.dto.order.OrderItemResponseDto;
+import mate.academy.bookstore.model.order.OrderItem;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(config = MapperConfig.class)
+public interface OrderItemMapper {
+ @Mapping(target = "bookId", source = "book.id")
+ OrderItemResponseDto toDto(OrderItem orderItem);
+}
diff --git a/src/main/java/mate/academy/bookstore/mapper/OrderMapper.java b/src/main/java/mate/academy/bookstore/mapper/OrderMapper.java
new file mode 100644
index 0000000..d5bf279
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/mapper/OrderMapper.java
@@ -0,0 +1,13 @@
+package mate.academy.bookstore.mapper;
+
+import mate.academy.bookstore.config.MapperConfig;
+import mate.academy.bookstore.dto.order.OrderResponseDto;
+import mate.academy.bookstore.model.order.Order;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(config = MapperConfig.class, uses = OrderItemMapper.class)
+public interface OrderMapper {
+ @Mapping(source = "user.id", target = "userId")
+ OrderResponseDto toDto(Order order);
+}
diff --git a/src/main/java/mate/academy/bookstore/model/Book.java b/src/main/java/mate/academy/bookstore/model/Book.java
index 346ee69..a16299c 100644
--- a/src/main/java/mate/academy/bookstore/model/Book.java
+++ b/src/main/java/mate/academy/bookstore/model/Book.java
@@ -16,12 +16,14 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
+import lombok.ToString;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
@Getter
@Setter
-@EqualsAndHashCode(of = {"id", "title", "author", "isbn", "categories"})
+@EqualsAndHashCode(exclude = {"categories"})
+@ToString(exclude = {"categories"})
@Entity
@SQLDelete(sql = "UPDATE books SET is_deleted = true WHERE id=?")
@SQLRestriction(value = "is_deleted=false")
diff --git a/src/main/java/mate/academy/bookstore/model/CartItem.java b/src/main/java/mate/academy/bookstore/model/CartItem.java
index 92a658d..dc568be 100644
--- a/src/main/java/mate/academy/bookstore/model/CartItem.java
+++ b/src/main/java/mate/academy/bookstore/model/CartItem.java
@@ -12,10 +12,12 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
+import lombok.ToString;
@Getter
@Setter
-@EqualsAndHashCode(of = {"id", "shoppingCart", "book"})
+@EqualsAndHashCode(exclude = {"shoppingCart", "book"})
+@ToString(exclude = {"shoppingCart", "book"})
@Entity
@Table(name = "cart_items")
public class CartItem {
diff --git a/src/main/java/mate/academy/bookstore/model/Category.java b/src/main/java/mate/academy/bookstore/model/Category.java
index c15dd4c..17a3d56 100644
--- a/src/main/java/mate/academy/bookstore/model/Category.java
+++ b/src/main/java/mate/academy/bookstore/model/Category.java
@@ -6,12 +6,16 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
+import lombok.ToString;
import org.hibernate.annotations.SoftDelete;
@Getter
@Setter
+@EqualsAndHashCode
+@ToString
@Entity
@SoftDelete
@Table(name = "categories")
diff --git a/src/main/java/mate/academy/bookstore/model/Role.java b/src/main/java/mate/academy/bookstore/model/Role.java
index 38271de..d9533ae 100644
--- a/src/main/java/mate/academy/bookstore/model/Role.java
+++ b/src/main/java/mate/academy/bookstore/model/Role.java
@@ -8,12 +8,16 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
+import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
@Getter
@Setter
+@EqualsAndHashCode
+@ToString
@Entity
@Table(name = "roles")
public class Role implements GrantedAuthority {
diff --git a/src/main/java/mate/academy/bookstore/model/ShoppingCart.java b/src/main/java/mate/academy/bookstore/model/ShoppingCart.java
index 8895ac7..f22d523 100644
--- a/src/main/java/mate/academy/bookstore/model/ShoppingCart.java
+++ b/src/main/java/mate/academy/bookstore/model/ShoppingCart.java
@@ -12,15 +12,17 @@
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
-import org.hibernate.annotations.SoftDelete;
+import lombok.ToString;
@Getter
@Setter
@Entity
-@SoftDelete
+@EqualsAndHashCode(exclude = {"user", "cartItems"})
+@ToString(exclude = {"user", "cartItems"})
@NoArgsConstructor
@Table(name = "shopping_carts")
public class ShoppingCart {
@@ -32,7 +34,7 @@ public class ShoppingCart {
@JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false)
private User user;
- @OneToMany(mappedBy = "shoppingCart", cascade = CascadeType.REMOVE)
+ @OneToMany(mappedBy = "shoppingCart", cascade = CascadeType.ALL)
private Set cartItems = new HashSet<>();
public ShoppingCart(User user) {
diff --git a/src/main/java/mate/academy/bookstore/model/User.java b/src/main/java/mate/academy/bookstore/model/User.java
index f4b9cb3..72c468b 100644
--- a/src/main/java/mate/academy/bookstore/model/User.java
+++ b/src/main/java/mate/academy/bookstore/model/User.java
@@ -15,6 +15,7 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
+import lombok.ToString;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLRestriction;
import org.springframework.security.core.GrantedAuthority;
@@ -22,7 +23,8 @@
@Getter
@Setter
-@EqualsAndHashCode(of = {"id", "email", "firstName", "lastName", "shippingAddress"})
+@EqualsAndHashCode(exclude = {"roles"})
+@ToString(exclude = {"roles"})
@Entity
@SQLDelete(sql = "UPDATE users SET is_deleted=true WHERE id=?")
@SQLRestriction(value = "is_deleted=false")
diff --git a/src/main/java/mate/academy/bookstore/model/order/Order.java b/src/main/java/mate/academy/bookstore/model/order/Order.java
new file mode 100644
index 0000000..c6c813e
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/model/order/Order.java
@@ -0,0 +1,59 @@
+package mate.academy.bookstore.model.order;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.HashSet;
+import java.util.Set;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import mate.academy.bookstore.model.User;
+import org.hibernate.annotations.CreationTimestamp;
+import org.hibernate.annotations.SoftDelete;
+
+@Getter
+@Setter
+@EqualsAndHashCode(exclude = {"user", "orderItems"})
+@ToString(exclude = {"user", "orderItems"})
+@SoftDelete
+@Entity
+@Table(name = "orders")
+public class Order {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "user_id", nullable = false)
+ private User user;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Status status;
+
+ @Column(nullable = false)
+ private BigDecimal total;
+
+ @CreationTimestamp
+ private LocalDateTime orderDate;
+
+ @Column(nullable = false)
+ private String shippingAddress;
+
+ @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
+ private Set orderItems = new HashSet<>();
+}
diff --git a/src/main/java/mate/academy/bookstore/model/order/OrderItem.java b/src/main/java/mate/academy/bookstore/model/order/OrderItem.java
new file mode 100644
index 0000000..fc63dfe
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/model/order/OrderItem.java
@@ -0,0 +1,45 @@
+package mate.academy.bookstore.model.order;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.math.BigDecimal;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import mate.academy.bookstore.model.Book;
+import org.hibernate.annotations.SoftDelete;
+
+@Getter
+@Setter
+@ToString(exclude = {"order", "book"})
+@EqualsAndHashCode(exclude = {"order", "book"})
+@SoftDelete
+@Entity
+@Table(name = "order_items")
+public class OrderItem {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "order_id", nullable = false)
+ private Order order;
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "book_id", nullable = false)
+ private Book book;
+
+ @Column(nullable = false)
+ private Integer quantity;
+
+ @Column(nullable = false)
+ private BigDecimal price;
+}
diff --git a/src/main/java/mate/academy/bookstore/model/order/Status.java b/src/main/java/mate/academy/bookstore/model/order/Status.java
new file mode 100644
index 0000000..e8ab549
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/model/order/Status.java
@@ -0,0 +1,10 @@
+package mate.academy.bookstore.model.order;
+
+public enum Status {
+ PENDING,
+ PACKAGING,
+ SHIPPING,
+ DELIVERED,
+ COMPLETED,
+ CANCELED
+}
diff --git a/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java b/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java
index 8f35a9b..ea2900e 100644
--- a/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java
+++ b/src/main/java/mate/academy/bookstore/repository/book/BookRepository.java
@@ -1,10 +1,9 @@
package mate.academy.bookstore.repository.book;
import java.util.List;
+import java.util.Optional;
import mate.academy.bookstore.model.Book;
-import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
-import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -17,10 +16,7 @@ public interface BookRepository extends JpaRepository, JpaSpecificat
List findAllBooks(Pageable pageable);
@EntityGraph(attributePaths = {"categories"})
- Page findAll(Specification specification, Pageable pageable);
-
- @EntityGraph(attributePaths = {"categories"})
- Book findBookByIsbn(String isbn);
+ Optional findBookByIsbn(String isbn);
@Query("SELECT b FROM Book b LEFT JOIN FETCH b.categories c WHERE c.id = :categoryId")
List findAllBooksByCategoryId(@Param("categoryId") Long categoryId, Pageable pageable);
diff --git a/src/main/java/mate/academy/bookstore/repository/cart/CartItemRepository.java b/src/main/java/mate/academy/bookstore/repository/cart/CartItemRepository.java
index 31e4643..d72841b 100644
--- a/src/main/java/mate/academy/bookstore/repository/cart/CartItemRepository.java
+++ b/src/main/java/mate/academy/bookstore/repository/cart/CartItemRepository.java
@@ -9,4 +9,6 @@
public interface CartItemRepository extends JpaRepository {
Optional findByShoppingCartAndBook(ShoppingCart shoppingCart, Book book);
+
+ void deleteByIdAndShoppingCart(Long itemId, ShoppingCart cart);
}
diff --git a/src/main/java/mate/academy/bookstore/repository/cart/ShoppingCartRepository.java b/src/main/java/mate/academy/bookstore/repository/cart/ShoppingCartRepository.java
index 8983711..f94aad9 100644
--- a/src/main/java/mate/academy/bookstore/repository/cart/ShoppingCartRepository.java
+++ b/src/main/java/mate/academy/bookstore/repository/cart/ShoppingCartRepository.java
@@ -2,12 +2,9 @@
import java.util.Optional;
import mate.academy.bookstore.model.ShoppingCart;
-import mate.academy.bookstore.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
public interface ShoppingCartRepository extends JpaRepository {
- @Query("FROM ShoppingCart sc JOIN FETCH sc.cartItems WHERE sc.user = :user")
- Optional findByUser(User user);
+ Optional findByUserId(Long userId);
}
diff --git a/src/main/java/mate/academy/bookstore/repository/order/OrderItemRepository.java b/src/main/java/mate/academy/bookstore/repository/order/OrderItemRepository.java
new file mode 100644
index 0000000..2f097e5
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/repository/order/OrderItemRepository.java
@@ -0,0 +1,20 @@
+package mate.academy.bookstore.repository.order;
+
+import java.util.List;
+import java.util.Optional;
+import mate.academy.bookstore.model.User;
+import mate.academy.bookstore.model.order.OrderItem;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface OrderItemRepository extends JpaRepository {
+
+ @Query("SELECT oi FROM OrderItem oi JOIN oi.order o WHERE o.id = :orderId AND o.user = :user")
+ List findByOrderIdAndUser(@Param("orderId") Long orderId, @Param("user") User user);
+
+ @Query("SELECT oi FROM OrderItem oi LEFT JOIN oi.order o WHERE o.id = :orderId "
+ + "AND oi.id = :itemId AND o.user = :user")
+ Optional findByOrderIdAndItemIdAndUser(
+ @Param("orderId") Long orderId, @Param("itemId") Long itemId, @Param("user") User user);
+}
diff --git a/src/main/java/mate/academy/bookstore/repository/order/OrderRepository.java b/src/main/java/mate/academy/bookstore/repository/order/OrderRepository.java
new file mode 100644
index 0000000..d7b04d3
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/repository/order/OrderRepository.java
@@ -0,0 +1,17 @@
+package mate.academy.bookstore.repository.order;
+
+import java.util.List;
+import java.util.Optional;
+import mate.academy.bookstore.model.order.Order;
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface OrderRepository extends JpaRepository {
+
+ @EntityGraph(attributePaths = {"orderItems"})
+ List findByUserId(Long userId);
+
+ @Override
+ @EntityGraph(attributePaths = {"user"})
+ Optional findById(Long orderId);
+}
diff --git a/src/main/java/mate/academy/bookstore/service/OrderService.java b/src/main/java/mate/academy/bookstore/service/OrderService.java
new file mode 100644
index 0000000..a25a75b
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/service/OrderService.java
@@ -0,0 +1,20 @@
+package mate.academy.bookstore.service;
+
+import java.util.List;
+import mate.academy.bookstore.dto.order.OrderItemResponseDto;
+import mate.academy.bookstore.dto.order.OrderRequestDto;
+import mate.academy.bookstore.dto.order.OrderResponseDto;
+import mate.academy.bookstore.dto.order.OrderStatusDto;
+import mate.academy.bookstore.model.User;
+
+public interface OrderService {
+ void createOrder(OrderRequestDto order, User user);
+
+ List getAllOrders(User user);
+
+ void updateStatus(OrderStatusDto statusDto, Long orderId, Long userId);
+
+ List getAllOrderItems(Long orderId, User user);
+
+ OrderItemResponseDto getOrderItem(Long orderId, Long itemId, User user);
+}
diff --git a/src/main/java/mate/academy/bookstore/service/ShoppingCartService.java b/src/main/java/mate/academy/bookstore/service/ShoppingCartService.java
index bce1cd1..7390d36 100644
--- a/src/main/java/mate/academy/bookstore/service/ShoppingCartService.java
+++ b/src/main/java/mate/academy/bookstore/service/ShoppingCartService.java
@@ -18,4 +18,6 @@ public interface ShoppingCartService {
CartItemResponseDto updateQuantity(Long itemId, QuantityRequestDto quantity);
void deleteCartItem(Long cartItemId);
+
+ ShoppingCart getShoppingCart(User user);
}
diff --git a/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java
index 8c98c99..ba3972d 100644
--- a/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java
+++ b/src/main/java/mate/academy/bookstore/service/impl/BookServiceImpl.java
@@ -35,7 +35,7 @@ public BookServiceImpl(BookRepository bookRepository, BookMapper bookMapper,
@Override
public BookDto save(CreateBookRequestDto requestDto) {
String isbn = requestDto.getIsbn();
- if (bookRepository.findBookByIsbn(isbn) != null) {
+ if (bookRepository.findBookByIsbn(isbn).isPresent()) {
throw new DuplicateIsbnException("Book with ISBN " + isbn + " already exists");
}
@@ -63,7 +63,7 @@ public BookDto updateById(Long id, CreateBookRequestDto requestDto) {
String requestIsbn = requestDto.getIsbn();
if (!Objects.equals(existingBook.getIsbn(), requestIsbn)
- && bookRepository.findBookByIsbn(requestIsbn) != null) {
+ && bookRepository.findBookByIsbn(requestIsbn).isPresent()) {
throw new DuplicateIsbnException("Book with ISBN " + requestIsbn + " already exists");
}
diff --git a/src/main/java/mate/academy/bookstore/service/impl/CategoryServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/CategoryServiceImpl.java
index a1c4b10..5f7b186 100644
--- a/src/main/java/mate/academy/bookstore/service/impl/CategoryServiceImpl.java
+++ b/src/main/java/mate/academy/bookstore/service/impl/CategoryServiceImpl.java
@@ -34,7 +34,7 @@ public CategoryDto getById(Long id) {
@Override
public CategoryDto save(CategoryDto categoryDto) {
- String name = categoryDto.name();
+ String name = categoryDto.getName();
if (categoryRepository.findByName(name) != null) {
throw new DuplicateEntityException("Category with name " + name + " already exists");
}
@@ -48,7 +48,7 @@ public CategoryDto save(CategoryDto categoryDto) {
public CategoryDto update(Long id, CategoryDto categoryDto) {
Category existingCategory = findCategoryByIdOrElseThrow(id);
- String categoryName = categoryDto.name();
+ String categoryName = categoryDto.getName();
if (!Objects.equals(existingCategory.getName(), categoryName)
&& categoryRepository.findByName(categoryName) != null) {
throw new DuplicateEntityException(
diff --git a/src/main/java/mate/academy/bookstore/service/impl/OrderServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/OrderServiceImpl.java
new file mode 100644
index 0000000..0dd7606
--- /dev/null
+++ b/src/main/java/mate/academy/bookstore/service/impl/OrderServiceImpl.java
@@ -0,0 +1,128 @@
+package mate.academy.bookstore.service.impl;
+
+import jakarta.transaction.Transactional;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import mate.academy.bookstore.dto.order.OrderItemResponseDto;
+import mate.academy.bookstore.dto.order.OrderRequestDto;
+import mate.academy.bookstore.dto.order.OrderResponseDto;
+import mate.academy.bookstore.dto.order.OrderStatusDto;
+import mate.academy.bookstore.exception.EntityNotFoundException;
+import mate.academy.bookstore.mapper.OrderItemMapper;
+import mate.academy.bookstore.mapper.OrderMapper;
+import mate.academy.bookstore.model.CartItem;
+import mate.academy.bookstore.model.ShoppingCart;
+import mate.academy.bookstore.model.User;
+import mate.academy.bookstore.model.order.Order;
+import mate.academy.bookstore.model.order.OrderItem;
+import mate.academy.bookstore.model.order.Status;
+import mate.academy.bookstore.repository.cart.ShoppingCartRepository;
+import mate.academy.bookstore.repository.order.OrderItemRepository;
+import mate.academy.bookstore.repository.order.OrderRepository;
+import mate.academy.bookstore.service.OrderService;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class OrderServiceImpl implements OrderService {
+ private final ShoppingCartRepository cartRepository;
+ private final OrderRepository orderRepository;
+ private final OrderItemRepository itemRepository;
+ private final OrderItemMapper itemMapper;
+ private final OrderMapper orderMapper;
+
+ @Transactional
+ @Override
+ public void createOrder(OrderRequestDto orderDto, User user) {
+ ShoppingCart cart = cartRepository.findByUserId(user.getId()).orElseThrow(() ->
+ new EntityNotFoundException("The authorized user does not have a shopping cart"));
+
+ Set items = cart.getCartItems();
+ if (items.isEmpty()) {
+ throw new EntityNotFoundException("Unable to proceed: Cart is empty");
+ }
+
+ Order order = createNewOrder(orderDto, user, cart);
+ Set orderItems = createOrderItems(order, items);
+ order.setOrderItems(orderItems);
+
+ cartRepository.delete(cart);
+ orderRepository.save(order);
+ }
+
+ @Transactional
+ @Override
+ public List getAllOrders(User currentUser) {
+ return orderRepository.findByUserId(currentUser.getId()).stream()
+ .map(orderMapper::toDto)
+ .toList();
+ }
+
+ @Transactional
+ @Override
+ public void updateStatus(OrderStatusDto statusDto, Long orderId, Long userId) {
+ Order order = orderRepository.findById(orderId).orElseThrow(() ->
+ new EntityNotFoundException(
+ "Unable to proceed: Order not found with id: " + orderId));
+ order.setStatus(statusDto.getStatus());
+ orderRepository.save(order);
+ }
+
+ @Override
+ public List getAllOrderItems(Long orderId, User user) {
+ List items = itemRepository.findByOrderIdAndUser(orderId, user);
+
+ if (items.isEmpty()) {
+ throw new EntityNotFoundException(
+ "Unable to proceed: No order items found for user with id: " + user.getId());
+ }
+ return items.stream()
+ .map(itemMapper::toDto)
+ .toList();
+ }
+
+ @Override
+ public OrderItemResponseDto getOrderItem(Long orderId, Long itemId, User user) {
+ return itemRepository.findByOrderIdAndItemIdAndUser(orderId, itemId, user)
+ .map(itemMapper::toDto)
+ .orElseThrow(() -> new EntityNotFoundException(
+ "Unable to proceed: The requested item with id " + itemId
+ + " was not found in order with id " + orderId));
+ }
+
+ private Set createOrderItems(Order order, Set cartItems) {
+ return cartItems.stream()
+ .map(cartItem -> {
+ OrderItem item = new OrderItem();
+ item.setBook(cartItem.getBook());
+ item.setQuantity(cartItem.getQuantity());
+ item.setOrder(order);
+ item.setPrice(cartItem.getBook().getPrice());
+ return itemRepository.save(item);
+ })
+ .collect(Collectors.toSet());
+ }
+
+ private Order createNewOrder(OrderRequestDto orderDto, User user, ShoppingCart cart) {
+ Order order = new Order();
+ order.setUser(user);
+ order.setStatus(Status.PENDING);
+ order.setTotal(calculateTotalPrice(cart));
+ order.setOrderDate(LocalDateTime.now());
+ order.setShippingAddress(orderDto.getShippingAddress());
+
+ return orderRepository.save(order);
+ }
+
+ private BigDecimal calculateTotalPrice(ShoppingCart cart) {
+ return cart.getCartItems().stream()
+ .map(i -> i.getBook()
+ .getPrice()
+ .multiply(BigDecimal.valueOf(i.getQuantity())))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+}
diff --git a/src/main/java/mate/academy/bookstore/service/impl/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/bookstore/service/impl/ShoppingCartServiceImpl.java
index a379e76..29a6279 100644
--- a/src/main/java/mate/academy/bookstore/service/impl/ShoppingCartServiceImpl.java
+++ b/src/main/java/mate/academy/bookstore/service/impl/ShoppingCartServiceImpl.java
@@ -37,23 +37,24 @@ public ShoppingCart create(User user) {
@Override
@Transactional
public ShoppingCartDto getByUser(User user) {
- ShoppingCart cart = getCart(user);
+ ShoppingCart cart = getShoppingCart(user);
return cartMapper.toDto(cart);
}
@Override
@Transactional
public CartItemResponseDto addCartItem(User user, CartItemRequestDto requestItemDto) {
- ShoppingCart cart = getCart(user);
+ ShoppingCart cart = getShoppingCart(user);
Book book = getBook(requestItemDto.getBookId());
CartItem cartItem = itemRepository.findByShoppingCartAndBook(cart, book)
.orElseGet(() -> {
CartItem item = new CartItem();
item.setShoppingCart(cart);
+ item.setBook(book);
return item;
});
- cartItem.setBook(book);
+
cartItem.setQuantity(requestItemDto.getQuantity());
return itemMapper.toDto(itemRepository.save(cartItem));
}
@@ -71,14 +72,15 @@ public CartItemResponseDto updateQuantity(Long itemId, QuantityRequestDto quanti
@Transactional
public void deleteCartItem(Long itemId) {
CartItem cartItem = getCartItem(itemId);
+ ShoppingCart cart = cartItem.getShoppingCart();
- ShoppingCart shoppingCart = cartItem.getShoppingCart();
- shoppingCart.getCartItems().remove(cartItem);
- itemRepository.delete(cartItem);
+ itemRepository.deleteByIdAndShoppingCart(itemId, cart);
}
- private ShoppingCart getCart(User user) {
- return cartRepository.findByUser(user)
+ @Override
+ @Transactional
+ public ShoppingCart getShoppingCart(User user) {
+ return cartRepository.findByUserId(user.getId())
.orElseGet(() -> cartRepository.save(new ShoppingCart(user)));
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 95dd7a0..63fa4bc 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -10,5 +10,5 @@ spring.jpa.open-in-view=false
server.servlet.context-path=/api
-jwt.expiration=300000
+jwt.expiration=30000000
jwt.secret=${JWT_SECRET}
diff --git a/src/main/resources/db/changelog/changes/10-create-shopping-carts-table.yaml b/src/main/resources/db/changelog/changes/10-create-shopping-carts-table.yaml
index 7b032b0..71f6dc8 100644
--- a/src/main/resources/db/changelog/changes/10-create-shopping-carts-table.yaml
+++ b/src/main/resources/db/changelog/changes/10-create-shopping-carts-table.yaml
@@ -17,12 +17,6 @@ databaseChangeLog:
type: bigint
constraints:
nullable: false
- - column:
- name: deleted
- type: bit
- defaultValueBoolean: false
- constraints:
- nullable: false
- addForeignKeyConstraint:
baseTableName: shopping_carts
baseColumnNames: user_id
diff --git a/src/main/resources/db/changelog/changes/12-create-orders-table.yaml b/src/main/resources/db/changelog/changes/12-create-orders-table.yaml
new file mode 100644
index 0000000..8f71e33
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/12-create-orders-table.yaml
@@ -0,0 +1,51 @@
+databaseChangeLog:
+ - changeSet:
+ id: create-orders-table
+ author: nklimovych
+ changes:
+ - createTable:
+ tableName: orders
+ columns:
+ - column:
+ name: id
+ type: bigint
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ - column:
+ name: user_id
+ type: bigint
+ constraints:
+ nullable: false
+ - column:
+ name: status
+ type: ENUM('PENDING','PACKAGING', 'SHIPPING', 'DELIVERED', 'COMPLETED', 'CANCELED')
+ constraints:
+ nullable: false
+ - column:
+ name: total
+ type: decimal(10, 2)
+ constraints:
+ nullable: false
+ - column:
+ name: order_date
+ type: timestamp
+ constraints:
+ nullable: false
+ - column:
+ name: shipping_address
+ type: varchar(255)
+ constraints:
+ nullable: false
+ - column:
+ name: deleted
+ type: bit
+ defaultValueBoolean: false
+ constraints:
+ nullable: false
+ - addForeignKeyConstraint:
+ baseTableName: orders
+ baseColumnNames: user_id
+ constraintName: fk_orders_user
+ referencedTableName: users
+ referencedColumnNames: id
diff --git a/src/main/resources/db/changelog/changes/13-create-order-items-table.yaml b/src/main/resources/db/changelog/changes/13-create-order-items-table.yaml
new file mode 100644
index 0000000..0c0ca1d
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/13-create-order-items-table.yaml
@@ -0,0 +1,52 @@
+databaseChangeLog:
+ - changeSet:
+ id: create-order-items-table
+ author: nklimovych
+ changes:
+ - createTable:
+ tableName: order_items
+ columns:
+ - column:
+ name: id
+ type: bigint
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ - column:
+ name: order_id
+ type: bigint
+ constraints:
+ nullable: false
+ - column:
+ name: book_id
+ type: bigint
+ constraints:
+ nullable: false
+ - column:
+ name: quantity
+ type: int
+ constraints:
+ nullable: false
+ - column:
+ name: price
+ type: decimal(10,2)
+ constraints:
+ nullable: false
+ - column:
+ name: deleted
+ type: bit
+ defaultValueBoolean: false
+ constraints:
+ nullable: false
+ - addForeignKeyConstraint:
+ baseTableName: order_items
+ baseColumnNames: order_id
+ constraintName: fk_order_items_order
+ referencedTableName: orders
+ referencedColumnNames: id
+ - addForeignKeyConstraint:
+ baseTableName: order_items
+ baseColumnNames: book_id
+ constraintName: fk_order_items_book
+ referencedTableName: books
+ referencedColumnNames: id
diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml
index 4f15483..b4eb09c 100644
--- a/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -21,3 +21,7 @@ databaseChangeLog:
file: db/changelog/changes/10-create-shopping-carts-table.yaml
- include:
file: db/changelog/changes/11-create-cart-items-table.yaml
+ - include:
+ file: db/changelog/changes/12-create-orders-table.yaml
+ - include:
+ file: db/changelog/changes/13-create-order-items-table.yaml