Notifica di compleanno: auguri al festeggiato, avviso agli altri (DD-047)
Ogni giorno, dalle 8:00 (Europe/Rome), un job pg_cron orario manda a chi compie gli anni «Buon compleanno, <Nome>! 🎂» e a tutti gli altri membri attivi, allenatori compresi, «Oggi è il compleanno di <Nome>» (con più compleanni lo stesso giorno un solo messaggio a testa, nomi in ordine alfabetico). Solo titolo, senza corpo né età. Canali: push, centro notifiche ed email. - migration m31: tipi `compleanno` e `compleanno_auguri`, registro `compleanni_notificati` (una sola volta per destinatario e giorno, anche se la notifica viene eliminata), funzione `genera_avvisi_compleanno()` e job `avvisi-compleanno`, trigger push esteso ai nuovi tipi. Non ancora applicata in produzione. - 29 febbraio non gestito: chi è nato quel giorno è avvisato solo negli anni bisestili (limite noto, documentato). - test: integrazione `notifiche-compleanno` (ora e ora legale, destinatari, più compleanni, registro, coda push/email, 29 febbraio, job pianificato); unit sui nuovi tipi e sul corpo vuoto del service worker. - documentazione: DD-047, catalogo e testi delle notifiche, DATABASE, CHANGELOG, ROADMAP, PROJECT_STATE. Chiude la issue #11. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7347fdba6e
commit
985fc2fcc1
15 files changed
+706
-43
No files matched your search
@@ -0,0 +1,156 @@
|
||||
-- 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();
|
||||
Reference in new issue
Block a user