Skip to content

Commit

Permalink
Merge pull request #10 from viacheslav-torbin/shopping-cart
Browse files Browse the repository at this point in the history
Shopping cart implemented
  • Loading branch information
viacheslav-torbin authored Nov 18, 2023
2 parents 1441c19 + 580bf31 commit 23317ca
Show file tree
Hide file tree
Showing 17 changed files with 439 additions and 1 deletion.
77 changes: 77 additions & 0 deletions src/main/java/org/bookstore/controller/ShoppingCartController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.bookstore.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.bookstore.dto.shoppingcart.CartItemRequestDto;
import org.bookstore.dto.shoppingcart.CartItemUpdateDto;
import org.bookstore.dto.shoppingcart.ShoppingCartDto;
import org.bookstore.model.User;
import org.bookstore.service.ShoppingCartService;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
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 = "Shopping cart management",
description = "Endpoints for managing user`s shopping carts")
@RestController
@RequiredArgsConstructor
@Validated
@RequestMapping(value = "/cart")
public class ShoppingCartController {
private final ShoppingCartService cartService;

@Operation(summary = "Get the shopping cart of a user")
@GetMapping
@PreAuthorize("hasRole('USER')")
public ShoppingCartDto getShoppingCart(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return cartService.getCart(user.getId());
}

@Operation(summary = "Add a book to the shopping cart", description = """
Add a new book or update the quantity of existing in the cart of user""")
@PostMapping
@PreAuthorize("hasRole('USER')")
public ShoppingCartDto addBook(Authentication authentication,
@RequestBody @Valid CartItemRequestDto cartItem) {
User user = (User) authentication.getPrincipal();
return cartService.addBook(user.getId(), cartItem);
}

@Operation(summary = "Update the number of books in the cart", description = """
Update the number of existing books in the cart of a logged-in user.
Parameters : cart item id and new quantity""")
@PutMapping("/cart-items/{cartItemId}")
@PreAuthorize("hasRole('USER')")
public ShoppingCartDto updateBookQuantity(Authentication authentication,
@PathVariable @Positive Long cartItemId,
@RequestBody @Valid CartItemUpdateDto item) {
User user = (User) authentication.getPrincipal();
return cartService.updateQuantity(user.getId(), cartItemId, item);
}

@Operation(summary = "Delete book from the cart of user", description = """
Delete book from the cart of a user.
Parameter: id of cart item""")
@DeleteMapping("/cart-items/{cartItemId}")
@PreAuthorize("hasRole('USER')")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteBookFromCart(Authentication authentication,
@PathVariable @Positive Long cartItemId) {
User user = (User) authentication.getPrincipal();
cartService.deleteBook(user.getId(), cartItemId);
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/bookstore/dto/shoppingcart/CartItemDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.bookstore.dto.shoppingcart;

public record CartItemDto(Long id,
Long bookId,
String bookTitle,
Integer quantity) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.bookstore.dto.shoppingcart;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record CartItemRequestDto(@NotNull @Positive Long bookId,
@NotNull @Positive Integer quantity) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.bookstore.dto.shoppingcart;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

public record CartItemUpdateDto(@NotNull @Positive Integer quantity) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.bookstore.dto.shoppingcart;

import java.util.List;

public record ShoppingCartDto(Long id,
Long userId,
List<CartItemDto> cartItems) {
}
24 changes: 24 additions & 0 deletions src/main/java/org/bookstore/mapper/CartItemMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.bookstore.mapper;

import org.bookstore.dto.shoppingcart.CartItemDto;
import org.bookstore.dto.shoppingcart.CartItemRequestDto;
import org.bookstore.model.CartItem;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.NullValueCheckStrategy;

@Mapper(
componentModel = "spring",
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
implementationPackage = "<PACKAGE_NAME>.impl"
)
public interface CartItemMapper {
@Mapping(source = "book.id", target = "bookId")
@Mapping(source = "book.title", target = "bookTitle")
CartItemDto toDto(CartItem cartItem);

CartItem toCartItem(CartItemRequestDto itemDto);
}

25 changes: 25 additions & 0 deletions src/main/java/org/bookstore/mapper/ShoppingCartMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.bookstore.mapper;

import org.bookstore.dto.shoppingcart.CartItemDto;
import org.bookstore.dto.shoppingcart.ShoppingCartDto;
import org.bookstore.model.CartItem;
import org.bookstore.model.ShoppingCart;
import org.mapstruct.InjectionStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.NullValueCheckStrategy;

@Mapper(
componentModel = "spring",
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
implementationPackage = "<PACKAGE_NAME>.impl"
)
public interface ShoppingCartMapper {
@Mapping(source = "user.id", target = "userId")
ShoppingCartDto toDto(ShoppingCart shoppingCart);

@Mapping(source = "book.id", target = "bookId")
@Mapping(source = "book.title", target = "bookTitle")
CartItemDto toCartItemDto(CartItem cartItem);
}
35 changes: 35 additions & 0 deletions src/main/java/org/bookstore/model/CartItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.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.OneToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
@Table(name = "cart_items")
public class CartItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@JoinColumn(name = "shopping_cart_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
private ShoppingCart shoppingCart;

@JoinColumn(name = "book_id", nullable = false)
@OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true)
private Book book;

@Column(nullable = false)
private int quantity;
}
45 changes: 45 additions & 0 deletions src/main/java/org/bookstore/model/ShoppingCart.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.bookstore.model;

import jakarta.persistence.CascadeType;
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.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Entity
@Data
@Table(name = "shopping_carts")
public class ShoppingCart {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "user_id", nullable = false)
@EqualsAndHashCode.Exclude
@ToString.Exclude
private User user;

@OneToMany(mappedBy = "shoppingCart", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<CartItem> cartItems = new HashSet<>();

public void addItemToCart(CartItem cartItem) {
cartItem.setShoppingCart(this);
cartItems.add(cartItem);
}

public void removeItemFromCart(CartItem cartItem) {
cartItems.remove(cartItem);
cartItem.setShoppingCart(null);
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/bookstore/repository/CartItemRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.bookstore.repository;

import java.util.Optional;
import org.bookstore.model.CartItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface CartItemRepository extends JpaRepository<CartItem, Long> {
Optional<CartItem> findByIdAndShoppingCartId(Long cartItemId, Long id);
}
12 changes: 12 additions & 0 deletions src/main/java/org/bookstore/repository/ShoppingCartRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.bookstore.repository;

import org.bookstore.model.ShoppingCart;
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<ShoppingCart, Long> {
@EntityGraph(attributePaths = {"cartItems", "user", "cartItems.book"})
ShoppingCart findByUserId(Long id);
}
18 changes: 18 additions & 0 deletions src/main/java/org/bookstore/service/ShoppingCartService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.bookstore.service;

import org.bookstore.dto.shoppingcart.CartItemRequestDto;
import org.bookstore.dto.shoppingcart.CartItemUpdateDto;
import org.bookstore.dto.shoppingcart.ShoppingCartDto;
import org.bookstore.model.User;

public interface ShoppingCartService {
void createShoppingCart(User user);

ShoppingCartDto getCart(Long userId);

ShoppingCartDto addBook(Long userId, CartItemRequestDto item);

ShoppingCartDto updateQuantity(Long userId, Long cartItemId, CartItemUpdateDto itemDto);

void deleteBook(Long userId, Long itemId);
}
103 changes: 103 additions & 0 deletions src/main/java/org/bookstore/service/impl/ShoppingCartServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.bookstore.service.impl;

import jakarta.transaction.Transactional;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.bookstore.dto.shoppingcart.CartItemRequestDto;
import org.bookstore.dto.shoppingcart.CartItemUpdateDto;
import org.bookstore.dto.shoppingcart.ShoppingCartDto;
import org.bookstore.exceptions.EntityNotFoundException;
import org.bookstore.mapper.CartItemMapper;
import org.bookstore.mapper.ShoppingCartMapper;
import org.bookstore.model.Book;
import org.bookstore.model.CartItem;
import org.bookstore.model.ShoppingCart;
import org.bookstore.model.User;
import org.bookstore.repository.BookRepository;
import org.bookstore.repository.CartItemRepository;
import org.bookstore.repository.ShoppingCartRepository;
import org.bookstore.service.ShoppingCartService;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ShoppingCartServiceImpl implements ShoppingCartService {
private final CartItemRepository cartItemRepository;
private final ShoppingCartRepository shoppingCartRepository;
private final BookRepository bookRepository;
private final ShoppingCartMapper shoppingCartMapper;
private final CartItemMapper cartItemMapper;

@Transactional
@Override
public void createShoppingCart(User user) {
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUser(user);
shoppingCartRepository.save(shoppingCart);
}

@Transactional
@Override
public ShoppingCartDto getCart(Long userId) {
return shoppingCartMapper.toDto(shoppingCartRepository.findByUserId(userId));
}

@Transactional
@Override
public ShoppingCartDto addBook(Long userId, CartItemRequestDto itemDto) {
Book book = bookRepository.findById(itemDto.bookId())
.orElseThrow(() -> new EntityNotFoundException(
String.format("Book with id: %d is not found", itemDto.bookId())
));
ShoppingCart cart = shoppingCartRepository.findByUserId(userId);
Set<CartItem> cartItems = cart.getCartItems();
for (CartItem item : cartItems) {
if (Objects.equals(item.getBook().getId(), itemDto.bookId())) {
item.setQuantity(item.getQuantity() + itemDto.quantity());
shoppingCartRepository.save(cart);
return shoppingCartMapper.toDto(cart);
}
}
addCartItemToShoppingCart(itemDto, book, cart);
shoppingCartRepository.save(cart);
return shoppingCartMapper.toDto(cart);
}

@Transactional
@Override
public ShoppingCartDto updateQuantity(Long userId,
Long cartItemId, CartItemUpdateDto itemDto) {
ShoppingCart cart = shoppingCartRepository.findByUserId(userId);
CartItem cartItem = cartItemRepository
.findByIdAndShoppingCartId(cartItemId, cart.getId())
.map(item -> {
item.setQuantity(itemDto.quantity());
return item;
}).orElseThrow(() -> new EntityNotFoundException(
String.format("No cart item with id: %d for user: %d", cartItemId, userId)
));
cartItemRepository.save(cartItem);
return shoppingCartMapper.toDto(cart);
}

@Transactional
@Override
public void deleteBook(Long userId, Long itemId) {
ShoppingCart shoppingCart = shoppingCartRepository.findByUserId(userId);
CartItem cartItem = cartItemRepository
.findByIdAndShoppingCartId(itemId, shoppingCart.getId())
.orElseThrow(() -> new EntityNotFoundException(
String.format("No such cartItem with id %d in shopping cart", itemId)
));
shoppingCart.removeItemFromCart(cartItem);
}

private void addCartItemToShoppingCart(CartItemRequestDto itemDto,
Book book,
ShoppingCart cart) {
CartItem cartItem = cartItemMapper.toCartItem(itemDto);
cartItem.setBook(book);
cart.addItemToCart(cartItem);
}
}
Loading

0 comments on commit 23317ca

Please sign in to comment.