Agrego carpeta del source sql de la Base de Datos
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
-- ============================================================================
|
||||
-- fc_eliminar_actividad
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Borrado físico de una actividad. Operación **destructiva en cascada**:
|
||||
-- borra plantillas horarias (regulares y de días especiales), todos los
|
||||
-- turnos pasados y futuros de la actividad, y todas las reservas
|
||||
-- históricas y futuras asociadas a esos turnos. La pérdida es irreversible.
|
||||
--
|
||||
-- Si la actividad está incluida en uno o más planes (tipos_cuota), el
|
||||
-- borrado se rechaza — esas FK no cascadean. El operador debe primero
|
||||
-- sacar la actividad de los planes (o desactivarla si quiere conservar
|
||||
-- las asociaciones).
|
||||
--
|
||||
-- DOMINIO
|
||||
-- El dominio de horarios (Documentation/DominioHorarios.md §7.2) describe
|
||||
-- sólo dos operaciones de baja para una actividad: **suspender** y
|
||||
-- **restablecer**. La eliminación física no está en §7.2. Vive acá
|
||||
-- como vía operativa del operador, sostenida por §9.6 (libertad
|
||||
-- operativa con advertencia): la UI muestra la advertencia previa, el
|
||||
-- operador asume la consecuencia y la función ejecuta.
|
||||
--
|
||||
-- §11 exige que los cambios estructurales sean reconstruibles. Esta
|
||||
-- función lo cubre vía evento `actividad_eliminada` con un payload
|
||||
-- **minimalista** alineado con la política del sistema: los eventos
|
||||
-- guardan lo que puede volver a ser necesario operativamente, no
|
||||
-- estadística histórica. Por eso el snapshot es sólo `{id, nombre}` (lo
|
||||
-- mínimo para identificar qué se borró) y los conteos cuentan sólo
|
||||
-- compromisos vivos rotos (`turnos_futuros`, `reservas_vivas`). Los
|
||||
-- pasados no se cuentan: la política de retención los iba a descartar
|
||||
-- de todas formas. Los detalles de la actividad (duración, capacidad,
|
||||
-- etc.) no se conservan: el dominio no admite "deshacer eliminación";
|
||||
-- si vuelve a ser necesaria, el operador la crea de cero.
|
||||
--
|
||||
-- No mirar a esta función como la contraparte de "suspender" — son dos
|
||||
-- cosas distintas. Suspender es reversible y preserva historia;
|
||||
-- eliminar es terminal y la destruye. Para suspender, usar
|
||||
-- `fc_modificar_actividad` con `{activo: false}`.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_actividad_id INT ID de la actividad a eliminar.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'eliminar_actividad'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'Sesión inválida o usuario no encontrado.'
|
||||
-- - 'No se puede eliminar la actividad X porque está incluida en uno o
|
||||
-- más planes. Sacala de los planes primero, o desactivala si querés
|
||||
-- conservarla en el sistema.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- BOOL TRUE si eliminó la actividad, FALSE si no encontró ninguna fila
|
||||
-- con ese ID. El frontend arma el preview previo a la confirmación con
|
||||
-- las consultas existentes (turnos/reservas filtrados por actividad).
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - DELETE en actividades. Cascadea a horario_actividad,
|
||||
-- horario_actividad_especial, turnos, y de ahí a reservas.
|
||||
-- - INSERT en eventos: tipo 'actividad_eliminada', valor_anterior con
|
||||
-- `{actividad: {id, nombre}, destruido: {turnos_futuros, reservas_vivas}}`,
|
||||
-- valor_actual NULL. referencia_id queda NULL porque actividades.id
|
||||
-- es INT y eventos.referencia_id es UUID; el id va embebido en el
|
||||
-- snapshot.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_eliminar_actividad(
|
||||
p_token UUID,
|
||||
p_actividad_id INT
|
||||
)
|
||||
RETURNS BOOL
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
VOLATILE
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_actor_id UUID;
|
||||
v_actividad_id INT;
|
||||
v_actividad_nombre TEXT;
|
||||
v_count_turnos_futuros INT;
|
||||
v_count_reservas_vivas INT;
|
||||
BEGIN
|
||||
------------------------------------------------------------
|
||||
-- 1. Permisos + actor
|
||||
------------------------------------------------------------
|
||||
PERFORM internal.validate_permission(p_token, 'eliminar_actividad');
|
||||
|
||||
SELECT s.usuario_id
|
||||
INTO v_actor_id
|
||||
FROM internal.sesiones s
|
||||
WHERE s.token = p_token;
|
||||
|
||||
IF v_actor_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 2. Identidad mínima de la actividad (debe existir para continuar)
|
||||
------------------------------------------------------------
|
||||
SELECT a.id, a.nombre
|
||||
INTO v_actividad_id, v_actividad_nombre
|
||||
FROM actividades a
|
||||
WHERE a.id = p_actividad_id;
|
||||
|
||||
IF v_actividad_id IS NULL THEN
|
||||
-- No existe: nada que borrar, nada que loggear.
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 3. Pre-conteo de compromisos vivos (lo único que importa
|
||||
-- para reconstrucción operativa; los pasados igual iban
|
||||
-- a desaparecer por retención).
|
||||
------------------------------------------------------------
|
||||
SELECT COUNT(*)::INT INTO v_count_turnos_futuros
|
||||
FROM turnos
|
||||
WHERE actividad_id = p_actividad_id
|
||||
AND fecha >= CURRENT_DATE;
|
||||
|
||||
SELECT COUNT(*)::INT INTO v_count_reservas_vivas
|
||||
FROM reservas r
|
||||
JOIN turnos t ON r.turno_id = t.id
|
||||
WHERE t.actividad_id = p_actividad_id
|
||||
AND t.fecha >= CURRENT_DATE
|
||||
AND r.cancelada = false;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 4. Borrado físico (cascadea por FK)
|
||||
------------------------------------------------------------
|
||||
BEGIN
|
||||
DELETE FROM actividades a WHERE a.id = p_actividad_id;
|
||||
EXCEPTION
|
||||
WHEN foreign_key_violation THEN
|
||||
-- El único origen esperado es actividades_tipos_cuota (no cascadea).
|
||||
RAISE EXCEPTION 'No se puede eliminar la actividad % porque está incluida en uno o más planes. Sacala de los planes primero, o desactivala si querés conservarla en el sistema.', p_actividad_id;
|
||||
END;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 5. Trazabilidad del cambio estructural destructivo (§11)
|
||||
-- Si log_evento fallara, la transacción entera revierte
|
||||
-- (el DELETE no queda firme sin trazabilidad).
|
||||
------------------------------------------------------------
|
||||
PERFORM internal.log_evento(
|
||||
p_tipo => 'actividad_eliminada',
|
||||
p_tabla => 'actividades',
|
||||
p_referencia_id => NULL,
|
||||
p_cliente_id => NULL,
|
||||
p_actor_id => v_actor_id,
|
||||
p_valor_anterior => jsonb_build_object(
|
||||
'actividad', jsonb_build_object(
|
||||
'id', v_actividad_id,
|
||||
'nombre', v_actividad_nombre
|
||||
),
|
||||
'destruido', jsonb_build_object(
|
||||
'turnos_futuros', v_count_turnos_futuros,
|
||||
'reservas_vivas', v_count_reservas_vivas
|
||||
)
|
||||
),
|
||||
p_valor_actual => NULL,
|
||||
p_descripcion => NULL
|
||||
);
|
||||
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,110 @@
|
||||
-- ============================================================================
|
||||
-- fc_insertar_actividad
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Crea una actividad nueva (entidad reservable: yoga, funcional, etc.).
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Alta de una actividad (Documentation/DominioHorarios.md §7.2 — el
|
||||
-- agrupamiento operativo, no la disciplina física). Es un acto del
|
||||
-- operador sobre el catálogo: no produce reservas huérfanas por sí
|
||||
-- misma, pero ingresa al alcance de los **cambios estructurales** (§8,
|
||||
-- §11) — abre nuevas opciones reservables que después la plantilla
|
||||
-- horaria podrá referenciar.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_datos JSONB con el payload. Claves:
|
||||
-- nombre TEXT (obligatorio) no vacío.
|
||||
-- duracion SMALLINT (obligatorio) en minutos, > 0.
|
||||
-- capacidad_por_defecto SMALLINT (obligatorio) > 0.
|
||||
-- libre BOOLEAN (opcional, default FALSE) si la
|
||||
-- actividad es "libre" (sin turnos).
|
||||
-- activo BOOLEAN (opcional, default TRUE).
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'crear_actividad'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'El nombre de la actividad es obligatorio.'
|
||||
-- - 'La duración es obligatoria y debe ser mayor a 0.'
|
||||
-- - 'La capacidad por defecto es obligatoria y debe ser mayor a 0.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con la actividad recién creada: {id, nombre, duracion,
|
||||
-- capacidad_por_defecto, libre, activo, status: 'success'}.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- INSERT en actividades.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_insertar_actividad(
|
||||
p_token UUID,
|
||||
p_datos JSONB
|
||||
)
|
||||
RETURNS JSONB -- Cambiado a JSONB para evitar errores de posición de columnas
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
VOLATILE -- Checklist #2: Modifica datos
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_actividad_insertada RECORD;
|
||||
BEGIN
|
||||
------------------------------------------------------------
|
||||
-- Checklist #3: Validar permisos (Centralizado)
|
||||
------------------------------------------------------------
|
||||
-- Acción sugerida: 'crear_actividad'
|
||||
PERFORM internal.validate_permission(p_token, 'crear_actividad');
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 1. Validaciones de Entrada
|
||||
------------------------------------------------------------
|
||||
-- Validamos que los campos obligatorios vengan y NO sean nulos
|
||||
IF (p_datos->>'nombre') IS NULL OR length(p_datos->>'nombre') = 0 THEN
|
||||
RAISE EXCEPTION 'El nombre de la actividad es obligatorio.';
|
||||
END IF;
|
||||
|
||||
IF (p_datos->>'duracion') IS NULL OR (p_datos->>'duracion')::INT <= 0 THEN
|
||||
RAISE EXCEPTION 'La duración es obligatoria y debe ser mayor a 0.';
|
||||
END IF;
|
||||
|
||||
IF (p_datos->>'capacidad_por_defecto') IS NULL OR (p_datos->>'capacidad_por_defecto')::INT <= 0 THEN
|
||||
RAISE EXCEPTION 'La capacidad por defecto es obligatoria y debe ser mayor a 0.';
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 2. Insertar directo (Sin variables intermedias)
|
||||
------------------------------------------------------------
|
||||
INSERT INTO actividades (
|
||||
nombre,
|
||||
duracion,
|
||||
capacidad_por_defecto,
|
||||
libre,
|
||||
activo
|
||||
)
|
||||
VALUES (
|
||||
p_datos->>'nombre',
|
||||
(p_datos->>'duracion')::SMALLINT,
|
||||
(p_datos->>'capacidad_por_defecto')::SMALLINT,
|
||||
COALESCE((p_datos->>'libre')::BOOLEAN, FALSE), -- Default FALSE si no viene
|
||||
COALESCE((p_datos->>'activo')::BOOLEAN, TRUE) -- Default TRUE si no viene
|
||||
)
|
||||
RETURNING * INTO v_actividad_insertada; -- Capturamos todo el registro
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 3. Retorno JSONB Seguro
|
||||
------------------------------------------------------------
|
||||
-- Al usar keys explícitas, nunca más tendrás el error de columnas intercambiadas
|
||||
RETURN jsonb_build_object(
|
||||
'id', v_actividad_insertada.id,
|
||||
'nombre', v_actividad_insertada.nombre,
|
||||
'duracion', v_actividad_insertada.duracion,
|
||||
'capacidad_por_defecto', v_actividad_insertada.capacidad_por_defecto,
|
||||
'libre', v_actividad_insertada.libre,
|
||||
'activo', v_actividad_insertada.activo,
|
||||
'status', 'success'
|
||||
);
|
||||
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,180 @@
|
||||
-- ============================================================================
|
||||
-- fc_modificar_actividad
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Modifica los campos editables de una actividad existente. Semántica
|
||||
-- PATCH: sólo se actualizan las claves presentes en p_datos.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Modifica una actividad (Documentation/DominioHorarios.md §7.2). Esta
|
||||
-- función es el único punto de entrada para las dos operaciones nombradas
|
||||
-- del dominio sobre el ciclo de vida de una actividad:
|
||||
--
|
||||
-- - **Suspender** (`activo: true → false`). Cambio estructural (§11). Las
|
||||
-- consecuencias materializadas — borrar turnos futuros, rescatar
|
||||
-- reservas vivas como huérfanas, cerrar plantillas horarias — las
|
||||
-- ejecuta el trigger trg_desactivar_actividad / internal.cerrar_actividad,
|
||||
-- no esta función. Acá sólo flippeamos el flag y registramos el evento
|
||||
-- de trazabilidad.
|
||||
--
|
||||
-- - **Restablecer** (`activo: false → true`). Cambio estructural (§11). El
|
||||
-- dominio adopta la semántica "el operador define el horario nuevo": el
|
||||
-- restablecimiento no recrea plantillas ni resucita las que se cerraron
|
||||
-- al suspender. La actividad vuelve a ser ofrecible, y el operador
|
||||
-- crea las plantillas que correspondan (típicamente anunciándolas con
|
||||
-- anticipación). Las asociaciones a tipos_cuota nunca se tocan al
|
||||
-- suspender, así que vuelven a habilitar acceso automáticamente.
|
||||
--
|
||||
-- Cualquier otro cambio (nombre, duración, capacidad, libre) es metadata
|
||||
-- operativa, no produce evento ni dispara trigger.
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_actividad_id INT (obligatorio) ID de la actividad.
|
||||
-- p_datos JSONB con los campos a actualizar (todos opcionales):
|
||||
-- nombre, duracion, capacidad_por_defecto, libre, activo.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'modificar_actividades'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- - 'No existe la actividad con id X'
|
||||
-- - 'Sesión inválida o usuario no encontrado.'
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB con la actividad ya actualizada: {id, nombre, duracion,
|
||||
-- capacidad_por_defecto, libre, activo, status: 'success'}.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - UPDATE en actividades.
|
||||
-- - Si `activo` cambió, dispara trg_desactivar_actividad en el caso
|
||||
-- TRUE→FALSE (efectos materializados; ver internal.cerrar_actividad).
|
||||
-- - Si `activo` cambió, INSERT en eventos con tipo 'actividad_suspendida'
|
||||
-- o 'actividad_restablecida' (trazabilidad §11). referencia_id queda
|
||||
-- NULL porque actividades.id es INT y eventos.referencia_id es UUID;
|
||||
-- el id va embebido en los snapshots.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_modificar_actividad(
|
||||
p_token UUID,
|
||||
p_actividad_id INT, -- ID explícito
|
||||
p_datos JSONB
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
VOLATILE -- Checklist #2: Modifica datos
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_actor_id UUID;
|
||||
v_activo_anterior BOOLEAN;
|
||||
v_nombre_anterior TEXT;
|
||||
v_actividad_actualizada RECORD;
|
||||
v_tipo_evento TEXT;
|
||||
BEGIN
|
||||
------------------------------------------------------------
|
||||
-- Checklist #3: Validar permisos (Centralizado)
|
||||
------------------------------------------------------------
|
||||
PERFORM internal.validate_permission(p_token, 'modificar_actividades');
|
||||
|
||||
SELECT s.usuario_id
|
||||
INTO v_actor_id
|
||||
FROM internal.sesiones s
|
||||
WHERE s.token = p_token;
|
||||
|
||||
IF v_actor_id IS NULL THEN
|
||||
RAISE EXCEPTION 'Sesión inválida o usuario no encontrado.';
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Snapshot previo (necesario para detectar flip de `activo`
|
||||
-- y para el valor_anterior del evento de trazabilidad).
|
||||
------------------------------------------------------------
|
||||
SELECT a.activo, a.nombre
|
||||
INTO v_activo_anterior, v_nombre_anterior
|
||||
FROM actividades a
|
||||
WHERE a.id = p_actividad_id;
|
||||
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'No existe la actividad con id %', p_actividad_id;
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 1. UPDATE Dinámico y Seguro
|
||||
------------------------------------------------------------
|
||||
-- Patrón CASE WHEN ? para permitir actualizaciones parciales reales.
|
||||
UPDATE actividades
|
||||
SET
|
||||
nombre = CASE
|
||||
WHEN p_datos ? 'nombre' THEN p_datos->>'nombre'
|
||||
ELSE nombre
|
||||
END,
|
||||
|
||||
duracion = CASE
|
||||
WHEN p_datos ? 'duracion' THEN (p_datos->>'duracion')::SMALLINT
|
||||
ELSE duracion
|
||||
END,
|
||||
|
||||
capacidad_por_defecto = CASE
|
||||
WHEN p_datos ? 'capacidad_por_defecto' THEN (p_datos->>'capacidad_por_defecto')::SMALLINT
|
||||
ELSE capacidad_por_defecto
|
||||
END,
|
||||
|
||||
libre = CASE
|
||||
WHEN p_datos ? 'libre' THEN (p_datos->>'libre')::BOOLEAN
|
||||
ELSE libre
|
||||
END,
|
||||
|
||||
activo = CASE
|
||||
WHEN p_datos ? 'activo' THEN (p_datos->>'activo')::BOOLEAN
|
||||
ELSE activo
|
||||
END
|
||||
WHERE id = p_actividad_id
|
||||
RETURNING * INTO v_actividad_actualizada;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 2. Trazabilidad del cambio estructural (§11)
|
||||
-- Sólo si el flag `activo` efectivamente cambió.
|
||||
------------------------------------------------------------
|
||||
IF v_activo_anterior IS DISTINCT FROM v_actividad_actualizada.activo THEN
|
||||
v_tipo_evento := CASE
|
||||
WHEN v_actividad_actualizada.activo = FALSE THEN 'actividad_suspendida'
|
||||
ELSE 'actividad_restablecida'
|
||||
END;
|
||||
|
||||
PERFORM internal.log_evento(
|
||||
p_tipo => v_tipo_evento,
|
||||
p_tabla => 'actividades',
|
||||
p_referencia_id => NULL,
|
||||
p_cliente_id => NULL,
|
||||
p_actor_id => v_actor_id,
|
||||
p_valor_anterior => jsonb_build_object(
|
||||
'actividad_id', p_actividad_id,
|
||||
'nombre', v_nombre_anterior,
|
||||
'activo', v_activo_anterior
|
||||
),
|
||||
p_valor_actual => jsonb_build_object(
|
||||
'actividad_id', v_actividad_actualizada.id,
|
||||
'nombre', v_actividad_actualizada.nombre,
|
||||
'activo', v_actividad_actualizada.activo
|
||||
),
|
||||
p_descripcion => NULL
|
||||
);
|
||||
END IF;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 3. Retorno JSONB
|
||||
------------------------------------------------------------
|
||||
RETURN jsonb_build_object(
|
||||
'id', v_actividad_actualizada.id,
|
||||
'nombre', v_actividad_actualizada.nombre,
|
||||
'duracion', v_actividad_actualizada.duracion,
|
||||
'capacidad_por_defecto', v_actividad_actualizada.capacidad_por_defecto,
|
||||
'libre', v_actividad_actualizada.libre,
|
||||
'activo', v_actividad_actualizada.activo,
|
||||
'status', 'success'
|
||||
);
|
||||
|
||||
END;
|
||||
$function$;
|
||||
@@ -0,0 +1,64 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_actividades
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve las actividades configuradas. Permite filtrar por estado activo.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Lista el catálogo de actividades (Documentation/DominioHorarios.md §7.2).
|
||||
-- La actividad es el agrupamiento operativo del uso del gimnasio (no la
|
||||
-- disciplina física). El filtro `activo` se corresponde con la noción de
|
||||
-- **suspensión / restablecimiento** del dominio: una actividad inactiva
|
||||
-- "sigue existiendo conceptualmente, simplemente deja de ofrecerse".
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_solo_activas BOOLEAN (opcional, default NULL). Si NULL trae todas; si
|
||||
-- TRUE sólo las activas; si FALSE sólo las inactivas.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_actividades'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- Ninguno propio (sólo los del validador de permiso).
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array (puede ser vacío) con las filas completas de actividades
|
||||
-- ordenadas por nombre.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_actividades(
|
||||
p_token UUID,
|
||||
p_solo_activas BOOLEAN DEFAULT NULL -- Opcional: útil para dropdowns
|
||||
)
|
||||
RETURNS JSONB
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE -- Checklist #2: Solo lee datos
|
||||
AS $$
|
||||
DECLARE
|
||||
-- Sin variables extra
|
||||
BEGIN
|
||||
------------------------------------------------------------
|
||||
-- Checklist #3: Validar permisos
|
||||
------------------------------------------------------------
|
||||
-- Acción sugerida: 'ver_actividades'
|
||||
PERFORM internal.validate_permission(p_token, 'ver_actividades');
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Retornar JSONB estandarizado
|
||||
------------------------------------------------------------
|
||||
RETURN (
|
||||
SELECT COALESCE(jsonb_agg(to_jsonb(a) ORDER BY a.nombre), '[]'::jsonb)
|
||||
FROM actividades a
|
||||
WHERE
|
||||
-- Filtro opcional: Si p_solo_activas es NULL, trae todo.
|
||||
-- Si es TRUE, solo trae las activas = true.
|
||||
(p_solo_activas IS NULL OR a.activo = p_solo_activas)
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,61 @@
|
||||
-- ============================================================================
|
||||
-- fc_obtener_actividades_tipo_cuota
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Devuelve las actividades incluidas en un tipo de cuota (plan). Útil para
|
||||
-- mostrar el detalle de un plan o para validar si el cliente que tiene
|
||||
-- plan X puede reservar un turno de actividad Y.
|
||||
--
|
||||
-- DOMINIO
|
||||
-- Sirve a la consulta "qué actividades habilita un plan"
|
||||
-- (Documentation/DominioHorarios.md §7.2 actividad + §7.7 plan). Es la
|
||||
-- fuente con la que se materializa la primera condición de §9.1: "su plan
|
||||
-- está vigente y habilita la actividad del bloque".
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- p_token UUID sesión del actor.
|
||||
-- p_tipo_cuota_id UUID ID del plan a consultar.
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- Requiere permiso 'ver_actividades'.
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- Ninguno propio (sólo los del validador de permiso).
|
||||
--
|
||||
-- RETORNA
|
||||
-- JSONB array (puede ser vacío) de actividades asociadas: cada ítem
|
||||
-- trae {id, nombre, duracion, activo}, ordenado por nombre.
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- Ninguno (STABLE).
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fc_obtener_actividades_tipo_cuota(
|
||||
p_token UUID,
|
||||
p_tipo_cuota_id UUID
|
||||
)
|
||||
RETURNS JSONB
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
STABLE
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM internal.validate_permission(p_token, 'ver_actividades');
|
||||
|
||||
RETURN (
|
||||
SELECT COALESCE(jsonb_agg(
|
||||
jsonb_build_object(
|
||||
'id', a.id,
|
||||
'nombre', a.nombre,
|
||||
'duracion', a.duracion,
|
||||
'activo', a.activo
|
||||
)
|
||||
ORDER BY a.nombre ASC
|
||||
), '[]'::jsonb)
|
||||
FROM public.actividades_tipos_cuota atc
|
||||
JOIN public.actividades a ON atc.actividad_id = a.id
|
||||
WHERE atc.tipo_cuota_id = p_tipo_cuota_id
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,89 @@
|
||||
-- ============================================================================
|
||||
-- internal.cerrar_actividad
|
||||
-- ============================================================================
|
||||
-- PROPÓSITO
|
||||
-- Trigger function: cuando una actividad pasa de activo=true a activo=false,
|
||||
-- limpia el estado derivado: rescata reservas vivas como reservas huérfanas,
|
||||
-- borra los turnos futuros y cierra las plantillas horarias asociadas.
|
||||
--
|
||||
-- El trigger que la invoca (trg_desactivar_actividad, AFTER UPDATE OF activo
|
||||
-- ON actividades, WHEN OLD.activo = TRUE AND NEW.activo = FALSE) NO se
|
||||
-- versiona en este archivo. Vive como parte del DDL (por ahora en
|
||||
-- shift_functions/utility_functions.sql).
|
||||
--
|
||||
-- PARÁMETROS
|
||||
-- Implícitos del trigger (OLD.id, OLD.nombre, NEW).
|
||||
--
|
||||
-- AUTORIZACIÓN
|
||||
-- No aplica (trigger function).
|
||||
--
|
||||
-- ERRORES (RAISE EXCEPTION)
|
||||
-- Ninguno propio.
|
||||
--
|
||||
-- RETORNA
|
||||
-- trigger (la fila NEW).
|
||||
--
|
||||
-- EFECTOS SECUNDARIOS
|
||||
-- - INSERT en reservas_huerfanas para cada reserva activa sobre un turno
|
||||
-- futuro de la actividad desactivada.
|
||||
-- - DELETE de turnos futuros de la actividad.
|
||||
-- - DELETE de plantillas horarias (horario_actividad) cuya vigencia
|
||||
-- empieza hoy o en el futuro.
|
||||
-- - UPDATE de plantillas horarias vigentes: cierra su vigencia ayer
|
||||
-- (valido_hasta = CURRENT_DATE - 1).
|
||||
-- - Las plantillas históricas (ya vencidas) no se tocan.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION internal.cerrar_actividad()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = public
|
||||
SET timezone = 'America/Argentina/Buenos_Aires'
|
||||
AS $$
|
||||
BEGIN
|
||||
-- El trigger se filtra con WHEN, así que cuando entra acá ya sabemos que
|
||||
-- la actividad pasó de activo=true a activo=false.
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 1. Rescatar reservas activas sobre turnos futuros
|
||||
------------------------------------------------------------
|
||||
INSERT INTO reservas_huerfanas
|
||||
(cliente_id, actividad_nombre, fecha_original, hora_inicio_original)
|
||||
SELECT r.cliente_id, OLD.nombre, t.fecha, t.hora_inicio
|
||||
FROM reservas r
|
||||
JOIN turnos t ON r.turno_id = t.id
|
||||
WHERE t.actividad_id = OLD.id
|
||||
AND t.fecha >= CURRENT_DATE
|
||||
AND r.cancelada = false;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 2. Borrar turnos futuros (los pasados quedan para histórico/reportes)
|
||||
------------------------------------------------------------
|
||||
DELETE FROM turnos
|
||||
WHERE actividad_id = OLD.id
|
||||
AND fecha >= CURRENT_DATE;
|
||||
|
||||
------------------------------------------------------------
|
||||
-- 3. Limpiar plantillas según su posición temporal
|
||||
------------------------------------------------------------
|
||||
|
||||
-- 3.A Plantillas que empiezan hoy o en el futuro: borrarlas.
|
||||
-- Si la cerráramos en CURRENT_DATE-1 quedaría un intervalo vacío
|
||||
-- o invertido (valido_desde > valido_hasta).
|
||||
DELETE FROM horario_actividad
|
||||
WHERE actividad_id = OLD.id
|
||||
AND valido_desde >= CURRENT_DATE;
|
||||
|
||||
-- 3.B Plantillas que ya están rigiendo (valido_desde en el pasado y
|
||||
-- vigencia abierta o futura): cerrarlas en CURRENT_DATE - 1.
|
||||
UPDATE horario_actividad
|
||||
SET valido_hasta = CURRENT_DATE - 1
|
||||
WHERE actividad_id = OLD.id
|
||||
AND valido_desde < CURRENT_DATE
|
||||
AND (valido_hasta IS NULL OR valido_hasta >= CURRENT_DATE);
|
||||
|
||||
-- 3.C Plantillas históricas (valido_hasta < CURRENT_DATE): NO se tocan.
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
Reference in New Issue
Block a user