m23: un registro (promemoria_eventi_generati) ricorda i promemoria a 24 e 3 ore già generati, così eliminare la notifica non li fa rigenerare dal cron né riaccodare la mail a ogni giro. m24: la push dei promemoria, che nascono nel database, la manda il worker mailer/ con lo stesso modulo dell'app (coda notifiche_push_coda, esiti, backoff, iscrizioni scadute eliminate, push scartate oltre 3 ore). Il sondaggio pre-partita scrive anche notifiche_utente (tipo sondaggio_cacche): compare in-app e arriva per email. L'immagine del worker si costruisce dalla radice del repository per includere src/lib/webpush.server.ts. Test: 9 di integrazione per m23, 17 per la coda push, 6 per la route del sondaggio, nuovi casi unitari sul worker; prova end-to-end con un servizio push finto. Documentazione e catalogo delle notifiche aggiornati. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
198 lines
7.7 KiB
PL/PgSQL
198 lines
7.7 KiB
PL/PgSQL
-- M24 — Tre canali per ogni notifica: push dei promemoria e sondaggio in-app/email (DD-038)
|
|
--
|
|
-- Due lacune rispetto al requisito «ogni notifica arriva per push, in-app ed email»:
|
|
--
|
|
-- 1. I promemoria a 24 e 3 ore nascono nel database (job pg_cron, M17), che non può firmare né
|
|
-- cifrare una push Web: arrivavano solo in-app e per email. Qui si accodano in
|
|
-- `notifiche_push_coda`; la push la manda il worker `mailer/`, con lo stesso modulo
|
|
-- dell'app (`src/lib/webpush.server.ts`).
|
|
-- 2. Il sondaggio pre-partita partiva solo come push. La route `apri-sondaggio` ora scrive anche
|
|
-- `notifiche_utente` (tipo nuovo `sondaggio_cacche`): compare nel centro notifiche e la
|
|
-- mail parte dal trigger di M22. Serve solo estendere il vincolo CHECK sul tipo.
|
|
--
|
|
-- Come per le email (M22), la coda è una tabella a parte con RLS senza policy: la policy UPDATE
|
|
-- del giocatore su `notifiche_utente` gli permetterebbe di riportare a 'in_coda' una push già
|
|
-- partita. Solo la service role (il worker) la legge e la scrive.
|
|
|
|
-- --- Tipo `sondaggio_cacche` -------------------------------------------------------------------
|
|
|
|
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',
|
|
'sollecita_presenze',
|
|
'sondaggio_cacche'
|
|
)
|
|
);
|
|
|
|
-- --- Coda delle push dei promemoria ------------------------------------------------------------
|
|
|
|
CREATE TABLE public.notifiche_push_coda (
|
|
notifica_id uuid PRIMARY KEY REFERENCES public.notifiche_utente(id) ON DELETE CASCADE,
|
|
stato text NOT NULL DEFAULT 'in_coda'
|
|
CHECK (stato IN ('in_coda', 'in_invio', 'inviata', 'fallita', 'saltata')),
|
|
tentativi integer NOT NULL DEFAULT 0,
|
|
prossimo_tentativo timestamptz NOT NULL DEFAULT now(),
|
|
errore text,
|
|
inviata_il timestamptz,
|
|
aggiornata_il timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
COMMENT ON TABLE public.notifiche_push_coda IS
|
|
'Coda delle push per i promemoria evento (M24): una riga per notifica, scritta dal trigger e dal worker. Le altre notifiche mandano la push dall''app.';
|
|
|
|
CREATE INDEX notifiche_push_coda_da_inviare_idx
|
|
ON public.notifiche_push_coda (prossimo_tentativo)
|
|
WHERE stato = 'in_coda';
|
|
|
|
ALTER TABLE public.notifiche_push_coda ENABLE ROW LEVEL SECURITY;
|
|
REVOKE ALL ON public.notifiche_push_coda FROM PUBLIC, anon, authenticated;
|
|
GRANT ALL ON public.notifiche_push_coda TO service_role;
|
|
|
|
-- Solo i promemoria: turno palloni, sollecito presenze, messaggio dello staff e sondaggio la push
|
|
-- la mandano già dall'app, e una seconda dal worker sarebbe un doppione. Le notifiche già
|
|
-- presenti alla migration non si accodano: il trigger non ha backfill.
|
|
CREATE OR REPLACE FUNCTION public.accoda_push_promemoria()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
INSERT INTO public.notifiche_push_coda (notifica_id)
|
|
VALUES (NEW.id)
|
|
ON CONFLICT (notifica_id) DO NOTHING;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
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'))
|
|
EXECUTE FUNCTION public.accoda_push_promemoria();
|
|
|
|
-- --- RPC del worker ----------------------------------------------------------------------------
|
|
|
|
-- Prende fino a `p_max` push da spedire, le marca 'in_invio' e restituisce per ciascuna il testo e
|
|
-- le iscrizioni (dispositivi) del giocatore. `FOR UPDATE SKIP LOCKED`: due worker avviati per
|
|
-- errore non prendono mai la stessa riga. Prima ripulisce tre casi:
|
|
-- * righe ferme 'in_invio' da più di 10 minuti (worker morto a metà) tornano 'in_coda';
|
|
-- * promemoria creati da più di 3 ore diventano 'saltata': una push arrivata a evento iniziato
|
|
-- è solo rumore (worker fermo a lungo);
|
|
-- * giocatori senza nessun dispositivo iscritto diventano 'saltata', senza errore.
|
|
CREATE OR REPLACE FUNCTION public.prendi_push_promemoria(p_max integer DEFAULT 20)
|
|
RETURNS TABLE (
|
|
id_notifica uuid,
|
|
oggetto text,
|
|
testo text,
|
|
tentativi_fatti integer,
|
|
iscrizioni jsonb
|
|
)
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
UPDATE public.notifiche_push_coda c
|
|
SET stato = 'in_coda', aggiornata_il = now()
|
|
WHERE c.stato = 'in_invio'
|
|
AND c.aggiornata_il < now() - interval '10 minutes';
|
|
|
|
UPDATE public.notifiche_push_coda c
|
|
SET stato = 'saltata', aggiornata_il = now()
|
|
FROM public.notifiche_utente n
|
|
WHERE c.notifica_id = n.id
|
|
AND c.stato = 'in_coda'
|
|
AND c.prossimo_tentativo <= now()
|
|
AND (
|
|
n.creato_il < now() - interval '3 hours'
|
|
OR NOT EXISTS (
|
|
SELECT 1 FROM public.push_subscriptions s WHERE s.giocatore_id = n.giocatore_id
|
|
)
|
|
);
|
|
|
|
RETURN QUERY
|
|
WITH scelte AS (
|
|
SELECT c.notifica_id
|
|
FROM public.notifiche_push_coda c
|
|
WHERE c.stato = 'in_coda' AND c.prossimo_tentativo <= now()
|
|
ORDER BY c.prossimo_tentativo
|
|
LIMIT greatest(p_max, 0)
|
|
FOR UPDATE SKIP LOCKED
|
|
),
|
|
presi AS (
|
|
UPDATE public.notifiche_push_coda c
|
|
SET stato = 'in_invio', aggiornata_il = now()
|
|
FROM scelte s
|
|
WHERE c.notifica_id = s.notifica_id
|
|
RETURNING c.notifica_id, c.tentativi
|
|
)
|
|
SELECT p.notifica_id,
|
|
n.titolo,
|
|
n.corpo,
|
|
p.tentativi,
|
|
coalesce(
|
|
(SELECT jsonb_agg(jsonb_build_object(
|
|
'endpoint', s.endpoint, 'p256dh', s.p256dh, 'auth', s.auth))
|
|
FROM public.push_subscriptions s
|
|
WHERE s.giocatore_id = n.giocatore_id),
|
|
'[]'::jsonb
|
|
)
|
|
FROM presi p
|
|
JOIN public.notifiche_utente n ON n.id = p.notifica_id
|
|
ORDER BY n.creato_il;
|
|
END;
|
|
$$;
|
|
|
|
-- Registra l'esito. Agisce solo su righe 'in_invio', così un esito in ritardo non ne sovrascrive
|
|
-- uno già deciso.
|
|
-- 'inviata' → almeno un dispositivo ha accettato la push
|
|
-- 'riprova' → errore temporaneo, torna in coda dopo `p_prossimo`, conta un tentativo
|
|
-- 'fallita' → definitiva (rifiuto permanente o tentativi finiti)
|
|
-- 'saltata' → nessun dispositivo utile (tutte le iscrizioni erano scadute)
|
|
CREATE OR REPLACE FUNCTION public.esito_push_promemoria(
|
|
p_id uuid,
|
|
p_esito text,
|
|
p_errore text DEFAULT NULL,
|
|
p_prossimo timestamptz DEFAULT NULL
|
|
)
|
|
RETURNS void
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
BEGIN
|
|
IF p_esito NOT IN ('inviata', 'riprova', 'fallita', 'saltata') THEN
|
|
RAISE EXCEPTION 'esito non valido: %', p_esito;
|
|
END IF;
|
|
|
|
UPDATE public.notifiche_push_coda c
|
|
SET stato = CASE p_esito
|
|
WHEN 'riprova' THEN 'in_coda'
|
|
ELSE p_esito
|
|
END,
|
|
tentativi = c.tentativi + CASE WHEN p_esito IN ('riprova', 'fallita') THEN 1 ELSE 0 END,
|
|
prossimo_tentativo = CASE
|
|
WHEN p_esito = 'riprova' THEN coalesce(p_prossimo, now())
|
|
ELSE c.prossimo_tentativo
|
|
END,
|
|
errore = CASE WHEN p_esito = 'inviata' THEN NULL ELSE left(p_errore, 500) END,
|
|
inviata_il = CASE WHEN p_esito = 'inviata' THEN now() ELSE c.inviata_il END,
|
|
aggiornata_il = now()
|
|
WHERE c.notifica_id = p_id AND c.stato = 'in_invio';
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.accoda_push_promemoria() FROM PUBLIC, anon, authenticated;
|
|
REVOKE ALL ON FUNCTION public.prendi_push_promemoria(integer) FROM PUBLIC, anon, authenticated;
|
|
REVOKE ALL ON FUNCTION public.esito_push_promemoria(uuid, text, text, timestamptz)
|
|
FROM PUBLIC, anon, authenticated;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.prendi_push_promemoria(integer) TO service_role;
|
|
GRANT EXECUTE ON FUNCTION public.esito_push_promemoria(uuid, text, text, timestamptz)
|
|
TO service_role;
|