erp-core-database/migrations/20251212_001_fiscal_period_validation.sql

208 lines
6.7 KiB
PL/PgSQL

-- ============================================================================
-- MIGRACIÓN: Validación de Período Fiscal Cerrado
-- Fecha: 2025-12-12
-- Descripción: Agrega trigger para prevenir asientos en períodos cerrados
-- Impacto: Todas las verticales que usan el módulo financiero
-- Rollback: DROP TRIGGER y DROP FUNCTION incluidos al final
-- ============================================================================
-- ============================================================================
-- 1. FUNCIÓN DE VALIDACIÓN
-- ============================================================================
CREATE OR REPLACE FUNCTION financial.validate_period_not_closed()
RETURNS TRIGGER AS $$
DECLARE
v_period_status TEXT;
v_period_name TEXT;
BEGIN
-- Solo validar si hay un fiscal_period_id
IF NEW.fiscal_period_id IS NULL THEN
RETURN NEW;
END IF;
-- Obtener el estado del período
SELECT fp.status, fp.name INTO v_period_status, v_period_name
FROM financial.fiscal_periods fp
WHERE fp.id = NEW.fiscal_period_id;
-- Validar que el período no esté cerrado
IF v_period_status = 'closed' THEN
RAISE EXCEPTION 'ERR_PERIOD_CLOSED: No se pueden crear o modificar asientos en el período cerrado: %', v_period_name
USING ERRCODE = 'P0001';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION financial.validate_period_not_closed() IS
'Valida que no se creen asientos contables en períodos fiscales cerrados.
Lanza excepción ERR_PERIOD_CLOSED si el período está cerrado.';
-- ============================================================================
-- 2. TRIGGER EN JOURNAL_ENTRIES
-- ============================================================================
-- Eliminar trigger si existe (idempotente)
DROP TRIGGER IF EXISTS trg_validate_period_before_entry ON financial.journal_entries;
-- Crear trigger BEFORE INSERT OR UPDATE
CREATE TRIGGER trg_validate_period_before_entry
BEFORE INSERT OR UPDATE ON financial.journal_entries
FOR EACH ROW
EXECUTE FUNCTION financial.validate_period_not_closed();
COMMENT ON TRIGGER trg_validate_period_before_entry ON financial.journal_entries IS
'Previene la creación o modificación de asientos en períodos fiscales cerrados';
-- ============================================================================
-- 3. FUNCIÓN PARA CERRAR PERÍODO
-- ============================================================================
CREATE OR REPLACE FUNCTION financial.close_fiscal_period(
p_period_id UUID,
p_user_id UUID
)
RETURNS financial.fiscal_periods AS $$
DECLARE
v_period financial.fiscal_periods;
v_unposted_count INTEGER;
BEGIN
-- Obtener período
SELECT * INTO v_period
FROM financial.fiscal_periods
WHERE id = p_period_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Período fiscal no encontrado' USING ERRCODE = 'P0002';
END IF;
IF v_period.status = 'closed' THEN
RAISE EXCEPTION 'El período ya está cerrado' USING ERRCODE = 'P0003';
END IF;
-- Verificar que no haya asientos sin postear
SELECT COUNT(*) INTO v_unposted_count
FROM financial.journal_entries je
WHERE je.fiscal_period_id = p_period_id
AND je.status = 'draft';
IF v_unposted_count > 0 THEN
RAISE EXCEPTION 'Existen % asientos sin postear en este período. Postéelos antes de cerrar.',
v_unposted_count USING ERRCODE = 'P0004';
END IF;
-- Cerrar el período
UPDATE financial.fiscal_periods
SET status = 'closed',
closed_at = NOW(),
closed_by = p_user_id,
updated_at = NOW()
WHERE id = p_period_id
RETURNING * INTO v_period;
RETURN v_period;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION financial.close_fiscal_period(UUID, UUID) IS
'Cierra un período fiscal. Valida que todos los asientos estén posteados.';
-- ============================================================================
-- 4. FUNCIÓN PARA REABRIR PERÍODO (Solo admins)
-- ============================================================================
CREATE OR REPLACE FUNCTION financial.reopen_fiscal_period(
p_period_id UUID,
p_user_id UUID,
p_reason TEXT DEFAULT NULL
)
RETURNS financial.fiscal_periods AS $$
DECLARE
v_period financial.fiscal_periods;
BEGIN
-- Obtener período
SELECT * INTO v_period
FROM financial.fiscal_periods
WHERE id = p_period_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Período fiscal no encontrado' USING ERRCODE = 'P0002';
END IF;
IF v_period.status = 'open' THEN
RAISE EXCEPTION 'El período ya está abierto' USING ERRCODE = 'P0005';
END IF;
-- Reabrir el período
UPDATE financial.fiscal_periods
SET status = 'open',
closed_at = NULL,
closed_by = NULL,
updated_at = NOW()
WHERE id = p_period_id
RETURNING * INTO v_period;
-- Registrar en log de auditoría
INSERT INTO system.logs (
tenant_id, level, module, message, context, user_id
)
SELECT
v_period.tenant_id,
'warning',
'financial',
'Período fiscal reabierto',
jsonb_build_object(
'period_id', p_period_id,
'period_name', v_period.name,
'reason', p_reason,
'reopened_by', p_user_id
),
p_user_id;
RETURN v_period;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION financial.reopen_fiscal_period(UUID, UUID, TEXT) IS
'Reabre un período fiscal cerrado. Registra en auditoría. Solo para administradores.';
-- ============================================================================
-- 5. ÍNDICE PARA PERFORMANCE
-- ============================================================================
CREATE INDEX IF NOT EXISTS idx_journal_entries_fiscal_period
ON financial.journal_entries(fiscal_period_id)
WHERE fiscal_period_id IS NOT NULL;
-- ============================================================================
-- ROLLBACK SCRIPT (ejecutar si es necesario revertir)
-- ============================================================================
/*
DROP TRIGGER IF EXISTS trg_validate_period_before_entry ON financial.journal_entries;
DROP FUNCTION IF EXISTS financial.validate_period_not_closed();
DROP FUNCTION IF EXISTS financial.close_fiscal_period(UUID, UUID);
DROP FUNCTION IF EXISTS financial.reopen_fiscal_period(UUID, UUID, TEXT);
DROP INDEX IF EXISTS financial.idx_journal_entries_fiscal_period;
*/
-- ============================================================================
-- VERIFICACIÓN
-- ============================================================================
DO $$
BEGIN
-- Verificar que el trigger existe
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'trg_validate_period_before_entry'
) THEN
RAISE EXCEPTION 'Error: Trigger no fue creado correctamente';
END IF;
RAISE NOTICE 'Migración completada exitosamente: Validación de período fiscal';
END $$;