Agrego carpeta del source sql de la Base de Datos

This commit is contained in:
Pablo
2026-08-22 19:10:49 -03:00
parent 11e36bd6c2
commit 88d724fbcf
94 changed files with 7820 additions and 0 deletions
@@ -0,0 +1,113 @@
-- ============================================================================
-- internal.limpiar_eventos_antiguos
-- ============================================================================
-- PROPÓSITO
-- Higiene operativa: elimina filas de public.eventos cuya fecha
-- esté fuera de la ventana de retención correspondiente a su tipo.
-- No archiva: los eventos no son información que valga conservar
-- más allá del operativo (ver Documentation/PoliticaRetencion.md
-- §4.2 — el archivo registra cobros, no su historia de correcciones).
--
-- DOMINIO
-- Implementa la higiene de eventos descrita en
-- Documentation/PoliticaRetencion.md §7. Dos ventanas distintas
-- porque los eventos de pagos viven más tiempo en la base que el
-- resto: una corrección o anulación de pago suele auditarse más
-- tarde que el cambio que la motivó. Tres meses es el horizonte
-- suficiente para ese seguimiento; el resto (acciones admin sobre
-- actividades, horarios, etc.) no requiere esa cola.
--
-- REGLA DE DISCRIMINACIÓN
-- Evento de pagos sii `tipo LIKE 'pago_%'`. Lo determina el
-- prefijo, no una tabla aparte ni un campo nuevo. Quien inserta
-- en public.eventos elige el `tipo` y respeta el prefijo cuando
-- corresponda; hoy lo cumplen `pago_editado` y `pago_anulado`
-- (insertados por fc_editar_pago y fc_anular_pago vía
-- internal.log_evento). Cualquier evento futuro relacionado a
-- pagos debe seguir esta convención para heredar la ventana más
-- larga.
--
-- PARÁMETROS
-- Ninguno. Las dos ventanas en meses se leen de internal.app_config:
-- - `retencion.eventos_pagos_meses` (default 3).
-- - `retencion.eventos_otros_meses` (default 2).
--
-- AUTORIZACIÓN
-- No valida permiso propio. El schema `internal` no es alcanzable
-- por roles cliente (ver hardening en
-- `database/schema/00_schemas.sql`). Callers legítimos: pg_cron y
-- fachadas `public.fc_*` SECURITY DEFINER que orquesten el pipeline.
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio.
--
-- RETORNA
-- JSONB con corte y eliminados desglosados por grupo (pagos / otros)
-- más un total agregado, útil para inspección de operación.
--
-- EFECTOS SECUNDARIOS
-- - DELETE de eventos `pago_*` con `fecha_evento < now() - ventana_pagos`.
-- - DELETE de eventos no-`pago_*` con `fecha_evento < now() - ventana_otros`.
-- - **No** registra evento propio (vía internal.log_evento o de otro
-- modo). Hacerlo crearía un loop trivial: cada corrida generaría
-- un evento que la próxima corrida vería viejo y volvería a
-- producir otro. La purga es deliberadamente silenciosa; la
-- visibilidad se garantiza por el contador devuelto en el JSONB
-- y, eventualmente, por la vista admin del pipeline (US-R21).
-- ============================================================================
CREATE OR REPLACE FUNCTION internal.limpiar_eventos_antiguos()
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
SET timezone = 'America/Argentina/Buenos_Aires'
VOLATILE
AS $$
DECLARE
v_meses_pagos INT;
v_meses_otros INT;
v_corte_pagos TIMESTAMPTZ;
v_corte_otros TIMESTAMPTZ;
v_eliminados_pagos INT;
v_eliminados_otros INT;
BEGIN
v_meses_pagos := internal.get_config_int('retencion.eventos_pagos_meses', 3);
v_meses_otros := internal.get_config_int('retencion.eventos_otros_meses', 2);
v_corte_pagos := now() - (v_meses_pagos * INTERVAL '1 month');
v_corte_otros := now() - (v_meses_otros * INTERVAL '1 month');
DELETE FROM eventos
WHERE tipo LIKE 'pago_%'
AND fecha_evento < v_corte_pagos;
GET DIAGNOSTICS v_eliminados_pagos = ROW_COUNT;
DELETE FROM eventos
WHERE tipo NOT LIKE 'pago_%'
AND fecha_evento < v_corte_otros;
GET DIAGNOSTICS v_eliminados_otros = ROW_COUNT;
RETURN jsonb_build_object(
'status', 'success',
'corte_pagos', v_corte_pagos,
'corte_otros', v_corte_otros,
'meses_pagos', v_meses_pagos,
'meses_otros', v_meses_otros,
'eliminados_pagos', v_eliminados_pagos,
'eliminados_otros', v_eliminados_otros,
'eliminados_total', v_eliminados_pagos + v_eliminados_otros,
'mensaje', format(
'Eliminados %s eventos pago_* anteriores a %s y %s eventos no-pago anteriores a %s.',
v_eliminados_pagos, v_corte_pagos,
v_eliminados_otros, v_corte_otros
)
);
END;
$$;
-- Defensa en profundidad: aunque el hardening del schema (00_schemas.sql)
-- ya bloquee USAGE y EXECUTE por default, revocamos explícitamente acá
-- por si en algún futuro alguien afloja las defensas a nivel schema.
REVOKE ALL ON FUNCTION internal.limpiar_eventos_antiguos()
FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,90 @@
-- ============================================================================
-- internal.log_evento
-- ============================================================================
-- PROPÓSITO
-- Inserta una fila en public.eventos. Single entry point para que
-- cualquier función pública registre un evento auditable (ediciones,
-- anulaciones, acciones administrativas) sin tocar la tabla
-- directamente.
--
-- Hoy lo usan fc_editar_pago y fc_anular_pago para registrar
-- pago_editado y pago_anulado.
--
-- CAP DE SEGURIDAD (US-R12)
-- Antes de insertar, chequea el conteo vigente de public.eventos
-- contra `retencion.eventos_cap` (default 50000). Si registrar el
-- evento haría que la tabla alcance o supere el cap, RECHAZA con
-- excepción explícita en vez de insertar. No es FIFO: el rechazo
-- hace fallar la función que dispara el evento y corta la cascada,
-- de modo que un bug que dispare eventos en loop se vuelve ruidoso
-- en vez de comerse la cuota de la base en silencio. El cap es un
-- freno de emergencia, no la vía normal de control de tamaño (eso
-- lo hace la purga mensual, internal.limpiar_eventos_antiguos).
--
-- PARÁMETROS
-- p_tipo TEXT discriminador del evento (ej. 'pago_editado').
-- p_tabla TEXT nombre de la tabla referenciada (ej. 'pagos').
-- p_referencia_id UUID ID de la fila referenciada en esa tabla.
-- p_cliente_id UUID cliente afectado (para eventos sobre clientes).
-- p_actor_id UUID usuario que ejecutó la acción.
-- p_valor_anterior JSONB snapshot del estado anterior (opcional).
-- p_valor_actual JSONB snapshot del estado nuevo (opcional).
-- p_descripcion TEXT (opcional, default NULL) descripción libre /
-- motivo.
--
-- AUTORIZACIÓN
-- Función interna sin validación propia.
--
-- ERRORES (RAISE EXCEPTION)
-- - 'No se puede registrar el evento ...: public.eventos alcanzó su
-- cap de seguridad ...' (US-R12).
--
-- RETORNA
-- void.
--
-- EFECTOS SECUNDARIOS
-- INSERT en public.eventos.
-- ============================================================================
CREATE OR REPLACE FUNCTION internal.log_evento(
p_tipo text,
p_tabla text,
p_referencia_id uuid,
p_cliente_id uuid,
p_actor_id uuid,
p_valor_anterior jsonb,
p_valor_actual jsonb,
p_descripcion text DEFAULT NULL::text
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_cap INT;
v_count INT;
BEGIN
-- Cap de seguridad (US-R12): freno anti-loop. Rechazamos el insert
-- cuyo conteo resultante igualaría o superaría el cap, de modo que el
-- cap es el valor que el conteo no debe alcanzar. Bug ruidoso > bug
-- silencioso: que falle la cascada y nos enteremos.
v_cap := internal.get_config_int('retencion.eventos_cap', 50000);
SELECT count(*) INTO v_count FROM public.eventos;
IF v_count + 1 >= v_cap THEN
RAISE EXCEPTION
'No se puede registrar el evento "%": public.eventos alcanzó su cap de seguridad (% filas). Suele indicar un bug que dispara eventos en loop; revisar la causa antes de subir retencion.eventos_cap en internal.app_config.',
p_tipo, v_cap;
END IF;
INSERT INTO public.eventos (
tipo, tabla_referencia, referencia_id,
cliente_id, actor_id,
valor_anterior, valor_actual, descripcion
) VALUES (
p_tipo, p_tabla, p_referencia_id,
p_cliente_id, p_actor_id,
p_valor_anterior, p_valor_actual, p_descripcion
);
END;
$$;