4 Commits
Author SHA1 Message Date
davideandClaude Sonnet 5.5 3bf07049e9 Porta la versione a 1.4.0: «Presenza modificata» + copertura test e docs
Sezione 1.4.0 del CHANGELOG, versione in package.json e package-lock.json,
PROJECT_STATE aggiornato. Il test di presenza-modificata copre ora i confini
delle 6 ore, ogni stato, allenatore inattivo, admin senza slot, doppio ruolo e
ora dell'evento non valida. Documentazione delle notifiche allineata (riga 9,
copertura test, email, DATABASE). Nessuna nuova migration: la m32 e la m33 sono
applicate in produzione (09/10/2026).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 11:24:11 +02:00
davideandClaude Sonnet 5.5 986098d933 DD-050: «Presenza modificata» anche agli allenatori (m33)
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 <noreply@anthropic.com>
2026-10-09 11:06:46 +02:00
davideandClaude Sonnet 5.5 274fed93c5 Implementa DD-050: avviso «Presenza modificata», in prova solo per gli admin
Migration m32: tipo presenza_modificata, funzione e trigger su risposte_presenze
che avvisano gli admin attivi se una risposta cambia a 0-6 ore da una partita o
un allenamento (push, email e centro notifiche). Gli allenatori non la ricevono
finché dura la prova. Applicata in produzione il 09/10/2026.
Test di integrazione e unitario; DD-050, notifiche, DATABASE, CHANGELOG,
ROADMAP e PROJECT_STATE aggiornati (slot g18 ora giocatore).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 10:54:47 +02:00
davideandClaude Sonnet 5.5 f0e6a3a6cc Documenta DD-050: avviso ad admin e allenatori se una presenza cambia a meno di 6 ore dall'evento
Solo documentazione: nessun codice né migration. Decisione in valutazione.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 17:47:12 +02:00
17 changed files with 762 additions and 22 deletions

No files matched your search

+6 -7
View File
@@ -9,15 +9,15 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
## Stato generale ## Stato generale
- **Versione:** 1.3.1 (in produzione su `main` la 1.3.0, tag `v1.3.0`) — il Calendario mostra solo i prossimi - **Versione:** 1.4.0 (in produzione su `main` la 1.3.0, tag `v1.3.0`) — avviso «Presenza modificata» ad admin e
allenatori (DD-050, `m32`-`m33`). La 1.3.1: il Calendario mostra solo i prossimi
compleanni del mese; tolta la serie «Conferme 24h» con i badge «Risposta lampo» e «Mai un forfait». La 1.3.0 compleanni del mese; tolta la serie «Conferme 24h» con i badge «Risposta lampo» e «Mai un forfait». La 1.3.0
portava la notifica di compleanno, gli obiettivi cliccabili, le presenze sui convocati, gli avvisi in Home portava la notifica di compleanno, gli obiettivi cliccabili, le presenze sui convocati, gli avvisi in Home
ridisegnati e i caratteri più grandi. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md). ridisegnati e i caratteri più grandi. Dettaglio in [docs/CHANGELOG.md](docs/CHANGELOG.md).
- **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio - **Backend:** Supabase proprietario (`kfkcldwncxqaixetsjes`). Lovable Cloud e il vecchio
Project Ref `hetycilxgkdmccelwerq` non si usano più. Project Ref `hetycilxgkdmccelwerq` non si usano più.
- **Migration:** 42 file in `supabase/migrations/`, fino a `m31_notifica_compleanno`. Le prime 41 (fino a - **Migration:** 44 file in `supabase/migrations/`, fino a `m33_presenza_modificata_allenatori`. Tutte applicate in produzione (verificato con `npx supabase migration list` il
`m30_sollecito_solo_24h`) sono applicate in produzione (verificato con `npx supabase migration list` il 09/10/2026; la `m32` è l'avviso «Presenza modificata» ai soli admin, DD-050). La `m33`, che lo estende agli allenatori, è stata applicata il 09/10/2026. La `m29` toglie
01/10/2026); la `m31` no, ancora da applicare (DD-047). La `m29` toglie
`foto_path`: il codice che non la legge più (1.2.2) va rilasciato. `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 - **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 ([docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md)). Il codice con l'interruttore «Email» in Profilo
@@ -33,9 +33,8 @@ schema in [docs/DATABASE.md](docs/DATABASE.md), le procedure in
- Si lavora direttamente su `main` (DD-019); Cursor e Claude Code sono gli ambienti di sviluppo. - Si lavora direttamente su `main` (DD-019); Cursor e Claude Code sono gli ambienti di sviluppo.
- Dev e produzione condividono lo stesso progetto Supabase: un account di prova che collega uno - Dev e produzione condividono lo stesso progetto Supabase: un account di prova che collega uno
slot lo occupa anche in produzione e va liberato da un admin. slot lo occupa anche in produzione e va liberato da un admin.
- Lo slot `g18` «Beta Tester» è impostato come allenatore in produzione (24/09/2026). Numero 99 - Lo slot `g18` «Beta Tester» è un giocatore in produzione (dal 09/10/2026, prima era allenatore), numero 99 e
e ruolo «Palleggiatore» sono rimasti apposta per la 0.9.2: da svuotare da `/admin` se ancora ruolo «Palleggiatore»; non è collegato a nessun account. Serve per provare le notifiche con un secondo utente.
presenti.
## Note ## Note
+12
View File
@@ -13,6 +13,18 @@ versioni `0.y.z` erano pre-release.
## [Non rilasciato] ## [Non rilasciato]
## [1.4.0] - 2026-10-09
Gli admin e gli allenatori vengono avvisati quando una presenza cambia a ridosso di una partita o di un allenamento.
Due migration, `m32` e `m33`, entrambe applicate in produzione il 09/10/2026.
### Aggiunto
- **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` (aggiunge gli allenatori, applicata il 09/10/2026).
## [1.3.1] - 2026-10-06 ## [1.3.1] - 2026-10-06
Il Calendario mostra solo i prossimi compleanni del mese, e le serie di presenze sono due invece di tre: sparisce Il Calendario mostra solo i prossimi compleanni del mese, e le serie di presenze sono due invece di tre: sparisce
+4 -3
View File
@@ -122,11 +122,11 @@ votante e votato convocati all'evento (RLS, `evento_permette_voto()`, DD-027).
- Non c'è una notifica alla sola creazione di un evento: conta l'avvicinarsi della data. - Non c'è una notifica alla sola creazione di un evento: conta l'avvicinarsi della data.
- Destinatari di un evento (`giocatori_destinatari_evento()`): giocatori attivi, i convocati o tutta - Destinatari di un evento (`giocatori_destinatari_evento()`): giocatori attivi, i convocati o tutta
la rosa se `convocati` è vuoto; mai gli allenatori (DD-039). la rosa se `convocati` è vuoto; mai gli allenatori (DD-039).
- Il `CHECK` sul tipo ammette anche `compleanno` e `compleanno_auguri` (M31, DD-047), `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e - Il `CHECK` sul tipo ammette anche `presenza_modificata` (M32, DD-050), `compleanno` e `compleanno_auguri` (M31, DD-047), `turno_palloni_12h`, `_6h`, `_3h`, `turno_palloni_revocato` e
`sollecita_presenze_24h`, `_12h`, `_6h`, scritti dai job (DD-040). `turno_palloni` e `sollecita_presenze_24h`, `_12h`, `_6h`, scritti dai job (DD-040). `turno_palloni` e
`sollecita_presenze` restano i tipi dei pulsanti manuali. `sollecita_presenze` restano i tipi dei pulsanti manuali.
- Il trigger delle email salta tutti i tipi `turno_palloni*`; quello delle push accoda i tipi - Il trigger delle email salta tutti i tipi `turno_palloni*`; quello delle push accoda i tipi
automatici (compleanni compresi) ma non quelli dei pulsanti. automatici (compleanni e `presenza_modificata` compresi) ma non quelli dei pulsanti. `presenza_modificata` ha `evento_id` NULL (come `admin`): la UNIQUE non si applica e ogni modifica resta una riga (M32, DD-050).
### Code di invio (`notifiche_email_coda`, `notifiche_push_coda`) ### Code di invio (`notifiche_email_coda`, `notifiche_push_coda`)
@@ -153,12 +153,13 @@ riservate alla service role (anon e utenti autenticati non possono chiamarle). T
[modules/notifiche.md](modules/notifiche.md); le decisioni in DD-037, DD-038 e DD-040. [modules/notifiche.md](modules/notifiche.md); le decisioni in DD-037, DD-038 e DD-040.
| Funzione (migration) | Cosa fa | Job `pg_cron` | | 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_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_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_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_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) | | `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. | — | | `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. | — | | `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). | — | | `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). | — |
+2
View File
@@ -49,6 +49,8 @@ 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, - [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) 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
admin e allenatori ([specifica](modules/notifiche.md#catalogo-delle-notifiche), DD-050)
## Prossimo ## Prossimo
+73
View File
@@ -0,0 +1,73 @@
# DD-050 — Avviso a admin e allenatori se una presenza cambia a meno di 6 ore dall'evento
**Data:** 8 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)
se ne accorge solo riaprendo l'evento: un «assente» detto due ore prima di una partita può far saltare la
formazione o il numero minimo per l'allenamento. Oggi nessuna notifica avvisa chi gestisce gli eventi di una
risposta (`notifiche.md`, «Cosa non genera nessuna notifica»).
**Decisione**
1. **Cosa fa scattare l'avviso.** Una riga di `risposte_presenze` che cambia — nuova, modificata o ritirata —
quando mancano **da 0 a 6 ore** all'inizio di una **partita o un allenamento** (non agli altri tipi di
evento). Scatta per **qualsiasi
cambio di stato**, da qualunque valore a qualunque altro (presente, assente, forse, in ritardo,
infortunato) e anche per la prima risposta e per il ritiro; non scatta solo se lo stato resta quello di
prima (nessun cambio reale). Vale per qualunque giocatore, anche se la
modifica la fa un admin per conto suo.
2. **Chi la riceve:** gli **admin e gli allenatori attivi** (chi ha il ruolo `admin` o `allenatore` in
`user_roles` e uno slot attivo in `giocatori_squadra`), **tranne la persona a cui appartiene la presenza**
(se un admin cambia la propria, non avvisa se stesso). Chi ha il ruolo ma nessuno slot collegato non può
ricevere notifiche, perché `notifiche_utente` è per slot.
3. **Come funziona.** Un trigger `AFTER INSERT OR UPDATE OF stato OR DELETE` su `risposte_presenze` chiama una
funzione `SECURITY DEFINER` (`avvisa_presenza_modificata()`, migration `m32`). La funzione:
1. ignora il caso in cui lo stato prima e dopo è lo stesso;
2. legge l'evento e continua solo se `tipo` è `partita` o `allenamento` e `inizio_evento(data, ora) - now()`
sta tra 0 e 6 ore (stessa `inizio_evento()` di M26: fuso `Europe/Rome`, ora illeggibile = nessun avviso);
3. inserisce una riga in `notifiche_utente` per ogni destinatario, con il tipo nuovo `presenza_modificata`.
Dal database in poi il resto è già fatto: il trigger di M22/M26 accoda l'**email**, il trigger delle push
(esteso con il tipo nuovo) accoda la **push**, la riga è il **centro notifiche in-app** (DD-038). Nessuna
route dell'app e nessun codice lato client oltre al nuovo valore di `TipoNotifica`.
4. **Testo.** Titolo `Presenza modificata: <Nome Cognome>`; corpo su righe: `Evento: <titolo>`,
`Data: <GG/MM/AAAA>, ore <ora>`, `Da: <stato prima>`, `A: <stato dopo>`. Gli stati sono «presente»,
«assente», «forse», «in ritardo», «infortunato», «nessuna risposta» (prima di una risposta nuova, o dopo
un ritiro). Non c'è un link all'evento: il pulsante «Apri CrAPP» della mail e il tocco sulla push aprono l'app.
5. **Una notifica per modifica, senza raggruppare.** `evento_id` resta NULL (come i messaggi `admin`), così la
`UNIQUE (giocatore_id, evento_id, tipo)` non si applica e due modifiche dello stesso evento non si
sovrascrivono. Contro: se la stessa persona cambia idea cinque volte, ognuno dei destinatari riceve cinque
notifiche e cinque email.
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 (conclusa)**
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**
- **Una route dell'app invocata dal client dopo il salvataggio** (come `sollecita-presenze`) → il client
scrive su `risposte_presenze` direttamente con RLS: una route in più sarebbe aggirabile e dipenderebbe dal
fatto che l'app sia ancora aperta al momento del salvataggio. Il trigger copre ogni scrittura.
- **Raggruppare in una sola notifica per evento** (upsert con `evento_id`) → perde il dettaglio di chi ha
cambiato cosa, e l'upsert non rifa partire la push (il trigger delle push è solo `INSERT`).
- **Notificare anche chi ha il ruolo ma è il giocatore stesso** → inutile: sa già cosa ha fatto.
**Conseguenze**
- 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.
- 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.
**Riesame**
Dopo qualche settimana d'uso: se arrivano troppe notifiche per lo stesso giocatore, raggruppare; se 6 ore
sono troppe o poche, spostare la soglia (una costante nella funzione).
+2 -1
View File
@@ -47,7 +47,7 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
**Accettate** **Accettate**
| ID | Titolo | | ID | Titolo |
| ------------------- | ------------------------------------------- | | ------------------- | --------------------------------------------------------- |
| [DD-001](DD-001.md) | Indipendenza da Lovable | | [DD-001](DD-001.md) | Indipendenza da Lovable |
| [DD-002](DD-002.md) | Sviluppo document-first | | [DD-002](DD-002.md) | Sviluppo document-first |
| [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive | | [DD-004](DD-004.md) | Ogni versione aggiunge, non riscrive |
@@ -86,6 +86,7 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
| [DD-038](DD-038.md) | Tre canali per ogni notifica | | [DD-038](DD-038.md) | Tre canali per ogni notifica |
| [DD-039](DD-039.md) | Niente promemoria all'allenatore | | [DD-039](DD-039.md) | Niente promemoria all'allenatore |
| [DD-040](DD-040.md) | Palloni e solleciti automatici | | [DD-040](DD-040.md) | Palloni e solleciti automatici |
| [DD-050](DD-050.md) | Avviso se una presenza cambia a meno di 6 ore dall'evento |
| [DD-049](DD-049.md) | Caratteri più grandi in tutta l'app | | [DD-049](DD-049.md) | Caratteri più grandi in tutta l'app |
| [DD-048](DD-048.md) | Via la tessera dalla dashboard | | [DD-048](DD-048.md) | Via la tessera dalla dashboard |
| [DD-047](DD-047.md) | Notifica di compleanno | | [DD-047](DD-047.md) | Notifica di compleanno |
+1
View File
@@ -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. | | 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. | | 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. | | 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. | | 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 Le email arrivano solo se il suo slot ha un account collegato e un'email registrata e se non ha
+1 -1
View File
@@ -49,7 +49,7 @@ App su Vercel ──scrive──▶ notifiche_utente ──trigger──▶ noti
## Quali notifiche e a chi ## Quali notifiche e a chi
**Ogni riga nuova di `notifiche_utente` genera una mail**: messaggio admin, sollecito presenze, sondaggio pre-partita, compleanno (DD-047: solo il titolo, senza corpo; la mail ha oggetto, link e avviso di disattivazione). **Fa eccezione il turno palloni** (DD-040, dalla **Ogni riga nuova di `notifiche_utente` genera una mail**: messaggio admin, sollecito presenze, sondaggio pre-partita, compleanno (DD-047: solo il titolo, senza corpo; la mail ha oggetto, link e avviso di disattivazione), presenza modificata (DD-050, ad admin e allenatori). **Fa eccezione il turno palloni** (DD-040, dalla
`m26`): per i tipi `turno_palloni`, `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato` il `m26`): per i tipi `turno_palloni`, `turno_palloni_12h`, `_6h`, `_3h` e `turno_palloni_revocato` il
trigger non accoda nulla, restano push e centro notifiche. Un tipo nuovo che in futuro finisse in quella tabella trigger non accoda nulla, restano push e centro notifiche. Un tipo nuovo che in futuro finisse in quella tabella
viaggerebbe per email senza altro lavoro. Le notifiche già presenti quando si applica la migration viaggerebbe per email senza altro lavoro. Le notifiche già presenti quando si applica la migration
+7 -4
View File
@@ -44,10 +44,11 @@ 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ì | | 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 | | 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ì | | 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` 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 Le notifiche generate dal server (righe 1-6, 8 e 9) 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 differenza sta in come parte la push: quelle dei pulsanti (3 e 6) la mandano le route dell'app, quelle
automatiche (4, 5 e 8; la 1 e la 2 non esistono più) nascono nel database e la manda il worker `mailer/` (vedi «Push dei automatiche (4, 5, 8 e 9; la 1 e la 2 non esistono più) nascono nel database e la manda il worker `mailer/` (vedi «Push dei
promemoria»). Il sondaggio riscrive la notifica esistente se l'admin preme di nuovo il pulsante per promemoria»). Il sondaggio riscrive la notifica esistente se l'admin preme di nuovo il pulsante per
lo stesso evento (vedi «Centro notifiche in-app»). lo stesso evento (vedi «Centro notifiche in-app»).
@@ -75,6 +76,7 @@ disattivazione). Tra `<…>` i valori che cambiano a ogni invio; le date sono `G
| 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | `<partita> · ore <ora>. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` | | 6 | Sondaggio pre-partita | `💩 Sondaggio pre-partita aperto` | `<partita> · ore <ora>. Quante cacche hai fatto? Rispondi prima del fischio d'inizio.` |
| 8a | Compleanno, auguri al festeggiato | `Buon compleanno, <Nome>! 🎂` | Nessuno: solo il titolo | | 8a | Compleanno, auguri al festeggiato | `Buon compleanno, <Nome>! 🎂` | Nessuno: solo il titolo |
| 8b | Compleanno, avviso agli altri | `Oggi è il compleanno di <Nome>` — con più festeggiati `<A> e <B>`, o `<A>, <B> e <C>`, in ordine alfabetico per nome | Nessuno: solo il titolo. L'età non compare né negli auguri né nell'avviso | | 8b | Compleanno, avviso agli altri | `Oggi è il compleanno di <Nome>` — con più festeggiati `<A> e <B>`, o `<A>, <B> e <C>`, in ordine alfabetico per nome | Nessuno: solo il titolo. L'età non compare né negli auguri né nell'avviso |
| 9 | Presenza modificata (DD-050) | `Presenza modificata: <Nome Cognome>` | `Evento: <titolo>`<br>`Data: <GG/MM/AAAA>, ore <ora>`<br>`Da: <stato prima>`<br>`A: <stato dopo>` — stati: presente, assente, forse, in ritardo, infortunato, nessuna risposta |
Le righe 4a-4c e 5 (e le 1-2, finché esistevano) nascono nelle funzioni SQL della migration `m26` Le righe 4a-4c e 5 (e le 1-2, finché esistevano) nascono nelle funzioni SQL della migration `m26`
(`genera_promemoria_eventi`, `genera_avvisi_palloni_fascia`, `genera_revoche_palloni`, (`genera_promemoria_eventi`, `genera_avvisi_palloni_fascia`, `genera_revoche_palloni`,
@@ -102,7 +104,7 @@ dall'emoji. Non entrano nel centro notifiche e non generano email.
- Creare, spostare, modificare o cancellare un evento. Dalla `m28` (DD-043) non parte nemmeno più un - Creare, spostare, modificare o cancellare un evento. Dalla `m28` (DD-043) non parte nemmeno più un
promemoria prima dell'inizio: restano solo il turno palloni e il sollecito presenze. promemoria prima dell'inizio: restano solo il turno palloni e il sollecito presenze.
- L'arrivo di un risultato, di una pagella o di un voto MVP. - L'arrivo di un risultato, di una pagella o di un voto MVP.
- Un giocatore che risponde a un evento: nessun avviso a chi gestisce gli eventi. - Un giocatore che risponde a un evento: nessun avviso a chi gestisce gli eventi, **salvo** quando la risposta cambia a meno di 6 ore da una partita o un allenamento (riga 9, DD-050).
- Assenze e infortuni. - Assenze e infortuni.
- La registrazione o il collegamento di un nuovo giocatore. - La registrazione o il collegamento di un nuovo giocatore.
- Il certificato in scadenza: resta una card in Home (DD-035). - Il certificato in scadenza: resta una card in Home (DD-035).
@@ -159,7 +161,7 @@ descrivono come funzionavano fino alla `m28`: la funzione esiste ancora ma nessu
### Test che verificano il catalogo ### Test che verificano il catalogo
| Cosa | Test | | 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) | | 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` | | 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` | | Turno palloni: incaricato e chi li aveva prima, solo allenamenti e partite, riservato agli admin | `destinatari-notifiche.test.ts`, `unit/palloni-core.test.ts` |
@@ -167,6 +169,7 @@ descrivono come funzionavano fino alla `m28`: la funzione esiste ancora ma nessu
| 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` | | 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` | | 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` |
| Compleanno: ora (anche con l'ora legale), festeggiato e altri, più compleanni lo stesso giorno, una sola volta anche se eliminata, push ed email in coda, 29 febbraio, job pianificato (DD-047, `m31`) | `integration/notifiche-compleanno.test.ts` | | Compleanno: ora (anche con l'ora legale), festeggiato e altri, più compleanni lo stesso giorno, una sola volta anche se eliminata, push ed email in coda, 29 febbraio, job pianificato (DD-047, `m31`) | `integration/notifiche-compleanno.test.ts` |
| Presenza modificata: finestra di 0-6 ore, solo partite e allenamenti, admin e allenatori attivi (non gli inattivi né chi non ha slot, una sola notifica a chi ha due ruoli), mai il giocatore stesso, tutti gli stati, nessun avviso se lo stato non cambia o l'evento è cancellato, push ed email in coda (DD-050, `m32`-`m33`) | `integration/presenza-modificata.test.ts`, `unit/notifiche-utente.test.ts` (tipo) |
| Worker: esiti, backoff, tetto giornaliero, iscrizioni scadute | `unit/mailer-core.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`) | | L'allenatore è fuori dalla rosa di gioco | `unit/giocatori-squadra.test.ts` (`inRosa`) |
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "crapp", "name": "crapp",
"version": "1.3.1", "version": "1.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "crapp", "name": "crapp",
"version": "1.3.1", "version": "1.4.0",
"dependencies": { "dependencies": {
"@supabase/supabase-js": "^2.111.0", "@supabase/supabase-js": "^2.111.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "crapp", "name": "crapp",
"version": "1.3.1", "version": "1.4.0",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"type": "module", "type": "module",
+2 -1
View File
@@ -26,7 +26,8 @@ export type TipoNotifica =
| "sollecita_presenze_6h" | "sollecita_presenze_6h"
| "sondaggio_cacche" | "sondaggio_cacche"
| "compleanno" | "compleanno"
| "compleanno_auguri"; | "compleanno_auguri"
| "presenza_modificata";
export type NotificaUtente = { export type NotificaUtente = {
id: string; id: string;
@@ -0,0 +1,140 @@
-- M32 — Avviso se una presenza cambia a meno di 6 ore dall'evento (DD-050)
--
-- Quando una riga di `risposte_presenze` cambia (nuova, modificata o ritirata) e mancano da 0 a 6 ore
-- all'inizio di una partita o di un allenamento, ai destinatari arriva una notifica «Presenza modificata».
--
-- FASE DI PROVA: i destinatari sono solo gli admin (DD-050, «Fase di prova»). Per aprirla agli allenatori
-- basta aggiungere 'allenatore' alla lista dei ruoli qui sotto, con una nuova migration.
--
-- Dal database in poi il resto è già fatto: il trigger di M22/M26 accoda l'email, quello delle push
-- (esteso qui con il tipo nuovo) accoda la push, la riga è il centro notifiche in-app (DD-038).
-- --- Tipo 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',
'presenza_modificata'
)
);
-- --- Funzione e trigger ------------------------------------------------------------------------
--
-- `evento_id` resta NULL (come i messaggi `admin`): la UNIQUE (giocatore_id, evento_id, tipo) non si
-- applica e due modifiche dello stesso evento restano due notifiche distinte.
--
-- Cancellare un evento elimina le risposte con un trigger AFTER DELETE su `eventi_app` (M14): quando
-- scatta questo, l'evento non esiste più, quindi non parte nessun avviso.
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')
);
RETURN NULL;
END;
$$;
REVOKE ALL ON FUNCTION public.avvisa_presenza_modificata() FROM PUBLIC, anon, authenticated;
CREATE OR REPLACE FUNCTION public.nome_stato_presenza(p_stato text)
RETURNS text
LANGUAGE sql IMMUTABLE AS $$
SELECT CASE WHEN p_stato = 'ritardo' THEN 'in ritardo' ELSE coalesce(p_stato, 'nessuna risposta') END;
$$;
CREATE TRIGGER risposte_presenze_avvisa_modifica
AFTER INSERT OR UPDATE OF stato OR DELETE ON public.risposte_presenze
FOR EACH ROW
EXECUTE FUNCTION public.avvisa_presenza_modificata();
-- --- Push: anche la presenza modificata --------------------------------------------------------
--
-- Stesso trigger di M31 con il tipo nuovo. L'email passa già dal trigger di M22/M26.
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',
'presenza_modificata'
))
EXECUTE FUNCTION public.accoda_push_promemoria();
@@ -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;
$$;
+2 -2
View File
@@ -25,9 +25,9 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
## Struttura ## Struttura
| Cartella | Cosa verifica | Serve rete? | | 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 | | `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), 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ì | | `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ì | | `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 | — | | `helpers/` | Avvio del server di test e mini-harness condiviso | — |
@@ -0,0 +1,440 @@
/**
* 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 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;
* - ogni modifica è una notifica a sé (nessun raggruppamento) e ha la coda email e la coda push.
*
* Gira solo sullo stack locale (`npx supabase start`). Usa gli slot `g4` (chi risponde), `g5` (admin),
* `g6` (giocatore), `g9` (allenatore inattivo) e `g10` (allenatore) della rosa seed, ripristinati alla fine.
*/
import assert from "node:assert/strict";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("presenza modificata", "stack locale non attivo (npx supabase start)");
riepilogo("presenza-modificata");
} else {
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
console.log(`presenza modificata su ${URL_BASE}`);
const PASSWORD = "prova-presenza-modificata-123";
const RISPONDE = "g4";
const ADMIN = "g5";
const GIOCATORE = "g6";
const ALLENATORE = "g10";
const INATTIVO = "g9";
const PREFISSO = "test-presenza-mod-";
const rest = (percorso: string, token: string, init?: RequestInit) =>
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
...init,
headers: {
apikey: token === SERVIZIO ? SERVIZIO : ANON,
Authorization: `Bearer ${token}`,
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
async function leggi<T>(percorso: string): Promise<T[]> {
const res = await rest(percorso, SERVIZIO);
if (!res.ok) throw new Error(`select su ${percorso}: ${res.status} ${await res.text()}`);
return (await res.json()) as T[];
}
async function scrivi(percorso: string, metodo: string, corpo?: unknown, token = SERVIZIO) {
const res = await rest(percorso, token, {
method: metodo,
body: corpo === undefined ? null : JSON.stringify(corpo),
});
if (!res.ok) throw new Error(`${metodo} su ${percorso}: ${res.status} ${await res.text()}`);
}
async function creaUtente(email: string): Promise<{ id: string; token: string }> {
const res = await fetch(`${URL_BASE}/auth/v1/admin/users`, {
method: "POST",
headers: {
apikey: SERVIZIO,
Authorization: `Bearer ${SERVIZIO}`,
"content-type": "application/json",
},
body: JSON.stringify({ email, password: PASSWORD, email_confirm: true }),
});
const corpo = (await res.json()) as { id?: string };
if (!corpo.id) throw new Error(`creazione utente fallita: ${JSON.stringify(corpo)}`);
const login = await fetch(`${URL_BASE}/auth/v1/token?grant_type=password`, {
method: "POST",
headers: { apikey: ANON, "content-type": "application/json" },
body: JSON.stringify({ email, password: PASSWORD }),
});
const t = (await login.json()) as { access_token?: string };
if (!t.access_token) throw new Error("accesso fallito");
return { id: corpo.id, token: t.access_token };
}
/** Data (AAAA-MM-GG) e ora (HH:MM) a Roma, tra `ore` ore da adesso. */
function traOre(ore: number): { data: string; ora: string } {
const parti: Record<string, string> = Object.fromEntries(
new Intl.DateTimeFormat("en-GB", {
timeZone: "Europe/Rome",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
})
.formatToParts(new Date(Date.now() + ore * 3_600_000))
.map((p) => [p.type, p.value]),
);
return {
data: `${parti["year"]}-${parti["month"]}-${parti["day"]}`,
ora: `${parti["hour"]}:${parti["minute"]}`,
};
}
type Riga = { id: string; giocatore_id: string; titolo: string; corpo: string };
const notifiche = () =>
leggi<Riga>(
"notifiche_utente?tipo=eq.presenza_modificata&select=id,giocatore_id,titolo,corpo&order=creato_il",
);
const svuota = async () => {
await rest("notifiche_utente?tipo=eq.presenza_modificata", SERVIZIO, { method: "DELETE" });
};
const risposta = (evento: string, stato: string) =>
scrivi("risposte_presenze?on_conflict=evento_id,giocatore_id", "POST", {
evento_id: evento,
giocatore_id: RISPONDE,
stato,
});
const aggiorna = (evento: string, stato: string, giocatore = RISPONDE) =>
scrivi(`risposte_presenze?evento_id=eq.${evento}&giocatore_id=eq.${giocatore}`, "PATCH", {
stato,
});
const ritira = (evento: string) =>
scrivi(`risposte_presenze?evento_id=eq.${evento}&giocatore_id=eq.${RISPONDE}`, "DELETE");
async function creaEvento(id: string, tipo: string, ore: number, titolo = `Prova ${id}`) {
const { data, ora } = traOre(ore);
await scrivi("eventi_app", "POST", { id, tipo, titolo, data, ora });
return { data, ora, titolo };
}
let tokenAdmin = "";
const idUtenti: string[] = [];
const idEventi: string[] = [];
const originali = new Map<string, Record<string, unknown>>();
try {
const admin = await creaUtente(`${PREFISSO}admin-${Date.now()}@example.test`);
const allenatore = await creaUtente(`${PREFISSO}allenatore-${Date.now()}@example.test`);
const inattivo = await creaUtente(`${PREFISSO}inattivo-${Date.now()}@example.test`);
const senzaSlot = await creaUtente(`${PREFISSO}senza-slot-${Date.now()}@example.test`);
idUtenti.push(admin.id, allenatore.id, inattivo.id, senzaSlot.id);
await scrivi("user_roles", "POST", { user_id: admin.id, role: "admin" });
tokenAdmin = admin.token;
for (const id of [RISPONDE, ADMIN, GIOCATORE, ALLENATORE, INATTIVO]) {
const [riga] = await leggi<Record<string, unknown>>(
`giocatori_squadra?id=eq.${id}&select=tipo,attivo,auth_user_id`,
);
originali.set(id, riga ?? { tipo: "giocatore", attivo: true, auth_user_id: null });
}
// Il trigger di `giocatori_squadra` rifiuta la service key: serve il JWT dell'admin.
await scrivi(
`giocatori_squadra?id=eq.${ADMIN}`,
"PATCH",
{ auth_user_id: admin.id },
tokenAdmin,
);
await scrivi(
`giocatori_squadra?id=eq.${ALLENATORE}`,
"PATCH",
{ tipo: "allenatore", auth_user_id: allenatore.id },
tokenAdmin,
);
// Un allenatore con slot disattivato, un admin senza nessuno slot e un admin che ha anche il
// ruolo allenatore: nessuno dei primi due può ricevere, il terzo riceve una sola notifica.
await scrivi(
`giocatori_squadra?id=eq.${INATTIVO}`,
"PATCH",
{ tipo: "allenatore", auth_user_id: inattivo.id, attivo: false },
tokenAdmin,
);
await scrivi("user_roles", "POST", { user_id: senzaSlot.id, role: "admin" });
await scrivi("user_roles", "POST", { user_id: admin.id, role: "allenatore" });
await svuota();
const [chi] = await leggi<{ nome: string; cognome: string }>(
`giocatori_squadra?id=eq.${RISPONDE}&select=nome,cognome`,
);
const nomeCompleto = `${chi!.nome} ${chi!.cognome}`.trim();
// 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=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",
);
const idAdmin = new Set(ruoli.map((r) => r.user_id));
return slot
.filter((s) => idAdmin.has(s.auth_user_id) && s.id !== RISPONDE)
.map((s) => s.id)
.sort();
};
// --- Partita a 3 ore ---------------------------------------------------------------------
const ev = await creaEvento(`${PREFISSO}partita`, "partita", 3, "Partita di prova");
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 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("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), "l'allenatore attivo è tra i destinatari");
assert.ok(!destinatari.includes(RISPONDE), "chi risponde non avvisa se stesso");
});
await prova("la riga ha la coda email e la coda push", async () => {
const [mia] = (await notifiche()).filter((r) => r.giocatore_id === ADMIN);
const email = await leggi<{ notifica_id: string }>(
`notifiche_email_coda?notifica_id=eq.${mia!.id}&select=notifica_id`,
);
assert.equal(email.length, 1, "email in coda");
const push = await leggi<{ notifica_id: string }>(
`notifiche_push_coda?notifica_id=eq.${mia!.id}&select=notifica_id`,
);
assert.equal(push.length, 1, "push in coda");
});
await prova("modifica: ogni cambio è una notifica a sé, con Da e A giusti", async () => {
const prima = (await notifiche()).length;
await aggiorna(`${PREFISSO}partita`, "ritardo");
const righe = await notifiche();
assert.equal(righe.length, prima + (await attesi()).length);
const ultima = righe.filter((r) => r.giocatore_id === ADMIN).at(-1)!;
assert.match(ultima.corpo, /\nDa: presente\nA: in ritardo$/);
});
await prova("stesso stato di prima: nessun avviso", async () => {
const prima = (await notifiche()).length;
await aggiorna(`${PREFISSO}partita`, "ritardo");
assert.equal((await notifiche()).length, prima);
});
await prova("ritiro della risposta: «nessuna risposta» come stato finale", async () => {
await ritira(`${PREFISSO}partita`);
const ultima = (await notifiche()).filter((r) => r.giocatore_id === ADMIN).at(-1)!;
assert.match(ultima.corpo, /\nDa: in ritardo\nA: nessuna risposta$/);
});
await prova("la presenza dell'admin non avvisa l'admin stesso", async () => {
await svuota();
await scrivi("risposte_presenze", "POST", {
evento_id: `${PREFISSO}partita`,
giocatore_id: ADMIN,
stato: "assente",
});
const destinatari = (await notifiche()).map((r) => r.giocatore_id);
assert.ok(!destinatari.includes(ADMIN));
await scrivi(
`risposte_presenze?evento_id=eq.${PREFISSO}partita&giocatore_id=eq.${ADMIN}`,
"DELETE",
);
});
// --- Casi in cui non deve partire nulla --------------------------------------------------
await prova("allenamento a 5 ore: avvisa", async () => {
await svuota();
await creaEvento(`${PREFISSO}allenamento`, "allenamento", 5);
idEventi.push(`${PREFISSO}allenamento`);
await risposta(`${PREFISSO}allenamento`, "assente");
assert.equal((await notifiche()).length, (await attesi()).length);
});
for (const [nome, tipo, ore] of [
["oltre le 6 ore", "partita", 7],
["già iniziato", "partita", -1],
["un tipo che non è partita né allenamento", "evento", 3],
] as const) {
await prova(`nessun avviso: ${nome}`, async () => {
await svuota();
const id = `${PREFISSO}no-${idEventi.length}`;
await creaEvento(id, tipo, ore);
idEventi.push(id);
await risposta(id, "presente");
assert.equal((await notifiche()).length, 0);
});
}
await prova("cancellare l'evento elimina le risposte senza avvisare nessuno", async () => {
await svuota();
const id = `${PREFISSO}cancellato`;
await creaEvento(id, "partita", 2);
idEventi.push(id);
await risposta(id, "presente");
await svuota();
await scrivi(`eventi_app?id=eq.${id}`, "DELETE");
assert.equal((await notifiche()).length, 0);
});
await prova(
"destinatari: né inattivi né senza slot, e una sola notifica a chi ha due ruoli",
async () => {
await svuota();
const id = `${PREFISSO}destinatari`;
await creaEvento(id, "partita", 4);
idEventi.push(id);
await risposta(id, "presente");
const destinatari = (await notifiche()).map((r) => r.giocatore_id);
assert.ok(!destinatari.includes(INATTIVO), "slot disattivato");
assert.deepEqual([...destinatari].sort(), await attesi());
assert.equal(
destinatari.filter((d) => d === ADMIN).length,
1,
"chi ha sia il ruolo admin sia allenatore riceve una sola notifica",
);
},
);
await prova(
"cambio di un giocatore: avvisati admin e allenatore, mai il giocatore stesso",
async () => {
await svuota();
const id = `${PREFISSO}per-conto`;
await creaEvento(id, "partita", 2);
idEventi.push(id);
await risposta(id, "presente");
await aggiorna(id, "assente");
const destinatari = new Set((await notifiche()).map((r) => r.giocatore_id));
assert.ok(destinatari.has(ALLENATORE) && destinatari.has(ADMIN));
assert.ok(!destinatari.has(RISPONDE));
},
);
await prova("tutti gli stati hanno il loro nome nel testo", async () => {
await svuota();
const id = `${PREFISSO}stati`;
await creaEvento(id, "allenamento", 1);
idEventi.push(id);
const sequenza = ["presente", "assente", "forse", "ritardo", "infortunato"];
await risposta(id, sequenza[0]!);
for (const stato of sequenza.slice(1)) await aggiorna(id, stato);
const corpi = (await notifiche())
.filter((r) => r.giocatore_id === ADMIN)
.map((r) => r.corpo.split("\n").slice(2).join("|"));
assert.deepEqual(corpi, [
"Da: nessuna risposta|A: presente",
"Da: presente|A: assente",
"Da: assente|A: forse",
"Da: forse|A: in ritardo",
"Da: in ritardo|A: infortunato",
]);
});
// Confini della finestra: la soglia è di 6 ore esatte (da 0 a 6).
for (const [nome, ore, avvisa] of [
["a 5 ore e 50 minuti", 5 + 50 / 60, true],
["a 6 ore e 10 minuti", 6 + 10 / 60, false],
["a 10 minuti", 10 / 60, true],
["iniziato da 10 minuti", -10 / 60, false],
] as const) {
await prova(`finestra: evento ${nome} ${avvisa ? "avvisa" : "non avvisa"}`, async () => {
await svuota();
const id = `${PREFISSO}soglia-${idEventi.length}`;
await creaEvento(id, "partita", ore);
idEventi.push(id);
await risposta(id, "presente");
assert.equal((await notifiche()).length > 0, avvisa);
});
}
await prova("ora dell'evento illeggibile: nessun avviso e nessun errore", async () => {
await svuota();
const id = `${PREFISSO}ora-illeggibile`;
const { data } = traOre(3);
await scrivi("eventi_app", "POST", {
id,
tipo: "partita",
titolo: "Ora strana",
data,
ora: "boh",
});
idEventi.push(id);
await risposta(id, "presente");
assert.equal((await notifiche()).length, 0);
});
await prova("la funzione non è chiamabile dai client", async () => {
for (const token of [ANON, admin.token]) {
const res = await fetch(`${URL_BASE}/rest/v1/rpc/avvisa_presenza_modificata`, {
method: "POST",
headers: {
apikey: ANON,
Authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: "{}",
});
assert.ok(!res.ok, `risposta ${res.status}: un client non deve poterla eseguire`);
}
});
} finally {
await svuota();
for (const id of idEventi) {
await rest(`risposte_presenze?evento_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
await rest(`eventi_app?id=eq.${id}`, SERVIZIO, { method: "DELETE" });
}
if (tokenAdmin) {
for (const [id, valori] of originali) {
await rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
method: "PATCH",
body: JSON.stringify(valori),
});
}
}
for (const id of idUtenti) {
await rest(`user_roles?user_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
await fetch(`${URL_BASE}/auth/v1/admin/users/${id}`, {
method: "DELETE",
headers: { apikey: SERVIZIO, Authorization: `Bearer ${SERVIZIO}` },
});
}
riepilogo("presenza-modificata");
}
}
+1
View File
@@ -61,6 +61,7 @@ const tipiNuovi: TipoNotifica[] = [
"sollecita_presenze_6h", "sollecita_presenze_6h",
"compleanno", "compleanno",
"compleanno_auguri", "compleanno_auguri",
"presenza_modificata",
]; ];
const corpoMultiriga = "Data: 01/10/2026\nOra: 21:00\nLuogo: PalaCRAP"; const corpoMultiriga = "Data: 01/10/2026\nOra: 21:00\nLuogo: PalaCRAP";
for (const tipo of tipiNuovi) { for (const tipo of tipiNuovi) {