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..86a1f3e --- /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 @Valid 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..99fd744 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java @@ -0,0 +1,11 @@ +package com.senkiv.bookstore.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record OrderRequestDto( + @Size(max = 255, message = "Shipping address must not exceed 255 characters") + @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 new file mode 100644 index 0000000..8c29819 --- /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 status, + 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..c0d2efa --- /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.NotNull; + +public record OrderStatusUpdateDto( + @NotNull + OrderStatus orderStatus +) { +} 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/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/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..a35d826 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/model/Order.java @@ -0,0 +1,60 @@ +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.CreationTimestamp; +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(nullable = false) + private User user; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private OrderStatus status = OrderStatus.CREATED; + @Column(nullable = false) + private BigDecimal total = BigDecimal.ZERO; + @CreationTimestamp + @Column(nullable = false) + private LocalDateTime orderDate; + @Column(nullable = false) + private String shippingAddress; + @OneToMany(mappedBy = "order", + cascade = CascadeType.ALL, orphanRemoval = true) + private Set orderItems = new HashSet<>(); + @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 new file mode 100644 index 0000000..cbc9fa7 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/model/OrderItem.java @@ -0,0 +1,40 @@ +package com.senkiv.bookstore.model; + +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.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(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Order order; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false) + private Book book; + @Column(nullable = false) + private int quantity; + @Column(nullable = false) + private BigDecimal price; + @Column(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/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/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..3bdf401 --- /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 findByIdAndOrderIdAndOrderUserId(Long itemId, Long orderId, Long userId); +} 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/impl/OrderServiceImpl.java b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java new file mode 100644 index 0000000..20940f1 --- /dev/null +++ b/src/main/java/com/senkiv/bookstore/service/impl/OrderServiceImpl.java @@ -0,0 +1,129 @@ +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.OrderProcessingException; +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 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 { + private static final String TOTAL_CALCULATION_ERR_MESSAGE = + "Error during total price calculation."; + private static final String NO_ORDER_FOR_USER_MESSAGE = + "User with id %s does not have order with id %s"; + private static final String NO_ITEM_IN_ORDER_MESSAGE = + "There is no item with id %s in order with id %s"; + 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; + + @Override + public OrderResponseDto createOrder(Long userId, OrderRequestDto orderRequestDto) { + ShoppingCart shoppingCart = getShoppingCart(userId); + User owner = shoppingCart.getUser(); + if (shoppingCart.isEmpty()) { + throw new OrderProcessingException(EMPTY_CART_MESSAGE); + } + Order order = createEmptyOrder(owner); + order.setShippingAddress(orderRequestDto.shippingAddress()); + Set orderItems = transferOrderItemsFromCartItems(shoppingCart, order); + order.setOrderItems(orderItems); + order.setTotal(calculateTotalPrice(order)); + orderRepository.save(order); + shoppingCart.clearShoppingCart(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) { + return orderItemRepository.findByIdAndOrderIdAndOrderUserId(itemId, orderId, userId) + .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.setStatus(dto.orderStatus()); + return orderMapper.toDto(order); + } + + 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 OrderProcessingException( + TOTAL_CALCULATION_ERR_MESSAGE)); + } +} 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