L'allenatore non riceve i promemoria degli eventi; test dei destinatari per ruolo (DD-039)
m25: giocatori_destinatari_evento() include solo i giocatori attivi, quindi l'allenatore non riceve più i promemoria a 24 e 3 ore, per nessun tipo di evento (fino alla m24 li riceveva sempre, DD-034). Riceve ancora i messaggi dello staff. Test: nuovo destinatari-notifiche.test.ts (14 controlli sulle route vere con un servizio push finto: promemoria, messaggio dello staff, sondaggio, sollecito, turno palloni, permessi di allenatore e giocatore) e aggiornato permessi-allenatore. Documentazione: DD-039, tabella delle notifiche dell'allenatore, catalogo e tabella dei test che lo verificano. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9013b703b
commit
4760e8650c
11 files changed
+665
-69
No files matched your search
+7
-3
@@ -24,10 +24,10 @@ da cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16).
|
||||
- Cursor e Claude Code come ambienti di sviluppo
|
||||
- Vercel configurato; Environment Variables aggiornate al nuovo Supabase (Preview e Production)
|
||||
- Supabase proprietario attivo — Project Ref: `kfkcldwncxqaixetsjes`
|
||||
- 35 migration in `supabase/migrations/`, fino a `m24_push_promemoria_e_sondaggio` (29/09/2026).
|
||||
- 36 migration in `supabase/migrations/`, fino a `m25_promemoria_solo_giocatori` (29/09/2026).
|
||||
Le prime 33 (fino a `m22_notifiche_email`) sono applicate in produzione, verificato con
|
||||
`npx supabase migration list` il 29/09/2026; **`m23` e `m24` sono verificate solo in locale e non
|
||||
ancora applicate in produzione**
|
||||
`npx supabase migration list` il 29/09/2026; **`m23`, `m24` e `m25` sono verificate solo in locale
|
||||
e non ancora applicate in produzione**
|
||||
- Worker delle notifiche email (`mailer/`) in funzione su un Raspberry Pi con Docker dal
|
||||
29/09/2026: invia via Gmail le notifiche di `notifiche_utente`. Installazione e gestione in
|
||||
[docs/WORKER_EMAIL.md](docs/WORKER_EMAIL.md); il codice con l'interruttore «Email» in Profilo è
|
||||
@@ -61,6 +61,10 @@ da cancellazioni precedenti a M14 sono state bonificate una tantum (M15/M16).
|
||||
- Migration `m19_backfill_nascita_da_profili_esistenti` (15/09/2026, DD-031): completa il
|
||||
backfill di M18 per tutti i profili già esistenti — **in produzione** (risulta applicata
|
||||
in `npx supabase migration list` del 24/09/2026)
|
||||
- Migration `m25_promemoria_solo_giocatori` (29/09/2026, DD-039): `giocatori_destinatari_evento()`
|
||||
include solo i giocatori attivi, quindi l'allenatore non riceve più i promemoria degli eventi
|
||||
(li riceveva sempre dalla `m21`). Cambia solo quella funzione; verificata in locale con
|
||||
`npx supabase db reset` e i test di integrazione, **non ancora in produzione**
|
||||
- Migration `m24_push_promemoria_e_sondaggio` (29/09/2026, DD-038): tabella `notifiche_push_coda`
|
||||
con trigger e due funzioni per il worker (la push dei promemoria a 24 e 3 ore), e nuovo tipo
|
||||
`sondaggio_cacche` in `notifiche_utente` (il sondaggio pre-partita compare in-app e per email).
|
||||
|
||||
@@ -15,6 +15,11 @@ versioni `0.y.z` erano pre-release.
|
||||
|
||||
### Modificato
|
||||
|
||||
- **L'allenatore non riceve più i promemoria degli eventi** (24 e 3 ore prima, DD-039): per lui non
|
||||
c'è nulla da confermare, dato che non risponde alle presenze e non è convocabile, e per una cena di
|
||||
squadra il promemoria non aveva senso. Riceve ancora i messaggi dello staff. Migration
|
||||
`m25_promemoria_solo_giocatori`.
|
||||
|
||||
- **Ogni notifica arriva su tutti e tre i canali** (DD-038): i promemoria a 24 e 3 ore, che
|
||||
arrivavano solo in-app e per email, partono ora anche come push, mandata dal worker `mailer/`
|
||||
(migration `m24_push_promemoria_e_sondaggio`). Il sondaggio pre-partita, che era solo push,
|
||||
|
||||
+10
-10
@@ -75,16 +75,16 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa
|
||||
|
||||
## Turni e notifiche
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. |
|
||||
| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | |
|
||||
| `promemoria_push` | Non più usata. | Serviva da coda del testo quando la push partiva vuota; dal payload cifrato non la scrive né la legge nessuno. Tabella ancora presente, da eliminare con una migrazione. |
|
||||
| `notifiche_utente` | Centro notifiche in-app, con stato letto/non letto, sull'avatar del profilo. | Introdotta dalla migration `m17_notifiche_utente`. Cinque sorgenti (le prime quattro dalla `m17`, il sondaggio dalla `m24`), tutte in parallelo alla push esistente tranne i promemoria, la cui push la manda il worker (`notifiche_push_coda`): messaggio libero dell'admin (`notifica-personalizzata.ts`), un promemoria automatico prima di un evento — 24h e 3h prima, due `cron.schedule` (estensione `pg_cron`, abilitata il 31/07/2026 ma usata per la prima volta qui) — il turno palloni (`promemoria-palloni.ts`) e il sollecito presenze (`sollecita-presenze.ts`) e il sondaggio pre-partita (`apri-sondaggio.ts`, tipo `sondaggio_cacche`, `m24`), questi ultimi tre avviati a mano da un admin (il sollecito anche da un allenatore, DD-034). Volutamente non c'è una notifica sulla sola creazione di un evento: conta l'avvicinarsi della data, non il momento in cui è stato messo in calendario. I destinatari di un evento seguono la stessa convenzione di `convocatiEvento()`/`evento_permette_voto()` (M13): `convocati` vuoto = tutta la rosa attiva. Gli allenatori attivi sono sempre tra i destinatari dei promemoria evento, convocati o no (`giocatori_destinatari_evento()` riscritta da M21, DD-034); il sollecito presenze (avviato da admin o allenatore) e il turno palloni non arrivano mai a loro. Letta/segnata come letta dal client con `useNotificheMie()`/`useSegnaLette()` (`src/lib/notifiche-utente.ts`), RLS-only, nessuna route API dedicata per queste due operazioni. |
|
||||
| `notifiche_email_coda` | Coda delle email per le notifiche in-app: una riga per notifica, con stato d'invio. | Migration `m22_notifiche_email` (DD-036). Chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`. Stati: `in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`; colonne `tentativi`, `prossimo_tentativo` (backoff), `errore` (solo codici SMTP), `inviata_il`, `aggiornata_il`. La riempie il trigger `notifiche_utente_accoda_email` (`AFTER INSERT OR UPDATE OF creato_il`): ogni notifica nuova e ogni rinvio via `upsert` finiscono in coda; le notifiche presenti prima della migration no. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`: la usa solo il worker `mailer/` con la service role, tramite tre funzioni `SECURITY DEFINER` riservate a `service_role` — `prendi_notifiche_email(p_max)` (lotto con `FOR UPDATE SKIP LOCKED`, salta gli slot senza account o email o con le email spente, recupera le righe ferme `in_invio` da più di 10 minuti), `esito_notifica_email(...)` e `email_inviate_ultime_24h()`. Tabella separata da `notifiche_utente` perché la policy `UPDATE` del giocatore su quella riga gli permetterebbe di riportare a `in_coda` una mail già partita. Specifica in [notifiche-email.md](modules/notifiche-email.md). |
|
||||
| `preferenze_utente` | Preferenze del singolo account (non dello slot giocatore). | Migration `m22_notifiche_email`. Chiave `auth_user_id` → `auth.users(id)` con cascade; oggi una sola colonna, `email_notifiche` (default `true`), più `aggiornata_il`. **Nessuna riga = email attive**: la riga nasce solo quando il giocatore tocca l'interruttore «Email» in Profilo → Opzioni (upsert da `useEmailNotifiche()`, `src/lib/preferenze-utente.ts`). RLS: ognuno legge, inserisce e aggiorna solo la propria riga. |
|
||||
| `promemoria_eventi_generati` | Registro dei promemoria evento (24 e 3 ore) già generati, per giocatore, evento e tipo. | Migration `m23_promemoria_gia_generati` (DD-037). Chiave `(giocatore_id, evento_id, tipo)` con `ON DELETE CASCADE` verso `giocatori_squadra` ed `eventi_app` (sparisce con l'evento, come le altre tabelle collegate, DD-029); `generato_il`. Scritta solo da `genera_promemoria_eventi()`, che crea la notifica in `notifiche_utente` soltanto per le righe appena registrate: eliminare la notifica dal centro notifiche non fa rigenerare il promemoria né accoda una nuova mail. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. La migration registra come già generati i promemoria presenti al momento dell'applicazione. |
|
||||
| `notifiche_push_coda` | Coda delle push per i promemoria a 24 e 3 ore: una riga per notifica, con stato d'invio. | Migration `m24_push_promemoria_e_sondaggio` (DD-038). Stessa struttura di `notifiche_email_coda`: chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`, stati `in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`, `tentativi`, `prossimo_tentativo`, `errore` (solo codici HTTP), `inviata_il`, `aggiornata_il`. La riempie il trigger `notifiche_utente_accoda_push_promemoria` (`AFTER INSERT`, solo tipi `evento_promemoria_24h` e `evento_promemoria_3h`: le altre notifiche mandano la push dall'app; nessun backfill). Le usa solo il worker `mailer/` con la service role, tramite `prendi_push_promemoria(p_max)` (lotto con `FOR UPDATE SKIP LOCKED` con testo e dispositivi iscritti del giocatore; salta i giocatori senza dispositivi e i promemoria creati da oltre 3 ore, recupera le righe ferme `in_invio` da più di 10 minuti) ed `esito_push_promemoria(...)`, entrambe `SECURITY DEFINER` riservate a `service_role`. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. |
|
||||
| Tabella | Scopo | Note |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `turni_palloni` | Gestione dei turni palloni. | Solo turni **confermati**. Gli allenamenti non ricevono proposta automatica (vedi [palloni.md](modules/palloni.md)); M10 azzera i turni salvati su allenamenti da oggi in poi. |
|
||||
| `push_subscriptions` | Dispositivi registrati per le notifiche Push. | |
|
||||
| `promemoria_push` | Non più usata. | Serviva da coda del testo quando la push partiva vuota; dal payload cifrato non la scrive né la legge nessuno. Tabella ancora presente, da eliminare con una migrazione. |
|
||||
| `notifiche_utente` | Centro notifiche in-app, con stato letto/non letto, sull'avatar del profilo. | Introdotta dalla migration `m17_notifiche_utente`. Cinque sorgenti (le prime quattro dalla `m17`, il sondaggio dalla `m24`), tutte in parallelo alla push esistente tranne i promemoria, la cui push la manda il worker (`notifiche_push_coda`): messaggio libero dell'admin (`notifica-personalizzata.ts`), un promemoria automatico prima di un evento — 24h e 3h prima, due `cron.schedule` (estensione `pg_cron`, abilitata il 31/07/2026 ma usata per la prima volta qui) — il turno palloni (`promemoria-palloni.ts`) e il sollecito presenze (`sollecita-presenze.ts`) e il sondaggio pre-partita (`apri-sondaggio.ts`, tipo `sondaggio_cacche`, `m24`), questi ultimi tre avviati a mano da un admin (il sollecito anche da un allenatore, DD-034). Volutamente non c'è una notifica sulla sola creazione di un evento: conta l'avvicinarsi della data, non il momento in cui è stato messo in calendario. I destinatari di un evento seguono la stessa convenzione di `convocatiEvento()`/`evento_permette_voto()` (M13): `convocati` vuoto = tutta la rosa attiva. I promemoria evento arrivano solo ai giocatori attivi: ai convocati, o a tutti se `convocati` è vuoto (`giocatori_destinatari_evento()`, riscritta da M21 per includere sempre gli allenatori e da M25 per escluderli, DD-039); il sollecito presenze (avviato da admin o allenatore) e il turno palloni non arrivano mai a loro. Letta/segnata come letta dal client con `useNotificheMie()`/`useSegnaLette()` (`src/lib/notifiche-utente.ts`), RLS-only, nessuna route API dedicata per queste due operazioni. |
|
||||
| `notifiche_email_coda` | Coda delle email per le notifiche in-app: una riga per notifica, con stato d'invio. | Migration `m22_notifiche_email` (DD-036). Chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`. Stati: `in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`; colonne `tentativi`, `prossimo_tentativo` (backoff), `errore` (solo codici SMTP), `inviata_il`, `aggiornata_il`. La riempie il trigger `notifiche_utente_accoda_email` (`AFTER INSERT OR UPDATE OF creato_il`): ogni notifica nuova e ogni rinvio via `upsert` finiscono in coda; le notifiche presenti prima della migration no. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`: la usa solo il worker `mailer/` con la service role, tramite tre funzioni `SECURITY DEFINER` riservate a `service_role` — `prendi_notifiche_email(p_max)` (lotto con `FOR UPDATE SKIP LOCKED`, salta gli slot senza account o email o con le email spente, recupera le righe ferme `in_invio` da più di 10 minuti), `esito_notifica_email(...)` e `email_inviate_ultime_24h()`. Tabella separata da `notifiche_utente` perché la policy `UPDATE` del giocatore su quella riga gli permetterebbe di riportare a `in_coda` una mail già partita. Specifica in [notifiche-email.md](modules/notifiche-email.md). |
|
||||
| `preferenze_utente` | Preferenze del singolo account (non dello slot giocatore). | Migration `m22_notifiche_email`. Chiave `auth_user_id` → `auth.users(id)` con cascade; oggi una sola colonna, `email_notifiche` (default `true`), più `aggiornata_il`. **Nessuna riga = email attive**: la riga nasce solo quando il giocatore tocca l'interruttore «Email» in Profilo → Opzioni (upsert da `useEmailNotifiche()`, `src/lib/preferenze-utente.ts`). RLS: ognuno legge, inserisce e aggiorna solo la propria riga. |
|
||||
| `promemoria_eventi_generati` | Registro dei promemoria evento (24 e 3 ore) già generati, per giocatore, evento e tipo. | Migration `m23_promemoria_gia_generati` (DD-037). Chiave `(giocatore_id, evento_id, tipo)` con `ON DELETE CASCADE` verso `giocatori_squadra` ed `eventi_app` (sparisce con l'evento, come le altre tabelle collegate, DD-029); `generato_il`. Scritta solo da `genera_promemoria_eventi()`, che crea la notifica in `notifiche_utente` soltanto per le righe appena registrate: eliminare la notifica dal centro notifiche non fa rigenerare il promemoria né accoda una nuova mail. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. La migration registra come già generati i promemoria presenti al momento dell'applicazione. |
|
||||
| `notifiche_push_coda` | Coda delle push per i promemoria a 24 e 3 ore: una riga per notifica, con stato d'invio. | Migration `m24_push_promemoria_e_sondaggio` (DD-038). Stessa struttura di `notifiche_email_coda`: chiave `notifica_id` → `notifiche_utente(id)` con `ON DELETE CASCADE`, stati `in_coda`, `in_invio`, `inviata`, `fallita`, `saltata`, `tentativi`, `prossimo_tentativo`, `errore` (solo codici HTTP), `inviata_il`, `aggiornata_il`. La riempie il trigger `notifiche_utente_accoda_push_promemoria` (`AFTER INSERT`, solo tipi `evento_promemoria_24h` e `evento_promemoria_3h`: le altre notifiche mandano la push dall'app; nessun backfill). Le usa solo il worker `mailer/` con la service role, tramite `prendi_push_promemoria(p_max)` (lotto con `FOR UPDATE SKIP LOCKED` con testo e dispositivi iscritti del giocatore; salta i giocatori senza dispositivi e i promemoria creati da oltre 3 ore, recupera le righe ferme `in_invio` da più di 10 minuti) ed `esito_push_promemoria(...)`, entrambe `SECURITY DEFINER` riservate a `service_role`. RLS attiva **senza policy** e permessi tolti a `anon`/`authenticated`. |
|
||||
|
||||
## Funzioni speciali
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ Serve a rispondere a domande del tipo:
|
||||
| [DD-036](#dd-036--notifiche-email-via-gmail-da-un-worker-docker-sullhost-di-casa) | Notifiche email via Gmail |
|
||||
| [DD-037](#dd-037--i-promemoria-evento-ricordano-di-essere-già-stati-generati) | Promemoria evento senza ritorni |
|
||||
| [DD-038](#dd-038--ogni-notifica-ha-tre-canali-la-push-dei-promemoria-parte-dal-worker) | Tre canali per ogni notifica |
|
||||
| [DD-039](#dd-039--lallenatore-non-riceve-i-promemoria-degli-eventi) | Niente promemoria all'allenatore |
|
||||
|
||||
**In valutazione**
|
||||
|
||||
@@ -1449,7 +1450,8 @@ collega uno slot di tipo allenatore: è quindi l'admin a concederlo, registrando
|
||||
immediato e non aspetta la query dei ruoli.
|
||||
- L'allenatore non vede le tab Stagione e Badge del Profilo, né Obiettivi e Badge di Squadra.
|
||||
- In Squadra compare nella tab Rosa con «Allenatore» al posto del ruolo, ricavato da `tipo`;
|
||||
riceve i promemoria degli eventi e può sollecitare le presenze, non il turno palloni.
|
||||
riceve i promemoria degli eventi (dal 29/09/2026 non più: DD-039) e può sollecitare le presenze,
|
||||
non il turno palloni.
|
||||
- Non vede badge né classifiche o sondaggi sulle cacche, in nessuna schermata.
|
||||
|
||||
**Riesame**
|
||||
@@ -1712,3 +1714,44 @@ Vercel e avevano già i tre canali.
|
||||
Se l'host di casa si rivela poco affidabile per una notifica sensibile al tempo come la push, o se
|
||||
il database dovesse comunque chiamare l'app per altro, valutare di nuovo la chiamata dal database
|
||||
all'app.
|
||||
|
||||
### DD-039 — L'allenatore non riceve i promemoria degli eventi
|
||||
|
||||
**Data:** 29 settembre 2026
|
||||
**Stato:** Accettata
|
||||
|
||||
**Contesto**
|
||||
DD-034 stabilì che l'allenatore riceve i promemoria a 24 e 3 ore prima di un evento, sempre, convocato
|
||||
o no (`giocatori_destinatari_evento()` riscritta da M21). Ma un promemoria serve a ricordare di
|
||||
esserci, e l'allenatore non può rispondere alle presenze, non è convocabile e non compare tra i
|
||||
partecipanti (DD-034). L'incoerenza si vede negli eventi extra-campo, come una cena di squadra:
|
||||
riceveva l'avviso (in-app, per email e, dopo M24, come push) senza poter dire se ci sarebbe stato né
|
||||
comparire nell'elenco.
|
||||
|
||||
**Decisione**
|
||||
I destinatari dei promemoria sono i **giocatori attivi**: i convocati, o tutta la rosa se
|
||||
`convocati` è vuoto. L'allenatore non li riceve, per nessun tipo di evento. Cambia solo la funzione
|
||||
SQL (migration `m25_promemoria_solo_giocatori`, `g.tipo = 'giocatore'` al posto di «o allenatore
|
||||
sempre»); cron, criteri di selezione, registro dei promemoria (DD-037), code email e push e testi
|
||||
restano invariati.
|
||||
|
||||
**Alternative scartate**
|
||||
|
||||
- Toglierli solo per gli eventi extra-campo → scartata su indicazione esplicita di toglierli per
|
||||
gli eventi in generale: anche per un allenamento o una partita l'allenatore non è tra i
|
||||
partecipanti.
|
||||
- Farlo rispondere agli eventi extra-campo, così da tenere il promemoria → scartata: servono
|
||||
risposte presenze e regole del database per una categoria nuova, e il conteggio dei presenti.
|
||||
|
||||
**Conseguenze**
|
||||
|
||||
- Le notifiche che l'allenatore riceve dal server sono i **messaggi dello staff** e nient'altro
|
||||
(sollecito, turno palloni, sondaggio e notifiche smart non lo riguardano già).
|
||||
- I promemoria già generati per un allenatore prima della migration non si toccano.
|
||||
- Un allenatore non è avvisato dell'inizio di un evento che gestisce: sa quando è perché lo ha
|
||||
creato lui o lo vede in Calendario.
|
||||
- Sostituisce la parte di DD-034 sui promemoria; il resto di DD-034 resta valido.
|
||||
|
||||
**Riesame**
|
||||
Se l'allenatore dovesse poter partecipare a certi eventi (per esempio la cena), si riapre insieme
|
||||
alla sua possibilità di rispondere.
|
||||
+49
-30
@@ -122,47 +122,65 @@ risposte presenze altrui, gestione dei ruoli.
|
||||
|
||||
## Notifiche
|
||||
|
||||
L'allenatore riceve i promemoria automatici degli eventi (24h e 3h prima, centro notifiche
|
||||
in-app di M17) come i giocatori, anche se non è convocato: `giocatori_destinatari_evento()`
|
||||
lo include sempre. Riceve anche i messaggi dell'admin, e può attivare le push dal proprio
|
||||
Profilo. Non riceve il sollecito presenze, il turno palloni né il sondaggio cacche, che
|
||||
riguardano chi gioca.
|
||||
Cosa riceve l'allenatore, notifica per notifica (il catalogo completo, con i canali di tutti, sta in
|
||||
[notifiche.md](notifiche.md#catalogo-delle-notifiche)):
|
||||
|
||||
| Notifica | L'allenatore la riceve? | Note |
|
||||
| ------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Promemoria a 24 e 3 ore prima di un evento | **no** | Dal 29/09/2026 (m25, DD-039) `giocatori_destinatari_evento()` include solo i giocatori attivi: l'allenatore non può rispondere alle presenze né essere convocato, quindi il promemoria non gli chiederebbe nulla che possa fare (per esempio una cena di squadra). Prima gli arrivava sempre, convocato o no (m21). |
|
||||
| Messaggio dello staff | **sì** | In-app ed email vanno a tutta la rosa attiva, allenatori compresi; la push ai suoi dispositivi iscritti, o solo a lui se il messaggio è per lui. |
|
||||
| Sollecito presenze | no | L'allenatore non risponde alle presenze (DD-034). Può invece **far partire** il sollecito. |
|
||||
| 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. |
|
||||
| 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
|
||||
spento l'interruttore «Email»; la push solo sui dispositivi su cui ha attivato «Notifiche» dal
|
||||
proprio Profilo (l'interruttore è identico a quello dei giocatori). Il centro notifiche in-app
|
||||
funziona come per i giocatori, con i soli messaggi dello staff.
|
||||
|
||||
## Esclusione dalla rosa
|
||||
|
||||
L'allenatore ha uno slot in `giocatori_squadra` (DD-034), quindi ogni lettura della "rosa di
|
||||
gioco" lo esclude con `inRosa()` (`attivo` e `tipo = giocatore`) al posto del vecchio filtro
|
||||
su `attivo`. Fanno eccezione la tab Rosa di Squadra, i compleanni del Calendario
|
||||
(`useAnagraficaRosa({ conAllenatori: true })`), i destinatari dei promemoria evento e dei
|
||||
messaggi admin, che lo includono.
|
||||
(`useAnagraficaRosa({ conAllenatori: true })`) e i destinatari dei messaggi admin, che lo
|
||||
includono.
|
||||
|
||||
| Livello | Dove si esclude l'allenatore |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| client | `useRosa()`, `useAnagraficaRosa()`, `useTurniPalloni()`, `usePresenzeUltimoMeseTutti()`, form convocati di `/eventi`, `EventoCard`, `TurnoPalloni`, `VotazioneMvp`, `VotoSocial` |
|
||||
| identità | `useGiocatoreInCampo()` è `null` per l'allenatore: niente risposta presenze, voti, pagelle, cacche, promemoria palloni |
|
||||
| server | `destinatariSollecito()`, `promemoria-palloni.ts`, `apri-sondaggio.ts` |
|
||||
| database | policy presenze e cacche «la propria riga» solo per `tipo = giocatore`; `evento_permette_voto()` rifiuta votante o votato allenatore |
|
||||
| admin | tab Squadra (conteggi, export CSI) e Profili (allenatori in un gruppo a parte, senza documenti né tesseramento) |
|
||||
| Livello | Dove si esclude l'allenatore |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| client | `useRosa()`, `useAnagraficaRosa()`, `useTurniPalloni()`, `usePresenzeUltimoMeseTutti()`, form convocati di `/eventi`, `EventoCard`, `TurnoPalloni`, `VotazioneMvp`, `VotoSocial`, `useIo()` e `useGiocatoreCorrente()` (quindi le notifiche smart) |
|
||||
| identità | `useGiocatoreInCampo()` è `null` per l'allenatore: niente risposta presenze, voti, pagelle, cacche, promemoria palloni |
|
||||
| server | `destinatariSollecito()`, `promemoria-palloni.ts`, `apri-sondaggio.ts` |
|
||||
| database | policy presenze e cacche «la propria riga» solo per `tipo = giocatore`; `evento_permette_voto()` rifiuta votante o votato allenatore |
|
||||
| admin | tab Squadra (conteggi, export CSI) e Profili (allenatori in un gruppo a parte, senza documenti né tesseramento) |
|
||||
|
||||
Il compleanno dell'allenatore resta visibile in Calendario (è un membro della squadra, non un
|
||||
giocatore).
|
||||
|
||||
## Permessi (riepilogo)
|
||||
|
||||
| Azione | Giocatore | Allenatore | Admin |
|
||||
| ----------------------------------------------- | ---------- | ------------------ | ------------------ |
|
||||
| Modificare nome e cognome | no | sì, i propri | tutti |
|
||||
| Dati personali e foto profilo | i propri | i propri (ridotti) | tutti (non i file) |
|
||||
| Documento, certificato, foto tessera | i propri | — | lettura/download |
|
||||
| Avviso certificati in Home | il proprio | no | tutta la rosa |
|
||||
| Creare/modificare/eliminare eventi, convocati | no | sì | sì |
|
||||
| Sollecitare le presenze | no | sì | sì |
|
||||
| Promemoria palloni, notifica personalizzata | no | no | sì |
|
||||
| Rispondere alle presenze, votare, essere votato | sì | no | sì (se giocatore) |
|
||||
| Tab Stagione e Badge del profilo | sì | no | sì |
|
||||
| Tab Obiettivi e Badge di Squadra | sì | no | sì |
|
||||
| Badge e classifiche/dati sulle cacche | sì | no | sì |
|
||||
| Dashboard `/admin` | no | no | sì |
|
||||
| Azione | Giocatore | Allenatore | Admin |
|
||||
| ------------------------------------------------------------------- | ---------- | ------------------ | ----------------------------------- |
|
||||
| Modificare nome e cognome | no | sì, i propri | tutti |
|
||||
| Dati personali e foto profilo | i propri | i propri (ridotti) | tutti (non i file) |
|
||||
| Documento, certificato, foto tessera | i propri | — | lettura/download |
|
||||
| Avviso certificati in Home | il proprio | no | tutta la rosa |
|
||||
| Creare/modificare/eliminare eventi, convocati | no | sì | sì |
|
||||
| Sollecitare le presenze | no | sì | sì |
|
||||
| Promemoria palloni, notifica personalizzata | no | no | sì |
|
||||
| Ricevere i promemoria automatici degli eventi (in-app, email, push) | sì | no (DD-039) | secondo il proprio slot |
|
||||
| Ricevere i messaggi dello staff | sì | sì | secondo il proprio slot |
|
||||
| Ricevere sollecito presenze, turno palloni, sondaggio cacche | sì | no | solo con uno slot giocatore in rosa |
|
||||
| Ricevere le notifiche smart (badge, serie, obiettivi) | sì | no | solo con uno slot giocatore in rosa |
|
||||
| Rispondere alle presenze, votare, essere votato | sì | no | sì (se giocatore) |
|
||||
| Tab Stagione e Badge del profilo | sì | no | sì |
|
||||
| Tab Obiettivi e Badge di Squadra | sì | no | sì |
|
||||
| Badge e classifiche/dati sulle cacche | sì | no | sì |
|
||||
| Dashboard `/admin` | no | no | sì |
|
||||
|
||||
Le notifiche sono legate allo **slot** (`giocatori_squadra`), non al ruolo di admin: chi è admin le riceve
|
||||
secondo il proprio slot, come giocatore o come allenatore, e non ne riceve se non ha uno slot.
|
||||
|
||||
## Implementazione
|
||||
|
||||
@@ -174,7 +192,7 @@ giocatore).
|
||||
del proprio allenatore; policy «L'allenatore aggiorna il proprio slot»; trigger
|
||||
`sincronizza_ruolo_allenatore` che aggiunge o toglie la riga `allenatore` in `user_roles` al
|
||||
collegamento, scollegamento, disattivazione o cambio di tipo; policy `eventi_app` admin o
|
||||
allenatore; presenze, cacche e voti chiusi all'allenatore; promemoria evento aperti.
|
||||
allenatore; presenze, cacche e voti chiusi all'allenatore.
|
||||
- **Client**: nell'app `numero` resta un `number` (0 per l'allenatore, NULL a database). I
|
||||
permessi (eventi, sollecito) leggono `user_roles` con `usePuoGestireEventi()`; cosa si vede
|
||||
dipende dal tipo del proprio slot (`useSonoAllenatore()`), che è immediato.
|
||||
@@ -183,7 +201,8 @@ giocatore).
|
||||
|
||||
## Scelte confermate
|
||||
|
||||
- Riceve i promemoria degli eventi: sì.
|
||||
- Non riceve i promemoria degli eventi (DD-039; fino alla m24 li riceveva sempre, DD-034). Riceve i
|
||||
messaggi dello staff.
|
||||
- Sollecita le presenze: sì. Promemoria palloni: no.
|
||||
- Badge e cacche: mai visibili all'allenatore, né classifiche né sondaggio.
|
||||
- In Squadra: nessuna sezione Staff; compare in Rosa come gli altri, con «Allenatore» al
|
||||
|
||||
@@ -59,8 +59,9 @@ e i suoi canali: tutte le notifiche generate dal server passano da `notifiche_ut
|
||||
arrivano anche per email (DD-038).
|
||||
|
||||
**Chi le riceve:** chi ha la riga in `notifiche_utente`, con la stessa logica delle notifiche
|
||||
in-app. Gli admin e gli allenatori non hanno un percorso a parte: ricevono ciò che riceverebbero
|
||||
come giocatori, secondo i destinatari già definiti in M17 e DD-034.
|
||||
in-app. Gli admin non hanno un percorso a parte: ricevono ciò che riceverebbero secondo il proprio
|
||||
slot. L'allenatore riceve solo i messaggi dello staff, non i promemoria degli eventi (DD-039), né il
|
||||
sollecito, il turno palloni e il sondaggio (DD-034, DD-038).
|
||||
|
||||
Il certificato in scadenza **non genera mail**: non è una notifica ma una card calcolata in Home
|
||||
(DD-035). Se un giorno lo si volesse anche per email sarebbe una sorgente nuova di
|
||||
|
||||
@@ -36,13 +36,13 @@ la colonna Email coincide con la colonna In-app).
|
||||
|
||||
| # | Notifica | Cosa la fa partire | Destinatari | Push | In-app | Email |
|
||||
| --- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------ | ----- |
|
||||
| 1 | **Promemoria a 24 ore** — «Promemoria: _titolo_», corpo «data alle ora» | Job `pg_cron` `promemoria-eventi-24h`, ogni ora, per gli eventi che iniziano entro le prossime 24 ore. Tipo `evento_promemoria_24h`. La push la manda il worker (M24). | I convocati dell'evento, o tutta la rosa attiva se `convocati` è vuoto; gli allenatori attivi sempre, convocati o no. | sì (worker) | sì | sì |
|
||||
| 1 | **Promemoria a 24 ore** — «Promemoria: _titolo_», corpo «data alle ora» | Job `pg_cron` `promemoria-eventi-24h`, ogni ora, per gli eventi che iniziano entro le prossime 24 ore. Tipo `evento_promemoria_24h`. La push la manda il worker (M24). | I convocati dell'evento, o tutta la rosa attiva se `convocati` è vuoto; mai gli allenatori (DD-039). | sì (worker) | sì | sì |
|
||||
| 2 | **Promemoria a 3 ore** — stesso testo | Job `pg_cron` `promemoria-eventi-3h`, ogni 15 minuti, per gli eventi che iniziano entro le prossime 3 ore. Tipo `evento_promemoria_3h`. La push la manda il worker (M24). | Come il promemoria a 24 ore. | sì (worker) | sì | sì |
|
||||
| 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** — «Tocca a te prendere i palloni» / «Porta i palloni» | Un admin, con il pulsante nella pagina dell'evento. Solo eventi di tipo allenamento o partita. Route `promemoria-palloni`, tipo `turno_palloni`. | 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ì | sì |
|
||||
| 5 | **Sollecito presenze** — «Manca la tua risposta» | Un admin o un allenatore, con il pulsante nella pagina dell'evento. Route `sollecita-presenze`, tipo `sollecita_presenze`. | I giocatori attivi (mai gli allenatori) che non hanno ancora 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; 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 |
|
||||
|
||||
Tutte le notifiche generate dal server (righe 1-6) hanno tutti e tre i canali (DD-038). La
|
||||
differenza sta in come parte la push: quelle dei pulsanti (3-6) la mandano le route dell'app, quelle
|
||||
@@ -50,6 +50,8 @@ dei promemoria (1 e 2) nascono nel database e la manda il worker `mailer/` (vedi
|
||||
promemoria»). Le righe 4 e 5 riscrivono la notifica esistente se l'admin preme di nuovo il pulsante
|
||||
per lo stesso evento, e lo stesso fa il sondaggio (vedi «Centro notifiche in-app»).
|
||||
|
||||
Cosa riceve l'allenatore, notifica per notifica: [allenatore.md](allenatore.md#notifiche).
|
||||
|
||||
### Cosa non genera nessuna notifica
|
||||
|
||||
- Creare, spostare, modificare o cancellare un evento: contano solo i promemoria prima
|
||||
@@ -89,6 +91,23 @@ Sono l'effetto attuale della logica, non scelte documentate altrove:
|
||||
- **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: 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` |
|
||||
| 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` |
|
||||
| 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
|
||||
catena `useIo()` → `useRosa()` → `useNotificheSmart()` è fatta di hook e si conosce dalla lettura del
|
||||
codice; l'unico pezzo puro, `inRosa`, è coperto.
|
||||
|
||||
---
|
||||
|
||||
## Dati
|
||||
@@ -184,6 +203,13 @@ traguardo esatto, obiettivo di squadra tra il 90 e il 100%, badge social vinto.
|
||||
ha un id deterministico; quelli già mostrati sono salvati in `localStorage` per non
|
||||
ripetersi — deduplica puramente locale al dispositivo, non sincronizzata.
|
||||
|
||||
**Chi le riceve.** Solo chi ha selezionato sul dispositivo uno slot **giocatore in rosa**:
|
||||
`CelebrazioneBadge` (montato una volta in `__root.tsx`) passa a `useNotificheSmart()` il giocatore
|
||||
letto da `useGiocatoreCorrente()`, cioè `useIo()`, che lo cerca in `useRosa()`. Quell'elenco esclude
|
||||
gli allenatori (`inRosa`, DD-034), che non hanno statistiche: per loro `useIo()` è `null` e
|
||||
`useNotificheSmart()` esce subito, senza calcolare nulla né mostrare la card celebrativa o la
|
||||
notifica del sistema. Vedi [allenatore.md](allenatore.md#notifiche).
|
||||
|
||||
---
|
||||
|
||||
## Centro notifiche in-app (M17)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
-- M25 — L'allenatore non riceve i promemoria degli eventi (DD-039)
|
||||
--
|
||||
-- La migration M21 (DD-034) aggiunse `g.tipo = 'allenatore'` ai destinatari dei promemoria a 24 e
|
||||
-- 3 ore: l'allenatore attivo li riceveva sempre, convocato o no. Ma un promemoria chiede di
|
||||
-- esserci, e l'allenatore non può rispondere alle presenze, non è convocabile e non compare tra i
|
||||
-- partecipanti: per un evento come la cena di squadra riceveva un avviso su cui non poteva fare
|
||||
-- nulla. Da ora i destinatari sono solo i giocatori attivi: i convocati, o tutta la rosa se
|
||||
-- `convocati` è vuoto.
|
||||
--
|
||||
-- Cambia solo questa funzione: `genera_promemoria_eventi()` (M17, M23), i job pg_cron, le code
|
||||
-- email e push e il resto delle notifiche restano invariati. I promemoria già generati per un
|
||||
-- allenatore prima della migration non vengono toccati. I messaggi dello staff continuano ad
|
||||
-- arrivargli: non dipendono da questa funzione.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.giocatori_destinatari_evento(p_evento_id text)
|
||||
RETURNS SETOF text
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT g.id
|
||||
FROM public.eventi_app e
|
||||
JOIN public.giocatori_squadra g
|
||||
ON g.attivo = true
|
||||
AND g.tipo = 'giocatore'
|
||||
AND (cardinality(e.convocati) = 0 OR g.id = ANY(e.convocati))
|
||||
WHERE e.id = p_evento_id;
|
||||
$$;
|
||||
+6
-6
@@ -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) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), 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) 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
|
||||
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
/**
|
||||
* Destinatari delle notifiche per ruolo: `bun test/integration/destinatari-notifiche.test.ts`.
|
||||
*
|
||||
* Verifica, chiamando le route vere, le regole del catalogo delle notifiche
|
||||
* (docs/modules/notifiche.md) che finora erano coperte solo dalla lettura del codice:
|
||||
*
|
||||
* - i promemoria a 24 e 3 ore arrivano ai convocati (a tutti i giocatori attivi se `convocati`
|
||||
* è vuoto), mai all'allenatore (DD-039);
|
||||
* - la push del sondaggio pre-partita non arriva ai dispositivi dell'allenatore, quella del
|
||||
* messaggio dello staff sì (DD-034, DD-038);
|
||||
* - il sollecito presenze arriva a chi non ha risposto o ha risposto «forse», mai all'allenatore,
|
||||
* ed è permesso ad allenatore e admin ma non a un giocatore;
|
||||
* - il turno palloni arriva all'incaricato e a chi li aveva prima, solo per allenamenti e partite,
|
||||
* ed è riservato agli admin.
|
||||
*
|
||||
* Le push vanno a un servizio push finto in locale: nessuna richiesta esce dalla macchina.
|
||||
* Gira solo sullo stack locale (`npx supabase start`). Il server di sviluppo che riceve le
|
||||
* chiamate deve avere chiavi VAPID (il test le imposta se avvia lui il server; con `BASE_URL`
|
||||
* valgono quelle di quel server) e usa gli slot `g1`, `g2`, `g4`, `g5` e `g6` della rosa seed,
|
||||
* ripristinati alla fine.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { avviaServer } from "../helpers/server";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("destinatari notifiche", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("destinatari-notifiche");
|
||||
} else {
|
||||
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
||||
console.log(`destinatari notifiche su ${URL_BASE}`);
|
||||
|
||||
// Il server di sviluppo eredita queste: senza, le route punterebbero al progetto cloud.
|
||||
process.env["SUPABASE_URL"] = URL_BASE;
|
||||
process.env["SUPABASE_PUBLISHABLE_KEY"] = ANON;
|
||||
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
|
||||
|
||||
const b64u = (dati: ArrayBuffer | Uint8Array) =>
|
||||
Buffer.from(dati as ArrayBuffer).toString("base64url");
|
||||
|
||||
// Chiavi VAPID di prova (P-256), nel formato di `webpush.server.ts`: la chiave privata reale
|
||||
// non deve mai servire ai test.
|
||||
const vapid = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [
|
||||
"sign",
|
||||
]);
|
||||
const jwk = await crypto.subtle.exportKey("jwk", vapid.privateKey);
|
||||
process.env["VAPID_PUBLIC_KEY"] = b64u(
|
||||
new Uint8Array([
|
||||
4,
|
||||
...Buffer.from(jwk.x ?? "", "base64url"),
|
||||
...Buffer.from(jwk.y ?? "", "base64url"),
|
||||
]),
|
||||
);
|
||||
process.env["VAPID_PRIVATE_KEY"] = jwk.d ?? "";
|
||||
|
||||
async function chiaviDispositivo() {
|
||||
const k = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, [
|
||||
"deriveBits",
|
||||
]);
|
||||
return {
|
||||
p256dh: b64u(await crypto.subtle.exportKey("raw", k.publicKey)),
|
||||
auth: b64u(crypto.getRandomValues(new Uint8Array(16))),
|
||||
};
|
||||
}
|
||||
|
||||
const PREFISSO = "test-destinatari-notifiche";
|
||||
const PASSWORD = "prova-destinatari-notifiche-123";
|
||||
const G_UNO = "g1";
|
||||
const G_DUE = "g2";
|
||||
const G_QUATTRO = "g4";
|
||||
const G_CINQUE = "g5";
|
||||
const ALLENATORE = "g6";
|
||||
const PARTITA = `${PREFISSO}-partita`;
|
||||
const PRESENZE = `${PREFISSO}-presenze`;
|
||||
const PALLONI_1 = `${PREFISSO}-palloni-1`;
|
||||
const PALLONI_2 = `${PREFISSO}-palloni-2`;
|
||||
const GENERICO = `${PREFISSO}-generico`;
|
||||
const PROMEMORIA_CONVOCATI = `${PREFISSO}-promemoria-convocati`;
|
||||
const PROMEMORIA_TUTTI = `${PREFISSO}-promemoria-tutti`;
|
||||
const TITOLO_STAFF = `${PREFISSO} messaggio ${Date.now()}`;
|
||||
|
||||
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, token: string, metodo: string, corpo?: unknown) {
|
||||
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 };
|
||||
}
|
||||
|
||||
// --- Servizio push finto: registra a quale dispositivo arriva ogni push ------------------
|
||||
const ricevute: string[] = [];
|
||||
const servizioPush: Server = createServer((req, res) => {
|
||||
req.resume();
|
||||
req.on("end", () => {
|
||||
ricevute.push((req.url ?? "").replace(/^\//, ""));
|
||||
res.statusCode = 201;
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
await new Promise<void>((ok) => servizioPush.listen(0, "127.0.0.1", ok));
|
||||
const porta = (servizioPush.address() as AddressInfo).port;
|
||||
const endpoint = (slot: string) => `http://127.0.0.1:${porta}/${slot}`;
|
||||
const svuota = () => {
|
||||
ricevute.length = 0;
|
||||
};
|
||||
/** A quali slot è arrivata una push dall'ultimo `svuota()`, senza ripetizioni e in ordine. */
|
||||
const destinatariPush = () => [...new Set(ricevute)].sort();
|
||||
|
||||
type Notifica = { giocatore_id: string; titolo: string; tipo: string };
|
||||
const notifiche = (filtro: string) =>
|
||||
leggi<Notifica>(
|
||||
`notifiche_utente?${filtro}&select=giocatore_id,titolo,tipo&order=giocatore_id`,
|
||||
);
|
||||
const slot = (righe: Notifica[]) => righe.map((r) => r.giocatore_id);
|
||||
|
||||
const idUtenti: string[] = [];
|
||||
let server: { baseUrl: string; stop: () => void } | null = null;
|
||||
let tokenAdmin = "";
|
||||
const originali = new Map<string, { tipo: string; auth_user_id: string | null }>();
|
||||
|
||||
try {
|
||||
const admin = await creaUtente(`${PREFISSO}-admin-${Date.now()}@example.test`);
|
||||
const allenatore = await creaUtente(`${PREFISSO}-allenatore-${Date.now()}@example.test`);
|
||||
const giocatore = await creaUtente(`${PREFISSO}-giocatore-${Date.now()}@example.test`);
|
||||
idUtenti.push(admin.id, allenatore.id, giocatore.id);
|
||||
await scrivi("user_roles", SERVIZIO, "POST", { user_id: admin.id, role: "admin" });
|
||||
tokenAdmin = admin.token;
|
||||
|
||||
const [g6] = await leggi<{ tipo: string; auth_user_id: string | null }>(
|
||||
`giocatori_squadra?id=eq.${ALLENATORE}&select=tipo,auth_user_id`,
|
||||
);
|
||||
originali.set(ALLENATORE, g6 ?? { tipo: "giocatore", auth_user_id: null });
|
||||
// Il trigger di `giocatori_squadra` rifiuta la service key: serve il JWT dell'admin. Il
|
||||
// trigger di M21 assegna il ruolo `allenatore` all'account collegato a uno slot allenatore.
|
||||
await scrivi(`giocatori_squadra?id=eq.${ALLENATORE}`, tokenAdmin, "PATCH", {
|
||||
tipo: "allenatore",
|
||||
auth_user_id: allenatore.id,
|
||||
});
|
||||
|
||||
for (const s of [G_QUATTRO, G_CINQUE, ALLENATORE]) {
|
||||
await scrivi("push_subscriptions", SERVIZIO, "POST", {
|
||||
giocatore_id: s,
|
||||
endpoint: endpoint(s),
|
||||
...(await chiaviDispositivo()),
|
||||
});
|
||||
}
|
||||
for (const [id, tipo, data] of [
|
||||
[PARTITA, "partita", "2031-04-01"],
|
||||
[PRESENZE, "allenamento", "2031-04-02"],
|
||||
[PALLONI_1, "allenamento", "2031-05-01"],
|
||||
[PALLONI_2, "allenamento", "2031-05-08"],
|
||||
[GENERICO, "evento", "2031-05-09"],
|
||||
] as const) {
|
||||
await scrivi("eventi_app", SERVIZIO, "POST", {
|
||||
id,
|
||||
tipo,
|
||||
titolo: `${PREFISSO} ${id}`,
|
||||
data,
|
||||
ora: "20:00",
|
||||
convocati: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Promemoria: eventi che iniziano tra 2 ore, dentro le finestre dei job a 24 e a 3 ore.
|
||||
const tra2Ore = new Date(Date.now() + 2 * 3_600_000);
|
||||
const dataRoma = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Rome" }).format(tra2Ore);
|
||||
const oraRoma = new Intl.DateTimeFormat("it-IT", {
|
||||
timeZone: "Europe/Rome",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}).format(tra2Ore);
|
||||
for (const [id, convocati] of [
|
||||
[PROMEMORIA_CONVOCATI, [G_QUATTRO]],
|
||||
[PROMEMORIA_TUTTI, []],
|
||||
] as const) {
|
||||
await scrivi("eventi_app", SERVIZIO, "POST", {
|
||||
id,
|
||||
tipo: "allenamento",
|
||||
titolo: `${PREFISSO} ${id}`,
|
||||
data: dataRoma,
|
||||
ora: oraRoma,
|
||||
convocati: [...convocati],
|
||||
});
|
||||
}
|
||||
|
||||
server = await avviaServer();
|
||||
console.log(`route su ${server.baseUrl}`);
|
||||
const chiama = (percorso: string, token: string, corpo: unknown) =>
|
||||
fetch(`${server!.baseUrl}/api/public/${percorso}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(corpo),
|
||||
});
|
||||
|
||||
const attivi = (
|
||||
await leggi<{ id: string; tipo: string }>("giocatori_squadra?attivo=eq.true&select=id,tipo")
|
||||
).sort((a, b) => a.id.localeCompare(b.id));
|
||||
const giocatoriAttivi = attivi.filter((g) => g.tipo === "giocatore").map((g) => g.id);
|
||||
assert.ok(attivi.some((g) => g.id === ALLENATORE && g.tipo === "allenatore"));
|
||||
|
||||
// --- 0. Promemoria a 24 e 3 ore (generati dal database) --------------------------------
|
||||
await prova(
|
||||
"il promemoria arriva ai soli convocati: l'allenatore non lo riceve (DD-039)",
|
||||
async () => {
|
||||
for (const [tipo, finestra] of [
|
||||
["evento_promemoria_24h", "24 hours"],
|
||||
["evento_promemoria_3h", "3 hours"],
|
||||
] as const) {
|
||||
const res = await fetch(`${URL_BASE}/rest/v1/rpc/genera_promemoria_eventi`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ p_tipo: tipo, p_finestra: finestra }),
|
||||
});
|
||||
assert.ok(res.ok, `genera_promemoria_eventi ${tipo}`);
|
||||
const righe = await notifiche(`evento_id=eq.${PROMEMORIA_CONVOCATI}&tipo=eq.${tipo}`);
|
||||
assert.deepEqual(slot(righe), [G_QUATTRO], tipo);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
await prova(
|
||||
"con convocati vuoto il promemoria arriva a tutti i giocatori attivi, mai all'allenatore",
|
||||
async () => {
|
||||
for (const tipo of ["evento_promemoria_24h", "evento_promemoria_3h"]) {
|
||||
const righe = await notifiche(`evento_id=eq.${PROMEMORIA_TUTTI}&tipo=eq.${tipo}`);
|
||||
assert.deepEqual(slot(righe), giocatoriAttivi, tipo);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- 1. Messaggio dello staff ---------------------------------------------------------
|
||||
await prova(
|
||||
"controllo: la push arriva al servizio finto (il server ha le chiavi VAPID)",
|
||||
async () => {
|
||||
svuota();
|
||||
const res = await chiama("notifica-personalizzata", tokenAdmin, {
|
||||
messaggio: `${TITOLO_STAFF} controllo`,
|
||||
giocatoreId: G_QUATTRO,
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(
|
||||
destinatariPush(),
|
||||
[G_QUATTRO],
|
||||
"il server di prova non manda push: servono VAPID_PUBLIC_KEY e VAPID_PRIVATE_KEY, oppure non usare BASE_URL",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await prova(
|
||||
"il messaggio dello staff a tutti raggiunge anche i dispositivi dell'allenatore",
|
||||
async () => {
|
||||
svuota();
|
||||
const res = await chiama("notifica-personalizzata", tokenAdmin, {
|
||||
messaggio: TITOLO_STAFF,
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(destinatariPush(), [ALLENATORE, G_QUATTRO, G_CINQUE].sort());
|
||||
const righe = await notifiche(`corpo=eq.${encodeURIComponent(TITOLO_STAFF)}&tipo=eq.admin`);
|
||||
assert.deepEqual(
|
||||
slot(righe),
|
||||
attivi.map((g) => g.id),
|
||||
"in-app: tutta la rosa attiva, allenatore compreso",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await prova("il messaggio a un solo giocatore non arriva agli altri dispositivi", async () => {
|
||||
svuota();
|
||||
await chiama("notifica-personalizzata", tokenAdmin, {
|
||||
messaggio: `${TITOLO_STAFF} solo g5`,
|
||||
giocatoreId: G_CINQUE,
|
||||
});
|
||||
assert.deepEqual(destinatariPush(), [G_CINQUE]);
|
||||
});
|
||||
|
||||
await prova(
|
||||
"un allenatore o un giocatore non possono mandare il messaggio dello staff",
|
||||
async () => {
|
||||
for (const token of [allenatore.token, giocatore.token]) {
|
||||
const res = await chiama("notifica-personalizzata", token, { messaggio: "no" });
|
||||
assert.ok(!res.ok, `stato ${res.status}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// --- 2. Sondaggio pre-partita ---------------------------------------------------------
|
||||
await prova("il sondaggio non manda la push ai dispositivi dell'allenatore", async () => {
|
||||
svuota();
|
||||
const res = await chiama("apri-sondaggio", tokenAdmin, { eventoId: PARTITA });
|
||||
assert.equal(res.status, 200);
|
||||
assert.deepEqual(destinatariPush(), [G_QUATTRO, G_CINQUE].sort());
|
||||
});
|
||||
|
||||
await prova("il sondaggio non crea l'avviso in-app per l'allenatore", async () => {
|
||||
const righe = await notifiche(`evento_id=eq.${PARTITA}&tipo=eq.sondaggio_cacche`);
|
||||
assert.deepEqual(slot(righe), giocatoriAttivi);
|
||||
});
|
||||
|
||||
await prova("un allenatore non può aprire il sondaggio", async () => {
|
||||
const res = await chiama("apri-sondaggio", allenatore.token, { eventoId: PARTITA });
|
||||
assert.ok(!res.ok, `stato ${res.status}`);
|
||||
});
|
||||
|
||||
// --- 3. Sollecito presenze ------------------------------------------------------------
|
||||
await scrivi("risposte_presenze", SERVIZIO, "POST", [
|
||||
{ evento_id: PRESENZE, giocatore_id: G_QUATTRO, stato: "presente" },
|
||||
{ evento_id: PRESENZE, giocatore_id: G_CINQUE, stato: "forse" },
|
||||
]);
|
||||
|
||||
await prova(
|
||||
"l'allenatore può sollecitare: arriva a chi non ha risposto e a chi ha detto «forse»",
|
||||
async () => {
|
||||
svuota();
|
||||
const res = await chiama("sollecita-presenze", allenatore.token, { eventoId: PRESENZE });
|
||||
assert.equal(res.status, 200);
|
||||
const righe = await notifiche(`evento_id=eq.${PRESENZE}&tipo=eq.sollecita_presenze`);
|
||||
assert.deepEqual(
|
||||
slot(righe),
|
||||
giocatoriAttivi.filter((id) => id !== G_QUATTRO),
|
||||
"esclusi chi ha già risposto e l'allenatore, incluso chi ha risposto «forse»",
|
||||
);
|
||||
assert.ok(slot(righe).includes(G_CINQUE));
|
||||
assert.deepEqual(destinatariPush(), [G_CINQUE], "push solo ai dispositivi dei sollecitati");
|
||||
},
|
||||
);
|
||||
|
||||
await prova("un giocatore non può sollecitare le presenze", async () => {
|
||||
const res = await chiama("sollecita-presenze", giocatore.token, { eventoId: PRESENZE });
|
||||
assert.ok(!res.ok, `stato ${res.status}`);
|
||||
});
|
||||
|
||||
// --- 4. Turno palloni -----------------------------------------------------------------
|
||||
await scrivi("turni_palloni", SERVIZIO, "POST", [
|
||||
{ evento_id: PALLONI_1, giocatore_id: G_UNO },
|
||||
{ evento_id: PALLONI_2, giocatore_id: G_DUE },
|
||||
]);
|
||||
|
||||
await prova("il turno palloni arriva all'incaricato e a chi li aveva prima", async () => {
|
||||
const res = await chiama("promemoria-palloni", tokenAdmin, { eventoId: PALLONI_2 });
|
||||
assert.equal(res.status, 200);
|
||||
const righe = await notifiche(`evento_id=eq.${PALLONI_2}&tipo=eq.turno_palloni`);
|
||||
assert.deepEqual(slot(righe), [G_UNO, G_DUE]);
|
||||
const per = new Map(righe.map((r) => [r.giocatore_id, r.titolo]));
|
||||
assert.equal(per.get(G_DUE), "Tocca a te prendere i palloni");
|
||||
assert.equal(per.get(G_UNO), "Porta i palloni");
|
||||
});
|
||||
|
||||
await prova(
|
||||
"per un evento che non è allenamento né partita non parte nessun avviso",
|
||||
async () => {
|
||||
await scrivi("turni_palloni", SERVIZIO, "POST", {
|
||||
evento_id: GENERICO,
|
||||
giocatore_id: G_QUATTRO,
|
||||
});
|
||||
const res = await chiama("promemoria-palloni", tokenAdmin, { eventoId: GENERICO });
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal((await notifiche(`evento_id=eq.${GENERICO}&tipo=eq.turno_palloni`)).length, 0);
|
||||
},
|
||||
);
|
||||
|
||||
await prova("un allenatore non può far partire il turno palloni", async () => {
|
||||
const res = await chiama("promemoria-palloni", allenatore.token, { eventoId: PALLONI_2 });
|
||||
assert.ok(!res.ok, `stato ${res.status}`);
|
||||
});
|
||||
} finally {
|
||||
server?.stop();
|
||||
servizioPush.close();
|
||||
await rest(
|
||||
`push_subscriptions?endpoint=like.${encodeURIComponent(`http://127.0.0.1:${porta}/*`)}`,
|
||||
SERVIZIO,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
await rest(`notifiche_utente?corpo=like.${encodeURIComponent(`${PREFISSO}*`)}`, SERVIZIO, {
|
||||
method: "DELETE",
|
||||
});
|
||||
for (const id of [
|
||||
PARTITA,
|
||||
PRESENZE,
|
||||
PALLONI_1,
|
||||
PALLONI_2,
|
||||
GENERICO,
|
||||
PROMEMORIA_CONVOCATI,
|
||||
PROMEMORIA_TUTTI,
|
||||
]) {
|
||||
await rest(`notifiche_utente?evento_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
||||
await rest(`turni_palloni?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("destinatari-notifiche");
|
||||
}
|
||||
}
|
||||
@@ -284,21 +284,37 @@ if (!locale) {
|
||||
assert.ok(!votato.ok, `l'allenatore non si vota (${votato.status})`);
|
||||
});
|
||||
|
||||
await prova("i promemoria evento arrivano anche all'allenatore non convocato", async () => {
|
||||
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ convocati: [GIOCATORE] }),
|
||||
});
|
||||
const res = await rest("rpc/giocatori_destinatari_evento", SERVIZIO, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ p_evento_id: EVENTO }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
const destinatari = ((await res.json()) as Array<string | Record<string, string>>).map((d) =>
|
||||
typeof d === "string" ? d : Object.values(d)[0],
|
||||
);
|
||||
assert.deepEqual([...destinatari].sort(), [GIOCATORE, SLOT].sort());
|
||||
});
|
||||
await prova(
|
||||
"i promemoria evento non arrivano all'allenatore, convocato o no (DD-039)",
|
||||
async () => {
|
||||
const destinatari = async () => {
|
||||
const res = await rest("rpc/giocatori_destinatari_evento", SERVIZIO, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ p_evento_id: EVENTO }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
return ((await res.json()) as Array<string | Record<string, string>>).map((d) =>
|
||||
typeof d === "string" ? d : Object.values(d)[0],
|
||||
);
|
||||
};
|
||||
|
||||
// Con i convocati: solo quelli, allenatore escluso anche se attivo.
|
||||
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ convocati: [GIOCATORE] }),
|
||||
});
|
||||
assert.deepEqual(await destinatari(), [GIOCATORE]);
|
||||
|
||||
// Senza convocati (tutta la rosa): i giocatori attivi, mai l'allenatore.
|
||||
await rest(`eventi_app?id=eq.${EVENTO}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ convocati: [] }),
|
||||
});
|
||||
const tutti = await destinatari();
|
||||
assert.ok(tutti.includes(GIOCATORE), "il giocatore attivo è tra i destinatari");
|
||||
assert.ok(!tutti.includes(SLOT), "l'allenatore non è tra i destinatari");
|
||||
},
|
||||
);
|
||||
|
||||
await prova("scollegare l'account toglie il ruolo", async () => {
|
||||
const res = await rest(`giocatori_squadra?id=eq.${SLOT}`, tokenAdmin, {
|
||||
|
||||
Reference in new issue
Block a user