From 9481068932271d1be81b118ce51ee86f5ccecb6c Mon Sep 17 00:00:00 2001 From: Serhii Senkiv Date: Thu, 29 May 2025 19:24:27 +0200 Subject: [PATCH 1/3] Implementation of Order feature. --- .../bookstore/controller/OrderController.java | 90 ++++++++++ .../bookstore/dto/OrderItemResponseDto.java | 11 ++ .../senkiv/bookstore/dto/OrderRequestDto.java | 9 + .../bookstore/dto/OrderResponseDto.java | 15 ++ .../bookstore/dto/OrderStatusUpdateDto.java | 10 ++ .../bookstore/mapper/OrderItemMapper.java | 34 ++++ .../senkiv/bookstore/mapper/OrderMapper.java | 11 ++ .../com/senkiv/bookstore/model/Order.java | 58 +++++++ .../com/senkiv/bookstore/model/OrderItem.java | 39 +++++ .../senkiv/bookstore/model/OrderStatus.java | 7 + .../bookstore/repository/BookRepository.java | 2 + .../repository/CartItemRepository.java | 2 + .../repository/OrderItemRepository.java | 15 ++ .../bookstore/repository/OrderRepository.java | 16 ++ .../bookstore/repository/RoleRepository.java | 4 +- .../repository/ShoppingCartRepository.java | 2 + .../bookstore/service/OrderService.java | 20 +++ .../service/ShoppingCartService.java | 5 + .../service/impl/OrderServiceImpl.java | 156 ++++++++++++++++++ .../service/impl/ShoppingCartServiceImpl.java | 2 +- .../changes/11-create-order-table.yaml | 48 ++++++ .../changes/12-create-order-items-table.yaml | 45 +++++ .../db/changelog/db.changelog-master.yaml | 4 + 23 files changed, 602 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/senkiv/bookstore/controller/OrderController.java create mode 100644 src/main/java/com/senkiv/bookstore/dto/OrderItemResponseDto.java create mode 100644 src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java create mode 100644 src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java create mode 100644 src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java create mode 100644 src/main/java/com/senkiv/bookstore/mapper/OrderItemMapper.java create mode 100644 src/main/java/com/senkiv/bookstore/mapper/OrderMapper.java create mode 100644 src/main/java/com/senkiv/bookstore/model/Order.java create mode 100644 src/main/java/com/senkiv/bookstore/model/OrderItem.java create mode 100644 src/main/java/com/senkiv/bookstore/model/OrderStatus.java create mode 100644 src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java create mode 100644 src/main/java/com/senkiv/bookstore/repository/OrderRepository.java create mode 100644 src/main/java/com/senkiv/bookstore/service/OrderService.java create mode 100644 src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java create mode 100644 src/main/resources/db/changelog/changes/11-create-order-table.yaml create mode 100644 src/main/resources/db/changelog/changes/12-create-order-items-table.yaml diff --git a/src/main/java/com/senkiv/bookstore/controller/OrderController.java b/src/main/java/com/senkiv/bookstore/controller/OrderController.java new file mode 100644 index 0000000..01c1b7b --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/controller/OrderController.java @@ -0,0 +1,90 @@ +package com.senkiv.bookstore.controller; + +import com.senkiv.bookstore.dto.OrderItemResponseDto; +import com.senkiv.bookstore.dto.OrderRequestDto; +import com.senkiv.bookstore.dto.OrderResponseDto; +import com.senkiv.bookstore.dto.OrderStatusUpdateDto; +import com.senkiv.bookstore.model.User; +import com.senkiv.bookstore.service.OrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.UserDetails; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Order API") +@RestController +@RequestMapping("/orders") +@RequiredArgsConstructor +public class OrderController { + private final OrderService orderService; + + @Operation(description = "Retrieves history of user`s orders.") + @PreAuthorize("hasRole('USER')") + @GetMapping + public Page retrieveOrders( + @AuthenticationPrincipal UserDetails userDetails, + Pageable pageable) { + return orderService.retrieveHistoryOrders(pageable, getUserFromDetails(userDetails)); + } + + @Operation(description = "Places an order with items from user`s shopping cart.") + @PreAuthorize("hasRole('USER')") + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public OrderResponseDto createOrder( + @AuthenticationPrincipal UserDetails userDetails, + @RequestBody @Valid OrderRequestDto dto) { + return orderService.createOrder(getUserFromDetails(userDetails), dto); + } + + @Operation(description = "Retrieves all items from one of user`s orders.") + @PreAuthorize("hasRole('USER')") + @GetMapping("/{orderId}/items") + public Page retrieveItemsForSpecificOrder( + @AuthenticationPrincipal UserDetails userDetails, + @PathVariable Long orderId, + Pageable pageable) { + return orderService.getOrderItems(pageable, orderId, getUserFromDetails(userDetails)); + } + + @Operation(description = "Retrieves specific item from one of user`s orders") + @PreAuthorize("hasRole('USER')") + @GetMapping("/{orderId}/items/{itemId}") + public OrderItemResponseDto retrieveSpecificItem( + @AuthenticationPrincipal UserDetails userDetails, + @PathVariable Long orderId, + @PathVariable Long itemId + ) { + return orderService.getSpecificItem(itemId, orderId, getUserFromDetails(userDetails)); + + } + + @Operation(description = "Updates status of specific order.") + @PreAuthorize("hasRole('ADMIN')") + @PatchMapping("/{orderId}") + public OrderResponseDto updateStatus( + @PathVariable Long orderId, + @RequestBody OrderStatusUpdateDto dto + ) { + return orderService.updateStatus(dto, orderId); + } + + private Long getUserFromDetails(UserDetails userDetails) { + User authenticatedUser = (User) userDetails; + return authenticatedUser.getId(); + } +} diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderItemResponseDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderItemResponseDto.java new file mode 100644 index 0000000..f97b5d0 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/dto/OrderItemResponseDto.java @@ -0,0 +1,11 @@ +package com.senkiv.bookstore.dto; + +import java.math.BigDecimal; + +public record OrderItemResponseDto( + Long id, + Long bookId, + int quantity, + BigDecimal price +) { +} diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java new file mode 100644 index 0000000..fb4eaea --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java @@ -0,0 +1,9 @@ +package com.senkiv.bookstore.dto; + +import jakarta.validation.constraints.Size; + +public record OrderRequestDto( + @Size(max = 255, message = "Shipping address must not exceed 255 characters") + String shippingAddress +) { +} diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java new file mode 100644 index 0000000..942e2dc --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java @@ -0,0 +1,15 @@ +package com.senkiv.bookstore.dto; + +import com.senkiv.bookstore.model.OrderStatus; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Set; + +public record OrderResponseDto( + Long id, + OrderStatus orderStatus, + BigDecimal total, + LocalDateTime orderDate, + String shippingAddress, + Set orderItems) { +} diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java new file mode 100644 index 0000000..10d2be7 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java @@ -0,0 +1,10 @@ +package com.senkiv.bookstore.dto; + +import com.senkiv.bookstore.model.OrderStatus; +import jakarta.validation.constraints.NotBlank; + +public record OrderStatusUpdateDto( + @NotBlank + OrderStatus orderStatus +) { +} diff --git a/src/main/java/com/senkiv/bookstore/mapper/OrderItemMapper.java b/src/main/java/com/senkiv/bookstore/mapper/OrderItemMapper.java new file mode 100644 index 0000000..c180fc2 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/mapper/OrderItemMapper.java @@ -0,0 +1,34 @@ +package com.senkiv.bookstore.mapper; + +import com.senkiv.bookstore.config.MapperConfig; +import com.senkiv.bookstore.dto.OrderItemResponseDto; +import com.senkiv.bookstore.model.Book; +import com.senkiv.bookstore.model.CartItem; +import com.senkiv.bookstore.model.OrderItem; +import java.math.BigDecimal; +import java.util.Set; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapperConfig.class) +public interface OrderItemMapper { + @Mapping(source = "book", target = "price", qualifiedByName = "bookPrice") + @Mapping(target = "id", ignore = true) + OrderItem toOrderItem(CartItem cartItem); + + @Mapping(target = "bookId", source = "book", qualifiedByName = "bookId") + OrderItemResponseDto toDto(OrderItem orderItem); + + Set map(Set orderItems); + + @Named("bookPrice") + static BigDecimal getBookPrice(Book book) { + return book.getPrice(); + } + + @Named("bookId") + default Long getBookId(Book book) { + return book.getId(); + } +} diff --git a/src/main/java/com/senkiv/bookstore/mapper/OrderMapper.java b/src/main/java/com/senkiv/bookstore/mapper/OrderMapper.java new file mode 100644 index 0000000..529be2c --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/mapper/OrderMapper.java @@ -0,0 +1,11 @@ +package com.senkiv.bookstore.mapper; + +import com.senkiv.bookstore.config.MapperConfig; +import com.senkiv.bookstore.dto.OrderResponseDto; +import com.senkiv.bookstore.model.Order; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class, uses = OrderItemMapper.class) +public interface OrderMapper { + OrderResponseDto toDto(Order order); +} diff --git a/src/main/java/com/senkiv/bookstore/model/Order.java b/src/main/java/com/senkiv/bookstore/model/Order.java new file mode 100644 index 0000000..cbdebdb --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/model/Order.java @@ -0,0 +1,58 @@ +package com.senkiv.bookstore.model; + +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.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +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.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Table(name = "orders") +@SQLDelete(sql = "UPDATE orders SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +@Getter +@Setter +@NamedEntityGraph( + name = "order-with-items", + attributeNodes = @NamedAttributeNode("orderItems") +) +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + private OrderStatus orderStatus = OrderStatus.CREATED; + @Column(name = "total", nullable = false) + private BigDecimal total = BigDecimal.ZERO; + @Column(name = "order_date", nullable = false) + private LocalDateTime orderDate = LocalDateTime.now(); + @Column(name = "shipping_address", nullable = false) + private String shippingAddress; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "order", + cascade = CascadeType.ALL, orphanRemoval = true) + private Set orderItems = new HashSet<>(); + @Column(name = "is_deleted", columnDefinition = "TINYINT", nullable = false) + private boolean isDeleted; +} diff --git a/src/main/java/com/senkiv/bookstore/model/OrderItem.java b/src/main/java/com/senkiv/bookstore/model/OrderItem.java new file mode 100644 index 0000000..5648ed7 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/model/OrderItem.java @@ -0,0 +1,39 @@ +package com.senkiv.bookstore.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Table(name = "order_items") +@SQLDelete(sql = "UPDATE order_items SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +@Getter +@Setter +public class OrderItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @ManyToOne + @JoinColumn(name = "order_id", nullable = false) + private Order order; + @ManyToOne + @JoinColumn(name = "book_id", nullable = false) + private Book book; + @Column(name = "quantity", nullable = false) + private int quantity; + @Column(name = "price", nullable = false) + private BigDecimal price; + @Column(name = "is_deleted", columnDefinition = "TINYINT", nullable = false) + private boolean isDeleted; +} diff --git a/src/main/java/com/senkiv/bookstore/model/OrderStatus.java b/src/main/java/com/senkiv/bookstore/model/OrderStatus.java new file mode 100644 index 0000000..9fb6518 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/model/OrderStatus.java @@ -0,0 +1,7 @@ +package com.senkiv.bookstore.model; + +public enum OrderStatus { + CREATED, + IN_PROGRESS, + COMPLETED +} diff --git a/src/main/java/com/senkiv/bookstore/repository/BookRepository.java b/src/main/java/com/senkiv/bookstore/repository/BookRepository.java index daa286c..b195c8d 100644 --- a/src/main/java/com/senkiv/bookstore/repository/BookRepository.java +++ b/src/main/java/com/senkiv/bookstore/repository/BookRepository.java @@ -8,7 +8,9 @@ import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; +@Repository public interface BookRepository extends JpaRepository, JpaSpecificationExecutor { boolean existsBookByIsbn(String isbn); diff --git a/src/main/java/com/senkiv/bookstore/repository/CartItemRepository.java b/src/main/java/com/senkiv/bookstore/repository/CartItemRepository.java index 82ebb62..36c9970 100644 --- a/src/main/java/com/senkiv/bookstore/repository/CartItemRepository.java +++ b/src/main/java/com/senkiv/bookstore/repository/CartItemRepository.java @@ -3,7 +3,9 @@ import com.senkiv.bookstore.model.CartItem; import java.util.Set; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface CartItemRepository extends JpaRepository { Set findCartItemByShoppingCartId(Long id); } diff --git a/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java b/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java new file mode 100644 index 0000000..25285ce --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java @@ -0,0 +1,15 @@ +package com.senkiv.bookstore.repository; + +import com.senkiv.bookstore.model.OrderItem; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OrderItemRepository extends JpaRepository { + Page findAllByOrderId(Long orderId, Pageable pageable); + + Optional findByIdAndOrderId(Long itemId, Long orderId); +} diff --git a/src/main/java/com/senkiv/bookstore/repository/OrderRepository.java b/src/main/java/com/senkiv/bookstore/repository/OrderRepository.java new file mode 100644 index 0000000..d5a655c --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/repository/OrderRepository.java @@ -0,0 +1,16 @@ +package com.senkiv.bookstore.repository; + +import com.senkiv.bookstore.model.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface OrderRepository extends JpaRepository { + @EntityGraph("order-with-items") + Page findOrdersByUserId(Long userId, Pageable pageable); + + boolean existsByIdAndUserId(Long orderId, Long userId); +} diff --git a/src/main/java/com/senkiv/bookstore/repository/RoleRepository.java b/src/main/java/com/senkiv/bookstore/repository/RoleRepository.java index a0275ae..d864494 100644 --- a/src/main/java/com/senkiv/bookstore/repository/RoleRepository.java +++ b/src/main/java/com/senkiv/bookstore/repository/RoleRepository.java @@ -4,9 +4,9 @@ import com.senkiv.bookstore.model.Role.RoleName; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface RoleRepository extends JpaRepository { - boolean existsRoleByRoleName(RoleName roleName); - Optional findByRoleName(RoleName roleName); } diff --git a/src/main/java/com/senkiv/bookstore/repository/ShoppingCartRepository.java b/src/main/java/com/senkiv/bookstore/repository/ShoppingCartRepository.java index de60119..31ada36 100644 --- a/src/main/java/com/senkiv/bookstore/repository/ShoppingCartRepository.java +++ b/src/main/java/com/senkiv/bookstore/repository/ShoppingCartRepository.java @@ -4,7 +4,9 @@ import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface ShoppingCartRepository extends JpaRepository { @EntityGraph("cart-with-items-user") Optional findShoppingCartByUserId(Long id); diff --git a/src/main/java/com/senkiv/bookstore/service/OrderService.java b/src/main/java/com/senkiv/bookstore/service/OrderService.java new file mode 100644 index 0000000..9690040 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/service/OrderService.java @@ -0,0 +1,20 @@ +package com.senkiv.bookstore.service; + +import com.senkiv.bookstore.dto.OrderItemResponseDto; +import com.senkiv.bookstore.dto.OrderRequestDto; +import com.senkiv.bookstore.dto.OrderResponseDto; +import com.senkiv.bookstore.dto.OrderStatusUpdateDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface OrderService { + OrderResponseDto createOrder(Long userId, OrderRequestDto orderRequestDto); + + Page retrieveHistoryOrders(Pageable pageable, Long id); + + Page getOrderItems(Pageable pageable, Long orderId, Long userId); + + OrderItemResponseDto getSpecificItem(Long itemId, Long orderId, Long userId); + + OrderResponseDto updateStatus(OrderStatusUpdateDto dto, Long orderId); +} diff --git a/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java b/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java index a3edd89..f15f0cd 100644 --- a/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java +++ b/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java @@ -3,6 +3,7 @@ import com.senkiv.bookstore.dto.CartItemRequestDto; import com.senkiv.bookstore.dto.CartItemUpdateQuantityDto; import com.senkiv.bookstore.dto.ShoppingCartResponseDto; +import com.senkiv.bookstore.model.ShoppingCart; import com.senkiv.bookstore.model.User; public interface ShoppingCartService { @@ -16,4 +17,8 @@ ShoppingCartResponseDto updateQuantity(Long userId, Long cartItemId, void deleteCartItem(Long userId, Long cartItemId); void createUsersCart(User user); + + static boolean isCartEmpty(ShoppingCart shoppingCart) { + return shoppingCart.getCartItems().isEmpty(); + } } diff --git a/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java new file mode 100644 index 0000000..b5f03a4 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java @@ -0,0 +1,156 @@ +package com.senkiv.bookstore.service.impl; + +import com.senkiv.bookstore.dto.OrderItemResponseDto; +import com.senkiv.bookstore.dto.OrderRequestDto; +import com.senkiv.bookstore.dto.OrderResponseDto; +import com.senkiv.bookstore.dto.OrderStatusUpdateDto; +import com.senkiv.bookstore.exception.DataProcessingException; +import com.senkiv.bookstore.mapper.OrderItemMapper; +import com.senkiv.bookstore.mapper.OrderMapper; +import com.senkiv.bookstore.model.Order; +import com.senkiv.bookstore.model.OrderItem; +import com.senkiv.bookstore.model.ShoppingCart; +import com.senkiv.bookstore.model.User; +import com.senkiv.bookstore.repository.OrderItemRepository; +import com.senkiv.bookstore.repository.OrderRepository; +import com.senkiv.bookstore.repository.ShoppingCartRepository; +import com.senkiv.bookstore.service.OrderService; +import com.senkiv.bookstore.service.ShoppingCartService; +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Transactional +@Service +@RequiredArgsConstructor +public class OrderServiceImpl implements OrderService { + public static final String NO_ADDRESS_MESSAGE = + "Update your shipping address."; + public static final String TOTAL_CALCULATION_ERR_MESSAGE = + "Error during total price calculation."; + public static final String NO_ORDER_FOR_USER_MESSAGE = + "User with id %s does not have order with id %s"; + public static final String NO_ITEM_IN_ORDER_MESSAGE = + "There is no item with id %s in order with id %s"; + public static final String EMPTY_CART_MESSAGE = + "You cart is empty. Cannot place an order."; + public static final String NO_ORDER_WITH_SUCH_ID = + "There is no order with such id %s."; + private final OrderRepository orderRepository; + private final OrderItemRepository orderItemRepository; + private final ShoppingCartRepository shoppingCartRepository; + private final OrderMapper orderMapper; + private final OrderItemMapper orderItemMapper; + + public OrderResponseDto createOrder(Long userId, OrderRequestDto orderRequestDto) { + ShoppingCart shoppingCart = getShoppingCart(userId); + User owner = shoppingCart.getUser(); + if (ShoppingCartService.isCartEmpty(shoppingCart)) { + throw new DataProcessingException(EMPTY_CART_MESSAGE); + } + Order order = createEmptyOrder(owner); + resolveShippingAddress(shoppingCart.getUser(), orderRequestDto, order); + Set orderItems = transferOrderItemsFromCartItems(shoppingCart, order); + order.setOrderItems(orderItems); + order.setTotal(calculateTotalPrice(order)); + orderRepository.save(order); + emptyShoppingCart(shoppingCart); + return orderMapper.toDto(order); + } + + @Override + public Page retrieveHistoryOrders(Pageable pageable, Long userId) { + return orderRepository.findOrdersByUserId(userId, pageable) + .map(orderMapper::toDto); + } + + @Override + public Page getOrderItems(Pageable pageable, Long orderId, Long userId) { + if (!orderRepository.existsByIdAndUserId(orderId, userId)) { + throw new EntityNotFoundException( + NO_ORDER_FOR_USER_MESSAGE.formatted(userId, orderId)); + } + return orderItemRepository.findAllByOrderId(orderId, pageable).map(orderItemMapper::toDto); + } + + @Override + public OrderItemResponseDto getSpecificItem(Long itemId, Long orderId, Long userId) { + if (!orderRepository.existsByIdAndUserId(orderId, userId)) { + throw new EntityNotFoundException( + NO_ORDER_FOR_USER_MESSAGE.formatted(userId, orderId)); + } + return orderItemRepository.findByIdAndOrderId(itemId, orderId).map(orderItemMapper::toDto) + .orElseThrow(() -> new EntityNotFoundException( + NO_ITEM_IN_ORDER_MESSAGE.formatted(itemId, + orderId))); + } + + @Override + public OrderResponseDto updateStatus(OrderStatusUpdateDto dto, Long orderId) { + Order order = orderRepository.findById(orderId) + .orElseThrow( + () -> new EntityNotFoundException(NO_ORDER_WITH_SUCH_ID.formatted(orderId + ))); + order.setOrderStatus(dto.orderStatus()); + return orderMapper.toDto(order); + } + + private void resolveShippingAddress(User user, OrderRequestDto orderRequestDto, + Order order) { + String updatedShippingAddress = orderRequestDto.shippingAddress(); + if (updatedShippingAddress != null && !updatedShippingAddress.isBlank()) { + order.setShippingAddress(updatedShippingAddress); + } else { + String usersShippingAddress = user.getShippingAddress(); + if (usersShippingAddress != null && !usersShippingAddress.isBlank()) { + order.setShippingAddress(usersShippingAddress); + } else { + throw new DataProcessingException(NO_ADDRESS_MESSAGE); + } + } + } + + private Set transferOrderItemsFromCartItems(ShoppingCart shoppingCart, Order order) { + return shoppingCart.getCartItems().stream() + .map(item -> { + OrderItem orderItem = orderItemMapper.toOrderItem(item); + orderItem.setOrder(order); + return orderItem; + }) + .collect(Collectors.toSet()); + } + + private ShoppingCart getShoppingCart(Long userId) { + return shoppingCartRepository.findShoppingCartByUserId(userId) + .orElseThrow(() -> new EntityNotFoundException( + ShoppingCartServiceImpl.SHOPPING_CART_INIT_ERROR_MSG.formatted( + userId))); + } + + private Order createEmptyOrder(User user) { + Order order = new Order(); + order.setUser(user); + return order; + } + + private BigDecimal calculateTotalPrice(Order order) { + return order.getOrderItems().stream() + .map(orderItem -> orderItem.getPrice() + .multiply(new BigDecimal(orderItem.getQuantity()))) + .reduce(BigDecimal::add).orElseThrow(() -> new ArithmeticException( + TOTAL_CALCULATION_ERR_MESSAGE)); + } + + private void emptyShoppingCart(ShoppingCart shoppingCart) { + shoppingCart.getCartItems() + .forEach(item -> item.setShoppingCart(null)); + shoppingCart.getCartItems().clear(); + + } +} diff --git a/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java index 9f894bd..17af942 100644 --- a/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java @@ -87,7 +87,7 @@ public void createUsersCart(User user) { shoppingCartRepository.save(shoppingCart); } - private ShoppingCart getUsersShoppingCart(Long userId) { + protected ShoppingCart getUsersShoppingCart(Long userId) { return shoppingCartRepository.findShoppingCartByUserId(userId) .orElseThrow(() -> new EntityNotFoundException( SHOPPING_CART_INIT_ERROR_MSG.formatted(userId))); diff --git a/src/main/resources/db/changelog/changes/11-create-order-table.yaml b/src/main/resources/db/changelog/changes/11-create-order-table.yaml new file mode 100644 index 0000000..1e270c1 --- /dev/null +++ b/src/main/resources/db/changelog/changes/11-create-order-table.yaml @@ -0,0 +1,48 @@ +databaseChangeLog: + - changeSet: + id: 11 + author: Serhii Senkiv + changes: + - createTable: + tableName: orders + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: user_id + type: BIGINT + constraints: + foreignKeyName: fk_user_order + referencedTableName: users + referencedColumnNames: id + - column: + name: status + type: VARCHAR(50) + constraints: + nullable: false + - column: + name: total + type: decimal + constraints: + nullable: false + - column: + name: order_date + type: DATETIME + constraints: + nullable: false + - column: + name: shipping_address + type: VARCHAR(255) + constraints: + nullable: false + - column: + name: is_deleted + type: boolean + defaultValue: false + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/12-create-order-items-table.yaml b/src/main/resources/db/changelog/changes/12-create-order-items-table.yaml new file mode 100644 index 0000000..332dcf0 --- /dev/null +++ b/src/main/resources/db/changelog/changes/12-create-order-items-table.yaml @@ -0,0 +1,45 @@ +databaseChangeLog: + - changeSet: + id: 12 + author: Serhii Senkiv + changes: + - createTable: + tableName: order_items + columns: + - column: + name: id + type: BIGINT + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: order_id + type: BIGINT + constraints: + foreignKeyName: fk_order_items + referencedTableName: orders + referencedColumnNames: id + - column: + name: book_id + type: BIGINT + constraints: + foreignKeyName: fk_book_order_item + referencedTableName: books + referencedColumnNames: id + - column: + name: quantity + type: integer + constraints: + nullable: false + - column: + name: price + type: decimal + constraints: + nullable: false + - column: + name: is_deleted + type: boolean + defaultValue: false + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 81dcc8e..38554e0 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -19,3 +19,7 @@ databaseChangeLog: file: db/changelog/changes/09-create-cart-items-table.yaml - include: file: db/changelog/changes/10-populate-books-categories.yaml + - include: + file: db/changelog/changes/11-create-order-table.yaml + - include: + file: db/changelog/changes/12-create-order-items-table.yaml From 1a1f59fa54892c1be46f24910fd5e8d9474a5592 Mon Sep 17 00:00:00 2001 From: Serhii Senkiv Date: Fri, 30 May 2025 10:41:50 +0200 Subject: [PATCH 2/3] Fixes as per comments. --- .../senkiv/bookstore/dto/OrderRequestDto.java | 2 + .../bookstore/dto/OrderStatusUpdateDto.java | 4 +- .../exception/OrderProcessingException.java | 7 +++ .../com/senkiv/bookstore/model/Order.java | 2 + .../com/senkiv/bookstore/model/OrderItem.java | 5 +- .../service/ShoppingCartService.java | 9 +++- .../service/impl/OrderServiceImpl.java | 53 +++++-------------- .../service/impl/ShoppingCartServiceImpl.java | 2 +- 8 files changed, 38 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/senkiv/bookstore/exception/OrderProcessingException.java diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java index fb4eaea..474183c 100644 --- a/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java +++ b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java @@ -1,9 +1,11 @@ package com.senkiv.bookstore.dto; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public record OrderRequestDto( @Size(max = 255, message = "Shipping address must not exceed 255 characters") + @NotNull String shippingAddress ) { } diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java index 10d2be7..c0d2efa 100644 --- a/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java +++ b/src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java @@ -1,10 +1,10 @@ package com.senkiv.bookstore.dto; import com.senkiv.bookstore.model.OrderStatus; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record OrderStatusUpdateDto( - @NotBlank + @NotNull OrderStatus orderStatus ) { } diff --git a/src/main/java/com/senkiv/bookstore/exception/OrderProcessingException.java b/src/main/java/com/senkiv/bookstore/exception/OrderProcessingException.java new file mode 100644 index 0000000..8dfea33 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/exception/OrderProcessingException.java @@ -0,0 +1,7 @@ +package com.senkiv.bookstore.exception; + +public class OrderProcessingException extends RuntimeException { + public OrderProcessingException(String message) { + super(message); + } +} diff --git a/src/main/java/com/senkiv/bookstore/model/Order.java b/src/main/java/com/senkiv/bookstore/model/Order.java index cbdebdb..272a694 100644 --- a/src/main/java/com/senkiv/bookstore/model/Order.java +++ b/src/main/java/com/senkiv/bookstore/model/Order.java @@ -21,6 +21,7 @@ import java.util.Set; import lombok.Getter; import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLRestriction; @@ -46,6 +47,7 @@ public class Order { private OrderStatus orderStatus = OrderStatus.CREATED; @Column(name = "total", nullable = false) private BigDecimal total = BigDecimal.ZERO; + @CreationTimestamp @Column(name = "order_date", nullable = false) private LocalDateTime orderDate = LocalDateTime.now(); @Column(name = "shipping_address", nullable = false) diff --git a/src/main/java/com/senkiv/bookstore/model/OrderItem.java b/src/main/java/com/senkiv/bookstore/model/OrderItem.java index 5648ed7..4313421 100644 --- a/src/main/java/com/senkiv/bookstore/model/OrderItem.java +++ b/src/main/java/com/senkiv/bookstore/model/OrderItem.java @@ -2,6 +2,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -24,10 +25,10 @@ public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id", nullable = false) private Order order; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "book_id", nullable = false) private Book book; @Column(name = "quantity", nullable = false) diff --git a/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java b/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java index f15f0cd..58cf688 100644 --- a/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java +++ b/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java @@ -18,7 +18,14 @@ ShoppingCartResponseDto updateQuantity(Long userId, Long cartItemId, void createUsersCart(User user); - static boolean isCartEmpty(ShoppingCart shoppingCart) { + default void clearShoppingCart(ShoppingCart shoppingCart) { + shoppingCart.getCartItems() + .forEach(item -> item.setShoppingCart(null)); + shoppingCart.getCartItems().clear(); + + } + + default boolean isCartEmpty(ShoppingCart shoppingCart) { return shoppingCart.getCartItems().isEmpty(); } } diff --git a/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java index b5f03a4..e3e6eea 100644 --- a/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java +++ b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java @@ -4,7 +4,7 @@ import com.senkiv.bookstore.dto.OrderRequestDto; import com.senkiv.bookstore.dto.OrderResponseDto; import com.senkiv.bookstore.dto.OrderStatusUpdateDto; -import com.senkiv.bookstore.exception.DataProcessingException; +import com.senkiv.bookstore.exception.OrderProcessingException; import com.senkiv.bookstore.mapper.OrderItemMapper; import com.senkiv.bookstore.mapper.OrderMapper; import com.senkiv.bookstore.model.Order; @@ -30,37 +30,36 @@ @Service @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { - public static final String NO_ADDRESS_MESSAGE = - "Update your shipping address."; - public static final String TOTAL_CALCULATION_ERR_MESSAGE = + private static final String TOTAL_CALCULATION_ERR_MESSAGE = "Error during total price calculation."; - public static final String NO_ORDER_FOR_USER_MESSAGE = + private static final String NO_ORDER_FOR_USER_MESSAGE = "User with id %s does not have order with id %s"; - public static final String NO_ITEM_IN_ORDER_MESSAGE = + private static final String NO_ITEM_IN_ORDER_MESSAGE = "There is no item with id %s in order with id %s"; - public static final String EMPTY_CART_MESSAGE = - "You cart is empty. Cannot place an order."; - public static final String NO_ORDER_WITH_SUCH_ID = + private static final String EMPTY_CART_MESSAGE = + "Your cart is empty. Cannot place an order."; + private static final String NO_ORDER_WITH_SUCH_ID = "There is no order with such id %s."; private final OrderRepository orderRepository; private final OrderItemRepository orderItemRepository; private final ShoppingCartRepository shoppingCartRepository; private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; + private final ShoppingCartService shoppingCartService; public OrderResponseDto createOrder(Long userId, OrderRequestDto orderRequestDto) { ShoppingCart shoppingCart = getShoppingCart(userId); User owner = shoppingCart.getUser(); - if (ShoppingCartService.isCartEmpty(shoppingCart)) { - throw new DataProcessingException(EMPTY_CART_MESSAGE); + if (shoppingCartService.isCartEmpty(shoppingCart)) { + throw new OrderProcessingException(EMPTY_CART_MESSAGE); } Order order = createEmptyOrder(owner); - resolveShippingAddress(shoppingCart.getUser(), orderRequestDto, order); + order.setShippingAddress(orderRequestDto.shippingAddress()); Set orderItems = transferOrderItemsFromCartItems(shoppingCart, order); order.setOrderItems(orderItems); order.setTotal(calculateTotalPrice(order)); orderRepository.save(order); - emptyShoppingCart(shoppingCart); + shoppingCartService.clearShoppingCart(shoppingCart); return orderMapper.toDto(order); } @@ -81,10 +80,6 @@ public Page getOrderItems(Pageable pageable, Long orderId, @Override public OrderItemResponseDto getSpecificItem(Long itemId, Long orderId, Long userId) { - if (!orderRepository.existsByIdAndUserId(orderId, userId)) { - throw new EntityNotFoundException( - NO_ORDER_FOR_USER_MESSAGE.formatted(userId, orderId)); - } return orderItemRepository.findByIdAndOrderId(itemId, orderId).map(orderItemMapper::toDto) .orElseThrow(() -> new EntityNotFoundException( NO_ITEM_IN_ORDER_MESSAGE.formatted(itemId, @@ -101,21 +96,6 @@ public OrderResponseDto updateStatus(OrderStatusUpdateDto dto, Long orderId) { return orderMapper.toDto(order); } - private void resolveShippingAddress(User user, OrderRequestDto orderRequestDto, - Order order) { - String updatedShippingAddress = orderRequestDto.shippingAddress(); - if (updatedShippingAddress != null && !updatedShippingAddress.isBlank()) { - order.setShippingAddress(updatedShippingAddress); - } else { - String usersShippingAddress = user.getShippingAddress(); - if (usersShippingAddress != null && !usersShippingAddress.isBlank()) { - order.setShippingAddress(usersShippingAddress); - } else { - throw new DataProcessingException(NO_ADDRESS_MESSAGE); - } - } - } - private Set transferOrderItemsFromCartItems(ShoppingCart shoppingCart, Order order) { return shoppingCart.getCartItems().stream() .map(item -> { @@ -143,14 +123,7 @@ private BigDecimal calculateTotalPrice(Order order) { return order.getOrderItems().stream() .map(orderItem -> orderItem.getPrice() .multiply(new BigDecimal(orderItem.getQuantity()))) - .reduce(BigDecimal::add).orElseThrow(() -> new ArithmeticException( + .reduce(BigDecimal::add).orElseThrow(() -> new OrderProcessingException( TOTAL_CALCULATION_ERR_MESSAGE)); } - - private void emptyShoppingCart(ShoppingCart shoppingCart) { - shoppingCart.getCartItems() - .forEach(item -> item.setShoppingCart(null)); - shoppingCart.getCartItems().clear(); - - } } diff --git a/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java index 17af942..9f894bd 100644 --- a/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java +++ b/src/main/java/com/senkiv/bookstore/service/impl/ShoppingCartServiceImpl.java @@ -87,7 +87,7 @@ public void createUsersCart(User user) { shoppingCartRepository.save(shoppingCart); } - protected ShoppingCart getUsersShoppingCart(Long userId) { + private ShoppingCart getUsersShoppingCart(Long userId) { return shoppingCartRepository.findShoppingCartByUserId(userId) .orElseThrow(() -> new EntityNotFoundException( SHOPPING_CART_INIT_ERROR_MSG.formatted(userId))); From 3002838389957a98e2e57414813f7ede255cb746 Mon Sep 17 00:00:00 2001 From: Serhii Senkiv Date: Fri, 30 May 2025 12:03:51 +0200 Subject: [PATCH 3/3] Fixes as per comments. --- .../bookstore/controller/OrderController.java | 2 +- .../senkiv/bookstore/dto/OrderRequestDto.java | 4 ++-- .../senkiv/bookstore/dto/OrderResponseDto.java | 2 +- .../exception/GlobalExceptionHandler.java | 6 +++++- .../java/com/senkiv/bookstore/model/Order.java | 18 +++++++++--------- .../com/senkiv/bookstore/model/OrderItem.java | 10 +++++----- .../senkiv/bookstore/model/ShoppingCart.java | 10 ++++++++++ .../repository/OrderItemRepository.java | 2 +- .../bookstore/service/ShoppingCartService.java | 12 ------------ .../service/impl/OrderServiceImpl.java | 12 ++++++------ 10 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/senkiv/bookstore/controller/OrderController.java b/src/main/java/com/senkiv/bookstore/controller/OrderController.java index 01c1b7b..86a1f3e 100644 --- a/src/main/java/com/senkiv/bookstore/controller/OrderController.java +++ b/src/main/java/com/senkiv/bookstore/controller/OrderController.java @@ -78,7 +78,7 @@ public OrderItemResponseDto retrieveSpecificItem( @PatchMapping("/{orderId}") public OrderResponseDto updateStatus( @PathVariable Long orderId, - @RequestBody OrderStatusUpdateDto dto + @RequestBody @Valid OrderStatusUpdateDto dto ) { return orderService.updateStatus(dto, orderId); } diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java index 474183c..99fd744 100644 --- a/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java +++ b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java @@ -1,11 +1,11 @@ package com.senkiv.bookstore.dto; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public record OrderRequestDto( @Size(max = 255, message = "Shipping address must not exceed 255 characters") - @NotNull + @NotBlank String shippingAddress ) { } diff --git a/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java b/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java index 942e2dc..8c29819 100644 --- a/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java +++ b/src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java @@ -7,7 +7,7 @@ public record OrderResponseDto( Long id, - OrderStatus orderStatus, + OrderStatus status, BigDecimal total, LocalDateTime orderDate, String shippingAddress, diff --git a/src/main/java/com/senkiv/bookstore/exception/GlobalExceptionHandler.java b/src/main/java/com/senkiv/bookstore/exception/GlobalExceptionHandler.java index 24dc59a..d00c131 100644 --- a/src/main/java/com/senkiv/bookstore/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/senkiv/bookstore/exception/GlobalExceptionHandler.java @@ -45,6 +45,11 @@ public ResponseEntity handleCategoryExistsException(CategoryExistsExcept return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST); } + @ExceptionHandler(exception = OrderProcessingException.class) + public ResponseEntity handleOrderProcessingException(OrderProcessingException ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + private static String getErrorMessage(ObjectError e) { if (e instanceof FieldError) { String field = ((FieldError) e).getField(); @@ -54,4 +59,3 @@ private static String getErrorMessage(ObjectError e) { return e.getDefaultMessage(); } } - diff --git a/src/main/java/com/senkiv/bookstore/model/Order.java b/src/main/java/com/senkiv/bookstore/model/Order.java index 272a694..a35d826 100644 --- a/src/main/java/com/senkiv/bookstore/model/Order.java +++ b/src/main/java/com/senkiv/bookstore/model/Order.java @@ -40,21 +40,21 @@ public class Order { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(nullable = false) private User user; - @Column(name = "status", nullable = false) + @Column(nullable = false) @Enumerated(EnumType.STRING) - private OrderStatus orderStatus = OrderStatus.CREATED; - @Column(name = "total", nullable = false) + private OrderStatus status = OrderStatus.CREATED; + @Column(nullable = false) private BigDecimal total = BigDecimal.ZERO; @CreationTimestamp - @Column(name = "order_date", nullable = false) - private LocalDateTime orderDate = LocalDateTime.now(); - @Column(name = "shipping_address", nullable = false) + @Column(nullable = false) + private LocalDateTime orderDate; + @Column(nullable = false) private String shippingAddress; - @OneToMany(fetch = FetchType.LAZY, mappedBy = "order", + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) private Set orderItems = new HashSet<>(); - @Column(name = "is_deleted", columnDefinition = "TINYINT", nullable = false) + @Column(columnDefinition = "TINYINT", nullable = false) private boolean isDeleted; } diff --git a/src/main/java/com/senkiv/bookstore/model/OrderItem.java b/src/main/java/com/senkiv/bookstore/model/OrderItem.java index 4313421..cbc9fa7 100644 --- a/src/main/java/com/senkiv/bookstore/model/OrderItem.java +++ b/src/main/java/com/senkiv/bookstore/model/OrderItem.java @@ -26,15 +26,15 @@ public class OrderItem { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "order_id", nullable = false) + @JoinColumn(nullable = false) private Order order; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "book_id", nullable = false) + @JoinColumn(nullable = false) private Book book; - @Column(name = "quantity", nullable = false) + @Column(nullable = false) private int quantity; - @Column(name = "price", nullable = false) + @Column(nullable = false) private BigDecimal price; - @Column(name = "is_deleted", columnDefinition = "TINYINT", nullable = false) + @Column(columnDefinition = "TINYINT", nullable = false) private boolean isDeleted; } diff --git a/src/main/java/com/senkiv/bookstore/model/ShoppingCart.java b/src/main/java/com/senkiv/bookstore/model/ShoppingCart.java index 3dacd34..24f3262 100644 --- a/src/main/java/com/senkiv/bookstore/model/ShoppingCart.java +++ b/src/main/java/com/senkiv/bookstore/model/ShoppingCart.java @@ -43,4 +43,14 @@ public class ShoppingCart { private boolean isDeleted; @OneToMany(mappedBy = "shoppingCart", cascade = CascadeType.ALL, orphanRemoval = true) private Set cartItems = new HashSet<>(); + + public void clearShoppingCart(ShoppingCart shoppingCart) { + shoppingCart.getCartItems() + .forEach(item -> item.setShoppingCart(null)); + shoppingCart.getCartItems().clear(); + } + + public boolean isEmpty() { + return cartItems.isEmpty(); + } } diff --git a/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java b/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java index 25285ce..3bdf401 100644 --- a/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java +++ b/src/main/java/com/senkiv/bookstore/repository/OrderItemRepository.java @@ -11,5 +11,5 @@ public interface OrderItemRepository extends JpaRepository { Page findAllByOrderId(Long orderId, Pageable pageable); - Optional findByIdAndOrderId(Long itemId, Long orderId); + Optional findByIdAndOrderIdAndOrderUserId(Long itemId, Long orderId, Long userId); } diff --git a/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java b/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java index 58cf688..a3edd89 100644 --- a/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java +++ b/src/main/java/com/senkiv/bookstore/service/ShoppingCartService.java @@ -3,7 +3,6 @@ import com.senkiv.bookstore.dto.CartItemRequestDto; import com.senkiv.bookstore.dto.CartItemUpdateQuantityDto; import com.senkiv.bookstore.dto.ShoppingCartResponseDto; -import com.senkiv.bookstore.model.ShoppingCart; import com.senkiv.bookstore.model.User; public interface ShoppingCartService { @@ -17,15 +16,4 @@ ShoppingCartResponseDto updateQuantity(Long userId, Long cartItemId, void deleteCartItem(Long userId, Long cartItemId); void createUsersCart(User user); - - default void clearShoppingCart(ShoppingCart shoppingCart) { - shoppingCart.getCartItems() - .forEach(item -> item.setShoppingCart(null)); - shoppingCart.getCartItems().clear(); - - } - - default boolean isCartEmpty(ShoppingCart shoppingCart) { - return shoppingCart.getCartItems().isEmpty(); - } } diff --git a/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java index e3e6eea..20940f1 100644 --- a/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java +++ b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java @@ -15,7 +15,6 @@ import com.senkiv.bookstore.repository.OrderRepository; import com.senkiv.bookstore.repository.ShoppingCartRepository; import com.senkiv.bookstore.service.OrderService; -import com.senkiv.bookstore.service.ShoppingCartService; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; import java.math.BigDecimal; @@ -45,12 +44,12 @@ public class OrderServiceImpl implements OrderService { private final ShoppingCartRepository shoppingCartRepository; private final OrderMapper orderMapper; private final OrderItemMapper orderItemMapper; - private final ShoppingCartService shoppingCartService; + @Override public OrderResponseDto createOrder(Long userId, OrderRequestDto orderRequestDto) { ShoppingCart shoppingCart = getShoppingCart(userId); User owner = shoppingCart.getUser(); - if (shoppingCartService.isCartEmpty(shoppingCart)) { + if (shoppingCart.isEmpty()) { throw new OrderProcessingException(EMPTY_CART_MESSAGE); } Order order = createEmptyOrder(owner); @@ -59,7 +58,7 @@ public OrderResponseDto createOrder(Long userId, OrderRequestDto orderRequestDto order.setOrderItems(orderItems); order.setTotal(calculateTotalPrice(order)); orderRepository.save(order); - shoppingCartService.clearShoppingCart(shoppingCart); + shoppingCart.clearShoppingCart(shoppingCart); return orderMapper.toDto(order); } @@ -80,7 +79,8 @@ public Page getOrderItems(Pageable pageable, Long orderId, @Override public OrderItemResponseDto getSpecificItem(Long itemId, Long orderId, Long userId) { - return orderItemRepository.findByIdAndOrderId(itemId, orderId).map(orderItemMapper::toDto) + return orderItemRepository.findByIdAndOrderIdAndOrderUserId(itemId, orderId, userId) + .map(orderItemMapper::toDto) .orElseThrow(() -> new EntityNotFoundException( NO_ITEM_IN_ORDER_MESSAGE.formatted(itemId, orderId))); @@ -92,7 +92,7 @@ public OrderResponseDto updateStatus(OrderStatusUpdateDto dto, Long orderId) { .orElseThrow( () -> new EntityNotFoundException(NO_ORDER_WITH_SUCH_ID.formatted(orderId ))); - order.setOrderStatus(dto.orderStatus()); + order.setStatus(dto.orderStatus()); return orderMapper.toDto(order); }