208 lines
6.7 KiB
PL/PgSQL
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 $$;
|