WishlistService.java

package com.rbctcsworld.ecommerce.wishlist;

import com.rbctcsworld.ecommerce.auth.AppUser;
import com.rbctcsworld.ecommerce.auth.UserRepository;
import com.rbctcsworld.ecommerce.cart.CartDtos.CartResponse;
import com.rbctcsworld.ecommerce.cart.CartService;
import com.rbctcsworld.ecommerce.common.exception.BusinessRuleException;
import com.rbctcsworld.ecommerce.common.exception.NotFoundException;
import com.rbctcsworld.ecommerce.product.Product;
import com.rbctcsworld.ecommerce.product.ProductRepository;
import com.rbctcsworld.ecommerce.wishlist.WishlistDtos.AddResult;
import com.rbctcsworld.ecommerce.wishlist.WishlistDtos.WishlistLine;
import com.rbctcsworld.ecommerce.wishlist.WishlistDtos.WishlistResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.List;

/**
 * Business rules:
 *  1. Only active products can be added (404 otherwise).
 *  2. Adding the same product twice is idempotent: no duplicate, 200 instead of 201.
 *  3. At most MAX_ITEMS products per wishlist (400).
 *  4. The price at the moment of adding is kept, so the customer sees a price drop.
 *  5. Move to cart = add 1 to the cart (all cart rules apply, e.g. 409 out of stock) and remove from
 *     the wishlist - in one transaction: if the cart refuses, the product STAYS on the wishlist.
 *  6. Removing a product that is not on the wishlist gives 404.
 */
@Service
@Transactional
public class WishlistService {

    public static final int MAX_ITEMS = 50;

    private final WishlistItemRepository items;
    private final ProductRepository products;
    private final UserRepository users;
    private final CartService cart;
    private final Clock clock;

    public WishlistService(WishlistItemRepository items, ProductRepository products, UserRepository users,
                           CartService cart, Clock clock) {
        this.items = items;
        this.products = products;
        this.users = users;
        this.cart = cart;
        this.clock = clock;
    }

    @Transactional(readOnly = true)
    public WishlistResponse get(String email) {
        return build(user(email).getId());
    }

    public AddResult add(String email, Long productId) {
        AppUser user = user(email);
        Product product = products.findByIdAndActiveTrue(productId)
                .orElseThrow(() -> new NotFoundException("Product not found: " + productId));
        if (items.findByUserIdAndProductId(user.getId(), productId).isPresent()) {
            return new AddResult(false, build(user.getId()));
        }
        if (items.countByUserId(user.getId()) >= MAX_ITEMS) {
            throw new BusinessRuleException("A wishlist can hold at most " + MAX_ITEMS + " products");
        }
        items.save(new WishlistItem(user.getId(), product, LocalDateTime.now(clock)));
        return new AddResult(true, build(user.getId()));
    }

    public WishlistResponse remove(String email, Long productId) {
        AppUser user = user(email);
        items.delete(owned(user.getId(), productId));
        return build(user.getId());
    }

    public CartResponse moveToCart(String email, Long productId) {
        AppUser user = user(email);
        WishlistItem item = owned(user.getId(), productId);
        CartResponse result = cart.addItem(email, productId, 1);   // throws -> rollback, item stays
        items.delete(item);
        return result;
    }

    private WishlistItem owned(Long userId, Long productId) {
        return items.findByUserIdAndProductId(userId, productId)
                .orElseThrow(() -> new NotFoundException("Product " + productId + " is not on your wishlist"));
    }

    private WishlistResponse build(Long userId) {
        List<WishlistLine> lines = items.findByUserIdOrderByIdDesc(userId).stream().map(WishlistService::toLine).toList();
        return new WishlistResponse(lines, lines.size());
    }

    static WishlistLine toLine(WishlistItem i) {
        Product p = i.getProduct();
        BigDecimal drop = i.getPriceWhenAdded().subtract(p.currentPrice());
        if (drop.signum() < 0) drop = BigDecimal.ZERO;
        return new WishlistLine(p.getId(), p.getSku(), p.displayName(), i.getPriceWhenAdded(), p.currentPrice(),
                drop.setScale(2), p.isActive() && p.getStock() > 0, p.getRatingAverage(), p.getRatingCount(),
                i.getCreatedAt());
    }

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