Saltar a contenido

🔀 Estados y Transiciones de Documentos - Implementación Real

Este documento define los estados oficiales implementados en la base de datos de GDI y las reglas de transición entre ellos.

📊 Estados Principales del Documento

Definición de ENUMs Implementados

CREATE TYPE document_status AS ENUM (
    'draft',        -- En edición colaborativa
    'sent_to_sign', -- Enviado al circuito de firmas  
    'signed',       -- Firmado y con validez legal oficial
    'rejected',     -- Rechazado por algún firmante
    'cancelled',    -- Cancelado antes de completar proceso
    'archived'      -- Archivado después de finalizado
);

Estados de Firmantes Individuales

CREATE TYPE document_signer_status AS ENUM (
    'pending',   -- Esperando su turno para firmar
    'signed',    -- Ya completó su firma
    'rejected'   -- Rechazó el documento
);

🔄 Diagrama de Estados Completo

Flujo Principal

📝 draft → 📤 sent_to_sign → ✅ signed → 📦 archived

Flujos de Excepción

📝 draft
   🗑️ deleted (is_deleted=true)

📤 sent_to_sign
   ❌ rejected → 🔄 draft (corrección)
   🚫 cancelled

✅ signed
   📦 archived

Evolución Visual del Encabezado

La siguiente imagen resume cómo cambia la presentación visual del encabezado del documento a medida que avanza por los estados principales.

Evolución de estados de encabezados


📝 ESTADO: draft

Descripción

Documento en proceso de creación y edición colaborativa. El contenido es modificable y los usuarios autorizados pueden colaborar en tiempo real.

Características

  • Contenido editable via editor colaborativo
  • Configuración de firmantes permitida
  • Guardado automático cada 30 segundos
  • Sin validez legal hasta firmarse

Campos Relevantes

-- Estado del documento
status = 'draft'

-- Metadatos de edición
created_by UUID NOT NULL,          -- Usuario creador
created_at TIMESTAMP DEFAULT NOW(),
last_modified_at TIMESTAMP DEFAULT NOW(),

-- Contenido
reference TEXT NOT NULL,           -- Referencia/motivo (obligatorio)
content JSONB NOT NULL,           -- Contenido enriquecido (obligatorio)

-- Control
is_deleted BOOLEAN DEFAULT false  -- Eliminación lógica

Validaciones en Estado draft

-- Validaciones obligatorias
CHECK (reference IS NOT NULL AND reference != ''),
CHECK (content IS NOT NULL AND content != '{}'),

Transiciones Permitidas DESDE draft

Transición Trigger Validaciones Requeridas
draftsent_to_sign Usuario envía a firmas • Contenido no vacío
• Al menos un firmante
• Numerador asignado
draftdeleted Eliminación lógica • Solo el creador
• Sin firmantes asignados

📤 ESTADO: sent_to_sign

Descripción

Documento enviado al circuito de firmas. El contenido se vuelve inmutable y los firmantes asignados deben proceder según el orden establecido.

Características

  • Contenido inmutable (no editable)
  • Encabezado provisional visible
  • Firmantes notificados según signing_order
  • Proceso de firma activo
  • Sin validez legal hasta completar todas las firmas

Campos Relevantes

-- Estado y timestamps
status = 'sent_to_sign',
sent_to_sign_at TIMESTAMP NOT NULL,  -- Momento de envío
sent_by UUID,                        -- Usuario que envió

-- Inmutabilidad
-- Los campos content y reference se vuelven read-only

Proceso de Orquestación de Firmas

-- Firmantes ordenados por signing_order
SELECT ds.*, u.full_name, u.email
FROM document_signers ds
JOIN users u ON ds.user_id = u.user_id
WHERE ds.document_id = ?
ORDER BY ds.signing_order ASC;

Estados de Firmantes Individuales

Cada firmante tiene su propio estado independiente:

-- Estado individual en document_signers
signing_order INTEGER,               -- Orden de firma (1, 2, 3...)
status document_signer_status,       -- pending, signed, rejected
signed_at TIMESTAMP,                 -- Momento de firma
observations TEXT,                   -- Comentarios del firmante
is_numerator BOOLEAN DEFAULT false   -- Si es el numerador final

Lógica de Progresión Secuencial

-- Siguiente firmante habilitado
SELECT ds.*
FROM document_signers ds
WHERE ds.document_id = ?
  AND ds.status = 'pending'
  AND ds.signing_order = (
    SELECT MIN(signing_order) 
    FROM document_signers 
    WHERE document_id = ? AND status = 'pending'
  );

Transiciones Permitidas DESDE sent_to_sign

Transición Trigger Condición
sent_to_signsigned Numerador firma • Todos los firmantes signed
• Numerador completa firma
• Número oficial asignado
sent_to_signrejected Cualquier firmante rechaza • Al menos un firmante rejected
• Motivo registrado
sent_to_signcancelled Cancelación administrativa • Autorización especial
• Proceso no completado

❌ ESTADO: rejected

Descripción

Documento rechazado por uno o más firmantes durante el proceso de firma. Requiere corrección antes de poder reenviar.

Características

  • Proceso de firma detenido
  • Motivos de rechazo registrados
  • Posibilidad de corrección habilitada
  • Sin validez legal

Datos de Rechazo

-- Tabla de rechazos
CREATE TABLE public.document_rejections (
    rejection_id uuid DEFAULT gen_random_uuid() NOT NULL,
    document_id uuid NOT NULL,
    rejected_by uuid NOT NULL,
    reason text,
    rejected_at timestamp without time zone DEFAULT now(),
    audit_data jsonb
);

Información del Rechazo

-- Consulta de rechazos para un documento
SELECT 
    dr.reason,
    dr.rejected_at,
    u.full_name as rejected_by_name,
    ds.signing_order,
    ds.observations
FROM document_rejections dr
JOIN users u ON dr.rejected_by = u.user_id
JOIN document_signers ds ON dr.document_id = ds.document_id 
    AND dr.rejected_by = ds.user_id
WHERE dr.document_id = ?
ORDER BY dr.rejected_at DESC;

Proceso de Corrección

  1. 📋 Revisión de Motivos: Usuario ve todos los rechazos
  2. ✏️ Edición Habilitada: Se reactiva editor colaborativo
  3. 🔄 Corrección: Se realizan cambios necesarios
  4. 📤 Reenvío: Nuevo ciclo draft → sent_to_sign

Transiciones Permitidas DESDE rejected

Transición Trigger Validaciones
rejecteddraft Iniciar corrección • Usuario autorizado
• Motivos revisados
rejectedcancelled Cancelar definitivamente • Autorización especial

✅ ESTADO: signed

Descripción

Documento completamente firmado con plena validez legal. El numerador ha asignado el número oficial y se ha generado el documento en official_documents.

Características

  • Validez legal plena
  • Número oficial asignado
  • PDF firmado generado
  • Contenido inmutable permanente
  • Encabezado oficial definitivo

Proceso de Finalización

-- Transición compleja que involucra múltiples tablas
BEGIN TRANSACTION;

-- 1. Confirmar última firma (numerador)
UPDATE document_signers 
SET status = 'signed', signed_at = NOW()
WHERE document_id = ? AND is_numerator = true;

-- 2. Confirmar reserva de número
UPDATE numeration_requests 
SET is_confirmed = true, confirmed_at = NOW()
WHERE document_id = ?;

-- 3. Crear documento oficial
INSERT INTO official_documents (
    document_id,
    document_type_id,
    numeration_requests_id,
    reference,
    content,
    official_number,
    year,
    department_id,
    numerator_id,
    signed_at,
    signed_pdf_url,
    signers
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?);

-- 4. Finalizar estado draft
UPDATE document_draft 
SET status = 'signed'
WHERE document_id = ?;

COMMIT;

Datos del Documento Oficial

-- Información completa del documento oficial
SELECT 
    dd.reference,
    dd.content,
    od.official_number,
    od.signed_at as official_date,
    od.signed_pdf_url,
    dt.name as document_type,
    dt.acronym,
    u.full_name as numerator_name
FROM document_draft dd
JOIN official_documents od ON dd.document_id = od.document_id
JOIN document_types dt ON dd.document_type_id = dt.document_type_id
JOIN users u ON od.numerator_id = u.user_id
WHERE dd.document_id = ?;

Firma Digital y Almacenamiento

  • Firma digital: Aplicada por GDI-Notary (:8001) con pyHanko (PAdES/CAdES)
  • Firma visual: Logo institucional, fecha, numero oficial y nombre del firmante en el PDF
  • Multi-firmante secuencial: Cada firmante firma en orden segun signing_order
  • PDF generado: Via GDI-PDFComposer (:8002) con Gotenberg (headless Chrome)
  • Almacenamiento: PDF oficial en bucket oficial de Cloudflare R2
  • Descarga segura: Via URLs firmadas temporales

Funcionalidades Habilitadas

  • Descarga PDF oficial
  • Busqueda por numero oficial
  • Vinculacion automatica a expediente
  • Inclusion en reportes oficiales
  • Consulta publica (segun permisos)

Transiciones Permitidas DESDE signed

Transición Trigger Notas
signedarchived Proceso de archivo • Después de período de vigencia
• Mantiene validez legal

🚫 ESTADO: cancelled

Descripción

Documento cancelado antes de completar el proceso de firma. No tiene validez legal y se mantiene solo para auditoría.

Características

  • Sin validez legal
  • Motivo de cancelación registrado
  • Historial preservado para auditoría
  • No se puede reactivar

Casos de Cancelación

  1. 👤 Cancelación por Usuario: Creador cancela antes de enviar a firma
  2. 🏛️ Cancelación Administrativa: Por decisión de department
  3. ⚠️ Cancelación por Error: Problemas técnicos o de configuración
  4. 📅 Cancelación por Timeout: Proceso demorado excesivamente

Registro de Cancelación

-- Registro en audit_data
UPDATE document_draft 
SET 
    status = 'cancelled',
    audit_data = jsonb_set(
        COALESCE(audit_data, '{}'),
        '{cancellation}',
        json_build_object(
            'cancelled_by', ?,
            'cancelled_at', NOW(),
            'reason', ?,
            'original_status', 'sent_to_sign'
        )
    )
WHERE document_id = ?;

Transiciones Permitidas DESDE cancelled

Transición Trigger Notas
cancelledarchived Proceso de archivo • Solo para limpieza
• Mantiene historia

📦 ESTADO: archived

Descripción

Documento archivado después de cumplir su ciclo de vida útil. Mantiene validez legal pero se considera histórico.

Características

  • Validez legal preservada (si venía de signed)
  • Solo lectura
  • Búsqueda limitada
  • Auditoría completa

Criterios de Archivo

  1. 📅 Tiempo: Documentos con más de X años
  2. 📊 Volumen: Gestión de espacio en BD
  3. 📋 Política: Según normativas municipales
  4. 🔄 Migración: A sistemas de archivo histórico

Proceso de Archivo

-- Archivo masivo por criterios
UPDATE document_draft 
SET status = 'archived'
WHERE status = 'signed' 
  AND signed_at < (NOW() - INTERVAL '5 years');

Transiciones Permitidas DESDE archived

Ninguna - Estado final del documento.


🗑️ ESTADO ESPECIAL: Eliminación Lógica

Concepto de is_deleted

No es un estado del enum document_status, sino un flag transversal:

is_deleted BOOLEAN DEFAULT false

Comportamiento

  • Preserva registro en base de datos
  • Oculta de interfaces de usuario
  • Mantiene integridad referencial
  • Permite auditoría completa

Reglas de Eliminación

Estado Original Eliminación Permitida Efecto
draft ✅ Sí Oculto, recuperable
sent_to_sign ❌ No Debe cancelarse
signed ❌ Nunca Documento oficial
rejected ✅ Sí Después de revisión
cancelled ✅ Sí Para limpieza

Consultas con Eliminación Lógica

-- Vista solo documentos activos
SELECT * FROM document_draft 
WHERE is_deleted = false;

-- Vista auditoría (incluye eliminados)
SELECT *, 
       CASE WHEN is_deleted THEN '[ELIMINADO]' ELSE '' END as status_flag
FROM document_draft;

⚠️ Validaciones de Transición

Reglas de Negocio Implementadas

-- Función de validación de transición
CREATE OR REPLACE FUNCTION validate_document_transition(
    p_document_id UUID,
    p_new_status document_status
) RETURNS BOOLEAN AS $
DECLARE
    current_status document_status;
    signer_count INTEGER;
    pending_signers INTEGER;
BEGIN
    -- Obtener estado actual
    SELECT status INTO current_status 
    FROM document_draft 
    WHERE document_id = p_document_id;

    -- Validar transiciones permitidas
    CASE 
        WHEN current_status = 'draft' AND p_new_status = 'sent_to_sign' THEN
            -- Validar contenido y firmantes
            SELECT COUNT(*) INTO signer_count
            FROM document_signers 
            WHERE document_id = p_document_id;

            RETURN signer_count > 0;

        WHEN current_status = 'sent_to_sign' AND p_new_status = 'signed' THEN
            -- Validar que todas las firmas estén completas
            SELECT COUNT(*) INTO pending_signers
            FROM document_signers 
            WHERE document_id = p_document_id 
              AND status = 'pending';

            RETURN pending_signers = 0;

        WHEN current_status = 'rejected' AND p_new_status = 'draft' THEN
            -- Permitir corrección
            RETURN true;

        ELSE
            -- Transición no permitida
            RETURN false;
    END CASE;
END;
$ LANGUAGE plpgsql;

Triggers de Validación

-- Trigger que valida transiciones antes de UPDATE
CREATE TRIGGER validate_document_status_change
    BEFORE UPDATE OF status ON document_draft
    FOR EACH ROW
    WHEN (OLD.status IS DISTINCT FROM NEW.status)
    EXECUTE FUNCTION check_valid_transition();

📊 Métricas por Estado

Distribución de Estados

-- Consulta de distribución actual
SELECT 
    status,
    COUNT(*) as total_documents,
    ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage
FROM document_draft 
WHERE is_deleted = false
GROUP BY status
ORDER BY total_documents DESC;

Tiempo Promedio por Estado

-- Análisis de tiempos de permanencia
WITH state_durations AS (
    SELECT 
        document_id,
        status,
        created_at,
        sent_to_sign_at,
        (SELECT signed_at FROM official_documents od WHERE od.document_id = dd.document_id) as signed_at
    FROM document_draft dd
    WHERE status = 'signed'
)
SELECT 
    AVG(sent_to_sign_at - created_at) as avg_draft_duration,
    AVG(signed_at - sent_to_sign_at) as avg_signing_duration
FROM state_durations;

KPIs de Transición

-- Tasa de éxito por tipo de documento
SELECT 
    dt.name,
    COUNT(CASE WHEN dd.status = 'signed' THEN 1 END) as signed_count,
    COUNT(CASE WHEN dd.status = 'rejected' THEN 1 END) as rejected_count,
    ROUND(
        COUNT(CASE WHEN dd.status = 'signed' THEN 1 END) * 100.0 / 
        COUNT(*), 2
    ) as success_rate
FROM document_draft dd
JOIN document_types dt ON dd.document_type_id = dt.document_type_id
WHERE dd.is_deleted = false
GROUP BY dt.name
ORDER BY success_rate DESC;

🔄 Diagramas de Flujo Detallados

Flujo Principal Completo

[INICIO] → draft → sent_to_sign → signed → archived → [FIN]
              ↓         ↓           ↓
           deleted   rejected   cancelled
                     draft (corrección)

Flujo de Firmantes

Firmante 1: pending → signed
Firmante 2: pending → signed  
Numerador: pending → signed → [DOCUMENTO OFICIAL]

Flujo de Excepciones

sent_to_sign → rejected → draft → sent_to_sign → signed
      ↓            ↓         ↓
   cancelled   cancelled   cancelled

🛠️ Comandos de Gestión de Estados

Consultas Útiles para Administración

-- Documentos "atorados" en sent_to_sign por más de 7 días
SELECT dd.*, dt.name
FROM document_draft dd
JOIN document_types dt ON dd.document_type_id = dt.document_type_id
WHERE dd.status = 'sent_to_sign'
  AND dd.sent_to_sign_at < (NOW() - INTERVAL '7 days');

-- Firmantes pendientes por documento
SELECT 
    dd.document_id,
    dd.reference,
    u.full_name as pending_signer,
    ds.signing_order
FROM document_draft dd
JOIN document_signers ds ON dd.document_id = ds.document_id
JOIN users u ON ds.user_id = u.user_id
WHERE dd.status = 'sent_to_sign'
  AND ds.status = 'pending'
ORDER BY dd.sent_to_sign_at ASC;

-- Documentos rechazados con motivos
SELECT 
    dd.reference,
    dr.reason,
    u.full_name as rejected_by,
    dr.rejected_at
FROM document_draft dd
JOIN document_rejections dr ON dd.document_id = dr.document_id
JOIN users u ON dr.rejected_by = u.user_id
WHERE dd.status = 'rejected'
ORDER BY dr.rejected_at DESC;