StockHistoryValidator.java

package com.smartsupplypro.inventory.validation;

import java.util.EnumSet;

import com.smartsupplypro.inventory.dto.StockHistoryDTO;
import com.smartsupplypro.inventory.enums.StockChangeReason;
import com.smartsupplypro.inventory.exception.InvalidRequestException;

/**
 * Validation utilities for stock history operations.
 *
 * <p><strong>Capabilities</strong>:
 * <ul>
 *   <li><strong>DTO Validation</strong>: Item ID, change value, reason, audit fields</li>
 *   <li><strong>Enum Validation</strong>: Stock change reason whitelist enforcement</li>
 *   <li><strong>Business Rules</strong>: Zero-delta only for PRICE_CHANGE, non-negative prices</li>
 *   <li><strong>Audit Trail</strong>: Mandatory createdBy field enforcement</li>
 * </ul>
 *
 * @see StockHistoryService
 * @see <a href="file:../../../../../../docs/architecture/patterns/validation-patterns.md">Validation Patterns</a>
 */
public class StockHistoryValidator {

    private StockHistoryValidator() {}

    /**
     * Validates stock history DTO before persistence.
     * Enforces item ID, non-zero change (except PRICE_CHANGE), valid reason, and audit fields.
     *
     * @param dto stock change data
     * @throws InvalidRequestException if validation fails
     */
    public static void validate(StockHistoryDTO dto) {
    if (dto.getItemId() == null || dto.getItemId().isBlank()) {
      throw new InvalidRequestException("Item ID cannot be null or empty");
    }

    final StockChangeReason reason;
    try {
      reason = dto.getReason() == null ? null : StockChangeReason.valueOf(dto.getReason());
    } catch (IllegalArgumentException ex) {
      throw new InvalidRequestException("Invalid stock change reason: " + dto.getReason());
    }
    if (reason == null) throw new InvalidRequestException("Stock change reason is required");

    // zero delta only for PRICE_CHANGE
    if (dto.getChange() == 0 && reason != StockChangeReason.PRICE_CHANGE) {
      throw new InvalidRequestException("Zero quantity change is only allowed for PRICE_CHANGE");
    }

    // enforce createdBy is provided  ***
    if (dto.getCreatedBy() == null || dto.getCreatedBy().isBlank()) {
        throw new InvalidRequestException("CreatedBy must be provided");
    }

    // optional: non-negative price for PRICE_CHANGE
    if (reason == StockChangeReason.PRICE_CHANGE &&
        dto.getPriceAtChange() != null &&
        dto.getPriceAtChange().signum() < 0) {
      throw new InvalidRequestException("priceAtChange must be >= 0 for PRICE_CHANGE");
    }
  }

    /**
     * Validates stock change reason is in allowed enum set.
     *
     * @param reason stock change reason
     * @throws IllegalArgumentException if reason is null or not allowed
     */
    public static void validateEnum(StockChangeReason reason) {
        if (reason == null || !EnumSet.of(
                StockChangeReason.SOLD,
                StockChangeReason.SCRAPPED,
                StockChangeReason.RETURNED_TO_SUPPLIER,
                StockChangeReason.RETURNED_BY_CUSTOMER,
                StockChangeReason.INITIAL_STOCK,
                StockChangeReason.MANUAL_UPDATE,
                StockChangeReason.PRICE_CHANGE
        ).contains(reason)) {
            throw new IllegalArgumentException("Invalid stock change reason: " + reason);
        }
    }
}
// This code provides the StockHistoryValidator class, which enforces validation rules for stock history data.