feat: configuración de destinatarios de notificaciones desde la app

Gestiona grupos de email y asignación por escenario en BD
(natur_reservas.notification_groups / notification_scenario_recipients)
en lugar de variables de entorno hardcodeadas. Incluye pantalla de ajustes,
endpoint admin protegido, envío de prueba y niveles reducida/completa.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:12:58 +01:00
parent 5656ad13ad
commit ee03f0b36d
19 changed files with 991 additions and 103 deletions

View File

@@ -0,0 +1,87 @@
-- Migración 012: Configuración de destinatarios de notificaciones
--
-- Mueve "quién recibe cada correo" desde variables de entorno (Dokploy) a la BD,
-- gestionable desde la app. Dos tablas:
-- notification_groups — grupos de destinatarios con un nivel de info fijo
-- notification_scenario_recipients — qué grupos recibe cada escenario (event_type)
--
-- IMPORTANTE: schema natur_reservas (la instancia Supabase es compartida; public es
-- otro proyecto). Ver CLAUDE.md §1.
-- ────────────────────────────────────────────────────────────
-- Grupos de destinatarios
-- ────────────────────────────────────────────────────────────
CREATE TABLE natur_reservas.notification_groups (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
level TEXT NOT NULL DEFAULT 'completa'
CHECK (level IN ('reducida', 'completa')),
emails TEXT[] NOT NULL DEFAULT '{}',
enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
COMMENT ON TABLE natur_reservas.notification_groups IS
'Grupos de destinatarios de notificaciones. level=reducida solo puede recibir la versión reducida de los correos (regla de privacidad).';
-- ────────────────────────────────────────────────────────────
-- Asignación escenario → grupos (N:M)
-- ────────────────────────────────────────────────────────────
CREATE TABLE natur_reservas.notification_scenario_recipients (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
scenario TEXT NOT NULL, -- igual que notification_events.event_type
group_id UUID NOT NULL
REFERENCES natur_reservas.notification_groups(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT uq_scenario_group UNIQUE (scenario, group_id)
);
CREATE INDEX idx_scenario_recipients_scenario
ON natur_reservas.notification_scenario_recipients(scenario);
COMMENT ON TABLE natur_reservas.notification_scenario_recipients IS
'Qué grupos reciben cada escenario (event_type). El motor de envío en apps/api lo consulta con service_role.';
-- ────────────────────────────────────────────────────────────
-- RLS: el acceso del frontend va por el API (service_role bypasea RLS).
-- Bloqueamos acceso directo de anon/authenticated; el API valida admin.
-- ────────────────────────────────────────────────────────────
ALTER TABLE natur_reservas.notification_groups ENABLE ROW LEVEL SECURITY;
ALTER TABLE natur_reservas.notification_scenario_recipients ENABLE ROW LEVEL SECURITY;
-- El API accede con service_role: bypasea RLS, pero necesita el GRANT de tabla
-- (en schemas custom no se concede automáticamente, a diferencia de public).
GRANT USAGE ON SCHEMA natur_reservas TO service_role;
GRANT ALL ON natur_reservas.notification_groups TO service_role;
GRANT ALL ON natur_reservas.notification_scenario_recipients TO service_role;
REVOKE ALL ON natur_reservas.notification_groups FROM anon;
REVOKE ALL ON natur_reservas.notification_scenario_recipients FROM anon;
-- ────────────────────────────────────────────────────────────
-- Semilla inicial (editable después desde la pantalla)
-- ────────────────────────────────────────────────────────────
WITH nuevos AS (
INSERT INTO natur_reservas.notification_groups (name, level, emails) VALUES
('Teneriffa', 'reducida',
ARRAY['uwe.dotschkis@teneriffa2000.de', 'j.reichstein@teneriffa2000.de']),
('Naturcalabacera (Admin)', 'completa',
ARRAY['administracion@naturcalabacera.com']),
('Limpieza', 'completa',
ARRAY['limpieza.naturcalabacera@gmail.com']) -- placeholder editable
RETURNING id, name
)
INSERT INTO natur_reservas.notification_scenario_recipients (scenario, group_id)
SELECT s.scenario, n.id
FROM nuevos n
JOIN (VALUES
('Teneriffa', 'reservation.created'),
('Teneriffa', 'reservation.updated'),
('Teneriffa', 'reservation.cancelled'),
('Naturcalabacera (Admin)', 'reservation.created'),
('Naturcalabacera (Admin)', 'reservation.updated'),
('Naturcalabacera (Admin)', 'reservation.cancelled'),
('Naturcalabacera (Admin)', 'reservation.invoice_second_notice'),
('Limpieza', 'reservation.pool_heating_notice')
) AS s(group_name, scenario) ON s.group_name = n.name;