InventoryItemValidator.java

package com.smartsupplypro.inventory.validation;

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

import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

import com.smartsupplypro.inventory.dto.InventoryItemDTO;
import com.smartsupplypro.inventory.exception.DuplicateResourceException;
import com.smartsupplypro.inventory.model.InventoryItem;
import com.smartsupplypro.inventory.repository.InventoryItemRepository;

/**
 * Validation utilities for inventory item operations.
 *
 * <p><strong>Capabilities</strong>:
 * <ul>
 *   <li><strong>Base Validation</strong>: Name, quantity, price, supplier ID, audit fields</li>
 *   <li><strong>Duplicate Detection</strong>: Name + price uniqueness enforcement</li>
 *   <li><strong>Existence Checks</strong>: Validates item exists before update/delete</li>
 *   <li><strong>Quantity Safety</strong>: Ensures non-negative stock after adjustments</li>
 *   <li><strong>Price Validation</strong>: Strictly positive price enforcement</li>
 * </ul>
 *
 * @see InventoryItemService
 * @see <a href="file:../../../../../../docs/architecture/patterns/validation-patterns.md">Validation Patterns</a>
 */
public class InventoryItemValidator {
    
    private InventoryItemValidator() {
        // Utility class - prevent instantiation
    }
    
    /**
     * Validates fundamental inventory item fields (name, quantity, price, supplier, audit).
     *
     * @param dto inventory item data
     * @throws IllegalArgumentException if validation fails
     */
    public static void validateBase(InventoryItemDTO dto) {
        if (dto.getName() == null || dto.getName().trim().isEmpty()) {
            throw new IllegalArgumentException("Product name cannot be null or empty");
        }
        if (dto.getQuantity() < 0) {
            throw new IllegalArgumentException("Quantity cannot be negative");
        }
        if (dto.getPrice() == null || dto.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("Price must be positive or greater than zero");
        }
        if (dto.getSupplierId() == null || dto.getSupplierId().trim().isEmpty()) {
            throw new IllegalArgumentException("Supplier ID must be provided");
        }
        if (dto.getCreatedBy() == null || dto.getCreatedBy().trim().isEmpty()) {
            throw new IllegalArgumentException("CreatedBy must be provided");
        }
    }
    
    /**
     * Validates no duplicate item exists with same name and price (for creation).
     *
     * @param id inventory item ID (current item to exclude)
     * @param name item name
     * @param price item price
     * @param inventoryRepo inventory repository
     * @throws DuplicateResourceException if duplicate found
     */
    public static void validateInventoryItemNotExists(
    String id, String name, BigDecimal price, InventoryItemRepository inventoryRepo) {
        List<InventoryItem> existingItems = inventoryRepo.findByNameIgnoreCase(name);
        for (InventoryItem item : existingItems) {
            if (!item.getId().equals(id) && item.getPrice().compareTo(price) == 0) {
                throw new DuplicateResourceException(
                "Another inventory item with this name and price already exists."
                );
            }
        }
    }
    
    /**
     * Validates no duplicate item exists with same name and price (for updates).
     *
     * @param name item name
     * @param price item price
     * @param inventoryRepo inventory repository
     * @throws DuplicateResourceException if duplicate found
     */
    public static void validateInventoryItemNotExists(
    String name, BigDecimal price, InventoryItemRepository inventoryRepo) {
        List<InventoryItem> existingItems = inventoryRepo.findByNameIgnoreCase(name);
        for (InventoryItem item : existingItems) {
            if (item.getPrice().compareTo(price) == 0) {
                throw new DuplicateResourceException(
                "An inventory item with this name and price already exists."
                );
            }
        }
    }
    
    /**
     * Validates inventory item exists by ID before update/delete operations.
     *
     * @param id inventory item ID
     * @param inventoryRepo inventory repository
     * @return found inventory item entity
     * @throws ResponseStatusException 404 if not found
     */
    public static InventoryItem validateExists(String id, InventoryItemRepository inventoryRepo) {
        return inventoryRepo.findById(id).orElseThrow(() ->
        new ResponseStatusException(HttpStatus.NOT_FOUND, "Item not found: " + id)
        );
    }
    
    /**
     * Validates resulting quantity is non-negative after adjustment.
     *
     * @param resultingQuantity quantity after delta application
     * @throws ResponseStatusException 422 if negative
     */
    public static void assertFinalQuantityNonNegative(int resultingQuantity) {
        if (resultingQuantity < 0) {
            throw new ResponseStatusException(
            HttpStatus.UNPROCESSABLE_ENTITY,
            "Resulting stock cannot be negative"
            );
        }
    }
    
    /**
     * Validates price is strictly positive (for update/patch operations).
     *
     * @param price price to validate
     * @throws ResponseStatusException 422 if null or not positive
     */
    public static void assertPriceValid(BigDecimal price) {
        if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ResponseStatusException(
            HttpStatus.UNPROCESSABLE_ENTITY,
            "Price must be greater than zero"
            );
        }
    }
}