WorkflowTraceService.java

package com.licensis.notaire.service;

import com.licensis.notaire.dto.DtoGestionWorkflowTrace;
import com.licensis.notaire.dto.DtoGestionWorkflowTrace.DtoHistorialEntry;
import com.licensis.notaire.dto.DtoWorkflowDefinition;
import com.licensis.notaire.dto.DtoWorkflowNode;
import com.licensis.notaire.dto.DtoWorkflowTransition;
import com.licensis.notaire.negocio.GestionDeEscritura;
import com.licensis.notaire.negocio.Historial;
import com.licensis.notaire.negocio.TipoDeTramite;
import com.licensis.notaire.negocio.Tramite;
import com.licensis.notaire.negocio.WorkflowDefinition;
import com.licensis.notaire.negocio.WorkflowNode;
import com.licensis.notaire.negocio.WorkflowTransition;
import com.licensis.notaire.repository.GestionDeEscrituraRepository;
import com.licensis.notaire.repository.HistorialRepository;
import com.licensis.notaire.repository.WorkflowNodeRepository;
import com.licensis.notaire.repository.WorkflowTransitionRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class WorkflowTraceService {

    private static final Logger log = LoggerFactory.getLogger(WorkflowTraceService.class);

    private final GestionDeEscrituraRepository gestionRepository;
    private final HistorialRepository historialRepository;
    private final WorkflowNodeRepository workflowNodeRepository;
    private final WorkflowTransitionRepository workflowTransitionRepository;

    public WorkflowTraceService(
            GestionDeEscrituraRepository gestionRepository,
            HistorialRepository historialRepository,
            WorkflowNodeRepository workflowNodeRepository,
            WorkflowTransitionRepository workflowTransitionRepository) {
        this.gestionRepository = gestionRepository;
        this.historialRepository = historialRepository;
        this.workflowNodeRepository = workflowNodeRepository;
        this.workflowTransitionRepository = workflowTransitionRepository;
    }

    /**
     * Builds the aggregated workflow trace for a given gestión.
     * <p>
     * Requires {@code @Transactional(readOnly = true)} because it navigates
     * LAZY associations ({@code GestionDeEscritura.tramiteList} and
     * {@code TipoDeTramite.workflowDefinition}) inside the same persistence context.
     */
    @Transactional(readOnly = true)
    public DtoGestionWorkflowTrace buildTrace(Integer gestionId) {
        // 1. Load gestión
        GestionDeEscritura gestion = gestionRepository.findById(gestionId)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Gestion not found with id: " + gestionId));

        // 2. Resolve workflow definition from the first tramite's tipo
        List<Tramite> tramites = gestion.getTramiteList();
        if (tramites == null || tramites.isEmpty()) {
            throw new IllegalArgumentException("Gestion " + gestionId + " has no tramites");
        }

        TipoDeTramite tipoTramite = tramites.get(0).getFkIdTipoTramite();
        if (tipoTramite == null) {
            throw new IllegalArgumentException(
                    "Tramite for gestion " + gestionId + " has no tipo de tramite");
        }

        WorkflowDefinition workflowDef = tipoTramite.getWorkflowDefinition();
        if (workflowDef == null) {
            throw new IllegalArgumentException(
                    "TipoDeTramite " + tipoTramite.getIdTipoTramite()
                            + " has no workflow definition assigned");
        }

        // 3. Fetch nodes and transitions
        List<WorkflowNode> nodeEntities = workflowNodeRepository
                .findByWorkflowDefinitionId(workflowDef.getId());
        List<WorkflowTransition> transitionEntities = workflowTransitionRepository
                .findByWorkflowDefinitionId(workflowDef.getId());

        // 4. Fetch historial for the gestión
        List<Historial> historialEntities = historialRepository
                .findByFkIdGestionIdGestion(gestionId);

        // 5. Compute node statuses
        Map<Integer, String> nodeStatuses = computeNodeStatuses(nodeEntities, historialEntities);

        // 6. Assemble DTO
        DtoGestionWorkflowTrace trace = new DtoGestionWorkflowTrace();
        trace.setGestionId(gestion.getIdGestion());
        trace.setNumero(gestion.getNumero());
        trace.setEncabezado(gestion.getEncabezado());
        trace.setFechaInicio(gestion.getFechaInicio());
        trace.setEstadoActual(gestion.getFkIdEstadoDeGestion() != null
                ? gestion.getFkIdEstadoDeGestion().getNombre() : null);

        // Workflow definition DTO
        DtoWorkflowDefinition defDto = new DtoWorkflowDefinition();
        defDto.setId(workflowDef.getId());
        defDto.setNombre(workflowDef.getNombre());
        defDto.setDescripcion(workflowDef.getDescripcion());
        defDto.setActivo(workflowDef.isActivo());
        defDto.setVersion(workflowDef.getVersion());
        trace.setWorkflowDefinition(defDto);

        // Node DTOs
        List<DtoWorkflowNode> nodeDtos = new ArrayList<>();
        for (WorkflowNode node : nodeEntities) {
            nodeDtos.add(node.toDto());
        }
        trace.setNodes(nodeDtos);

        // Transition DTOs
        List<DtoWorkflowTransition> transitionDtos = new ArrayList<>();
        for (WorkflowTransition transition : transitionEntities) {
            transitionDtos.add(transition.toDto());
        }
        trace.setTransitions(transitionDtos);

        // Historial DTOs
        List<DtoHistorialEntry> historialDtos = new ArrayList<>();
        for (Historial h : historialEntities) {
            DtoHistorialEntry entry = new DtoHistorialEntry();
            entry.setIdHistorial(h.getIdHistorial());
            entry.setEstadoGestionId(h.getFkIdEstadoGestion().getIdEstadoGestion());
            entry.setEstadoGestionNombre(h.getFkIdEstadoGestion().getNombre());
            entry.setFecha(h.getFecha());
            entry.setObservaciones(h.getObservaciones());
            historialDtos.add(entry);
        }
        trace.setHistorial(historialDtos);

        // Node statuses
        trace.setNodeStatuses(nodeStatuses);

        return trace;
    }

    /**
     * Computes per-node status by matching against the gestión's historial.
     * <p>
     * Rules:
     * <ul>
     *   <li>If the node's estadoGestionId matches the <strong>latest</strong>
     *       distinct estado in historial → {@code "in_progress"}</li>
     *   <li>If it matches an earlier distinct estado → {@code "completed"}</li>
     *   <li>If it does not appear in historial at all → {@code "pending"}</li>
     * </ul>
     */
    public static Map<Integer, String> computeNodeStatuses(
            List<WorkflowNode> nodes, List<Historial> historialList) {

        // Sort historial by fecha ASC
        List<Historial> sorted = new ArrayList<>(historialList);
        sorted.sort(Comparator.comparing(Historial::getFecha));

        // Collect distinct estadoGestionIds in order
        List<Integer> distinctEstadoIds = new ArrayList<>();
        for (Historial h : sorted) {
            Integer estadoId = h.getFkIdEstadoGestion().getIdEstadoGestion();
            if (!distinctEstadoIds.contains(estadoId)) {
                distinctEstadoIds.add(estadoId);
            }
        }

        // Classify each node
        Map<Integer, String> statuses = new HashMap<>();
        for (WorkflowNode node : nodes) {
            Integer nodeEstadoId = node.getEstadoDeGestion().getIdEstadoGestion();
            int idx = distinctEstadoIds.indexOf(nodeEstadoId);
            if (idx == -1) {
                statuses.put(node.getId(), "pending");
            } else if (idx == distinctEstadoIds.size() - 1) {
                statuses.put(node.getId(), "in_progress");
            } else {
                statuses.put(node.getId(), "completed");
            }
        }
        return statuses;
    }
}