From 6340daa9a5411feeb3abcb323e79e8160225776a Mon Sep 17 00:00:00 2001 From: Nazar <92024646+nklimovych@users.noreply.github.com> Date: Mon, 27 May 2024 11:34:07 +0300 Subject: [PATCH] Added Order entity with all related dto, service, mapper and controller (#14) * Add Order, OrderItem entities, and Status enum. * Added repository and dto layers, added mappers for related Order & OrderItem entities. * Added liquibase change files to create orders and order-items tables in DB. Fixed checkstyle issues. * Added service and controller layers. Code refactored. Fixed issue in ShoppingCartRepository with findByUser query. * Resolved issues: - Refactored order saving method in OrderServiceImpl. - Fixed issues with lazy initialization. - Added authorized user in the getOrderItem method in the controller so that any user cannot get another user's data. - Added equals & hashcode, and toString in OrderItem entity. - Added equals and hashcode to all entities. - Resolved issue when anyone can search for any order items. * Resolved issue with shopping cart: now when an order is placed, the shopping cart is removed. Removed soft delete in ShoppingCart * Fixed the issue where categories were not being added to saved books. * Added exclude constraint for EqualsAndHashcode and ToString annotations. * Added @CreationTimestamp for orderDate in Order entity. --- .gitignore | 5 +- .../bookstore/controller/OrderController.java | 81 +++++++++++ .../dto/book/CreateBookRequestDto.java | 4 + .../bookstore/dto/category/CategoryDto.java | 15 +- .../dto/order/OrderItemResponseDto.java | 10 ++ .../bookstore/dto/order/OrderRequestDto.java | 10 ++ .../bookstore/dto/order/OrderResponseDto.java | 16 +++ .../bookstore/dto/order/OrderStatusDto.java | 11 ++ .../academy/bookstore/mapper/BookMapper.java | 25 +++- .../bookstore/mapper/OrderItemMapper.java | 13 ++ .../academy/bookstore/mapper/OrderMapper.java | 13 ++ .../mate/academy/bookstore/model/Book.java | 4 +- .../academy/bookstore/model/CartItem.java | 4 +- .../academy/bookstore/model/Category.java | 4 + .../mate/academy/bookstore/model/Role.java | 4 + .../academy/bookstore/model/ShoppingCart.java | 8 +- .../mate/academy/bookstore/model/User.java | 4 +- .../academy/bookstore/model/order/Order.java | 59 ++++++++ .../bookstore/model/order/OrderItem.java | 45 ++++++ .../academy/bookstore/model/order/Status.java | 10 ++ .../repository/book/BookRepository.java | 8 +- .../repository/cart/CartItemRepository.java | 2 + .../cart/ShoppingCartRepository.java | 5 +- .../repository/order/OrderItemRepository.java | 20 +++ .../repository/order/OrderRepository.java | 17 +++ .../bookstore/service/OrderService.java | 20 +++ .../service/ShoppingCartService.java | 2 + .../service/impl/BookServiceImpl.java | 4 +- .../service/impl/CategoryServiceImpl.java | 4 +- .../service/impl/OrderServiceImpl.java | 128 ++++++++++++++++++ .../service/impl/ShoppingCartServiceImpl.java | 18 +-- .../10-create-shopping-carts-table.yaml | 6 - .../changes/12-create-orders-table.yaml | 51 +++++++ .../changes/13-create-order-items-table.yaml | 52 +++++++ .../db/changelog/db.changelog-master.yaml | 4 + 35 files changed, 641 insertions(+), 45 deletions(-) create mode 100644 src/main/java/mate/academy/bookstore/controller/OrderController.java create mode 100644 src/main/java/mate/academy/bookstore/dto/order/OrderItemResponseDto.java create mode 100644 src/main/java/mate/academy/bookstore/dto/order/OrderRequestDto.java create mode 100644 src/main/java/mate/academy/bookstore/dto/order/OrderResponseDto.java create mode 100644 src/main/java/mate/academy/bookstore/dto/order/OrderStatusDto.java create mode 100644 src/main/java/mate/academy/bookstore/mapper/OrderItemMapper.java create mode 100644 src/main/java/mate/academy/bookstore/mapper/OrderMapper.java create mode 100644 src/main/java/mate/academy/bookstore/model/order/Order.java create mode 100644 src/main/java/mate/academy/bookstore/model/order/OrderItem.java create mode 100644 src/main/java/mate/academy/bookstore/model/order/Status.java create mode 100644 src/main/java/mate/academy/bookstore/repository/order/OrderItemRepository.java create mode 100644 src/main/java/mate/academy/bookstore/repository/order/OrderRepository.java create mode 100644 src/main/java/mate/academy/bookstore/service/OrderService.java create mode 100644 src/main/java/mate/academy/bookstore/service/impl/OrderServiceImpl.java create mode 100644 src/main/resources/db/changelog/changes/12-create-orders-table.yaml create mode 100644 src/main/resources/db/changelog/changes/13-create-order-items-table.yaml 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/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/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