diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/AddProductResponse.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/AddProductResponse.java deleted file mode 100644 index fdac6b8..0000000 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/AddProductResponse.java +++ /dev/null @@ -1,3 +0,0 @@ -package com.kampus.kbazaar.cart; - -public record AddProductResponse(Boolean success, String message) {} diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/Cart.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/Cart.java index ba58757..074738a 100644 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/Cart.java +++ b/kbazaar/src/main/java/com/kampus/kbazaar/cart/Cart.java @@ -1,8 +1,12 @@ package com.kampus.kbazaar.cart; import com.kampus.kbazaar.promotion.Promotion; +import com.kampus.kbazaar.promotion.PromotionDiscount; import com.kampus.kbazaar.shopper.Shopper; import jakarta.persistence.*; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; import java.util.Set; import lombok.AllArgsConstructor; import lombok.Getter; @@ -21,7 +25,7 @@ public class Cart { private Long id; @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL) - private Set products; + private List cartProducts; @ManyToMany(cascade = {CascadeType.ALL}) @JoinTable( @@ -34,9 +38,73 @@ public class Cart { @JoinColumn(name = "shopper_id", referencedColumnName = "id") private Shopper shopper; + public BigDecimal getEntireCartDiscountAmount() { + BigDecimal totalCost = this.getTotalCost(); + Integer allProductQuantity = this.getAllProductQuantity(); + return this.getPromotions().stream() + .filter(prom -> prom.getApplicableTo().equals("ENTIRE_CART")) + .reduce( + BigDecimal.ZERO, + (acc, prom) -> + acc.add( + PromotionDiscount.getDiscountAmountFromPromotion( + totalCost, allProductQuantity, prom)), + BigDecimal::add); + } + + public Integer getAllProductQuantity() { + return this.getCartProducts().size(); + } + + public BigDecimal getTotalCost() { + return this.getCartProducts().stream() + .map(CartProduct::getFinalPrice) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + public BigDecimal getFinalTotalCost() { + return this.getTotalCost().subtract(this.getEntireCartDiscountAmount()); + } + public Cart addPromotion(Promotion promotion) { this.promotions.add(promotion); promotion.getCarts().add(this); return this; } + + public Optional addPromotionToProduct(Promotion promotion, String targetSku) { + Optional cartProduct = + this.getCartProducts().stream() + .filter(filtered -> filtered.getProduct().getSku().equals(targetSku)) + .findFirst(); + + cartProduct.ifPresent(cp -> cp.setPromotion(promotion)); + return cartProduct; + } + + public Cart addProduct(CartProduct cartProduct) { + Optional existingCartProduct = + this.cartProducts.stream() + .filter( + filtered -> + filtered.getProduct() + .getId() + .equals(cartProduct.getProduct().getId())) + .findFirst(); + + if (existingCartProduct.isPresent()) { + CartProduct existingCartProductFirst = existingCartProduct.get(); + existingCartProductFirst.setQuantity( + existingCartProductFirst.getQuantity() + cartProduct.getQuantity()); + return this; + } + + cartProduct.setCart(this); + this.cartProducts.add(cartProduct); + return this; + } + + public CartResponse toCartResponse() { + return CartResponse.fromCart(this); + } } diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartController.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartController.java index 13c0ce4..6cf568b 100644 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartController.java +++ b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartController.java @@ -1,6 +1,5 @@ package com.kampus.kbazaar.cart; -import com.kampus.kbazaar.shopper.ShopperService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -10,30 +9,23 @@ @RequiredArgsConstructor public class CartController { - private final ShopperService shopperService; + private final CartService cartService; @GetMapping("/carts") public ResponseEntity getCart() { // NOSONAR return ResponseEntity.ok().build(); } - @PostMapping("/carts/{username}/products") - public ResponseEntity addProduct( + @PostMapping("/carts/{username}/items") + public CartResponse addProduct( @PathVariable("username") String username, @RequestBody AddProductRequest addProductRequest) { - // Cart cart = cartService.findCartByUsername(username); - // Product product = - // productService.getProductBySku(addProductRequest.getProductSku()); - // if (product.getQuantity() < addProductRequest.getQuantity()) { - // return ResponseEntity.badRequest().build(); - // } - - // return ResponseEntity.ok(cartService - // .findCartByUsername(username)); - // .addProduct(product) - // .toAddProductResponse()); - - return ResponseEntity.ok("hey"); + return cartService + .addProductByUsernameAndProductSku( + username, + addProductRequest.getProductSku(), + addProductRequest.getQuantity()) + .toCartResponse(); } } diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartProduct.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartProduct.java index e65644a..cd6c7a9 100644 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartProduct.java +++ b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartProduct.java @@ -1,7 +1,13 @@ package com.kampus.kbazaar.cart; +import com.kampus.kbazaar.product.Product; +import com.kampus.kbazaar.promotion.Promotion; +import com.kampus.kbazaar.promotion.PromotionDiscount; import jakarta.persistence.*; +import java.math.BigDecimal; +import java.util.Optional; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -9,19 +15,46 @@ @Data @AllArgsConstructor @NoArgsConstructor +@Builder public class CartProduct { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; - @Column(name = "product_id") - private Long productId; + @ManyToOne + @JoinColumn(name = "product_id") + private Product product; - @Column(name = "quantity") - private Integer quantity; + @ManyToOne + @JoinColumn(name = "promotion_id") + private Promotion promotion; @ManyToOne @JoinColumn(name = "cart_id") private Cart cart; + + @Column(name = "quantity") + private Integer quantity; + + public BigDecimal getDiscountAmount() { + return PromotionDiscount.getDiscountAmountFromPromotion( + product.getPrice(), quantity, promotion); + } + + public BigDecimal getPrice() { + return product.getPrice().multiply(BigDecimal.valueOf(quantity)); + } + + public BigDecimal getFinalPrice() { + return this.getPrice().subtract(this.getDiscountAmount()); + } + + public static Optional of(Product product, Integer quantity, Cart cart) { + if (product.getQuantity() < quantity) { + return Optional.empty(); + } + return Optional.of( + CartProduct.builder().product(product).quantity(quantity).cart(cart).build()); + } } diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartRepository.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartRepository.java index 7e5c034..07d439a 100644 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartRepository.java +++ b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartRepository.java @@ -1,7 +1,10 @@ package com.kampus.kbazaar.cart; +import com.kampus.kbazaar.shopper.Shopper; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface CartRepository extends JpaRepository {} +public interface CartRepository extends JpaRepository { + Cart findByShopper(Shopper shopper); +} diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartResponse.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartResponse.java new file mode 100644 index 0000000..919c86f --- /dev/null +++ b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartResponse.java @@ -0,0 +1,56 @@ +package com.kampus.kbazaar.cart; + +import com.kampus.kbazaar.promotion.Promotion; +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class CartResponse { + private Long cartId; + private List items; + private List promotionCodes; + private BigDecimal totalCost; + private BigDecimal entireCartPromotionDiscount; + private BigDecimal finalTotalCost; + + public static CartResponse fromCart(Cart cart) { + List items = cart.getCartProducts().stream().map(CartResponse::initItem).toList(); + + return CartResponse.builder() + .cartId(cart.getId()) + .items(items) + .promotionCodes(cart.getPromotions().stream().map(Promotion::getCode).toList()) + .totalCost(cart.getTotalCost()) + .entireCartPromotionDiscount(cart.getEntireCartDiscountAmount()) + .finalTotalCost(cart.getFinalTotalCost()) + .build(); + } + + private static Item initItem(CartProduct cartProduct) { + return new Item( + cartProduct.getId(), + cartProduct.getProduct().getName(), + cartProduct.getQuantity(), + cartProduct.getPrice(), + cartProduct.getDiscountAmount(), + cartProduct.getFinalPrice(), + Optional.ofNullable(cartProduct.getPromotion()).map(Promotion::getCode).orElse("")); + } +} + +record Item( + Long id, + String name, + Integer quantity, + BigDecimal price, + BigDecimal discount, + BigDecimal finalPrice, + String promotionCode) {} diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartService.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartService.java index 33aab4f..1ba2465 100644 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartService.java +++ b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartService.java @@ -1,14 +1,39 @@ package com.kampus.kbazaar.cart; +import com.kampus.kbazaar.exceptions.InternalServerException; +import com.kampus.kbazaar.exceptions.NotFoundException; +import com.kampus.kbazaar.product.Product; +import com.kampus.kbazaar.product.ProductService; +import com.kampus.kbazaar.shopper.ShopperService; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class CartService { + private final ShopperService shopperService; + private final ProductService productService; private final CartRepository cartRepository; public Cart findCartByUsername(String username) { - return null; + return Optional.of(username) + .map(shopperService::getShopperByUsername) + .map(cartRepository::findByShopper) + .orElseThrow(() -> new NotFoundException("Failed to find cart by username")); + } + + public Cart addProductByUsernameAndProductSku( + String username, String productSku, Integer quantity) { + Product product = productService.getProductBySku(productSku); + Cart cart = findCartByUsername(username); + // TODO: add deduct product stock logic here + return CartProduct.of(product, quantity, cart) + .map(cart::addProduct) + .map(cartRepository::save) + .orElseThrow( + () -> + new InternalServerException( + "Product out of stock or failed to add product to cart")); } } diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartTemp.java b/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartTemp.java deleted file mode 100644 index b9cbca6..0000000 --- a/kbazaar/src/main/java/com/kampus/kbazaar/cart/CartTemp.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.kampus.kbazaar.cart; - -import com.kampus.kbazaar.product.Product; -import java.util.Set; -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class CartTemp { - private int userID; - private Set products; - - public CartTemp(int userID, Set products) { - this.userID = userID; - this.products = products; - } - - public CartTemp addProduct(Product product) { - products.add(product); - return this; - } - - public AddProductResponse toAddProductResponse() { - return new AddProductResponse(true, "Product added to cart"); - } -} diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/promotion/PromotionDiscount.java b/kbazaar/src/main/java/com/kampus/kbazaar/promotion/PromotionDiscount.java new file mode 100644 index 0000000..706355f --- /dev/null +++ b/kbazaar/src/main/java/com/kampus/kbazaar/promotion/PromotionDiscount.java @@ -0,0 +1,36 @@ +package com.kampus.kbazaar.promotion; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class PromotionDiscount { + public static BigDecimal getDiscountAmountFromPromotion( + BigDecimal price, Integer quantity, Promotion promotion) { + if (promotion == null) { + return BigDecimal.ZERO; + } + return switch (promotion.getDiscountType()) { + case "FIXED_AMOUNT" -> promotion.getDiscountAmount(); + case "PERCENTAGE" -> price.subtract( + price.multiply(promotion.getDiscountAmount()) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP)); + case "buy1_get1" -> { + if (quantity >= 2) { + yield price; + } + yield BigDecimal.ZERO; + } + case "buy2_get1" -> { + if (quantity >= 3) { + yield price; + } + yield BigDecimal.ZERO; + } + default -> BigDecimal.ZERO; + }; + } + + public static BigDecimal divideDiscount(BigDecimal price, Integer divider) { + return price.divide(BigDecimal.valueOf(divider), 2, RoundingMode.HALF_UP); + } +} diff --git a/kbazaar/src/main/java/com/kampus/kbazaar/shopper/ShopperService.java b/kbazaar/src/main/java/com/kampus/kbazaar/shopper/ShopperService.java index 0dad820..3975484 100644 --- a/kbazaar/src/main/java/com/kampus/kbazaar/shopper/ShopperService.java +++ b/kbazaar/src/main/java/com/kampus/kbazaar/shopper/ShopperService.java @@ -30,4 +30,10 @@ public ShopperResponse getByUsername(String username) { .map(Shopper::toResponse) .orElseThrow(() -> new NotFoundException("Shopper not found")); } + + public Shopper getShopperByUsername(String username) { + return shopperRepository + .findByUsername(username) + .orElseThrow(() -> new NotFoundException("Shopper not found")); + } }