diff --git a/src/main/java/mate/academy/bookshop/controller/OrderController.java b/src/main/java/mate/academy/bookshop/controller/OrderController.java new file mode 100644 index 0000000..dee84a9 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/controller/OrderController.java @@ -0,0 +1,85 @@ +package mate.academy.bookshop.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.bookshop.dto.order.OrderRequestDto; +import mate.academy.bookshop.dto.order.OrderResponseDto; +import mate.academy.bookshop.dto.order.OrderUpdateStatusDto; +import mate.academy.bookshop.dto.orderitem.OrderItemResponseDto; +import mate.academy.bookshop.model.User; +import mate.academy.bookshop.service.order.OrderService; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +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; + +@RestController +@RequiredArgsConstructor +@Tag(name = "Orders", description = "Operations for managing orders") +@RequestMapping("/orders") +public class OrderController { + private final OrderService orderService; + + @Operation(summary = "Get all orders for the authenticated user", + description = "Retrieves a list of orders for the currently authenticated user.") + @GetMapping + public List getOrders(Pageable pageable, Authentication authentication) { + Long authenticatedUserId = getAuthenticatedUserId(authentication); + return orderService.getOrderByUserId(pageable, authenticatedUserId); + } + + @Operation(summary = "Create a new order", + description = "Creates a new order for the authenticated user.") + @PostMapping + public OrderResponseDto createOrder(Authentication authentication, + @RequestBody @Valid OrderRequestDto orderRequestDto) { + Long authenticatedUserId = getAuthenticatedUserId(authentication); + return orderService.createOrderByUserId(authenticatedUserId, + orderRequestDto); + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @Operation(summary = "Update the status of an order", + description = "Updates the status of an order identified" + + " by orderId for the authenticated user.") + @PatchMapping("/{orderId}") + public OrderResponseDto updateStatus( + @PathVariable Long orderId, + @RequestBody @Valid OrderUpdateStatusDto orderUpdateStatusDto) { + return orderService.updateStatusByOrderId(orderId, orderUpdateStatusDto); + } + + @Operation(summary = "Get all items in a specific order", + description = "Retrieves the list of items for a specific" + + " order identified by orderId for the authenticated user.") + @GetMapping("/{orderId}/items") + public List getOrderItems(@PathVariable Long orderId, + Authentication authentication) { + Long authenticatedUserId = getAuthenticatedUserId(authentication); + return orderService.getOrderItemsByOrderId(orderId, authenticatedUserId); + } + + @Operation(summary = "Get a specific item from an order", + description = "Retrieves a specific item from an order " + + "by itemId, within the order identified by orderId.") + @GetMapping("/{orderId}/items/{itemId}") + public OrderItemResponseDto getOrderItem(@PathVariable Long orderId, + @PathVariable Long itemId, + Authentication authentication) { + Long authenticatedUserId = getAuthenticatedUserId(authentication); + return orderService.getOrderItemFromOrderById(orderId, itemId, authenticatedUserId); + } + + private Long getAuthenticatedUserId(Authentication authentication) { + return ((User) authentication.getPrincipal()).getId(); + } +} diff --git a/src/main/java/mate/academy/bookshop/dto/order/OrderRequestDto.java b/src/main/java/mate/academy/bookshop/dto/order/OrderRequestDto.java new file mode 100644 index 0000000..4387381 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/dto/order/OrderRequestDto.java @@ -0,0 +1,10 @@ +package mate.academy.bookshop.dto.order; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class OrderRequestDto { + @NotBlank + private String shippingAddress; +} diff --git a/src/main/java/mate/academy/bookshop/dto/order/OrderResponseDto.java b/src/main/java/mate/academy/bookshop/dto/order/OrderResponseDto.java new file mode 100644 index 0000000..39b2036 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/dto/order/OrderResponseDto.java @@ -0,0 +1,22 @@ +package mate.academy.bookshop.dto.order; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Data; +import mate.academy.bookshop.dto.orderitem.OrderItemResponseDto; + +@Data +public class OrderResponseDto { + private Long id; + + private Long userId; + + private List orderItemsDto; + + private LocalDateTime orderDate; + + private BigDecimal total; + + private String status; +} diff --git a/src/main/java/mate/academy/bookshop/dto/order/OrderUpdateStatusDto.java b/src/main/java/mate/academy/bookshop/dto/order/OrderUpdateStatusDto.java new file mode 100644 index 0000000..800bf59 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/dto/order/OrderUpdateStatusDto.java @@ -0,0 +1,6 @@ +package mate.academy.bookshop.dto.order; + +import jakarta.validation.constraints.NotBlank; + +public record OrderUpdateStatusDto(@NotBlank String status) { +} diff --git a/src/main/java/mate/academy/bookshop/dto/orderitem/OrderItemResponseDto.java b/src/main/java/mate/academy/bookshop/dto/orderitem/OrderItemResponseDto.java new file mode 100644 index 0000000..75ddbd9 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/dto/orderitem/OrderItemResponseDto.java @@ -0,0 +1,15 @@ +package mate.academy.bookshop.dto.orderitem; + +import java.math.BigDecimal; +import lombok.Data; + +@Data +public class OrderItemResponseDto { + private Long id; + + private Long bookId; + + private int quantity; + + private BigDecimal price; +} diff --git a/src/main/java/mate/academy/bookshop/exceptions/OrderProcessingException.java b/src/main/java/mate/academy/bookshop/exceptions/OrderProcessingException.java new file mode 100644 index 0000000..da88ef4 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/exceptions/OrderProcessingException.java @@ -0,0 +1,8 @@ +package mate.academy.bookshop.exceptions; + +public class OrderProcessingException extends RuntimeException { + public OrderProcessingException(String message) { + super(message); + } +} + diff --git a/src/main/java/mate/academy/bookshop/mapper/OrderItemMapper.java b/src/main/java/mate/academy/bookshop/mapper/OrderItemMapper.java new file mode 100644 index 0000000..9e80369 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/mapper/OrderItemMapper.java @@ -0,0 +1,20 @@ +package mate.academy.bookshop.mapper; + +import java.util.List; +import mate.academy.bookshop.config.MapperConfig; +import mate.academy.bookshop.dto.orderitem.OrderItemResponseDto; +import mate.academy.bookshop.model.OrderItem; +import org.mapstruct.IterableMapping; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapperConfig.class) +public interface OrderItemMapper { + @Named("orderItemToDto") + @Mapping(target = "bookId", source = "book.id") + OrderItemResponseDto toDto(OrderItem orderItem); + + @IterableMapping(qualifiedByName = "orderItemToDto") + List toOrderItemDtoList(List orderItems); +} diff --git a/src/main/java/mate/academy/bookshop/mapper/OrderMapper.java b/src/main/java/mate/academy/bookshop/mapper/OrderMapper.java new file mode 100644 index 0000000..2b81581 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/mapper/OrderMapper.java @@ -0,0 +1,27 @@ +package mate.academy.bookshop.mapper; + +import java.util.List; +import mate.academy.bookshop.config.MapperConfig; +import mate.academy.bookshop.dto.order.OrderRequestDto; +import mate.academy.bookshop.dto.order.OrderResponseDto; +import mate.academy.bookshop.dto.order.OrderUpdateStatusDto; +import mate.academy.bookshop.model.Order; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; + +@Mapper(config = MapperConfig.class, uses = OrderItemMapper.class) +public interface OrderMapper { + @Mapping(target = "userId", source = "user.id") + @Mapping(target = "orderItemsDto", source = "orderItems", qualifiedByName = "orderItemToDto") + @Mapping(target = "status", expression = "java(order.getStatus().name())") + @Mapping(target = "orderDate", source = "orderDate", dateFormat = "yyyy-MM-dd HH:mm:ss") + OrderResponseDto toDto(Order order); + + List toDto(List order); + + Order toEntity(OrderRequestDto orderRequestDto); + + void updateOrderStatusFromDto(OrderUpdateStatusDto orderUpdateStatusDto, + @MappingTarget Order order); +} diff --git a/src/main/java/mate/academy/bookshop/model/Order.java b/src/main/java/mate/academy/bookshop/model/Order.java new file mode 100644 index 0000000..02c4e24 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/model/Order.java @@ -0,0 +1,62 @@ +package mate.academy.bookshop.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.OneToMany; +import jakarta.persistence.Table; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Set; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Getter +@Setter +@SQLDelete(sql = "UPDATE orders SET is_deleted = true WHERE id=?") +@SQLRestriction(value = "is_deleted = false") +@Table(name = "orders") +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Status status; + + @Column(nullable = false) + private BigDecimal total; + + @Column(nullable = false) + private LocalDateTime orderDate; + + @Column(nullable = false) + private String shippingAddress; + + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true) + private Set orderItems; + + private boolean isDeleted = false; + + public enum Status { + PENDING, + CONFIRMED, + DELIVERED, + } +} diff --git a/src/main/java/mate/academy/bookshop/model/OrderItem.java b/src/main/java/mate/academy/bookshop/model/OrderItem.java new file mode 100644 index 0000000..6ae644d --- /dev/null +++ b/src/main/java/mate/academy/bookshop/model/OrderItem.java @@ -0,0 +1,38 @@ +package mate.academy.bookshop.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; + +@Entity +@Getter +@Setter +@Table(name = "order_items") +public class OrderItem { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "order_id", nullable = false) + private Order order; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "book_id", nullable = false) + private Book book; + + @Column(nullable = false) + private int quantity; + + @Column(nullable = false) + private BigDecimal price; +} diff --git a/src/main/java/mate/academy/bookshop/repository/OrderItemRepository.java b/src/main/java/mate/academy/bookshop/repository/OrderItemRepository.java new file mode 100644 index 0000000..4a465b0 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/repository/OrderItemRepository.java @@ -0,0 +1,15 @@ +package mate.academy.bookshop.repository; + +import java.util.List; +import java.util.Optional; +import mate.academy.bookshop.model.Order; +import mate.academy.bookshop.model.OrderItem; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrderItemRepository extends JpaRepository { + List findAllByOrder(Order order); + + Optional findByIdAndOrderIdAndOrderUserId(Long orderItemId, + Long orderId, + Long userId); +} diff --git a/src/main/java/mate/academy/bookshop/repository/OrderRepository.java b/src/main/java/mate/academy/bookshop/repository/OrderRepository.java new file mode 100644 index 0000000..fefe783 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/repository/OrderRepository.java @@ -0,0 +1,16 @@ +package mate.academy.bookshop.repository; + +import java.util.List; +import java.util.Optional; +import mate.academy.bookshop.model.Order; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OrderRepository extends JpaRepository { + @EntityGraph(attributePaths = {"orderItems", "orderItems.book"}) + List findAllByUserId(Pageable pageable, Long userId); + + @EntityGraph(attributePaths = {"orderItems", "orderItems.book"}) + Optional findByIdAndUserId(Long orderId, Long userId); +} diff --git a/src/main/java/mate/academy/bookshop/service/order/OrderService.java b/src/main/java/mate/academy/bookshop/service/order/OrderService.java new file mode 100644 index 0000000..96c0fbb --- /dev/null +++ b/src/main/java/mate/academy/bookshop/service/order/OrderService.java @@ -0,0 +1,23 @@ +package mate.academy.bookshop.service.order; + +import java.util.List; +import mate.academy.bookshop.dto.order.OrderRequestDto; +import mate.academy.bookshop.dto.order.OrderResponseDto; +import mate.academy.bookshop.dto.order.OrderUpdateStatusDto; +import mate.academy.bookshop.dto.orderitem.OrderItemResponseDto; +import org.springframework.data.domain.Pageable; + +public interface OrderService { + OrderResponseDto createOrderByUserId(Long userId, OrderRequestDto orderRequestDto); + + List getOrderByUserId(Pageable pageable, Long userId); + + OrderResponseDto updateStatusByOrderId(Long orderId, + OrderUpdateStatusDto orderUpdateStatusDto); + + List getOrderItemsByOrderId(Long orderId, Long userId); + + OrderItemResponseDto getOrderItemFromOrderById(Long orderId, + Long orderItemId, + Long userId); +} diff --git a/src/main/java/mate/academy/bookshop/service/order/OrderServiceImpl.java b/src/main/java/mate/academy/bookshop/service/order/OrderServiceImpl.java new file mode 100644 index 0000000..98839e7 --- /dev/null +++ b/src/main/java/mate/academy/bookshop/service/order/OrderServiceImpl.java @@ -0,0 +1,125 @@ +package mate.academy.bookshop.service.order; + +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.bookshop.dto.order.OrderRequestDto; +import mate.academy.bookshop.dto.order.OrderResponseDto; +import mate.academy.bookshop.dto.order.OrderUpdateStatusDto; +import mate.academy.bookshop.dto.orderitem.OrderItemResponseDto; +import mate.academy.bookshop.exceptions.EntityNotFoundException; +import mate.academy.bookshop.exceptions.OrderProcessingException; +import mate.academy.bookshop.mapper.OrderItemMapper; +import mate.academy.bookshop.mapper.OrderMapper; +import mate.academy.bookshop.model.CartItem; +import mate.academy.bookshop.model.Order; +import mate.academy.bookshop.model.OrderItem; +import mate.academy.bookshop.model.ShoppingCart; +import mate.academy.bookshop.repository.OrderItemRepository; +import mate.academy.bookshop.repository.OrderRepository; +import mate.academy.bookshop.repository.ShoppingCartRepository; +import mate.academy.bookshop.repository.UserRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class OrderServiceImpl implements OrderService { + private final OrderItemMapper orderItemMapper; + private final OrderMapper orderMapper; + private final OrderRepository orderRepository; + private final OrderItemRepository orderItemRepository; + private final ShoppingCartRepository shoppingCartRepository; + private final UserRepository userRepository; + + @Override + public OrderResponseDto createOrderByUserId(Long userId, OrderRequestDto orderRequestDto) { + ShoppingCart shoppingCart = shoppingCartRepository + .findByUserId(userId) + .orElseThrow(() -> new EntityNotFoundException("Shopping cart not found" + + " for user with id: " + userId + )); + if (shoppingCart.getCartItems().isEmpty()) { + throw new OrderProcessingException("Shopping cart is empty " + + "for user id: " + userId); + } + Order order = orderMapper.toEntity(orderRequestDto); + order.setUser(userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User with id " + + userId + " not found"))); + order.setStatus(Order.Status.PENDING); + order.setOrderDate(LocalDateTime.now()); + + Set orderItemSet = shoppingCart + .getCartItems() + .stream() + .map(cart -> mapToOrderItem(cart, order)) + .collect(Collectors.toSet()); + + order.setOrderItems(orderItemSet); + + BigDecimal totalPrice = orderItemSet + .stream() + .map(OrderItem::getPrice) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + order.setTotal(totalPrice); + orderRepository.save(order); + return orderMapper.toDto(order); + } + + @Override + public List getOrderByUserId(Pageable pageable, Long userId) { + List ordersByUser = orderRepository.findAllByUserId(pageable, userId); + return orderMapper.toDto(ordersByUser); + } + + @Override + public OrderResponseDto updateStatusByOrderId(Long orderId, + OrderUpdateStatusDto orderUpdateStatusDto) { + Order order = orderRepository + .findById(orderId) + .orElseThrow(() -> new EntityNotFoundException("Order not found with id: " + + orderId + )); + orderMapper.updateOrderStatusFromDto(orderUpdateStatusDto, order); + orderRepository.save(order); + return orderMapper.toDto(order); + } + + @Override + public List getOrderItemsByOrderId(Long orderId, Long userId) { + Order order = orderRepository + .findByIdAndUserId(orderId, userId) + .orElseThrow(() -> new EntityNotFoundException("Order not found " + + "for order id: " + orderId + " and user id: " + userId + )); + return orderItemMapper.toOrderItemDtoList(orderItemRepository.findAllByOrder(order)); + } + + @Override + public OrderItemResponseDto getOrderItemFromOrderById(Long orderId, + Long orderItemId, + Long userId) { + OrderItem orderItem = orderItemRepository + .findByIdAndOrderIdAndOrderUserId(orderItemId, orderId, userId) + .orElseThrow(() -> new EntityNotFoundException("Can't find order item by order id: " + + orderId + " and order item id: " + orderItemId)); + + return orderItemMapper.toDto(orderItem); + } + + private OrderItem mapToOrderItem(CartItem cart, Order order) { + OrderItem orderItem = new OrderItem(); + orderItem.setOrder(order); + orderItem.setBook(cart.getBook()); + orderItem.setQuantity(cart.getQuantity()); + orderItem.setPrice(cart.getBook().getPrice().multiply(new BigDecimal(cart.getQuantity()))); + return orderItem; + } +} diff --git a/src/main/java/mate/academy/bookshop/service/shoppingcart/ShoppingCartServiceImpl.java b/src/main/java/mate/academy/bookshop/service/shoppingcart/ShoppingCartServiceImpl.java index 660abb7..5ee360b 100644 --- a/src/main/java/mate/academy/bookshop/service/shoppingcart/ShoppingCartServiceImpl.java +++ b/src/main/java/mate/academy/bookshop/service/shoppingcart/ShoppingCartServiceImpl.java @@ -52,7 +52,7 @@ public ShoppingCartResponseDto addCartItem(CartItemRequestDto cartItemUpdateDto, .findFirst(); if (existingCartItem.isPresent()) { - CartItem cartItem = existingCartItem.get(); + CartItem cartItem = existingCartItem.orElseThrow(); cartItem.setQuantity(cartItem.getQuantity() + cartItemUpdateDto.getQuantity()); } else { CartItem cartItem = cartItemMapper.toEntity(cartItemUpdateDto); diff --git a/src/main/resources/db/changelog/changes/13-create-orders-table.yaml b/src/main/resources/db/changelog/changes/13-create-orders-table.yaml new file mode 100644 index 0000000..f64ab41 --- /dev/null +++ b/src/main/resources/db/changelog/changes/13-create-orders-table.yaml @@ -0,0 +1,49 @@ +databaseChangeLog: + - changeSet: + id: create-orders-table + author: trokhim + 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_order_user + referencedTableName: users + referencedColumnNames: id + nullable: false + - column: + name: status + type: varchar(255) + constraints: + nullable: false + - column: + name: total + type: decimal(19,2) + 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 + defaultValueBoolean: false + constraints: + nullable: false diff --git a/src/main/resources/db/changelog/changes/14-create-order-items-table.yaml b/src/main/resources/db/changelog/changes/14-create-order-items-table.yaml new file mode 100644 index 0000000..5cf505d --- /dev/null +++ b/src/main/resources/db/changelog/changes/14-create-order-items-table.yaml @@ -0,0 +1,41 @@ +databaseChangeLog: + - changeSet: + id: create-order-items-table + author: trokhim + 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_item_order + referencedTableName: orders + referencedColumnNames: id + nullable: false + - column: + name: book_id + type: bigint + constraints: + foreignKeyName: fk_order_item_book + referencedTableName: books + referencedColumnNames: id + nullable: false + - column: + name: quantity + type: int + constraints: + nullable: false + - column: + name: price + type: decimal(19,2) + 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 21b6003..aa5b67c 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/11-create-cart-items-table.yaml - include: file: db/changelog/changes/12-insert-shopping-carts.yaml + - include: + file: db/changelog/changes/13-create-orders-table.yaml + - include: + file: db/changelog/changes/14-create-order-items-table.yaml