Agrego carpeta del source sql de la Base de Datos
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user