-- ============================================================================ -- 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 $$;