SupplierValidator.java

package com.smartsupplypro.inventory.validation;

import java.util.Objects;
import java.util.function.BooleanSupplier;

import com.smartsupplypro.inventory.dto.SupplierDTO;
import com.smartsupplypro.inventory.exception.DuplicateResourceException;
import com.smartsupplypro.inventory.exception.InvalidRequestException;
import com.smartsupplypro.inventory.repository.SupplierRepository;

/**
 * Validation utilities for supplier operations.
 *
 * <p><strong>Capabilities</strong>:
 * <ul>
 *   <li><strong>Base Validation</strong>: Required field checks (name)</li>
 *   <li><strong>Uniqueness</strong>: Case-insensitive name enforcement</li>
 *   <li><strong>Deletion Safety</strong>: Prevents deletion with linked items</li>
 *   <li><strong>Exception Mapping</strong>: Throws 400/409 exceptions for GlobalExceptionHandler</li>
 * </ul>
 *
 * @see SupplierService
 * @see <a href="file:../../../../../../docs/architecture/patterns/validation-patterns.md">Validation Patterns</a>
 */
public final class SupplierValidator {

    private SupplierValidator() { }

    /**
     * Validates required supplier fields (name must be non-blank).
     *
     * @param dto supplier data
     * @throws InvalidRequestException if validation fails
     */
    public static void validateBase(SupplierDTO dto) {
        if (dto == null) {
            throw new InvalidRequestException("Supplier payload must not be null");
        }
        if (isBlank(dto.getName())) {
            throw new InvalidRequestException("Supplier name must not be blank");
        }
    }

    /**
     * Enforces unique supplier name (case-insensitive).
     *
     * @param repo supplier repository
     * @param name desired supplier name
     * @param excludeId current ID for update, null for create
     * @throws DuplicateResourceException if name already exists
     */
    public static void assertUniqueName(SupplierRepository repo, String name, String excludeId) {
        if (isBlank(name)) return; // validateBase already handles blank
        String trimmed = name.trim();

        var existingOpt = repo.findByNameIgnoreCase(trimmed).map(s -> (Object) s);
        var existing = existingOpt.orElse(null);
        if (existing != null) {
            String existingId = invokeGetId(existing);
            if (!Objects.equals(existingId, excludeId)) {
                throw new DuplicateResourceException("Supplier already exists");
            }
        }
    }

    /**
     * Prevents supplier deletion when linked inventory items exist.
     * Repo-agnostic: caller supplies boolean check for link existence.
     *
     * @param supplierId supplier ID to validate
     * @param hasAnyLinks supplier returning true if links exist
     * @throws InvalidRequestException if ID is blank
     * @throws IllegalStateException if links exist (409 Conflict)
     */
    public static void assertDeletable(String supplierId, BooleanSupplier hasAnyLinks) {
        if (isBlank(supplierId)) {
            throw new InvalidRequestException("Supplier id must be provided for deletion");
        }
        if (hasAnyLinks != null && hasAnyLinks.getAsBoolean()) {
            throw new IllegalStateException("Cannot delete supplier with linked items");
        }
    }

    // ---- helpers ------------------------------------------------------------

    private static boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }

    /**
     * Reflective accessor for entity ID to maintain repo-agnostic design.
     *
     * @param entity supplier entity
     * @return entity ID or null
     */
    private static String invokeGetId(Object entity) {
        try {
            var m = entity.getClass().getMethod("getId");
            Object v = m.invoke(entity);
            return v != null ? v.toString() : null;
        } catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException | SecurityException e) {
            return null; // keep existing behavior: silently fall back
        }
    }
}