ProductService.java

package com.learning.yasminishop.product;

import com.learning.yasminishop.category.CategoryRepository;

import com.learning.yasminishop.common.dto.PaginationResponse;
import com.learning.yasminishop.common.entity.Category;
import com.learning.yasminishop.common.entity.Product;
import com.learning.yasminishop.common.entity.Storage;
import com.learning.yasminishop.common.exception.AppException;
import com.learning.yasminishop.common.exception.ErrorCode;
import com.learning.yasminishop.product.dto.filter.ProductFilter;
import com.learning.yasminishop.product.dto.request.ProductRequest;
import com.learning.yasminishop.product.dto.response.ProductAdminResponse;
import com.learning.yasminishop.product.dto.response.ProductResponse;
import com.learning.yasminishop.product.mapper.ProductMapper;
import com.learning.yasminishop.storage.StorageRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Slf4j
public class ProductService {

    private final ProductRepository productRepository;
    private final CategoryRepository categoryRepository;
    private final StorageRepository storageRepository;


    private final ProductMapper productMapper;

    public PaginationResponse<ProductResponse> getAllProducts(
            ProductFilter productFilter,
            Pageable pageable) {

        // check if the categoryIds are valid
        List<Category> categories = categoryRepository.findAllById(List.of(productFilter.getCategoryIds()));
        if (categories.size() != productFilter.getCategoryIds().length) {
            throw new AppException(ErrorCode.CATEGORY_NOT_FOUND);
        }

        Page<Product> products = productRepository.findAll(
                Specification.where(ProductSpecifications.hasName(productFilter.getName()))
                        .and(ProductSpecifications.hasIsAvailable(productFilter.getIsAvailable()))
                        .and(ProductSpecifications.hasIsFeatured(productFilter.getIsFeatured()))
                        .and(ProductSpecifications.hasCategory(categories))
                        .and(ProductSpecifications.hasPrice(productFilter.getMinPrice(), productFilter.getMaxPrice()))
                        .and(ProductSpecifications.hasAverageRating(productFilter.getMinRating()))
                , pageable);


        return PaginationResponse.<ProductResponse>builder()
                .page(pageable.getPageNumber() + 1)
                .total(products.getTotalElements())
                .itemsPerPage(pageable.getPageSize())
                .data(products.map(productMapper::toProductResponse).toList())
                .build();
    }


    @Transactional
    public ProductAdminResponse create(ProductRequest productCreation) {

        if (productRepository.existsBySlug(productCreation.getSlug())) {
            throw new AppException(ErrorCode.SLUG_ALREADY_EXISTS);
        }

        if (productRepository.existsBySku(productCreation.getSku())) {
            throw new AppException(ErrorCode.SKU_ALREADY_EXISTS);
        }

        Set<String> categoryIds = productCreation.getCategoryIds();
        List<Category> categories = categoryRepository.findAllById(categoryIds);
        if (categories.size() != categoryIds.size()) {
            throw new AppException(ErrorCode.CATEGORY_NOT_FOUND);
        }

        Set<String> imageIds = productCreation.getImageIds();
        List<Storage> images = storageRepository.findAllById(imageIds);
        if (images.size() != imageIds.size()) {
            throw new AppException(ErrorCode.IMAGE_NOT_FOUND);
        }

        Product product = productMapper.toProduct(productCreation);
        product.setCategories(new HashSet<>(categories));
        product.setImages(new HashSet<>(images));
        product.setIsAvailable(true);
        product.setThumbnail(images.getFirst().getUrl());


        return productMapper.toProductAdminResponse(productRepository.save(product));
    }

    public ProductResponse getBySlug(String slug) {

        Product product = productRepository.findBySlug(slug)
                .orElseThrow(() -> new AppException(ErrorCode.PRODUCT_NOT_FOUND));

        return productMapper.toProductResponse(product);
    }

    public ProductAdminResponse getById(String id) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new AppException(ErrorCode.PRODUCT_NOT_FOUND));

        return productMapper.toProductAdminResponse(product);
    }

    public PaginationResponse<ProductAdminResponse> getAllProductsForAdmin(
            ProductFilter productFilter,
            Pageable pageable) {

        // check if the categoryIds are valid
        List<Category> categories = categoryRepository.findAllById(List.of(productFilter.getCategoryIds()));
        if (categories.size() != productFilter.getCategoryIds().length) {
            throw new AppException(ErrorCode.CATEGORY_NOT_FOUND);
        }

        Page<Product> products = productRepository.findAll(
                Specification.where(ProductSpecifications.hasName(productFilter.getName()))
                        .and(ProductSpecifications.hasIsAvailable(productFilter.getIsAvailable()))
                        .and(ProductSpecifications.hasIsFeatured(productFilter.getIsFeatured()))
                        .and(ProductSpecifications.hasCategory(categories))
                , pageable);


        return PaginationResponse.<ProductAdminResponse>builder()
                .page(pageable.getPageNumber() + 1)
                .total(products.getTotalElements())
                .itemsPerPage(pageable.getPageSize())
                .data(products.map(productMapper::toProductAdminResponse).toList())
                .build();
    }


    @Transactional
    public void toggleAvailability(List<String> ids) {
        List<Product> products = productRepository.findAllById(ids);

        if (products.size() != ids.size()) {
            throw new AppException(ErrorCode.PRODUCT_NOT_FOUND);
        }

        for (Product product : products) {
            product.setIsAvailable(!product.getIsAvailable());
        }

        productRepository.saveAll(products);
    }


    @Transactional
    public ProductAdminResponse update(String id, ProductRequest productUpdate) {

        Product product = productRepository.findById(id)
                .orElseThrow(() -> new AppException(ErrorCode.PRODUCT_NOT_FOUND));

        if (!product.getSlug().equals(productUpdate.getSlug()) && categoryRepository.existsBySlug(productUpdate.getSlug())) {
            throw new AppException(ErrorCode.SLUG_ALREADY_EXISTS);
        }

        if (!product.getSku().equals(productUpdate.getSku()) && categoryRepository.existsBySlug(productUpdate.getSku())) {
            throw new AppException(ErrorCode.SKU_ALREADY_EXISTS);
        }

        Set<String> categoryIds = productUpdate.getCategoryIds();
        List<Category> categories = categoryRepository.findAllById(categoryIds);
        if (categories.size() != categoryIds.size()) {
            throw new AppException(ErrorCode.CATEGORY_NOT_FOUND);
        }

        Set<String> imageIds = productUpdate.getImageIds();
        List<Storage> images = storageRepository.findAllById(imageIds);
        if (images.size() != imageIds.size()) {
            throw new AppException(ErrorCode.IMAGE_NOT_FOUND);
        }

        productMapper.updateProduct(product, productUpdate);
        product.setCategories(new HashSet<>(categories));
        product.setImages(new HashSet<>(images)); // update images
        product.setThumbnail(images.getFirst().getUrl());

        return productMapper.toProductAdminResponse(productRepository.save(product));
    }


    @Transactional
    public void delete(List<String> ids) {

        List<Product> products = productRepository.findAllById(ids);
        if (products.size() != ids.size()) {
            throw new AppException(ErrorCode.PRODUCT_NOT_FOUND);
        }

        // check if the product is in any order, cart
        for (Product product : products) {

            if (!product.getOrderItems().isEmpty()) {
                throw new AppException(ErrorCode.PRODUCT_IN_ORDER);
            }
            if (!product.getCartItems().isEmpty()) {
                throw new AppException(ErrorCode.PRODUCT_IN_CART);
            }
        }

        productRepository.deleteAll(products);
    }

}