Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/main/java/com/senkiv/bookstore/controller/OrderController.java
Original file line number Diff line number Diff line change
@@ -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<OrderResponseDto> 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<OrderItemResponseDto> 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();
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/senkiv/bookstore/dto/OrderItemResponseDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.senkiv.bookstore.dto;

import java.math.BigDecimal;

public record OrderItemResponseDto(
Long id,
Long bookId,
int quantity,
BigDecimal price
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/senkiv/bookstore/dto/OrderRequestDto.java
Original file line number Diff line number Diff line change
@@ -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
) {
}
15 changes: 15 additions & 0 deletions src/main/java/com/senkiv/bookstore/dto/OrderResponseDto.java
Original file line number Diff line number Diff line change
@@ -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<OrderItemResponseDto> orderItems) {
}
10 changes: 10 additions & 0 deletions src/main/java/com/senkiv/bookstore/dto/OrderStatusUpdateDto.java
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ public ResponseEntity<String> handleCategoryExistsException(CategoryExistsExcept
return new ResponseEntity<>(ex.getMessage(), HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(exception = OrderProcessingException.class)
public ResponseEntity<String> 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();
Expand All @@ -54,4 +59,3 @@ private static String getErrorMessage(ObjectError e) {
return e.getDefaultMessage();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.senkiv.bookstore.exception;

public class OrderProcessingException extends RuntimeException {
public OrderProcessingException(String message) {
super(message);
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/senkiv/bookstore/mapper/OrderItemMapper.java
Original file line number Diff line number Diff line change
@@ -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<OrderItemResponseDto> map(Set<OrderItem> orderItems);

@Named("bookPrice")
static BigDecimal getBookPrice(Book book) {
return book.getPrice();
}

@Named("bookId")
default Long getBookId(Book book) {
return book.getId();
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/senkiv/bookstore/mapper/OrderMapper.java
Original file line number Diff line number Diff line change
@@ -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);
}
60 changes: 60 additions & 0 deletions src/main/java/com/senkiv/bookstore/model/Order.java
Original file line number Diff line number Diff line change
@@ -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<OrderItem> orderItems = new HashSet<>();
@Column(columnDefinition = "TINYINT", nullable = false)
private boolean isDeleted;
}
40 changes: 40 additions & 0 deletions src/main/java/com/senkiv/bookstore/model/OrderItem.java
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions src/main/java/com/senkiv/bookstore/model/OrderStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.senkiv.bookstore.model;

public enum OrderStatus {
CREATED,
IN_PROGRESS,
COMPLETED
}
10 changes: 10 additions & 0 deletions src/main/java/com/senkiv/bookstore/model/ShoppingCart.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,14 @@ public class ShoppingCart {
private boolean isDeleted;
@OneToMany(mappedBy = "shoppingCart", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<CartItem> cartItems = new HashSet<>();

public void clearShoppingCart(ShoppingCart shoppingCart) {
shoppingCart.getCartItems()
.forEach(item -> item.setShoppingCart(null));
shoppingCart.getCartItems().clear();
}

public boolean isEmpty() {
return cartItems.isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Book, Long>, JpaSpecificationExecutor<Book> {
boolean existsBookByIsbn(String isbn);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CartItem, Long> {
Set<CartItem> findCartItemByShoppingCartId(Long id);
}
Original file line number Diff line number Diff line change
@@ -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<OrderItem, Long> {
Page<OrderItem> findAllByOrderId(Long orderId, Pageable pageable);

Optional<OrderItem> findByIdAndOrderIdAndOrderUserId(Long itemId, Long orderId, Long userId);
}
16 changes: 16 additions & 0 deletions src/main/java/com/senkiv/bookstore/repository/OrderRepository.java
Original file line number Diff line number Diff line change
@@ -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<Order, Long> {
@EntityGraph("order-with-items")
Page<Order> findOrdersByUserId(Long userId, Pageable pageable);

boolean existsByIdAndUserId(Long orderId, Long userId);
}
Loading