From 986098d933b182db7ef600c718d6b8582be91341 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 9 Oct 2026 11:06:46 +0200 Subject: [PATCH] =?UTF-8?q?DD-050:=20=C2=ABPresenza=20modificata=C2=BB=20a?= =?UTF-8?q?nche=20agli=20allenatori=20(m33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fine della fase di prova: la funzione avvisa admin e allenatori attivi. Test aggiornato (l'allenatore ora è tra i destinatari), DD-050, notifiche, allenatore, DATABASE, CHANGELOG, ROADMAP e PROJECT_STATE allineati. m33 non ancora applicata in produzione. Co-Authored-By: Claude Sonnet 5.5 --- PROJECT_STATE.md | 4 +- docs/CHANGELOG.md | 8 +-- docs/DATABASE.md | 22 +++---- docs/ROADMAP.md | 2 +- docs/decisions/DD-050.md | 15 ++--- docs/modules/allenatore.md | 1 + docs/modules/notifiche.md | 2 +- ...000_m33_presenza_modificata_allenatori.sql | 66 +++++++++++++++++++ test/README.md | 12 ++-- test/integration/presenza-modificata.test.ts | 57 ++++++++-------- 10 files changed, 129 insertions(+), 60 deletions(-) create mode 100644 supabase/migrations/20261009150000_m33_presenza_modificata_allenatori.sql diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 668f04f..55bade4 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -15,8 +15,8 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in ridisegnati e i caratteri più grandi. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md). - **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio Project Ref `hetycilxgkdmccelwerq` non si usano più. -- **Migration:** 43 file in `supabase/migrations/`, fino a `m32_presenza_modificata`. Tutte applicate in produzione (verificato con `npx supabase migration list` il - 09/10/2026); la `m32` (DD-050, avviso «Presenza modificata» agli admin in prova) è stata applicata quel giorno. La `m29` toglie +- **Migration:** 44 file in `supabase/migrations/`, fino a `m33_presenza_modificata_allenatori`. Fino alla `m32` applicate in produzione (verificato con `npx supabase migration list` il + 09/10/2026); la `m32` (DD-050, avviso «Presenza modificata» ai soli admin) è stata applicata quel giorno; la `m33` (anche agli allenatori) no, ancora da applicare. La `m29` toglie `foto_path`: il codice che non la legge più (1.2.2) va rilasciato. - **Worker delle notifiche** (`mailer/`): in funzione su un Raspberry Pi con Docker dal 29/09/2026 ([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f67b95d..0759750 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,10 +15,10 @@ versioni `0.y.z` erano pre-release. ### Aggiunto -- **Notifica «Presenza modificata» (DD-050), in prova solo per gli admin** — quando una risposta a una partita o a - un allenamento cambia, compare o viene ritirata a meno di 6 ore dall'inizio, gli admin ricevono una notifica - (centro notifiche, push, email) con chi, quale evento e lo stato prima e dopo. Gli allenatori non la ricevono - finché dura la prova. Migration `m32`, applicata in produzione il 09/10/2026. +- **Notifica «Presenza modificata» (DD-050)** — quando una risposta a una partita o a + un allenamento cambia, compare o viene ritirata a meno di 6 ore dall'inizio, gli admin e gli allenatori ricevono una notifica + (centro notifiche, push, email) con chi, quale evento e lo stato prima e dopo. Migration `m32` (solo admin, + applicata in produzione il 09/10/2026) e `m33` (anche allenatori). ## [1.3.1] - 2026-10-06 diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 74a917c..e772ccc 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -152,17 +152,17 @@ Le notifiche automatiche nascono nel database, da job `pg_cron` che chiamano fun riservate alla service role (anon e utenti autenticati non possono chiamarle). Testi e regole sono in [modules/notifiche.md](modules/notifiche.md); le decisioni in DD-037, DD-038 e DD-040. -| Funzione (migration) | Cosa fa | Job `pg_cron` | -| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | -| `genera_promemoria_eventi(tipo, finestra)` (M17, M23, M26) | Promemoria a 24 e 3 ore, con «oggi/domani», data, ora e luogo. **Non pianificata dalla M28** (DD-043): resta come funzione, nessun job la chiama. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | -| `genera_avvisi_palloni()` (M26, M27), con `genera_avvisi_palloni_fascia()` | Avviso del turno palloni a 3 ore (M27; in M26 erano 12, 6 e 3) per gli eventi con un incaricato confermato; poi chiama `genera_revoche_palloni()`. | `avvisi-palloni-automatici` (15 min) | -| `genera_revoche_palloni()` (M26) | Una revoca per chi aveva un avviso automatico e non è più destinatario, per eventi non ancora iniziati; guarda il registro. | (dentro `genera_avvisi_palloni()`) | -| `genera_solleciti_presenze()` (M26, M30), con `..._fascia()` | Sollecito a 24 ore (M30; in M26 erano 24, 12 e 6) ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | -| `genera_avvisi_compleanno(adesso)` (M31) | Dalle 8:00 (`Europe/Rome`) manda gli auguri a chi compie gli anni e l'avviso (solo titolo, senza età) agli altri membri attivi, un messaggio a testa; guarda il registro `compleanni_notificati`. `adesso` (default `now()`) serve ai test. 29 febbraio non gestito (DD-047). | `avvisi-compleanno` (ogni ora) | -| `avvisa_presenza_modificata()` (M32) | Trigger su `risposte_presenze` (insert, update dello stato, delete): se lo stato cambia a 0-6 ore da una partita o un allenamento, scrive una notifica `presenza_modificata` per ogni admin attivo (in prova, DD-050), tranne il giocatore della presenza. Usa `nome_stato_presenza(stato)` per i testi. | trigger `risposte_presenze_avvisa_modifica` | -| `inizio_evento(data, ora)` (M26) | Istante d'inizio di un evento in fuso `Europe/Rome`; `NULL` se l'ora non è valida, e l'evento viene solo escluso. | — | -| `giocatori_destinatari_evento(evento)` (M17, M21, M25) | Giocatori attivi destinatari: i convocati, o tutta la rosa se `convocati` è vuoto; mai gli allenatori. | — | -| `accoda_notifica_email()`, `accoda_push_promemoria()` (M22, M24, M26) | Trigger su `notifiche_utente`: la mail salta i tipi `turno_palloni*`; la push accoda i tipi automatici (non quelli dei pulsanti). | — | +| Funzione (migration) | Cosa fa | Job `pg_cron` | +| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | +| `genera_promemoria_eventi(tipo, finestra)` (M17, M23, M26) | Promemoria a 24 e 3 ore, con «oggi/domani», data, ora e luogo. **Non pianificata dalla M28** (DD-043): resta come funzione, nessun job la chiama. | `promemoria-eventi-24h` (ogni ora), `-3h` (15 min) | +| `genera_avvisi_palloni()` (M26, M27), con `genera_avvisi_palloni_fascia()` | Avviso del turno palloni a 3 ore (M27; in M26 erano 12, 6 e 3) per gli eventi con un incaricato confermato; poi chiama `genera_revoche_palloni()`. | `avvisi-palloni-automatici` (15 min) | +| `genera_revoche_palloni()` (M26) | Una revoca per chi aveva un avviso automatico e non è più destinatario, per eventi non ancora iniziati; guarda il registro. | (dentro `genera_avvisi_palloni()`) | +| `genera_solleciti_presenze()` (M26, M30), con `..._fascia()` | Sollecito a 24 ore (M30; in M26 erano 24, 12 e 6) ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | +| `genera_avvisi_compleanno(adesso)` (M31) | Dalle 8:00 (`Europe/Rome`) manda gli auguri a chi compie gli anni e l'avviso (solo titolo, senza età) agli altri membri attivi, un messaggio a testa; guarda il registro `compleanni_notificati`. `adesso` (default `now()`) serve ai test. 29 febbraio non gestito (DD-047). | `avvisi-compleanno` (ogni ora) | +| `avvisa_presenza_modificata()` (M32, M33) | Trigger su `risposte_presenze` (insert, update dello stato, delete): se lo stato cambia a 0-6 ore da una partita o un allenamento, scrive una notifica `presenza_modificata` per ogni admin e allenatore attivo (DD-050; la M32 solo admin), tranne il giocatore della presenza. Usa `nome_stato_presenza(stato)` per i testi. | trigger `risposte_presenze_avvisa_modifica` | +| `inizio_evento(data, ora)` (M26) | Istante d'inizio di un evento in fuso `Europe/Rome`; `NULL` se l'ora non è valida, e l'evento viene solo escluso. | — | +| `giocatori_destinatari_evento(evento)` (M17, M21, M25) | Giocatori attivi destinatari: i convocati, o tutta la rosa se `convocati` è vuoto; mai gli allenatori. | — | +| `accoda_notifica_email()`, `accoda_push_promemoria()` (M22, M24, M26) | Trigger su `notifiche_utente`: la mail salta i tipi `turno_palloni*`; la push accoda i tipi automatici (non quelli dei pulsanti). | — | ## Badge diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6187f4b..a926086 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -50,7 +50,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). - [x] Notifica di compleanno — auguri al festeggiato e avviso a tutti gli altri, ogni giorno dalle 8:00, su push, centro notifiche ed email ([specifica](modules/notifiche.md#catalogo-delle-notifiche), DD-047) - [x] Avviso «Presenza modificata» — una risposta che cambia a meno di 6 ore da una partita o un allenamento avvisa - gli admin (in prova solo loro, poi anche gli allenatori) ([specifica](modules/notifiche.md#catalogo-delle-notifiche), DD-050) + admin e allenatori ([specifica](modules/notifiche.md#catalogo-delle-notifiche), DD-050) ## Prossimo diff --git a/docs/decisions/DD-050.md b/docs/decisions/DD-050.md index 1e03268..f2e8608 100644 --- a/docs/decisions/DD-050.md +++ b/docs/decisions/DD-050.md @@ -1,7 +1,7 @@ # DD-050 — Avviso a admin e allenatori se una presenza cambia a meno di 6 ore dall'evento **Data:** 8 ottobre 2026 -**Stato:** Implementata in fase di prova, **solo per gli admin** (migration `m32`, applicata in produzione il 9 ottobre 2026) +**Stato:** Implementata per admin e allenatori (migration `m32`, solo admin, applicata in produzione il 9 ottobre 2026; `m33`, anche allenatori) **Contesto** Un giocatore può cambiare la risposta a un evento fino all'ultimo minuto. Chi organizza (admin e allenatore) @@ -44,14 +44,11 @@ risposta (`notifiche.md`, «Cosa non genera nessuna notifica»). 6. **Nessuna configurazione.** Né una soglia modificabile (6 ore è scritto nella funzione), né un interruttore per ruolo: l'email e la push seguono le preferenze già esistenti di ciascuno (`preferenze_utente`, DD-036). -**Fase di prova** +**Fase di prova (conclusa)** -Per ora l'avviso arriva **solo agli admin attivi** (ruolo `admin` e slot attivo), non agli allenatori: serve a -vedere in pratica quante notifiche genera prima di aprirlo a chi gestisce gli eventi. Il punto 2 resta la regola -a regime. Per allargarlo basta una nuova migration che aggiunge `'allenatore'` alla lista dei ruoli in -`avvisa_presenza_modificata()` (oggi `ur.role IN ('admin')`) e il rimando in -[allenatore.md](../modules/allenatore.md#notifiche); il resto non cambia. Finché dura la prova l'allenatore non -riceve nulla. +La `m32` (9 ottobre 2026) l'ha attivata per i soli admin, per vedere in pratica quante notifiche genera. Dopo la +prova la `m33` (stesso giorno) ha aggiunto `'allenatore'` alla lista dei ruoli in `avvisa_presenza_modificata()`: +ora vale la regola del punto 2. **Alternative scartate** @@ -66,7 +63,7 @@ riceve nulla. - Nuova migration `m32` (CHECK dei tipi, tre funzioni, due trigger); `docs/DATABASE.md` e il catalogo in `notifiche.md` si aggiornano con essa. Applicata in produzione il 9 ottobre 2026. -- A regime l'allenatore, che oggi non riceve nulla sulle presenze, riceve questa notifica (non in fase di prova): va aggiunta a +- L'allenatore, che prima non riceveva nulla sulle presenze, riceve questa notifica: aggiunta a [allenatore.md](../modules/allenatore.md#notifiche). - Rumore possibile a ridosso dell'evento (punto 5). Se in pratica dà fastidio, il primo ritocco è raggruppare per evento, non togliere l'avviso. diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index 2ceeae3..197f2f6 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -133,6 +133,7 @@ Cosa riceve l'allenatore, notifica per notifica (il catalogo completo, con i can | Turno palloni | no | I turni riguardano solo i giocatori. | | Sondaggio pre-partita | no | Né la push né l'avviso in-app e per email (DD-038): il sondaggio non lo riguarda. | | Compleanno (DD-047) | **sì** | Riceve l'avviso «Oggi è il compleanno di …» come tutta la squadra, e gli auguri nel giorno del proprio compleanno (la nascita è pubblica, DD-031). In-app, email e push come per i giocatori. | +| Presenza modificata (DD-050, `m33`) | **sì** | Riceve l'avviso quando un giocatore cambia, mette o ritira la risposta a meno di 6 ore da una partita o un allenamento, come gli admin. In-app, email e push. | | Notifiche smart (badge, serie, obiettivi, social) | **no** | Le mostra `CelebrazioneBadge` per il giocatore selezionato, letto da `useIo()`, che cerca l'id in `useRosa()`, da cui l'allenatore è escluso: per lui `useIo()` è `null` e `useNotificheSmart()` non fa nulla. Non ha statistiche da cui calcolarle. | Le email arrivano solo se il suo slot ha un account collegato e un'email registrata e se non ha diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index ca03bd0..ecce147 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -44,7 +44,7 @@ quelle del turno palloni**, che restano solo push e in-app: DD-040). | 6 | **Sondaggio pre-partita** — «💩 Sondaggio pre-partita aperto» | Un admin, dalla pagina della partita. Route `apri-sondaggio` (vedi [Scout Live](scout-live.md)). | Push: tutti i dispositivi iscritti, tranne quelli degli allenatori. In-app ed email (tipo `sondaggio_cacche`): i giocatori attivi, allenatori esclusi, anche senza dispositivi iscritti. | sì | sì | sì | | 7 | **Notifiche smart** — badge appena sbloccato, serie a un traguardo, obiettivo di squadra tra il 90 e il 100%, badge social vinto | L'app, mentre è aperta: sono calcolate in locale da `calcolaNotifiche()`. | Chi ha l'app aperta con uno slot giocatore in rosa selezionato; **mai l'allenatore**. Nessun invio verso altri dispositivi. | no (solo locale) | no | no | | 8 | **Compleanno** — auguri al festeggiato, avviso agli altri | **Automatica**, **una volta al giorno, dalle 8:00** (fuso `Europe/Rome`) per chi compie gli anni quel giorno (DD-047, `m31`): job `pg_cron` ogni ora, funzione `genera_avvisi_compleanno()`, registro `compleanni_notificati`; tipi `compleanno_auguri` e `compleanno`, push dal worker. Nessun pulsante manuale. | **Tutti i membri attivi**, allenatori compresi: il festeggiato riceve gli auguri, gli altri l'avviso. Con più compleanni lo stesso giorno, un solo messaggio a testa (agli altri con tutti i nomi, a ogni festeggiato solo i propri auguri). | sì | sì | sì | -| 9 | **Presenza modificata** — «Presenza modificata: _Nome Cognome_» _(DD-050, `m32`: in prova, solo admin)_ | **Automatica**, a ogni cambio di una risposta in `risposte_presenze` (nuova, modificata o ritirata) quando mancano da 0 a 6 ore all'inizio di una partita o di un allenamento: trigger sul database, tipo `presenza_modificata`, una notifica per modifica, push dal worker. | **In fase di prova solo gli admin attivi**; a regime anche gli allenatori attivi (DD-050). Mai il giocatore a cui appartiene la presenza. | sì | sì | sì | +| 9 | **Presenza modificata** — «Presenza modificata: _Nome Cognome_» _(DD-050, `m32` e `m33`)_ | **Automatica**, a ogni cambio di una risposta in `risposte_presenze` (nuova, modificata o ritirata) quando mancano da 0 a 6 ore all'inizio di una partita o di un allenamento: trigger sul database, tipo `presenza_modificata`, una notifica per modifica, push dal worker. | Gli **admin e gli allenatori attivi** (DD-050, `m33`; la `m32` li dava ai soli admin). Mai il giocatore a cui appartiene la presenza. | sì | sì | sì | Le notifiche generate dal server (righe 1-6 e 8) hanno tutti e tre i canali (DD-038), tranne il turno palloni (riga 4), che non manda email (DD-040). La differenza sta in come parte la push: quelle dei pulsanti (3 e 6) la mandano le route dell'app, quelle diff --git a/supabase/migrations/20261009150000_m33_presenza_modificata_allenatori.sql b/supabase/migrations/20261009150000_m33_presenza_modificata_allenatori.sql new file mode 100644 index 0000000..7febbe4 --- /dev/null +++ b/supabase/migrations/20261009150000_m33_presenza_modificata_allenatori.sql @@ -0,0 +1,66 @@ +-- M33 — «Presenza modificata» anche agli allenatori (DD-050) +-- +-- Fine della fase di prova della M32: l'avviso arriva agli admin e agli allenatori attivi (ruolo in +-- `user_roles` e slot attivo), tranne il giocatore a cui appartiene la presenza. Cambia solo la lista +-- dei ruoli nella funzione; trigger, tipo di notifica e code push/email restano quelli della M32. + +CREATE OR REPLACE FUNCTION public.avvisa_presenza_modificata() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + evento_id_r text := CASE WHEN TG_OP = 'DELETE' THEN OLD.evento_id ELSE NEW.evento_id END; + giocatore_r text := CASE WHEN TG_OP = 'DELETE' THEN OLD.giocatore_id ELSE NEW.giocatore_id END; + prima text := CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE OLD.stato END; + dopo text := CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE NEW.stato END; + ev record; + chi record; +BEGIN + IF prima IS NOT DISTINCT FROM dopo THEN + RETURN NULL; + END IF; + + SELECT e.titolo, e.data, e.ora, e.tipo, public.inizio_evento(e.data, e.ora) AS inizio + INTO ev + FROM public.eventi_app e + WHERE e.id = evento_id_r; + + IF NOT FOUND + OR ev.tipo NOT IN ('partita', 'allenamento') + OR ev.inizio IS NULL + OR ev.inizio - now() NOT BETWEEN interval '0' AND interval '6 hours' THEN + RETURN NULL; + END IF; + + SELECT trim(g.nome || ' ' || g.cognome) AS nome_completo + INTO chi + FROM public.giocatori_squadra g + WHERE g.id = giocatore_r; + + IF NOT FOUND THEN + RETURN NULL; + END IF; + + INSERT INTO public.notifiche_utente (giocatore_id, tipo, titolo, corpo) + SELECT d.id, + 'presenza_modificata', + 'Presenza modificata: ' || chi.nome_completo, + 'Evento: ' || ev.titolo + || E'\nData: ' || to_char(ev.data, 'DD/MM/YYYY') || ', ore ' || ev.ora + || E'\nDa: ' || public.nome_stato_presenza(prima) + || E'\nA: ' || public.nome_stato_presenza(dopo) + FROM public.giocatori_squadra d + WHERE d.attivo = true + AND d.id <> giocatore_r + AND EXISTS ( + SELECT 1 + FROM public.user_roles ur + WHERE ur.user_id = d.auth_user_id + AND ur.role IN ('admin', 'allenatore') + ); + + RETURN NULL; +END; +$$; diff --git a/test/README.md b/test/README.md index 7e0615c..18e1be1 100644 --- a/test/README.md +++ b/test/README.md @@ -24,12 +24,12 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push ## Struttura -| Cartella | Cosa verifica | Serve rete? | -| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi (calcolo e card di dettaglio), avvisi in Home, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No | -| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), l'avviso «Presenza modificata» agli admin con finestra di 6 ore, stati e nessun avviso a cancellazione dell'evento (`presenza-modificata`, M32, DD-050), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | -| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | -| `helpers/` | Avvio del server di test e mini-harness condiviso | — | +| Cartella | Cosa verifica | Serve rete? | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi (calcolo e card di dettaglio), avvisi in Home, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No | +| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), l'avviso «Presenza modificata» ad admin e allenatori con finestra di 6 ore, stati e nessun avviso a cancellazione dell'evento (`presenza-modificata`, M32-M33, DD-050), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì | +| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì | +| `helpers/` | Avvio del server di test e mini-harness condiviso | — | ## Server di sviluppo con Node vecchio diff --git a/test/integration/presenza-modificata.test.ts b/test/integration/presenza-modificata.test.ts index 6f43717..6229601 100644 --- a/test/integration/presenza-modificata.test.ts +++ b/test/integration/presenza-modificata.test.ts @@ -1,11 +1,11 @@ /** - * Avviso «Presenza modificata» (M32, DD-050): `bun test/integration/presenza-modificata.test.ts`. + * Avviso «Presenza modificata» (M32 e M33, DD-050): `bun test/integration/presenza-modificata.test.ts`. * * Scrive su `risposte_presenze` come farebbe l'app e verifica, sul database vero: * * - una risposta nuova, modificata o ritirata a meno di 6 ore da una partita o da un allenamento - * avvisa gli admin attivi, con titolo e corpo del catalogo; - * - in fase di prova (DD-050) non riceve nulla un giocatore semplice né un allenatore, e non riceve + * avvisa gli admin e gli allenatori attivi, con titolo e corpo del catalogo; + * - riceve l'avviso chi ha il ruolo admin o allenatore con uno slot attivo (M33); non riceve nulla un giocatore semplice, e non riceve * nulla l'admin a cui appartiene la presenza; * - nessun avviso se lo stato non cambia, se l'evento è oltre le 6 ore, già iniziato, o di un tipo che * non è partita né allenamento, né quando una risposta sparisce perché l'evento è stato cancellato; @@ -168,7 +168,9 @@ if (!locale) { // Il seed può avere altri admin con uno slot attivo: l'elenco atteso lo calcola il test. const attesi = async () => { - const ruoli = await leggi<{ user_id: string }>("user_roles?role=eq.admin&select=user_id"); + const ruoli = await leggi<{ user_id: string }>( + "user_roles?role=in.(admin,allenatore)&select=user_id", + ); const slot = await leggi<{ id: string; auth_user_id: string }>( "giocatori_squadra?attivo=eq.true&auth_user_id=not.is.null&select=id,auth_user_id", ); @@ -184,31 +186,34 @@ if (!locale) { idEventi.push(`${PREFISSO}partita`); const dataIt = `${ev.data.slice(8, 10)}/${ev.data.slice(5, 7)}/${ev.data.slice(0, 4)}`; - await prova("risposta nuova: avvisa gli admin, con il testo del catalogo", async () => { - await risposta(`${PREFISSO}partita`, "presente"); - const righe = await notifiche(); - assert.deepEqual(righe.map((r) => r.giocatore_id).sort(), await attesi()); - assert.ok( - righe.some((r) => r.giocatore_id === ADMIN), - "l'admin di prova è tra i destinatari", - ); - const mia = righe.find((r) => r.giocatore_id === ADMIN)!; - assert.equal(mia.titolo, `Presenza modificata: ${nomeCompleto}`); - assert.equal( - mia.corpo, - [ - `Evento: ${ev.titolo}`, - `Data: ${dataIt}, ore ${ev.ora}`, - "Da: nessuna risposta", - "A: presente", - ].join("\n"), - ); - }); + await prova( + "risposta nuova: avvisa admin e allenatori, con il testo del catalogo", + async () => { + await risposta(`${PREFISSO}partita`, "presente"); + const righe = await notifiche(); + assert.deepEqual(righe.map((r) => r.giocatore_id).sort(), await attesi()); + assert.ok( + righe.some((r) => r.giocatore_id === ADMIN), + "l'admin di prova è tra i destinatari", + ); + const mia = righe.find((r) => r.giocatore_id === ADMIN)!; + assert.equal(mia.titolo, `Presenza modificata: ${nomeCompleto}`); + assert.equal( + mia.corpo, + [ + `Evento: ${ev.titolo}`, + `Data: ${dataIt}, ore ${ev.ora}`, + "Da: nessuna risposta", + "A: presente", + ].join("\n"), + ); + }, + ); - await prova("in prova non ricevono nulla il giocatore semplice e l'allenatore", async () => { + await prova("il giocatore semplice non riceve nulla, l'allenatore sì", async () => { const destinatari = (await notifiche()).map((r) => r.giocatore_id); assert.ok(!destinatari.includes(GIOCATORE)); - assert.ok(!destinatari.includes(ALLENATORE)); + assert.ok(destinatari.includes(ALLENATORE), "l'allenatore attivo è tra i destinatari"); assert.ok(!destinatari.includes(RISPONDE), "chi risponde non avvisa se stesso"); });