diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cadcb38..046c1c7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -26,12 +26,14 @@ versioni `0.y.z` erano pre-release. ### Modificato +- **Sollecito presenze: un solo avviso automatico, 24 ore prima** — a chi non ha risposto o ha risposto + «forse»; prima ne partivano tre (24, 12 e 6 ore). Il pulsante «Sollecita» non cambia. Migration + `m30_sollecito_solo_24h`, applicata in produzione il 01/10/2026 (DD-045). + - **Turno palloni: un solo avviso, 3 ore prima** — a chi deve portare i palloni e a chi li deve prendere; prima ne partivano tre (12, 6 e 3 ore). Migration `m27_palloni_solo_3h`, applicata in produzione il 01/10/2026 (DD-042). -### Modificato - - **Avviso certificati: giocatore a 30 giorni** — il giocatore vede l'avviso giallo sul proprio certificato medico da un mese prima della scadenza (prima 7 giorni). Per gli admin non cambia nulla: 7 giorni prima in giallo, nero dopo la scadenza (DD-041). diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 819063e..3779090 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -156,7 +156,7 @@ riservate alla service role (anon e utenti autenticati non possono chiamarle). T | `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), con `..._fascia()` | Solleciti a 24, 12 e 6 ore ai destinatari dell'evento senza risposta o con «forse». | `solleciti-presenze-automatici` (15 min) | +| `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) | | `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). | — | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 58e9318..402191a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -45,7 +45,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`). da un worker Docker sull'host di casa via Gmail, con interruttore «Email» in Profilo ([specifica](modules/notifiche-email.md), DD-036) - [x] Notifiche automatiche — turno palloni 3 ore prima, sollecito - presenze a 24, 12 e 6 ore (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano; il promemoria a 24 e 3 ore è stato tolto, DD-043) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) + presenze 24 ore prima (i pulsanti «Avvisa chi è di turno» e «Sollecita» restano; il promemoria a 24 e 3 ore è stato tolto, DD-043) ([specifica](modules/notifiche.md#testi-delle-notifiche), DD-040) ## Prossimo diff --git a/docs/WORKER_EMAIL.md b/docs/WORKER_EMAIL.md index 17adc4d..3707a7a 100644 --- a/docs/WORKER_EMAIL.md +++ b/docs/WORKER_EMAIL.md @@ -1,7 +1,7 @@ # Worker email: installazione e gestione Come si mette in funzione, si controlla e si diagnostica il worker (`mailer/`) che invia le notifiche -per email e le push delle notifiche che nascono nel database: turno palloni a 3 ore con revoca, solleciti presenze a 24, 12 e 6 ore (DD-040). Cosa fa il canale email sta in +per email e le push delle notifiche che nascono nel database: turno palloni a 3 ore con revoca, sollecito presenze a 24 ore (DD-040). Cosa fa il canale email sta in [modules/notifiche-email.md](modules/notifiche-email.md), la push dei promemoria in [modules/notifiche.md](modules/notifiche.md#push-dei-promemoria-m24); perché è fatto così in [DD-036](decisions/DD-036.md) diff --git a/docs/decisions/DD-040.md b/docs/decisions/DD-040.md index 748f87f..c0ea9f0 100644 --- a/docs/decisions/DD-040.md +++ b/docs/decisions/DD-040.md @@ -1,7 +1,7 @@ # DD-040 — Promemoria con «oggi» o «domani», turno palloni e solleciti automatici **Data:** 30 settembre 2026 -**Stato:** Accettata (implementata: migration `m26`, applicata in produzione il 30 settembre 2026, job, route e test). Il turno palloni è poi passato a un solo avviso a 3 ore da [DD-042](DD-042.md). +**Stato:** Accettata (implementata: migration `m26`, applicata in produzione il 30 settembre 2026, job, route e test). Il sollecito presenze è poi passato a un solo avviso a 24 ore da [DD-045](DD-045.md). Il turno palloni è poi passato a un solo avviso a 3 ore da [DD-042](DD-042.md). **Contesto** Quattro notifiche su sei partivano da un pulsante premuto da un admin (DD-025 per i palloni): se nessuno diff --git a/docs/decisions/DD-042.md b/docs/decisions/DD-042.md index 3da08cc..ac911f6 100644 --- a/docs/decisions/DD-042.md +++ b/docs/decisions/DD-042.md @@ -19,7 +19,7 @@ chi deve portare i palloni e per chi deve prenderli. Sono troppi per un'informaz 3. **Migration `m27`:** `genera_avvisi_palloni()` chiama solo la fascia `turno_palloni_3h`. I tipi `turno_palloni_12h` e `_6h` **restano nei `CHECK`** e nella revoca, perché notifiche e righe di registro già generate li usano; non ne nascono di nuovi. -4. Il pulsante «Avvisa chi è di turno» non cambia. I solleciti presenze (24, 12 e 6 ore) non cambiano. +4. Il pulsante «Avvisa chi è di turno» non cambia. I solleciti presenze non cambiano qui (poi ridotti a uno, DD-045). **Alternative scartate** diff --git a/docs/decisions/DD-045.md b/docs/decisions/DD-045.md new file mode 100644 index 0000000..4ddfd9e --- /dev/null +++ b/docs/decisions/DD-045.md @@ -0,0 +1,34 @@ +# DD-045 — Sollecito presenze: un solo avviso automatico, 24 ore prima + +**Data:** 1 ottobre 2026 +**Stato:** Accettata (migration `m30`, applicata in produzione il 1 ottobre 2026; modifica la parte «sollecito presenze» +di [DD-040](DD-040.md)) + +**Contesto** +DD-040 manda il sollecito presenze a 24, 12 e 6 ore prima dell'evento: fino a tre messaggi per chi non +risponde o resta su «forse». Ne basta uno. + +**Decisione** + +1. Il sollecito automatico parte **una sola volta, 24 ore prima** (finestra «meno di 24 ore»), a chi in + quel momento non ha risposto o ha risposto «forse» tra i destinatari dell'evento (convocati, o tutta + la rosa attiva; mai l'allenatore). Un evento creato a meno di 24 ore dall'inizio lo riceve comunque, + al giro successivo del job. Testo e canali (push, centro notifiche, email) restano quelli di DD-040. +2. Il **pulsante «Sollecita»** e la route `sollecita-presenze` non cambiano: l'admin o l'allenatore possono + sempre rimandarlo a mano, anche dopo l'automatico. +3. **Migration `m30`:** `genera_solleciti_presenze()` chiama solo la fascia `sollecita_presenze_24h`. I tipi + `sollecita_presenze_12h` e `_6h` restano nei `CHECK` per le notifiche già generate; non ne nascono di nuovi. + +**Alternative scartate** + +- Mandarlo a un orario più vicino (12 o 6 ore) → scartata su indicazione esplicita: 24 ore. +- Togliere i tipi `12h` e `6h` dai `CHECK` → le righe già generate non sarebbero più valide. + +**Conseguenze** + +- Chi risponde «forse» o non risponde dopo il sollecito non riceve altri avvisi automatici: tocca al + pulsante manuale. +- Il massimo di mail automatiche per giocatore ed evento scende a una (il sollecito). + +**Riesame** +Se il tasso di risposta cala, valutare un secondo sollecito più vicino all'evento, solo per chi non ha mai risposto. diff --git a/docs/decisions/README.md b/docs/decisions/README.md index abe752a..8cdc861 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -46,50 +46,51 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand **Accettate** -| ID | Titolo | -| ------------------- | ----------------------------------------- | -| [DD-001](DD-001.md) | Indipendenza da Lovable | -| [DD-002](DD-002.md) | Sviluppo document-first | -| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | -| [DD-005](DD-005.md) | Mobile-first | -| [DD-006](DD-006.md) | AI solo se utile | -| [DD-007](DD-007.md) | Badge calcolati, non in DB | -| [DD-008](DD-008.md) | Gamification equa tra ruoli | -| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | -| [DD-010](DD-010.md) | Niente storico certificati | -| [DD-011](DD-011.md) | Auth reale prima del profilo | -| [DD-012](DD-012.md) | Rimandare la migrazione ID | -| [DD-013](DD-013.md) | Portabilità dello stack | -| [DD-015](DD-015.md) | Rosa da hardcoded a DB | -| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | -| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | -| [DD-018](DD-018.md) | Collegamento automatico per email | -| [DD-019](DD-019.md) | Il branch lo decide l'utente | -| [DD-020](DD-020.md) | Test obbligatori e verdi | -| [DD-021](DD-021.md) | Molle interrompibili con motion | -| [DD-022](DD-022.md) | App solo chiara | -| [DD-023](DD-023.md) | Scritture limitate per ruolo | -| [DD-024](DD-024.md) | Route di notifica autenticate | -| [DD-025](DD-025.md) | Promemoria palloni manuale | -| [DD-026](DD-026.md) | Payload push cifrato | -| [DD-027](DD-027.md) | Voto limitato ai convocati | -| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | -| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | -| [DD-030](DD-030.md) | Centro notifiche in-app | -| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | -| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | -| [DD-033](DD-033.md) | Gestione eventi solo da calendario | -| [DD-034](DD-034.md) | Ruolo allenatore | -| [DD-035](DD-035.md) | Avviso certificati in Home | -| [DD-036](DD-036.md) | Notifiche email via Gmail | -| [DD-037](DD-037.md) | Promemoria evento senza ritorni | -| [DD-038](DD-038.md) | Tre canali per ogni notifica | -| [DD-039](DD-039.md) | Niente promemoria all'allenatore | -| [DD-040](DD-040.md) | Palloni e solleciti automatici | -| [DD-044](DD-044.md) | Via la foto tessera dal profilo | -| [DD-043](DD-043.md) | Niente promemoria evento a 24 e 3 ore | -| [DD-042](DD-042.md) | Turno palloni: un solo avviso a 3 ore | -| [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | +| ID | Titolo | +| ------------------- | ------------------------------------------- | +| [DD-001](DD-001.md) | Indipendenza da Lovable | +| [DD-002](DD-002.md) | Sviluppo document-first | +| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | +| [DD-005](DD-005.md) | Mobile-first | +| [DD-006](DD-006.md) | AI solo se utile | +| [DD-007](DD-007.md) | Badge calcolati, non in DB | +| [DD-008](DD-008.md) | Gamification equa tra ruoli | +| [DD-009](DD-009.md) | CSI manuale, poi integrazione API | +| [DD-010](DD-010.md) | Niente storico certificati | +| [DD-011](DD-011.md) | Auth reale prima del profilo | +| [DD-012](DD-012.md) | Rimandare la migrazione ID | +| [DD-013](DD-013.md) | Portabilità dello stack | +| [DD-015](DD-015.md) | Rosa da hardcoded a DB | +| [DD-016](DD-016.md) | Schema dati Profilo Giocatore | +| [DD-017](DD-017.md) | L'admin scrive al posto del giocatore | +| [DD-018](DD-018.md) | Collegamento automatico per email | +| [DD-019](DD-019.md) | Il branch lo decide l'utente | +| [DD-020](DD-020.md) | Test obbligatori e verdi | +| [DD-021](DD-021.md) | Molle interrompibili con motion | +| [DD-022](DD-022.md) | App solo chiara | +| [DD-023](DD-023.md) | Scritture limitate per ruolo | +| [DD-024](DD-024.md) | Route di notifica autenticate | +| [DD-025](DD-025.md) | Promemoria palloni manuale | +| [DD-026](DD-026.md) | Payload push cifrato | +| [DD-027](DD-027.md) | Voto limitato ai convocati | +| [DD-028](DD-028.md) | Soglia minima Media voto e MVP | +| [DD-029](DD-029.md) | Pulizia a cascata evento cancellato | +| [DD-030](DD-030.md) | Centro notifiche in-app | +| [DD-031](DD-031.md) | Nascita pubblica sincronizzata | +| [DD-032](DD-032.md) | Stagione CSI dalle date delle gare | +| [DD-033](DD-033.md) | Gestione eventi solo da calendario | +| [DD-034](DD-034.md) | Ruolo allenatore | +| [DD-035](DD-035.md) | Avviso certificati in Home | +| [DD-036](DD-036.md) | Notifiche email via Gmail | +| [DD-037](DD-037.md) | Promemoria evento senza ritorni | +| [DD-038](DD-038.md) | Tre canali per ogni notifica | +| [DD-039](DD-039.md) | Niente promemoria all'allenatore | +| [DD-040](DD-040.md) | Palloni e solleciti automatici | +| [DD-045](DD-045.md) | Sollecito presenze: un solo avviso a 24 ore | +| [DD-044](DD-044.md) | Via la foto tessera dal profilo | +| [DD-043](DD-043.md) | Niente promemoria evento a 24 e 3 ore | +| [DD-042](DD-042.md) | Turno palloni: un solo avviso a 3 ore | +| [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni | **In valutazione** diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md index ee15632..10fb41b 100644 --- a/docs/modules/allenatore.md +++ b/docs/modules/allenatore.md @@ -114,7 +114,7 @@ a lui (`usePuoGestireEventi()`). Può anche **sollecitare le presenze** di un evento, come l'admin: la route `sollecita-presenze.ts` accetta admin o allenatore (`richiediGestoreEventi()`), e il -sollecito arriva solo ai giocatori che non hanno risposto, mai all'allenatore stesso. Con DD-040 si aggiungono solleciti automatici a 24, 12 e 6 ore prima dell'evento, ma il pulsante resta. +sollecito arriva solo ai giocatori che non hanno risposto, mai all'allenatore stesso. Con DD-040 si aggiungono un sollecito automatico 24 ore prima dell'evento, ma il pulsante resta. **Restano solo admin:** dashboard `/admin`, dati e documenti dei giocatori, export CSI, notifica personalizzata, promemoria palloni, apertura sondaggio cacche, correzione delle diff --git a/docs/modules/notifiche.md b/docs/modules/notifiche.md index 83139c8..9f3bbed 100644 --- a/docs/modules/notifiche.md +++ b/docs/modules/notifiche.md @@ -40,7 +40,7 @@ quelle del turno palloni**, che restano solo push e in-app: DD-040). | 2 | ~~Promemoria a 3 ore~~ — **rimosso** | Come la riga 1 (tipo `evento_promemoria_3h`). | — | — | — | — | | 3 | **Messaggio dello staff** — titolo fisso «Messaggio dallo staff», corpo il testo scritto (max 300 caratteri) | Un admin, dalla tab «Notifiche» della dashboard: «Invia messaggio a tutti» o il pulsante della riga di un giocatore. Route `notifica-personalizzata`, tipo `admin`. | Push: tutti i dispositivi iscritti, o quelli del giocatore scelto. In-app ed email: tutta la rosa attiva (allenatori compresi, anche senza dispositivi iscritti), o il giocatore scelto. | sì | sì | sì | | 4 | **Turno palloni** — «Turno palloni: incarico assegnato» / «Turno palloni: riconsegna» | **Automatica**, **una sola volta, 3 ore prima** dell'evento (DD-040, DD-042): job `pg_cron` ogni 15 minuti, solo per allenamenti e partite con un incaricato confermato; tipo `turno_palloni_3h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m27`), push dal worker. Se l'incaricato cambia, chi aveva già ricevuto un avviso riceve una volta «Turno palloni: incarico revocato» (tipo `turno_palloni_revocato`). **Manuale**, in più: un admin, con il pulsante nella pagina dell'evento; route `promemoria-palloni`, tipo `turno_palloni`, push dalla route. | L'incaricato del turno di quell'evento e, se è un'altra persona, chi aveva i palloni all'evento precedente. Se per l'evento non c'è un incaricato non parte nulla. | sì | sì | **no** (DD-040) | -| 5 | **Sollecito presenze** — «Conferma di partecipazione richiesta: _titolo_» | **Automatica**, in tre tempi (DD-040): **24, 12 e 6 ore** prima dell'evento, dallo stesso job; tipi `sollecita_presenze_24h`, `_12h`, `_6h`, push dal worker. **Manuale**, in più: un admin o un allenatore con il pulsante nella pagina dell'evento; route `sollecita-presenze`, tipo `sollecita_presenze`, push dalla route. | Automatica e manuale: i destinatari dell'evento (convocati, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori) che in quel momento non hanno risposto o hanno risposto «forse». | sì | sì | sì | +| 5 | **Sollecito presenze** — «Conferma di partecipazione richiesta: _titolo_» | **Automatica**, **una sola volta, 24 ore prima** dell'evento (DD-040, DD-045): per gli eventi che iniziano entro 24 ore, dallo stesso job; tipo `sollecita_presenze_24h` (`_12h` e `_6h` esistono solo nelle notifiche generate prima di `m30`), push dal worker. **Manuale**, in più: un admin o un allenatore con il pulsante nella pagina dell'evento; route `sollecita-presenze`, tipo `sollecita_presenze`, push dalla route. | Automatica e manuale: i destinatari dell'evento (convocati, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori) che in quel momento non hanno risposto o hanno risposto «forse». | sì | sì | sì | | 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 | @@ -136,28 +136,28 @@ descrivono come funzionavano fino alla `m28`: la funzione esiste ancora ma nessu proposta automatica di `completaTurni()` non salvata non basta. Se l'incaricato cambia dopo un avviso, il nuovo riceve il proprio al giro successivo e il precedente riceve una sola volta «Turno palloni: incarico revocato»; se viene poi riassegnato, non riceve un secondo avviso. -- **I solleciti cadono in finestre che non si sovrappongono** (DD-040): 24 ore tra 24 e 12 ore - prima dell'inizio, 12 ore tra 12 e 6, 6 ore sotto le 6. Un evento creato a meno di 12 ore - dall'inizio riceve solo quello della sua finestra. Chi risponde presente, assente o in ritardo - smette di riceverli; chi resta su «forse» li riceve tutti e tre. -- **Per ogni evento un giocatore riceve al massimo cinque mail automatiche** (DD-040): due - promemoria e tre solleciti, più quelle dei pulsanti manuali (sollecito, sondaggio). Il turno palloni +- **Il sollecito automatico è uno solo, a 24 ore** (DD-045): parte per gli eventi che iniziano entro 24 + ore, a chi in quel momento non ha risposto o ha risposto «forse». Un evento creato a meno di 24 + ore dall'inizio lo riceve comunque, una volta. Chi risponde dopo non ne riceve altri; chi resta su + «forse» non lo riceve una seconda volta. Altri solleciti solo col pulsante manuale. +- **Per ogni evento un giocatore riceve al massimo una mail automatica** (DD-045): il + sollecito presenze (il promemoria è stato tolto, DD-043), più quelle dei pulsanti manuali (sollecito, sondaggio). Il turno palloni (avvisi automatici, revoca e pulsante) non manda email: restano la push e l'alert in app. - **Non esistono preferenze per tipo di notifica:** un solo interruttore per la push («Notifiche») e uno per le email («Email»). Il centro notifiche in-app non si può spegnere. ### Test che verificano il catalogo -| Cosa | Test | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Promemoria 24 e 3 ore (funzione ancora presente ma non pianificata, M28): destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts`; niente job pianificati: `notifiche-automatiche.test.ts` (M28) | -| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | -| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | -| Sollecito presenze: chi non ha risposto o ha detto «forse», mai l'allenatore; permesso ad allenatore e admin, non ai giocatori | `destinatari-notifiche.test.ts`, `unit/presenze.test.ts` | -| Sondaggio pre-partita: push senza allenatori, avviso in-app ed email ai giocatori attivi, `upsert`, riservato agli admin | `destinatari-notifiche.test.ts`, `sondaggio-notifiche.test.ts`, `unit/cacche.test.ts` | -| Promemoria con «oggi/domani», turno palloni a 3 ore e revoca, solleciti a 24·12·6 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, `m27`) | `integration/notifiche-automatiche.test.ts`, `promemoria-eventi.test.ts` (testo oggi/domani), `destinatari-notifiche.test.ts` (pulsanti), `unit/palloni-core.test.ts`, `unit/presenze.test.ts` | -| Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.test.ts` | -| L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) | +| Cosa | Test | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promemoria 24 e 3 ore (funzione ancora presente ma non pianificata, M28): destinatari (convocati, tutti i giocatori attivi se `convocati` è vuoto, mai l'allenatore), deduplica, eliminazione, coda push ed email | `test/integration/destinatari-notifiche.test.ts`, `promemoria-eventi.test.ts`, `push-promemoria.test.ts`, `notifiche-email.test.ts`; niente job pianificati: `notifiche-automatiche.test.ts` (M28) | +| Messaggio dello staff: push a tutti i dispositivi (allenatore compreso) o a uno solo, in-app a tutta la rosa attiva, riservato agli admin | `destinatari-notifiche.test.ts`, `notifiche-utente.test.ts` | +| Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` | +| Sollecito presenze: chi non ha risposto o ha detto «forse», mai l'allenatore; permesso ad allenatore e admin, non ai giocatori | `destinatari-notifiche.test.ts`, `unit/presenze.test.ts` | +| Sondaggio pre-partita: push senza allenatori, avviso in-app ed email ai giocatori attivi, `upsert`, riservato agli admin | `destinatari-notifiche.test.ts`, `sondaggio-notifiche.test.ts`, `unit/cacche.test.ts` | +| Promemoria con «oggi/domani», turno palloni a 3 ore e revoca, solleciti a 24 ore (manuale compreso): finestre, destinatari, una sola volta, registro (DD-040, `m26`; palloni a 3 ore: DD-042, solleciti a 24 ore: DD-045, `m30`; `m27`) | `integration/notifiche-automatiche.test.ts`, `promemoria-eventi.test.ts` (testo oggi/domani), `destinatari-notifiche.test.ts` (pulsanti), `unit/palloni-core.test.ts`, `unit/presenze.test.ts` | +| Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.test.ts` | +| L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) | Le push dei test vanno a un servizio push finto in locale: si verifica **a chi** arrivano, non la consegna su un telefono. Non ha un test l'esclusione dell'allenatore dalle **notifiche smart**: la @@ -284,8 +284,7 @@ non richiede che il dispositivo abbia attivato «Notifiche», ha uno storico per stessa cosa delle notifiche smart (quelle restano locali, non salvate a database). Cinque sorgenti scrivono in `notifiche_utente`, mai il client (con DD-040 le sorgenti 3 e 4 hanno in più un -job `pg_cron` come la 2, che scrive `turno_palloni_3h`/`_revocato` (`_12h`/`_6h` solo fino a `m26`) e `sollecita_presenze_24h`/ -`_12h`/`_6h`; i pulsanti restano e scrivono `turno_palloni` e `sollecita_presenze`): +job `pg_cron` come la 2, che scrive `turno_palloni_3h`/`_revocato` (`_12h`/`_6h` solo fino a `m26`) e `sollecita_presenze_24h` (`_12h`/`_6h` solo fino a `m29`); i pulsanti restano e scrivono `turno_palloni` e `sollecita_presenze`): 1. **Messaggio admin** — `notifica-personalizzata.ts` inserisce una riga per destinatario in parallelo all'invio push esistente (a tutta la rosa attiva se `giocatoreId` è omesso). diff --git a/docs/modules/presenze.md b/docs/modules/presenze.md index 4c0cc82..1c9974c 100644 --- a/docs/modules/presenze.md +++ b/docs/modules/presenze.md @@ -79,7 +79,7 @@ src/routes/api/public/sollecita-presenze.ts > **Sollecito automatico (DD-040, migration `m26`).** Il pulsante «Sollecita» e la route > **restano**, ma `destinatariSollecito()` passa a guardare anche i convocati (convocati che non hanno -> risposto o hanno risposto «forse», o tutta la rosa se `convocati` è vuoto); in più un job `pg_cron` manda il sollecito **24, 12 e 6 ore prima** dell'evento a chi in +> risposto o hanno risposto «forse», o tutta la rosa se `convocati` è vuoto); in più un job `pg_cron` manda il sollecito **una sola volta, 24 ore prima** dell'evento (DD-045, migration `m30`) a chi in > quel momento non ha risposto o ha risposto «forse», tra i destinatari dell'evento (convocati, o > tutta la rosa attiva; mai l'allenatore). Testo e finestre nel [catalogo](notifiche.md#catalogo-delle-notifiche). > Il diagramma sopra è quello del pulsante, che da DD-040 legge anche `convocati` dell'evento. @@ -94,7 +94,7 @@ vale per tutta la rosa) — `eventiContanoPresenze()` in `presenze.ts`. - Aggiornamento ottimistico della cache locale dopo ogni salvataggio: nessuna rilettura dal server, la UI risponde subito. -- Il sollecito ha due vie (DD-040): tre solleciti **automatici** (24, 12 e 6 ore prima, job `pg_cron` +- Il sollecito ha due vie (DD-040): un sollecito **automatico** (24 ore prima, job `pg_cron` nel database) e il pulsante **manuale** accanto, che non sostituiscono. La route del pulsante verifica il ruolo lato server con `richiediGestoreEventi` (admin o allenatore, DD-024, DD-034). - Ognuno risponde **solo per sé**, e non è più una regola della sola interfaccia: dalla diff --git a/supabase/migrations/20261001190000_m30_sollecito_solo_24h.sql b/supabase/migrations/20261001190000_m30_sollecito_solo_24h.sql new file mode 100644 index 0000000..acfa0fe --- /dev/null +++ b/supabase/migrations/20261001190000_m30_sollecito_solo_24h.sql @@ -0,0 +1,20 @@ +-- M30 — Sollecito presenze: un solo avviso automatico, 24 ore prima (DD-045) +-- +-- Con M26 il job mandava il sollecito a 24, 12 e 6 ore. Ne basta uno, 24 ore prima: parte per gli +-- eventi che iniziano entro 24 ore (quindi anche per uno creato più tardi), a chi in quel momento +-- non ha risposto o ha risposto «forse». I tipi `sollecita_presenze_12h` e `_6h` restano ammessi dai +-- CHECK per le notifiche già generate, ma non ne nascono di nuove. Il pulsante manuale non cambia. + +CREATE OR REPLACE FUNCTION public.genera_solleciti_presenze() +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +BEGIN + PERFORM public.genera_solleciti_presenze_fascia('sollecita_presenze_24h', interval '0', interval '24 hours'); +END; +$$; + +REVOKE ALL ON FUNCTION public.genera_solleciti_presenze() FROM PUBLIC, anon, authenticated; +GRANT EXECUTE ON FUNCTION public.genera_solleciti_presenze() TO service_role; diff --git a/test/README.md b/test/README.md index 03d6bcb..40e20bf 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, 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 e M27, DD-040 e DD-042: palloni un solo avviso a 3 ore) 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), 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, 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), 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/notifiche-automatiche.test.ts b/test/integration/notifiche-automatiche.test.ts index 30f0251..a369a78 100644 --- a/test/integration/notifiche-automatiche.test.ts +++ b/test/integration/notifiche-automatiche.test.ts @@ -6,8 +6,7 @@ * creati con orari relativi a «adesso» e verifica: * * - che il promemoria a 24 e 3 ore non sia più pianificato (M28, DD-043); - * - le finestre (palloni: un solo avviso nelle 3 ore prima; solleciti 24·12·6 ore, contigue e senza - * sovrapposizione); + * - le finestre (palloni: un solo avviso nelle 3 ore prima; solleciti: un solo avviso nelle 24 ore prima, DD-045); * - i destinatari (incaricato e turno precedente; convocati senza risposta o con «forse»; mai * l'allenatore né chi non è più attivo); * - una sola generazione per giocatore, evento e tipo, anche se la notifica viene eliminata; @@ -567,7 +566,7 @@ if (!locale) { .sort(); await prova( - "ogni evento riceve il sollecito della sua fascia, a chi deve rispondere", + "un solo sollecito, entro 24 ore dall'inizio, a chi deve rispondere (DD-045)", async () => { await esegui("genera_solleciti_presenze"); assert.deepEqual( @@ -577,21 +576,18 @@ if (!locale) { ); const senzaRisposta = (await tuttiGiocatoriAttivi()).filter((g) => g !== G4); assert.deepEqual( - await destinatari(S12, "sollecita_presenze_12h"), + await destinatari(S12, "sollecita_presenze_24h"), senzaRisposta, "convocati vuoto: tutta la rosa attiva, meno chi ha risposto; g5 («forse») resta", ); assert.deepEqual( - await destinatari(S6, "sollecita_presenze_6h"), + await destinatari(S6, "sollecita_presenze_24h"), (await tuttiGiocatoriAttivi()).filter((g) => g !== G4), + "anche a 4 ore dall'inizio: il sollecito è uno solo, non uno per fascia", ); - assert.equal((await notifiche(S_FUORI)).length, 0, "tra 30 ore: fuori da ogni fascia"); - for (const [evento, altri] of [ - [S24, ["sollecita_presenze_12h", "sollecita_presenze_6h"]], - [S12, ["sollecita_presenze_24h", "sollecita_presenze_6h"]], - [S6, ["sollecita_presenze_24h", "sollecita_presenze_12h"]], - ] as const) { - for (const tipo of altri) { + assert.equal((await notifiche(S_FUORI)).length, 0, "tra 30 ore: non ancora"); + for (const evento of [S24, S12, S6]) { + for (const tipo of ["sollecita_presenze_12h", "sollecita_presenze_6h"]) { assert.equal((await notifiche(evento, tipo)).length, 0, `${evento} non ha ${tipo}`); } } @@ -615,8 +611,8 @@ if (!locale) { }>("giocatori_squadra?select=id,attivo,tipo"); for (const [id, tipo] of [ [S24, "sollecita_presenze_24h"], - [S12, "sollecita_presenze_12h"], - [S6, "sollecita_presenze_6h"], + [S12, "sollecita_presenze_24h"], + [S6, "sollecita_presenze_24h"], ] as const) { const evento = eventi.find((e) => e.id === id)!; const risposte = await leggi<{ giocatore_id: string; stato: string }>( @@ -656,41 +652,33 @@ if (!locale) { }); await prova( - "chi risponde smette di riceverli, chi resta su «forse» li riceve tutti e tre", + "chi resta su «forse» riceve il sollecito una sola volta, non a ogni fascia", async () => { await creaEvento(S_FORSE, "evento", tra(18), { convocati: [G4, G5, G8] }); await rispondi(S_FORSE, G5, "forse"); await esegui("genera_solleciti_presenze"); assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_24h"), [G4, G5, G8]); - await rispondi(S_FORSE, G4, "presente"); // risponde dopo il primo sollecito - await spostaEvento(S_FORSE, 9); // entra nella fascia delle 12 ore + await rispondi(S_FORSE, G4, "presente"); + await spostaEvento(S_FORSE, 9); await esegui("genera_solleciti_presenze"); - assert.deepEqual( - await destinatari(S_FORSE, "sollecita_presenze_12h"), - [G5, G8], - "g4 ha risposto: niente secondo sollecito; «forse» e nessuna risposta sì", - ); - - await rispondi(S_FORSE, G8, "assente"); await spostaEvento(S_FORSE, 4); await esegui("genera_solleciti_presenze"); - assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_6h"), [G5]); - assert.equal( - (await notifiche(S_FORSE)).filter((r) => r.giocatore_id === G5).length, - 3, - "g5, sempre «forse»: 24, 12 e 6 ore", + assert.deepEqual( + (await notifiche(S_FORSE)).map((r) => r.tipo), + ["sollecita_presenze_24h", "sollecita_presenze_24h", "sollecita_presenze_24h"], + "nessun secondo sollecito avvicinandosi all'evento, nemmeno a g5 («forse»)", ); }, ); - await prova("un evento creato tardi riceve solo il sollecito della sua fascia", async () => { + await prova("un evento creato tardi riceve comunque un solo sollecito", async () => { await creaEvento(S_TARDI, "evento", tra(5), { convocati: [G4] }); await esegui("genera_solleciti_presenze"); + await esegui("genera_solleciti_presenze"); assert.deepEqual( (await notifiche(S_TARDI)).map((r) => r.tipo), - ["sollecita_presenze_6h"], - "non tre solleciti insieme", + ["sollecita_presenze_24h"], ); });