CartService.java

package com.rbctcsworld.ecommerce.cart;

import com.rbctcsworld.ecommerce.auth.AppUser;
import com.rbctcsworld.ecommerce.auth.UserRepository;
import com.rbctcsworld.ecommerce.cart.CartDtos.CartLine;
import com.rbctcsworld.ecommerce.cart.CartDtos.CartResponse;
import com.rbctcsworld.ecommerce.common.exception.BusinessRuleException;
import com.rbctcsworld.ecommerce.common.exception.ConflictException;
import com.rbctcsworld.ecommerce.common.exception.NotFoundException;
import com.rbctcsworld.ecommerce.product.Product;
import com.rbctcsworld.ecommerce.product.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.List;

/**
 * Business rules:
 *  1. Only active products can be added.
 *  2. Adding a product already in the cart increases its quantity (no duplicate lines).
 *  3. Quantity per line: 1..MAX_QTY_PER_ITEM.
 *  4. Quantity may not exceed current stock (409 Conflict).
 *  5. A customer can only see/change their own cart items (others' ids give 404).
 *  Prices are NOT stored in the cart; the current product price is always shown.
 */
@Service
@Transactional
public class CartService {

    public static final int MAX_QTY_PER_ITEM = 10;

    private final CartItemRepository items;
    private final ProductRepository products;
    private final UserRepository users;

    public CartService(CartItemRepository items, ProductRepository products, UserRepository users) {
        this.items = items;
        this.products = products;
        this.users = users;
    }

    @Transactional(readOnly = true)
    public CartResponse getCart(String email) {
        return buildCart(user(email).getId());
    }

    public CartResponse addItem(String email, Long productId, int quantity) {
        AppUser user = user(email);
        Product product = products.findByIdAndActiveTrue(productId)
                .orElseThrow(() -> new NotFoundException("Product not found: " + productId));

        CartItem item = items.findByUserIdAndProductId(user.getId(), product.getId()).orElse(null);
        int newQuantity = (item == null ? 0 : item.getQuantity()) + quantity;
        validateQuantity(product, newQuantity);

        if (item == null) {
            item = new CartItem(user.getId(), product, newQuantity);
        } else {
            item.setQuantity(newQuantity);
        }
        items.save(item);
        return buildCart(user.getId());
    }

    public CartResponse updateItem(String email, Long itemId, int quantity) {
        AppUser user = user(email);
        CartItem item = ownedItem(itemId, user.getId());
        validateQuantity(item.getProduct(), quantity);
        item.setQuantity(quantity);
        items.save(item);
        return buildCart(user.getId());
    }

    public CartResponse removeItem(String email, Long itemId) {
        AppUser user = user(email);
        items.delete(ownedItem(itemId, user.getId()));
        return buildCart(user.getId());
    }

    public void clear(String email) {
        items.deleteByUserId(user(email).getId());
    }

    private void validateQuantity(Product product, int quantity) {
        if (quantity < 1 || quantity > MAX_QTY_PER_ITEM) {
            throw new BusinessRuleException("Quantity per item must be between 1 and " + MAX_QTY_PER_ITEM);
        }
        if (!product.isActive()) {
            throw new NotFoundException("Product not found: " + product.getId());
        }
        if (quantity > product.getStock()) {
            throw new ConflictException("Insufficient stock for " + product.getSku()
                    + ": requested " + quantity + ", available " + product.getStock());
        }
    }

    private CartItem ownedItem(Long itemId, Long userId) {
        return items.findByIdAndUserId(itemId, userId)
                .orElseThrow(() -> new NotFoundException("Cart item not found: " + itemId));
    }

    private AppUser user(String email) {
        return users.findByEmail(email)
                .orElseThrow(() -> new NotFoundException("User not found"));
    }

    private CartResponse buildCart(Long userId) {
        List<CartLine> lines = items.findByUserIdOrderByIdAsc(userId).stream().map(this::toLine).toList();
        int totalQuantity = lines.stream().mapToInt(CartLine::quantity).sum();
        BigDecimal subtotal = lines.stream().map(CartLine::lineTotal).reduce(BigDecimal.ZERO, BigDecimal::add);
        return new CartResponse(lines, totalQuantity, subtotal);
    }

    private CartLine toLine(CartItem i) {
        Product p = i.getProduct();
        BigDecimal lineTotal = p.currentPrice().multiply(BigDecimal.valueOf(i.getQuantity()));
        boolean available = p.isActive() && p.getStock() >= i.getQuantity();
        return new CartLine(i.getId(), p.getId(), p.getSku(), p.displayName(), p.currentPrice(),
                i.getQuantity(), lineTotal, available);
    }
}