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,91 @@
-- ============================================================================
-- fc_obtener_estado_archivo
-- ============================================================================
-- PROPÓSITO
-- Devuelve el estado del pipeline mensual de archivado leyendo
-- internal.archivo_exports, en un formato apto para la vista admin
-- (US-R21). Es la fachada de lectura de la bitácora de control: el
-- operador no escribe ni dispara el pipeline, sólo lo observa.
--
-- DOMINIO
-- Implementa la "visibilidad" de Documentation/PoliticaRetencion.md
-- §5.4. Reporta:
-- - ultimo_ok: el archivado exitoso más reciente por cada tipo
-- ('pagos', 'agregado_reservas'), con su mes objetivo y cuándo
-- se ejecutó.
-- - fallos_recientes: hasta 20 intentos `fallo`, más nuevo primero,
-- con su detalle_error para diagnóstico.
--
-- NO calcula "meses pendientes" (qué mes debería haberse archivado y no
-- se hizo): esa detección depende de la lógica de cadencia del pipeline
-- (qué mes cumple 12, qué mes recién cerró) y vive en US-R17/US-R21, no
-- acá. Esta función reporta lo que la tabla contiene, no lo que falta.
--
-- PARÁMETROS
-- p_token UUID de sesión del actor.
--
-- AUTORIZACIÓN
-- Requiere permiso 'ver_configuraciones_generales'. Se reusa ese permiso
-- (admin/superadmin) en vez de crear uno nuevo: el estado del pipeline es
-- información operativa del mismo tenor que la config general, y evitar
-- una acción nueva en internal.permisos mantiene esta entrega
-- autocontenida. Si más adelante el monitoreo (US-R21/US-R22) justifica
-- un permiso propio (ej. 'ver_estado_sistema'), migrar acá es trivial.
--
-- ERRORES (RAISE EXCEPTION)
-- - Los que propague internal.validate_permission (token / permiso).
--
-- RETORNA
-- JSONB { status, ultimo_ok, fallos_recientes }.
--
-- EFECTOS SECUNDARIOS
-- Ninguno (STABLE, sólo lectura).
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_obtener_estado_archivo(p_token UUID)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
SET timezone = 'America/Argentina/Buenos_Aires'
STABLE
AS $$
DECLARE
v_ultimo_ok JSONB;
v_fallos_recientes JSONB;
BEGIN
PERFORM internal.validate_permission(p_token, 'ver_configuraciones_generales');
-- Último `ok` por tipo: DISTINCT ON (tipo) ordenando por fecha desc.
SELECT COALESCE(jsonb_object_agg(s.tipo, s.info), '{}'::jsonb)
INTO v_ultimo_ok
FROM (
SELECT DISTINCT ON (tipo)
tipo,
jsonb_build_object(
'anio_mes_target', anio_mes_target,
'ejecutado_en', ejecutado_en
) AS info
FROM internal.archivo_exports
WHERE resultado = 'ok'
ORDER BY tipo, ejecutado_en DESC
) s;
-- Fallos recientes (hasta 20), más nuevo primero.
SELECT COALESCE(jsonb_agg(to_jsonb(f) ORDER BY f.ejecutado_en DESC), '[]'::jsonb)
INTO v_fallos_recientes
FROM (
SELECT anio_mes_target, tipo, ejecutado_en, detalle_error
FROM internal.archivo_exports
WHERE resultado = 'fallo'
ORDER BY ejecutado_en DESC
LIMIT 20
) f;
RETURN jsonb_build_object(
'status', 'success',
'ultimo_ok', v_ultimo_ok,
'fallos_recientes', v_fallos_recientes
);
END;
$$;
@@ -0,0 +1,89 @@
-- ============================================================================
-- fc_obtener_tamano_base
-- ============================================================================
-- PROPÓSITO
-- Termómetro de la base: devuelve, por tabla, la cantidad de filas y el
-- espacio que ocupa (tabla + índices + toast), más el tamaño total de la
-- base. Sirve para anticipar el momento en que nos acercamos al cap de
-- 500 MB del plan Free (Documentation/PoliticaRetencion.md §2, §5.5),
-- que es la razón de ser de toda la política de retención.
--
-- DOMINIO
-- Implementa el "dashboard de tamaño de BD" del backlog (US-R22). Es la
-- última línea de defensa que menciona §5.5: si el pipeline mensual
-- fallara en silencio, los datos viejos se acumularían y esta función lo
-- haría visible antes de chocar el límite.
--
-- El nombre diverge del `fc_estado_retencion` sugerido en el backlog (que
-- admitía "o similar"): "tamaño base" describe mejor qué reporta y no se
-- confunde con fc_obtener_estado_archivo (estado del pipeline).
--
-- PARÁMETROS
-- p_token UUID de sesión del actor.
--
-- AUTORIZACIÓN
-- Requiere permiso 'ver_configuraciones_generales' (admin/superadmin),
-- el mismo que el resto de las lecturas de estado del sistema. SECURITY
-- DEFINER para poder leer pg_catalog y contar tablas del schema internal.
--
-- ERRORES (RAISE EXCEPTION)
-- - Los que propague internal.validate_permission (token / permiso).
--
-- RETORNA
-- JSONB { status, db_total_bytes, db_total_tamano, tablas: [ {schema,
-- tabla, filas, bytes, tamano} ] }. `tablas` viene ordenado por tamaño
-- descendente (la más grande primero — la candidata a vigilar).
--
-- EFECTOS SECUNDARIOS
-- Ninguno (sólo lectura). El count(*) por tabla es exacto; a la escala
-- del sistema (≤200 usuarios, tablas chicas) es despreciable. Si la base
-- creciera mucho, conviene pasar a la estimación de pg_class.reltuples.
-- ============================================================================
CREATE OR REPLACE FUNCTION public.fc_obtener_tamano_base(p_token UUID)
RETURNS JSONB
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
DECLARE
v_tablas JSONB := '[]'::jsonb;
v_rec RECORD;
v_filas BIGINT;
v_total BIGINT;
BEGIN
PERFORM internal.validate_permission(p_token, 'ver_configuraciones_generales');
FOR v_rec IN
SELECT n.nspname AS schema_name,
c.relname AS table_name,
pg_total_relation_size(c.oid) AS bytes
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r'
AND n.nspname IN ('public', 'internal')
ORDER BY pg_total_relation_size(c.oid) DESC, n.nspname, c.relname
LOOP
EXECUTE format('SELECT count(*) FROM %I.%I', v_rec.schema_name, v_rec.table_name)
INTO v_filas;
v_tablas := v_tablas || jsonb_build_object(
'schema', v_rec.schema_name,
'tabla', v_rec.table_name,
'filas', v_filas,
'bytes', v_rec.bytes,
'tamano', pg_size_pretty(v_rec.bytes)
);
END LOOP;
v_total := pg_database_size(current_database());
RETURN jsonb_build_object(
'status', 'success',
'db_total_bytes', v_total,
'db_total_tamano', pg_size_pretty(v_total),
'tablas', v_tablas
);
END;
$$;
@@ -0,0 +1,79 @@
-- ============================================================================
-- internal.agregado_reservas_mensual
-- ============================================================================
-- PROPÓSITO
-- Calcula, para un mes dado, la concurrencia por actividad: cuántas
-- reservas hubo en total y cuántas se cancelaron. Es la fuente de datos
-- de la hoja ACTIVIDAD_MENSUAL del archivo histórico
-- (Documentation/PoliticaRetencion.md §4.1).
--
-- DOMINIO
-- El sistema descarta los turnos pasados (higiene, 2 meses) pero la
-- concurrencia mensual sí vale conservar agregada. Esta función produce
-- ese agregado ANTES de que el pipeline purgue los turnos del mes
-- (§5.2): el orden importa, porque una vez borrados los turnos las
-- reservas caen por CASCADE y el dato se pierde.
--
-- Sólo aparecen actividades con al menos una reserva en el mes (el JOIN
-- contra reservas las filtra naturalmente). Una actividad sin reservas
-- no aporta fila: su concurrencia fue cero y no hay nada que archivar.
--
-- "totales" incluye las canceladas; "canceladas" es el subconjunto soft-
-- deleted (reservas.cancelada = true). La asistencia neta se deriva
-- restando, pero se archivan los dos números crudos para no perder
-- información.
--
-- PARÁMETROS
-- p_anio_mes DATE. Cualquier día del mes objetivo; se trunca al día 1.
-- El mes se delimita con un rango semiabierto [inicio, inicio+1mes) sobre
-- turnos.fecha.
--
-- AUTORIZACIÓN
-- No valida permiso propio. El schema `internal` no es alcanzable por
-- roles cliente (ver hardening en `database/schema/00_schemas.sql`).
-- Caller legítimo: el pipeline mensual (US-R17). Read-only: no muta
-- estado; la persistencia ocurre cuando la Edge Function appendea al
-- XLSX.
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio.
--
-- RETORNA
-- SETOF filas (anio_mes, actividad_nombre, reservas_totales,
-- reservas_canceladas). Vacío si el mes no tuvo reservas. `anio_mes` se
-- devuelve como TEXTO 'YYYY-MM-DD' (misma razón que en pagos_del_mes: el
-- pipeline lo usa como clave de reemplazo idempotente en el XLSX).
-- ============================================================================
CREATE OR REPLACE FUNCTION internal.agregado_reservas_mensual(p_anio_mes DATE)
RETURNS TABLE (
anio_mes TEXT,
actividad_nombre TEXT,
reservas_totales INTEGER,
reservas_canceladas INTEGER
)
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
SET timezone = 'America/Argentina/Buenos_Aires'
STABLE
AS $$
SELECT
to_char(date_trunc('month', p_anio_mes), 'YYYY-MM-DD') AS anio_mes,
a.nombre AS actividad_nombre,
count(r.id)::int AS reservas_totales,
count(r.id) FILTER (WHERE r.cancelada)::int AS reservas_canceladas
FROM reservas r
JOIN turnos t ON t.id = r.turno_id
JOIN actividades a ON a.id = t.actividad_id
WHERE t.fecha >= date_trunc('month', p_anio_mes)::date
AND t.fecha < (date_trunc('month', p_anio_mes) + INTERVAL '1 month')::date
GROUP BY a.nombre
ORDER BY a.nombre;
$$;
-- 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.agregado_reservas_mensual(DATE)
FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,95 @@
-- ============================================================================
-- internal.pagos_del_mes
-- ============================================================================
-- PROPÓSITO
-- Devuelve el detalle de los pagos cobrados en un mes dado, ya con la
-- forma de la hoja PAGOS del archivo histórico
-- (Documentation/PoliticaRetencion.md §4.1). Es la fuente que el
-- pipeline mensual (US-R17) appendea al XLSX antes de borrar esos pagos
-- de la base.
--
-- DOMINIO
-- El archivo conserva "una fila por pago realmente cobrado" (§4.1).
-- Por eso:
-- - Se EXCLUYEN los pagos anulados (§4.2): una anulación se manifiesta
-- como ausencia de fila, no como fila marcada. anulado_at IS NULL.
-- - El "plan" sale del snapshot guardado en detalle->>'cuota_nombre'
-- al momento del cobro (lo escribe fc_insertar_pago), con fallback al
-- nombre vigente en tipos_cuota. El snapshot es lo correcto para un
-- archivo: preserva el nombre que tenía el plan cuando se cobró,
-- aunque después se renombre o se borre.
-- - El "metodo" y "registrado_por" se resuelven por join; sus tablas
-- (metodos_pago, usuarios) no se purgan, así que el dato sigue vivo.
--
-- NOTA sobre tipos: hoy todos los pagos son 'cuota_mensual' (los
-- correctivos —devolucion/ajuste— son una decisión cerrada de NO
-- modelar, DominioPagos §10). Si alguna vez se implementan, habrá que
-- revisar qué cuenta como "cobro real" para el archivo; por ahora el
-- único filtro necesario es el de anulados.
--
-- PARÁMETROS
-- p_anio_mes DATE. Cualquier día del mes objetivo; se compara contra
-- pagos.anio_mes_pagado, que ya está truncado al día 1 por constraint.
--
-- AUTORIZACIÓN
-- No valida permiso propio. Schema `internal` no alcanzable por roles
-- cliente (hardening en 00_schemas.sql). Caller legítimo: el pipeline
-- mensual (US-R17), vía conexión PG directa. Read-only.
--
-- ERRORES (RAISE EXCEPTION)
-- Ninguno propio.
--
-- RETORNA
-- SETOF filas con las columnas de la hoja PAGOS, en el orden del §4.1.
-- Vacío si el mes no tuvo pagos cobrados (caso normal del primer año,
-- §4.3).
--
-- Las columnas de fecha se devuelven como TEXTO estable
-- (`fecha_pago` = 'YYYY-MM-DD HH24:MI', `anio_mes_pagado` = 'YYYY-MM-DD').
-- Es deliberado: el pipeline (US-R17) usa `anio_mes_pagado` como clave
-- para "reemplazar las filas de este mes" en el XLSX de forma idempotente,
-- y un texto fijo se compara sin la ambigüedad de las celdas-fecha de
-- Excel. Ordena lexicográfico = cronológico.
-- ============================================================================
CREATE OR REPLACE FUNCTION internal.pagos_del_mes(p_anio_mes DATE)
RETURNS TABLE (
cliente_nombre TEXT,
cliente_dni TEXT,
fecha_pago TEXT,
anio_mes_pagado TEXT,
monto NUMERIC,
metodo TEXT,
plan TEXT,
registrado_por TEXT
)
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
SET timezone = 'America/Argentina/Buenos_Aires'
STABLE
AS $$
SELECT
trim(cl.nombre || ' ' || COALESCE(cl.apellido, '')) AS cliente_nombre,
cl.dni AS cliente_dni,
to_char(p.fecha_pago, 'YYYY-MM-DD HH24:MI') AS fecha_pago,
to_char(p.anio_mes_pagado, 'YYYY-MM-DD') AS anio_mes_pagado,
p.monto_total AS monto,
m.descripcion AS metodo,
COALESCE(p.detalle->>'cuota_nombre', tc.nombre) AS plan,
ru.nombre AS registrado_por
FROM pagos p
JOIN usuarios cl ON cl.id = p.cliente_id
LEFT JOIN metodos_pago m ON m.id = p.metodo_id
LEFT JOIN tipos_cuota tc ON tc.id = p.tipo_cuota_id
LEFT JOIN usuarios ru ON ru.id = p.created_by
WHERE p.anio_mes_pagado = date_trunc('month', p_anio_mes)::date
AND p.anulado_at IS NULL
ORDER BY cliente_nombre, p.fecha_pago;
$$;
-- 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.pagos_del_mes(DATE)
FROM PUBLIC, anon, authenticated;
@@ -0,0 +1,61 @@
-- ============================================================================
-- internal.pagos_mes_archivado
-- ============================================================================
-- PROPÓSITO
-- Predicado de dominio: ¿los pagos del mes dado ya fueron archivados al
-- XLSX histórico? Centraliza la definición de "mes archivado" para que
-- los guards de pagos (fc_editar_pago, fc_anular_pago — US-R11) y
-- cualquier futuro consumidor consulten un único criterio en vez de
-- reimplementar el EXISTS y arriesgar divergencias.
--
-- DOMINIO
-- Un mes de pagos está archivado sii existe un registro `ok` de archivado
-- para ese mes en internal.archivo_exports (tipo = 'pagos'). Ver
-- Documentation/PoliticaRetencion.md §5.3 y DominioPagos.md §14.
--
-- Sutilezas que el helper encierra:
-- - Sólo cuenta resultado = 'ok'. Un intento `fallo` NO archiva: el
-- mes sigue editable hasta que el archivado tenga éxito.
-- - El target se compara truncado al día 1, igual que se guarda.
-- - Las corridas vacías del primer año escriben `ok` con
-- anio_mes_target NULL (US-R17); como `NULL = <mes>` nunca es TRUE,
-- esas filas no marcan ningún mes como archivado. Correcto: no
-- archivaron nada.
--
-- PARÁMETROS
-- p_anio_mes DATE. El mes a consultar (cualquier día; se trunca).
--
-- AUTORIZACIÓN
-- No valida permiso propio. Schema `internal` no alcanzable por roles
-- cliente (hardening en 00_schemas.sql). Lo invocan fachadas públicas
-- SECURITY DEFINER (fc_editar_pago, fc_anular_pago) ya autorizadas.
--
-- RETORNA
-- boolean. TRUE si el mes está archivado; FALSE si no (incluye p_anio_mes
-- NULL).
--
-- EFECTOS SECUNDARIOS
-- Ninguno (STABLE, sólo lectura).
-- ============================================================================
CREATE OR REPLACE FUNCTION internal.pagos_mes_archivado(p_anio_mes DATE)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
SELECT EXISTS (
SELECT 1
FROM internal.archivo_exports
WHERE tipo = 'pagos'
AND resultado = 'ok'
AND anio_mes_target = date_trunc('month', p_anio_mes)::date
);
$$;
-- 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.pagos_mes_archivado(DATE)
FROM PUBLIC, anon, authenticated;