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,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;
$$;