Files
CRAPP/supabase/migrations/20261006120000_m31_notifica_compleanno.sql
T

156 lines
6.1 KiB
PL/PgSQL
Raw Normal View History

-- M31 — Notifica di compleanno (DD-047, issue #11)
--
-- Ogni giorno, dalle 8:00 (fuso Europe/Rome), chi ha il compleanno riceve il solo titolo di auguri e tutti gli
-- altri membri attivi della squadra (allenatori compresi) ricevono il solo avviso «Oggi è il compleanno
-- di ...», senza corpo e senza età.
-- Se in un giorno compiono gli anni più persone, ogni destinatario riceve un solo messaggio: chi non
-- festeggia con tutti i nomi, ogni festeggiato solo i propri auguri.
--
-- Canali come le altre notifiche automatiche (DD-038): la riga in `notifiche_utente` genera la mail
-- (il trigger di M26 lascia passare i nuovi tipi) e, con M31, anche la push dalla coda.
--
-- 29 febbraio: il confronto è tra giorno e mese, quindi chi è nato il 29 febbraio viene avvisato solo
-- negli anni bisestili. Limite noto e accettato (DD-047): non c'è un ripiego al 28 febbraio.
-- --- Tipi di notifica --------------------------------------------------------------------------
ALTER TABLE public.notifiche_utente DROP CONSTRAINT notifiche_utente_tipo_check;
ALTER TABLE public.notifiche_utente ADD CONSTRAINT notifiche_utente_tipo_check CHECK (
tipo IN (
'admin',
'evento_promemoria_24h',
'evento_promemoria_3h',
'turno_palloni',
'turno_palloni_12h',
'turno_palloni_6h',
'turno_palloni_3h',
'turno_palloni_revocato',
'sollecita_presenze',
'sollecita_presenze_24h',
'sollecita_presenze_12h',
'sollecita_presenze_6h',
'sondaggio_cacche',
'compleanno',
'compleanno_auguri'
)
);
-- --- Registro ----------------------------------------------------------------------------------
--
-- Una riga per destinatario e giorno: ricorda che l'avviso di quel giorno è già stato generato,
-- anche se il giocatore elimina la notifica (come `promemoria_eventi_generati`, DD-037). Non è legato
-- a un evento: i compleanni non sono `eventi_app`. Sparisce con il giocatore (DD-029).
CREATE TABLE public.compleanni_notificati (
giocatore_id text NOT NULL REFERENCES public.giocatori_squadra(id) ON DELETE CASCADE,
giorno date NOT NULL,
generato_il timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (giocatore_id, giorno)
);
COMMENT ON TABLE public.compleanni_notificati IS
'Avvisi di compleanno già generati per destinatario e giorno: indipendente da notifiche_utente, così eliminare la notifica non la fa rigenerare (M31, DD-047).';
-- Solo la funzione SECURITY DEFINER e la service role la toccano: RLS attiva senza policy.
ALTER TABLE public.compleanni_notificati ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON public.compleanni_notificati FROM PUBLIC, anon, authenticated;
GRANT ALL ON public.compleanni_notificati TO service_role;
-- --- Job ---------------------------------------------------------------------------------------
--
-- Gira ogni ora e agisce solo dalle 8:00 in poi, ora locale: un giro mancato si recupera all'ora
-- dopo e il registro impedisce di generare due volte lo stesso avviso. `pg_cron` ragiona in UTC e
-- l'ora legale sposta l'ora locale di una ora: per questo l'ora si legge qui, in `Europe/Rome`.
--
-- Il registro è per destinatario e giorno: chi entra in squadra o inserisce la data di nascita dopo
-- che gli avvisi del giorno sono partiti non genera un secondo giro (DD-047).
--
-- `p_adesso` esiste per i test, che simulano un'altra data; il job lo chiama senza argomenti.
CREATE OR REPLACE FUNCTION public.genera_avvisi_compleanno(p_adesso timestamptz DEFAULT now())
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
adesso timestamp := p_adesso AT TIME ZONE 'Europe/Rome';
oggi date := (p_adesso AT TIME ZONE 'Europe/Rome')::date;
BEGIN
IF extract(hour FROM adesso) < 8 THEN
RETURN;
END IF;
WITH festeggiati AS (
SELECT g.id, g.nome
FROM public.giocatori_squadra g
WHERE g.attivo = true
AND g.nascita IS NOT NULL
AND extract(month FROM g.nascita) = extract(month FROM oggi)
AND extract(day FROM g.nascita) = extract(day FROM oggi)
),
elenco AS (
SELECT array_agg(nome ORDER BY nome, id COLLATE "C") AS nomi
FROM festeggiati
),
nuovi AS (
INSERT INTO public.compleanni_notificati (giocatore_id, giorno)
SELECT g.id, oggi
FROM public.giocatori_squadra g
WHERE g.attivo = true
AND EXISTS (SELECT 1 FROM festeggiati)
ON CONFLICT DO NOTHING
RETURNING giocatore_id
)
INSERT INTO public.notifiche_utente (giocatore_id, tipo, titolo, corpo)
SELECT n.giocatore_id, 'compleanno_auguri',
'Buon compleanno, ' || f.nome || '! 🎂',
''
FROM nuovi n
JOIN festeggiati f ON f.id = n.giocatore_id
UNION ALL
SELECT n.giocatore_id, 'compleanno',
'Oggi è il compleanno di '
|| CASE WHEN cardinality(e.nomi) = 1 THEN e.nomi[1]
ELSE array_to_string(e.nomi[1:cardinality(e.nomi) - 1], ', ')
|| ' e ' || e.nomi[cardinality(e.nomi)]
END,
''
FROM nuovi n
CROSS JOIN elenco e
WHERE NOT EXISTS (SELECT 1 FROM festeggiati f WHERE f.id = n.giocatore_id);
END;
$$;
REVOKE ALL ON FUNCTION public.genera_avvisi_compleanno(timestamptz) FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.genera_avvisi_compleanno(timestamptz) TO service_role;
SELECT cron.schedule(
'avvisi-compleanno',
'0 * * * *',
$$ SELECT public.genera_avvisi_compleanno() $$
);
-- --- Push: anche i compleanni ------------------------------------------------------------------
--
-- Nascono nel database come le altre notifiche automatiche: nessuna route le manda, quindi la push
-- passa dalla coda (DD-038). Stesso trigger di M26 con i due tipi nuovi.
DROP TRIGGER notifiche_utente_accoda_push_promemoria ON public.notifiche_utente;
CREATE TRIGGER notifiche_utente_accoda_push_promemoria
AFTER INSERT ON public.notifiche_utente
FOR EACH ROW
WHEN (NEW.tipo IN (
'evento_promemoria_24h',
'evento_promemoria_3h',
'turno_palloni_12h',
'turno_palloni_6h',
'turno_palloni_3h',
'turno_palloni_revocato',
'sollecita_presenze_24h',
'sollecita_presenze_12h',
'sollecita_presenze_6h',
'compleanno',
'compleanno_auguri'
))
EXECUTE FUNCTION public.accoda_push_promemoria();