Sistema i buchi del badge Pagellone: soglia minima voti e voto limitato ai convocati
Il badge Pagellone si sblocca ora solo con almeno 5 pagelle ricevute (VOTI_MINIMI_PAGELLA): un voto solo poteva sbloccarlo o farlo sparire senza significatività statistica. Aggiunto Giocatore.votiPagella per farlo funzionare. La migration m13 (DD-027) estende le policy RLS di M11 su pagelle_voti, mvp_voti e badge_social_voti: votante e votato devono essere convocati all'evento (prima solo un filtro applicativo), e per le pagelle anche pagelle_chiuse=false. Le policy admin restano permissive di proposito. Applicata anche al progetto cloud. Copertura test completa: unit sulla soglia minima, integration sulle nuove policy RLS (permessi.test.ts) e un end-to-end reale (pagella-badge.test.ts) sul modello di mvp-badge.test.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,9 @@ Prima versione, pre-release.
|
||||
della partita, e può usarlo chiunque sia autenticato: uno per volta, grazie al lock.
|
||||
- Votazione MVP legata all'evento CrAPP e non al referto CSI o allo Scout: si apre due ore
|
||||
dopo `data`+`ora` della partita, anche senza risultato caricato — [modules/mvp.md](modules/mvp.md).
|
||||
- Badge Pagellone: la media pagelle conta per il badge solo con almeno `VOTI_MINIMI_PAGELLA`
|
||||
(5) voti ricevuti — prima un singolo voto poteva sbloccarlo o farlo sparire senza nessuna
|
||||
significatività statistica — [modules/badge.md](modules/badge.md).
|
||||
- Sondaggio pre-partita aperto dalle 8:00 del giorno della partita fino al fischio d'inizio
|
||||
(poi resta chiuso, anche nei giorni successivi) e pulsante «Avvisa tutti del sondaggio» per
|
||||
gli amministratori (`POST /api/public/apri-sondaggio`); nessun cron, l'invio è manuale.
|
||||
@@ -79,6 +82,10 @@ Prima versione, pre-release.
|
||||
`pagelle_voti`.
|
||||
- Al voto MVP partecipano solo i presenti (o in ritardo) di quell'evento; il filtro è
|
||||
applicativo, non RLS ([modules/mvp.md](modules/mvp.md)).
|
||||
- Migration `m13_convocati_e_pagelle_chiuse` (DD-027): la policy di M11 su `pagelle_voti`,
|
||||
`mvp_voti` e `badge_social_voti` verifica ora anche che votante e votato siano tra i
|
||||
convocati dell'evento, e per le sole pagelle che `pagelle_chiuse` sia falso — prima erano
|
||||
filtri solo applicativi, aggirabili scrivendo direttamente su PostgREST.
|
||||
- La suite copre i rifiuti `401` di `richiediAdmin` (DD-024), i permessi di
|
||||
`badge_social_voti` e le deroghe admin di M11, e verifica che un ripensamento non riscriva
|
||||
`risposto_il` (trigger di `m9`).
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ nessuna di esse da M4 (DD-011).
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `eventi_app` | solo admin (nell'app li gestisce la rotta `/eventi`, già riservata) |
|
||||
| `risposte_presenze`, `cacche_partita` | il giocatore sulla propria riga (`giocatore_id`), più gli admin |
|
||||
| `pagelle_voti`, `mvp_voti`, `badge_social_voti` | il votante sui propri voti (`votante_id`), più gli admin |
|
||||
| `pagelle_voti`, `mvp_voti`, `badge_social_voti` | il votante sui propri voti (`votante_id`), se votante e votato sono convocati all'evento (`m13`); solo per le pagelle anche `pagelle_chiuse = false`; gli admin senza questi vincoli |
|
||||
| `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` | qualsiasi autenticato: nell'interfaccia non hanno gate |
|
||||
| `profili_giocatore` | il giocatore sul proprio profilo, admin su tutti (DD-016, DD-017) |
|
||||
| `giocatori_squadra` | admin; il giocatore può solo reclamare uno slot libero (DD-016) |
|
||||
@@ -63,9 +63,9 @@ La tabella è verificata da `test/integration/permessi.test.ts` contro il databa
|
||||
|
||||
| Tabella | Scopo | Note |
|
||||
| ------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `mvp_voti` | Voti MVP assegnati a fine partita. | Un voto per votante e partita; auto-voto rifiutato (`mvp_no_autovoto`, migration `m12_niente_autovoto`). |
|
||||
| `pagelle_voti` | Voti anonimi assegnati ai giocatori. | Usati per il voto medio. Voto 1-10 e auto-voto rifiutato dai vincoli della v1.0. |
|
||||
| `badge_social_voti` | Voti social per i badge. | Un voto per categoria, votante e partita; auto-voto rifiutato (`badge_social_no_autovoto`, `m12_niente_autovoto`). |
|
||||
| `mvp_voti` | Voti MVP assegnati a fine partita. | Un voto per votante e partita; auto-voto rifiutato (`mvp_no_autovoto`, migration `m12_niente_autovoto`); votante e votato devono essere convocati all'evento (RLS, `m13_convocati_e_pagelle_chiuse`). |
|
||||
| `pagelle_voti` | Voti anonimi assegnati ai giocatori. | Usati per il voto medio. Voto 1-10 e auto-voto rifiutato dai vincoli della v1.0; votante/votato convocati e `pagelle_chiuse = false` richiesti dalla RLS di `m13_convocati_e_pagelle_chiuse`. |
|
||||
| `badge_social_voti` | Voti social per i badge. | Un voto per categoria, votante e partita; auto-voto rifiutato (`badge_social_no_autovoto`, `m12_niente_autovoto`); votante e votato devono essere convocati all'evento (RLS, `m13_convocati_e_pagelle_chiuse`). |
|
||||
|
||||
## Turni e notifiche
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ Serve a rispondere a domande del tipo:
|
||||
| [DD-024](#dd-024--le-route-che-avvisano-la-squadra-chiedono-le-credenziali) | Route di notifica autenticate |
|
||||
| [DD-025](#dd-025--il-promemoria-palloni-lo-manda-ladmin-per-un-evento) | Promemoria palloni manuale |
|
||||
| [DD-026](#dd-026--il-testo-della-notifica-viaggia-dentro-la-push) | Payload push cifrato |
|
||||
| [DD-027](#dd-027--chi-vota-deve-essere-convocato-non-solo-autenticato-come-sé-stesso) | Voto limitato ai convocati |
|
||||
|
||||
**In valutazione**
|
||||
|
||||
@@ -970,3 +971,56 @@ prima di inviare.
|
||||
**Riesame**
|
||||
Se servisse mandare payload più grandi del limite del protocollo, o se un servizio push
|
||||
smettesse di accettare corpi cifrati (nessuno lo fa: è lo standard).
|
||||
|
||||
### DD-027 — Chi vota deve essere convocato, non solo autenticato come sé stesso
|
||||
|
||||
**Stato:** accettata · **Data:** 8 settembre 2026
|
||||
|
||||
**Contesto**
|
||||
Un audit del modulo Badge (`docs/modules/badge.md`) ha trovato due filtri rimasti solo
|
||||
applicativi dopo DD-023: la policy di M11 garantisce che `votante_id` sia lo slot collegato
|
||||
all'account di chi scrive, ma non controlla che **votante e votato fossero convocati**
|
||||
all'evento — un utente che scrive direttamente su PostgREST (bypassando l'interfaccia) poteva
|
||||
votare o essere votato in una partita a cui non aveva partecipato, gonfiando `mediaVoto`,
|
||||
`mvp` o un badge social a piacere. Allo stesso modo, `eventi_app.pagelle_chiuse` nascondeva
|
||||
solo i bottoni in UI: un voto pagella "fuori tempo" restava tecnicamente possibile.
|
||||
|
||||
**Decisione**
|
||||
La policy "Ognuno gestisce i propri voti ..." di `pagelle_voti`, `mvp_voti` e
|
||||
`badge_social_voti` (M11) guadagna un controllo aggiuntivo tramite la funzione
|
||||
`evento_permette_voto()` (migration `m13_convocati_e_pagelle_chiuse`): verifica che sia
|
||||
`votante_id` sia `votato_id` compaiano in `eventi_app.convocati` per quel `match_id`
|
||||
(`convocati` vuoto = tutta la rosa, la stessa convenzione di `convocatiEvento()` in
|
||||
`eventi.ts`), e — solo per le pagelle — che `pagelle_chiuse` sia falso. Le policy admin
|
||||
restano invariate e permissive: un amministratore deve poter correggere un voto anche fuori
|
||||
convocazione o dopo la chiusura.
|
||||
|
||||
Il controllo si ferma alla **convocazione**, non alla **presenza reale**: per l'MVP, ad
|
||||
esempio, l'interfaccia limita già il voto ai soli presenti/in ritardo
|
||||
(`usePresenzeEvento`), un filtro più stretto che resta solo applicativo — un convocato ma
|
||||
assente passa ancora a livello database. Stringere fino a quel punto avrebbe richiesto
|
||||
leggere `risposte_presenze` dentro la policy, un salto di complessità non giustificato
|
||||
dall'audit che ha originato questa decisione.
|
||||
|
||||
**Alternative scartate**
|
||||
|
||||
- Un trigger `BEFORE INSERT/UPDATE` invece di RLS → si applicherebbe anche alla service key
|
||||
e agli admin, bloccando correzioni legittime fuori convocazione; la RLS, applicata solo
|
||||
alla policy non-admin, li esclude naturalmente.
|
||||
- Controllare anche la presenza reale (`risposte_presenze`), non solo la convocazione →
|
||||
scope maggiore del gap trovato in audit, e specifico dell'MVP (pagelle e badge social non
|
||||
hanno un concetto di "presente" distinto da "convocato" nell'interfaccia attuale).
|
||||
|
||||
**Conseguenze**
|
||||
|
||||
- Un evento senza `convocati` esplicito (lista vuota, il caso più comune oggi) non cambia
|
||||
comportamento: tutta la rosa resta votabile, come prima.
|
||||
- I test di `test/integration/permessi.test.ts` sono la definizione eseguibile anche di
|
||||
questa parte della tabella dei permessi (voti non convocati rifiutati, `pagelle_chiuse`
|
||||
rifiutata a database, controlli positivi che provano che un voto legittimo passa ancora).
|
||||
- Resta un gap conosciuto e documentato (non quello risolto qui): che il votante fosse
|
||||
davvero presente, non solo convocato, per MVP/pagelle/badge social.
|
||||
|
||||
**Riesame**
|
||||
Se un giorno servisse bloccare anche il voto di un convocato-ma-assente a livello database,
|
||||
non solo in UI.
|
||||
|
||||
+44
-13
@@ -31,8 +31,16 @@ badge assegnati per voto dai compagni.
|
||||
inclusive, vince l'ultima raggiunta o superata. Per la maggior parte dei badge il valore è
|
||||
già pronto: `Giocatore` arriva da `useRosa()` con presenze, palloni, serie, infortuni,
|
||||
ritardi, cacche e media pagelle già calcolati da altri moduli — `badges.ts` si limita a
|
||||
confrontarli con le soglie. Fanno eccezione, con logica propria descritta sotto, l'MVP e i
|
||||
badge social.
|
||||
confrontarli con le soglie. Fanno eccezione, con logica propria descritta sotto, il
|
||||
Pagellone, l'MVP e i badge social.
|
||||
- **Badge Pagellone** (`pagella`, in `badgeDefs`): a differenza degli altri badge da
|
||||
contatore, richiede un numero minimo di voti (`VOTI_MINIMI_PAGELLA = 5`, `badges.ts`) prima
|
||||
che `g.mediaVoto` conti — sotto soglia `valore(g)` è forzato a `0` (badge bloccato), anche
|
||||
con una media altissima. Aggiunto perché senza minimo un singolo voto poteva
|
||||
sbloccare/far sparire il badge senza nessuna significatività statistica (vedi
|
||||
[pagelle.md](pagelle.md) per la pipeline voto → media, qui non ripetuta). Il numero di voti
|
||||
ricevuti arriva in `Giocatore.votiPagella` (`rosa.ts`), popolato insieme a `mediaVoto` dalla
|
||||
stessa `mediePagelle()`.
|
||||
- **Badge social** (`badge-social.ts`, tabella `badge_social_voti`): 5 categorie fisse per
|
||||
partita ("Compagno affidabile", "Miglior spirito di squadra", "Fair play", "Meme della
|
||||
partita", "Cuore del gruppo"), votabili una volta a testa per categoria/partita
|
||||
@@ -72,7 +80,7 @@ soglia raggiunta o superata (soglie inclusive), oltre l'oro resta oro.
|
||||
| id | nome | come si guadagna | soglie B/A/O |
|
||||
| --- | --- | --- | --- |
|
||||
| `mvp` | MVP | partite vinte nettamente al voto MVP dei compagni (`g.mvp`, vedi pipeline sopra) | 1 / 3 / 5 |
|
||||
| `pagella` | Pagellone | media dei voti pagella ricevuti dai compagni a fine partita (`g.mediaVoto`) | 6.5 / 7.5 / 8.5 |
|
||||
| `pagella` | Pagellone | media dei voti pagella ricevuti dai compagni a fine partita (`g.mediaVoto`), solo se ne ha ricevuti almeno `VOTI_MINIMI_PAGELLA` (5) | 6.5 / 7.5 / 8.5 |
|
||||
| `palloni` | Sherpa dei palloni | quante volte ti sei incaricato di portare la sacca palloni (`g.palloni`) | 3 / 6 / 10 |
|
||||
| `presenze` | Presenza fissa | totale presenze a eventi/partite in stagione (`g.presenze`) | 5 / 15 / 30 |
|
||||
| `serie-allenamenti` | Sempre in palestra | allenamenti consecutivi presenti, senza saltarne uno (`g.serieAllenamenti`) | 3 / 6 / 10 |
|
||||
@@ -126,18 +134,17 @@ categoria): nessun bug trovato nella logica di calcolo di nessuno dei 16 badge.
|
||||
**Badge normali** — `badges.ts` testa la propria funzione pura (soglia → grado,
|
||||
`badges.test.ts`) sull'output di altri moduli:
|
||||
- `mvp`: soglie inclusive verificate (1→bronzo, 3→argento, 99→resta oro,
|
||||
`badges.test.ts:44-48`), progresso a metà (`:52-56`). **Unico badge normale con integration
|
||||
dedicato** (vedi sotto) perché la sua fonte, a differenza degli altri 5, passa da un'altra
|
||||
tabella di voto (`mvp_voti`) invece che da un contatore già calcolato altrove.
|
||||
`badges.test.ts:44-48`), progresso a metà (`:52-56`).
|
||||
- `pagella`: caso critico delle soglie decimali senza arrotondamento per eccesso — 6.4 →
|
||||
nessun grado, 6.5 → bronzo (`badges.test.ts:65-67`); un vero 6.49 non diventa "quasi
|
||||
bronzo".
|
||||
bronzo". Più la soglia minima di voti (vedi sotto).
|
||||
- `palloni`, `presenze`, `serie-allenamenti`, `serie-conferme`: stessa funzione di soglia già
|
||||
testata a fondo su `mvp`/`pagella`, coperti dagli invarianti generali
|
||||
(`badges.test.ts:154-159`: soglie crescenti, testi presenti, id unici) e da
|
||||
`collezioneBadge`/`prossimoTraguardo` con valori al massimo (`:119-144`).
|
||||
- Nessun integration dedicato per questi 5: non toccano il database, le statistiche sorgente
|
||||
- Nessun integration dedicato per questi 4: non toccano il database, le statistiche sorgente
|
||||
(`presenze.test.ts`, `palloni-core.test.ts`, ecc.) sono già coperte nei rispettivi moduli.
|
||||
`mvp` e `pagella` fanno eccezione (sotto) perché la loro fonte passa da una tabella di voto.
|
||||
|
||||
**Badge segreti** — ognuno testato con la propria condizione esatta e il confine appena sotto
|
||||
(`badges.test.ts:77-109`): `s-tiebreak` (mediaVoto 7.9 non basta, serve 8), `s-mai-forfait`
|
||||
@@ -154,12 +161,29 @@ copertura di questo badge):
|
||||
partita/votante, l'ultimo sostituisce) e rifiuto dell'autovoto a database
|
||||
(`mvp_no_autovoto`).
|
||||
- `permessi.test.ts` — RLS di `m11`: il proprio voto MVP si registra (caso positivo), non
|
||||
si può votare a nome di un altro (caso negativo).
|
||||
si può votare a nome di un altro (caso negativo); RLS di `m13` (sotto): un votante o un
|
||||
votato non convocati vengono rifiutati.
|
||||
- `mvp-badge.test.ts` — end-to-end reale: scrive voti su `mvp_voti`, rilegge via REST come
|
||||
fa `useVotiMvp()`, calcola `mvpVintiPerGiocatore()` e verifica che `statoBadge()` assegni
|
||||
il grado corretto (bronzo a 1-2 vittorie nette, argento a 3), incluso un pareggio che non
|
||||
deve contare come vittoria.
|
||||
|
||||
**Badge Pagellone — pipeline end-to-end e soglia minima di voti** (stessa sessione di sopra,
|
||||
dopo l'analisi che ha trovato il gap "un voto solo sblocca il badge"):
|
||||
- Unit: `badges.test.ts:69-88` — sotto `VOTI_MINIMI_PAGELLA` (5) il badge resta bloccato anche
|
||||
con `mediaVoto: 10`; esattamente a 5 la media torna a contare; sopra soglia valgono le
|
||||
normali soglie di grado (`mediaVoto: 6.5` con 5 voti → bronzo, non oro).
|
||||
- Integration (`npx supabase start` richiesto):
|
||||
- `scritture.test.ts` — semantica dell'`upsert` di `pagelle_voti` e rifiuto dell'autovoto
|
||||
(`pagelle_no_autovoto`), già presente prima di questa sessione.
|
||||
- `permessi.test.ts` — RLS di `m13`: un votante o un votato non convocati vengono rifiutati
|
||||
(per tutte e tre le tabelle di voto, non solo le pagelle), e un voto pagella dopo
|
||||
`pagelle_chiuse` viene rifiutato anche a database, non solo nascosto in UI.
|
||||
- `pagella-badge.test.ts` (nuovo) — end-to-end reale: scrive voti su `pagelle_voti`, rilegge
|
||||
via REST come fa `usePagelle()`, calcola `mediePagelle()` e verifica che `statoBadge()`
|
||||
tenga il badge bloccato sotto soglia, lo sblocchi al voto minimo con il grado giusto, e
|
||||
applichi le soglie normali sopra soglia.
|
||||
|
||||
**Badge social** — nessuna delle 5 categorie ha logica *propria* nel codice: l'id è solo una
|
||||
chiave di raggruppamento, `conteggioCategoria`/`vincitoreCategoria`/`badgeSocialVinti` sono
|
||||
identici per tutte (`badge-social.ts:107-158`). Testare a fondo 2-3 categorie copre l'intero
|
||||
@@ -176,7 +200,7 @@ meccanismo:
|
||||
| # | id | tipo | test unit | test integration |
|
||||
| - | --- | --- | --- | --- |
|
||||
| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) |
|
||||
| 2 | `pagella` | normale | ✅ | non necessario |
|
||||
| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) |
|
||||
| 3 | `palloni` | normale | ✅ | non necessario |
|
||||
| 4 | `presenze` | normale | ✅ | non necessario |
|
||||
| 5 | `serie-allenamenti` | normale | ✅ (limite noto sotto) | non necessario |
|
||||
@@ -219,12 +243,19 @@ Trovati in audit, nessuno bloccante (nessun bug nella logica di calcolo):
|
||||
risposte precedenti a `m9`: su quelle righe la serie è un'approssimazione.
|
||||
- Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già
|
||||
"ottenuto" può sparire o apparire retroattivamente.
|
||||
- La policy di M11 garantisce che il voto sia firmato con il proprio `votante_id`, ma non
|
||||
che il votato sia un giocatore convocato per quella partita: quello resta un filtro solo
|
||||
applicativo (vale per MVP, pagelle e badge social).
|
||||
- Notifiche "nuovo badge" solo locali al dispositivo (localStorage), si ripetono cambiando
|
||||
browser o dispositivo.
|
||||
|
||||
**Risolto (M13, `20260908120000_m13_convocati_e_pagelle_chiuse.sql`)**: prima la policy di M11
|
||||
garantiva solo che il voto fosse firmato con il proprio `votante_id`, non che il votato (né il
|
||||
votante) fossero convocati per quella partita — filtro solo applicativo, aggirabile scrivendo
|
||||
direttamente su PostgREST. Ora `evento_permette_voto()` lo verifica anche a database per
|
||||
`pagelle_voti`, `mvp_voti` e `badge_social_voti` (convocati vuoto = tutta la rosa, stessa
|
||||
convenzione di `convocatiEvento()`), e per le sole pagelle verifica anche che
|
||||
`eventi_app.pagelle_chiuse` sia falso — prima un voto "fuori tempo" restava tecnicamente
|
||||
possibile bypassando l'interfaccia. Le policy admin restano permissive: un amministratore può
|
||||
ancora correggere un voto anche fuori convocazione o dopo la chiusura.
|
||||
|
||||
---
|
||||
|
||||
## Evoluzioni possibili
|
||||
|
||||
+9
-3
@@ -48,9 +48,15 @@ restano nel database ma non vengono più letti da nessuna schermata).
|
||||
- Nessuna scadenza o chiusura della votazione: una volta aperta resta aperta indefinitamente.
|
||||
- Il voto è legato a chi lo scrive: da `m11_scritture_per_ruolo` la policy impone che
|
||||
`votante_id` sia lo slot collegato all'account (DD-023). Su chi viene votato l'unico
|
||||
vincolo è che non sia il votante stesso (`mvp_no_autovoto`): che votante e votato fossero
|
||||
presenti a quella partita, e che siano passate due ore dall'inizio, restano filtri solo
|
||||
applicativi — chi scrive su PostgREST li aggira.
|
||||
vincolo diretto è che non sia il votante stesso (`mvp_no_autovoto`).
|
||||
Da `m13_convocati_e_pagelle_chiuse` la stessa policy verifica anche che **sia il votante sia
|
||||
il votato** siano tra i **convocati** dell'evento (`evento_permette_voto()`, convocati vuoto
|
||||
= tutta la rosa): prima era un filtro solo applicativo, ora un giocatore non convocato non
|
||||
può più votare né essere votato scrivendo direttamente su PostgREST. Restano invece solo
|
||||
applicativi, non controllati da nessuna policy: che votante e votato fossero **presenti**
|
||||
(non solo convocati: `presente`/`ritardo` in `usePresenzeEvento`, un controllo più stretto
|
||||
della sola convocazione) a quella partita, e le due ore d'attesa dall'inizio evento
|
||||
(`votoMvpAperto()`) — un amministratore, o chiunque scriva su PostgREST, passa comunque.
|
||||
- In caso di parità, nessun MVP viene assegnato per quella partita.
|
||||
|
||||
---
|
||||
|
||||
+17
-11
@@ -29,9 +29,13 @@ UI), `UNIQUE (match_id, votante_id, votato_id)`.
|
||||
- `useVotaPagella()` fa un upsert su `(match_id, votante_id, votato_id)`: si può votare più
|
||||
volte, l'ultimo voto sovrascrive il precedente.
|
||||
- `mediePagelle()` calcola la media aritmetica (arrotondata a un decimale) per giocatore su
|
||||
tutti i voti della stagione; `pagellePartita()` la calcola per singola partita;
|
||||
`mediaSquadra()` su tutti i voti di tutti — mostrata come StatTile in `squadra.tsx`.
|
||||
- `useRosa()` inietta la media stagionale nel campo `mediaVoto` di ogni giocatore.
|
||||
**tutti i voti mai ricevuti** — l'app non ha un concetto di stagione/reset, quindi non è
|
||||
"la media di questa stagione" ma lo storico completo; `pagellePartita()` la calcola per
|
||||
singola partita; `mediaSquadra()` su tutti i voti di tutti — mostrata come StatTile in
|
||||
`squadra.tsx`.
|
||||
- `useRosa()` inietta questa media storica nel campo `mediaVoto` di ogni giocatore, insieme al
|
||||
numero di voti ricevuti (`votiPagella`) — usato dal badge Pagellone (vedi
|
||||
[badge.md](badge.md)) per richiedere un minimo di voti prima che la media conti.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,25 +43,27 @@ UI), `UNIQUE (match_id, votante_id, votato_id)`.
|
||||
|
||||
- Anti auto-voto imposto anche a livello database (constraint, non solo filtro UI).
|
||||
- L'admin può marcare un evento come `pagelleChiuse` (`eventi.ts`), che nasconde i bottoni di
|
||||
voto in UI.
|
||||
voto in UI **e**, da M13, rifiuta anche a database un voto scritto dopo la chiusura (RLS
|
||||
`evento_permette_voto()`, `pagelle_voti`).
|
||||
- Da M13 anche il votante e il votato devono essere convocati all'evento: verificato a
|
||||
database, non solo in UI (stessa RLS di sopra).
|
||||
|
||||
---
|
||||
|
||||
## Limiti noti
|
||||
|
||||
- **`pagelleChiuse` è solo un flag UI**: nessuna policy RLS lo controlla, quindi un voto
|
||||
"fuori tempo" resta tecnicamente possibile bypassando l'interfaccia.
|
||||
- **L'anonimato è solo applicativo, non tecnico**: la riga salvata contiene sia `votante_id`
|
||||
sia `votato_id`, leggibili da chiunque sia autenticato (policy SELECT aperta). La UI non
|
||||
mostra mai il votante, ma il dato non è né aggregato né mascherato lato server.
|
||||
- Nessun controllo a livello database che il votante sia realmente un convocato della
|
||||
partita: solo filtro applicativo.
|
||||
- La media non richiede un numero minimo di voti: con un solo voto ricevuto, la media
|
||||
coincide con quel voto.
|
||||
- La media mostrata nel profilo non richiede un numero minimo di voti: con un solo voto
|
||||
ricevuto, la media coincide con quel voto. Il badge Pagellone (`badge.md`) applica invece un
|
||||
minimo di voti prima di considerarla — la StatTile del profilo no.
|
||||
- Le due regole di M13 (convocazione, `pagelle_chiuse`) valgono solo per la policy "Ognuno
|
||||
gestisce i propri voti pagella": un amministratore può ancora correggere un voto fuori
|
||||
convocazione o dopo la chiusura, di proposito (deve poter sistemare un errore).
|
||||
|
||||
---
|
||||
|
||||
## Evoluzioni possibili
|
||||
|
||||
- Una RPC o vista che nasconda `votante_id` per un anonimato garantito anche lato dati.
|
||||
- Far rispettare `pagelleChiuse` anche via RLS.
|
||||
|
||||
+7
-1
@@ -58,6 +58,12 @@ export type BadgeDef = {
|
||||
notificaPush?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Voti minimi perché la media pagelle conti per il badge Pagellone: sotto soglia una singola
|
||||
* pagella potrebbe sbloccarlo (o farlo sparire) senza significatività statistica.
|
||||
*/
|
||||
export const VOTI_MINIMI_PAGELLA = 5;
|
||||
|
||||
export const badgeDefs: BadgeDef[] = [
|
||||
{
|
||||
id: "mvp",
|
||||
@@ -77,7 +83,7 @@ export const badgeDefs: BadgeDef[] = [
|
||||
unita: "di media voto",
|
||||
icon: ClipboardCheck,
|
||||
soglie: { bronzo: 6.5, argento: 7.5, oro: 8.5 },
|
||||
valore: (g) => g.mediaVoto,
|
||||
valore: (g) => (g.votiPagella >= VOTI_MINIMI_PAGELLA ? g.mediaVoto : 0),
|
||||
},
|
||||
{
|
||||
id: "palloni",
|
||||
|
||||
@@ -37,6 +37,8 @@ export type Giocatore = {
|
||||
mvp: number;
|
||||
/** Media delle pagelle ricevute dai compagni (1-10). */
|
||||
mediaVoto: number;
|
||||
/** Quante pagelle ha ricevuto: sotto la soglia minima il badge Pagellone resta bloccato. */
|
||||
votiPagella: number;
|
||||
/** Quante volte ha portato i palloni. */
|
||||
palloni: number;
|
||||
/** Partite di campionato con almeno 3 cacche dichiarate. */
|
||||
@@ -105,6 +107,7 @@ export const giocatori: Giocatore[] = rosaCSI
|
||||
serieConferme: 0,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
infortuni: 0,
|
||||
ritardi: 0,
|
||||
palloni: 0,
|
||||
|
||||
+4
-1
@@ -60,7 +60,10 @@ function arrotonda(n: number) {
|
||||
return Math.round(n * 10) / 10;
|
||||
}
|
||||
|
||||
/** Media stagionale di ciascun giocatore: giocatoreId -> media e numero di voti. */
|
||||
/**
|
||||
* Media storica di ciascun giocatore su tutti i voti mai ricevuti (l'app non ha un concetto
|
||||
* di stagione/reset): giocatoreId -> media e numero di voti.
|
||||
*/
|
||||
export function mediePagelle(voti: VotoPagella[]): Record<string, MediaPagella> {
|
||||
const somma: Record<string, { tot: number; n: number }> = {};
|
||||
for (const v of voti) {
|
||||
|
||||
@@ -91,6 +91,7 @@ export function useRosa(): Giocatore[] {
|
||||
serieConferme: serieConferme(g.id, eventi, tempi),
|
||||
mvp: mvpVinti[g.id] ?? 0,
|
||||
mediaVoto: medie[g.id]?.media ?? 0,
|
||||
votiPagella: medie[g.id]?.voti ?? 0,
|
||||
palloni: palloni[g.id] ?? 0,
|
||||
cacche: statCacche[g.id]?.giornateTop ?? 0,
|
||||
cacchePartita: statCacche[g.id]?.media ?? 0,
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
-- M13 — Le tabelle di voto controllano anche a database chi può votare chi.
|
||||
--
|
||||
-- Due gap segnalati in `docs/modules/badge.md` (audit del modulo Badge), entrambi finora
|
||||
-- solo filtri applicativi, aggirabili da chi scrive direttamente su PostgREST:
|
||||
--
|
||||
-- 1. Niente impediva di votare (o essere votati) da/per un giocatore non convocato
|
||||
-- all'evento — vale per pagelle_voti, mvp_voti, badge_social_voti.
|
||||
-- 2. `eventi_app.pagelle_chiuse` nascondeva solo i bottoni in UI: un voto "fuori tempo"
|
||||
-- restava tecnicamente possibile.
|
||||
--
|
||||
-- Le policy "Ognuno gestisce i propri voti ..." di M11 vengono estese con un controllo sui
|
||||
-- convocati dell'evento: `convocati` vuoto significa "tutta la rosa" (stessa convenzione di
|
||||
-- `convocatiEvento()` in eventi.ts), quindi il controllo si applica solo se la lista non è
|
||||
-- vuota. Le policy admin restano invariate: un amministratore può correggere un voto anche
|
||||
-- per un giocatore che non risultava convocato o dopo la chiusura delle pagelle.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.evento_permette_voto(
|
||||
p_match_id text, p_votante_id text, p_votato_id text, p_richiede_pagelle_aperte boolean
|
||||
) RETURNS boolean
|
||||
LANGUAGE sql STABLE AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.eventi_app e
|
||||
WHERE e.id = p_match_id
|
||||
AND (cardinality(e.convocati) = 0 OR p_votante_id = ANY(e.convocati))
|
||||
AND (cardinality(e.convocati) = 0 OR p_votato_id = ANY(e.convocati))
|
||||
AND (NOT p_richiede_pagelle_aperte OR NOT e.pagelle_chiuse)
|
||||
);
|
||||
$$;
|
||||
|
||||
-- --- pagelle: anche convocazione e chiusura --------------------------------------------
|
||||
DROP POLICY "Ognuno gestisce i propri voti pagella" ON public.pagelle_voti;
|
||||
|
||||
CREATE POLICY "Ognuno gestisce i propri voti pagella" ON public.pagelle_voti
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||
)
|
||||
AND public.evento_permette_voto(match_id, votante_id, votato_id, true)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||
)
|
||||
AND public.evento_permette_voto(match_id, votante_id, votato_id, true)
|
||||
);
|
||||
|
||||
-- --- MVP: solo convocazione, la votazione non ha un flag di chiusura -------------------
|
||||
DROP POLICY "Ognuno gestisce il proprio voto MVP" ON public.mvp_voti;
|
||||
|
||||
CREATE POLICY "Ognuno gestisce il proprio voto MVP" ON public.mvp_voti
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||
)
|
||||
AND public.evento_permette_voto(match_id, votante_id, votato_id, false)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||
)
|
||||
AND public.evento_permette_voto(match_id, votante_id, votato_id, false)
|
||||
);
|
||||
|
||||
-- --- badge social: solo convocazione, stesso motivo -------------------------------------
|
||||
DROP POLICY "Ognuno gestisce i propri voti social" ON public.badge_social_voti;
|
||||
|
||||
CREATE POLICY "Ognuno gestisce i propri voti social" ON public.badge_social_voti
|
||||
FOR ALL TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||
)
|
||||
AND public.evento_permette_voto(match_id, votante_id, votato_id, false)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.giocatori_squadra g
|
||||
WHERE g.id = votante_id AND g.auth_user_id = auth.uid()
|
||||
)
|
||||
AND public.evento_permette_voto(match_id, votante_id, votato_id, false)
|
||||
);
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Badge Pagellone end-to-end contro il database locale: `bun test/integration/pagella-badge.test.ts`.
|
||||
*
|
||||
* I test unitari (`test/unit/pagelle.test.ts`, `test/unit/badges.test.ts`) verificano
|
||||
* `mediePagelle()` e `statoBadge()` come funzioni pure, con voti costruiti a mano. Qui invece
|
||||
* si scrivono voti veri su `pagelle_voti`, si rileggono via REST con la stessa selezione di
|
||||
* `usePagelle()`, e si passa il risultato attraverso `mediePagelle()` fino a `statoBadge()` sul
|
||||
* badge `pagella`: se una colonna cambia nome o la mappatura si rompe, qui il grado del badge
|
||||
* torna sbagliato anche se i test unitari restano verdi, perché quelli non toccano mai il
|
||||
* database.
|
||||
*
|
||||
* Copre in particolare la soglia minima di voti (`VOTI_MINIMI_PAGELLA`, aggiunta per il gap
|
||||
* "un solo voto sblocca/toglie il badge" segnalato in `docs/modules/badge.md`): con voti reali
|
||||
* letti dal database, non solo con numeri scelti a mano.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
|
||||
* usa id con il prefisso `test-pagella-badge`, che nessun dato vero può avere.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { badgeDefs, statoBadge, VOTI_MINIMI_PAGELLA } from "@/lib/badges";
|
||||
import { mediePagelle, type VotoPagella } from "@/lib/pagelle";
|
||||
import { giocatori, type Giocatore } from "@/lib/crapp-data";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("badge Pagellone sul database", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("pagella-badge");
|
||||
} else {
|
||||
const { url: URL_BASE, servizio: SERVIZIO } = locale;
|
||||
console.log(`badge Pagellone su ${URL_BASE}`);
|
||||
|
||||
const PREFISSO = "test-pagella-badge";
|
||||
const pagellaDef = badgeDefs.find((b) => b.id === "pagella")!;
|
||||
|
||||
const rest = (percorso: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
async function upsert(riga: VotoPagella) {
|
||||
const res = await rest("pagelle_voti?on_conflict=match_id,votante_id,votato_id", {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify(riga),
|
||||
});
|
||||
if (!res.ok) throw new Error(`upsert su pagelle_voti: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
/** Rilegge esattamente come `usePagelle()`. */
|
||||
async function leggiVoti(): Promise<VotoPagella[]> {
|
||||
const res = await rest(
|
||||
`pagelle_voti?match_id=like.${PREFISSO}-*&select=match_id,votante_id,votato_id,voto`,
|
||||
);
|
||||
return (await res.json()) as VotoPagella[];
|
||||
}
|
||||
|
||||
/** Un giocatore azzerato, come in `test/unit/badges.test.ts`. */
|
||||
function giocatoreAzzerato(mediaVoto: number, votiPagella: number): Giocatore {
|
||||
return { ...giocatori[0]!, mediaVoto, votiPagella, mvp: 0, palloni: 0, presenze: 0 };
|
||||
}
|
||||
|
||||
try {
|
||||
await prova(
|
||||
`sotto ${VOTI_MINIMI_PAGELLA} voti il badge resta bloccato anche con media alta`,
|
||||
async () => {
|
||||
// "pg1" riceve 4 voti da 9-10 (media altissima) ma sotto la soglia minima di voti:
|
||||
// il badge non deve sbloccarsi nonostante la media sarebbe oro.
|
||||
for (const [i, votante] of ["va", "vb", "vc", "vd"].entries()) {
|
||||
await upsert({
|
||||
match_id: `${PREFISSO}-m${i + 1}`,
|
||||
votante_id: votante,
|
||||
votato_id: "pg1",
|
||||
voto: 9,
|
||||
});
|
||||
}
|
||||
|
||||
const voti = await leggiVoti();
|
||||
const medie = mediePagelle(voti);
|
||||
assert.equal(medie["pg1"]?.voti, 4, "4 voti scritti, 4 riletti");
|
||||
assert.equal(medie["pg1"]?.media, 9, "media alta");
|
||||
|
||||
const badgePg1 = statoBadge(
|
||||
pagellaDef,
|
||||
giocatoreAzzerato(medie["pg1"]!.media, medie["pg1"]!.voti),
|
||||
);
|
||||
assert.equal(badgePg1.grado, null, "4 voti < 5: il badge resta bloccato");
|
||||
},
|
||||
);
|
||||
|
||||
await prova(
|
||||
`al ${VOTI_MINIMI_PAGELLA}° voto il badge Pagellone si sblocca con il grado giusto`,
|
||||
async () => {
|
||||
// Un quinto voto a "pg1" (stesso giocatore del test precedente) fa scattare la
|
||||
// soglia minima: la media (9) sblocca subito l'oro (soglia 8.5).
|
||||
await upsert({
|
||||
match_id: `${PREFISSO}-m5`,
|
||||
votante_id: "ve",
|
||||
votato_id: "pg1",
|
||||
voto: 9,
|
||||
});
|
||||
|
||||
const medie = mediePagelle(await leggiVoti());
|
||||
assert.equal(medie["pg1"]?.voti, 5);
|
||||
const badgePg1 = statoBadge(
|
||||
pagellaDef,
|
||||
giocatoreAzzerato(medie["pg1"]!.media, medie["pg1"]!.voti),
|
||||
);
|
||||
assert.equal(badgePg1.grado, "oro", "5 voti raggiunti: la media conta, è oro");
|
||||
},
|
||||
);
|
||||
|
||||
await prova("le soglie di grado normali si applicano sopra il minimo di voti", async () => {
|
||||
// "pg2" riceve 5 voti che fanno una media di bronzo (6.5), non oro: verifica che sopra
|
||||
// la soglia minima il grado dipenda ancora dalla media, non solo dal numero di voti.
|
||||
for (const [i, votante] of ["va", "vb", "vc", "vd", "ve"].entries()) {
|
||||
await upsert({
|
||||
match_id: `${PREFISSO}-n${i + 1}`,
|
||||
votante_id: votante,
|
||||
votato_id: "pg2",
|
||||
voto: i < 4 ? 6 : 9, // (6*4+9)/5 = 6.6 -> arrotondato 6.6, sopra 6.5
|
||||
});
|
||||
}
|
||||
const medie = mediePagelle(await leggiVoti());
|
||||
assert.equal(medie["pg2"]?.voti, 5);
|
||||
assert.equal(medie["pg2"]?.media, 6.6);
|
||||
const badgePg2 = statoBadge(
|
||||
pagellaDef,
|
||||
giocatoreAzzerato(medie["pg2"]!.media, medie["pg2"]!.voti),
|
||||
);
|
||||
assert.equal(badgePg2.grado, "bronzo", "media 6.6: bronzo, non oro");
|
||||
});
|
||||
} finally {
|
||||
await rest(`pagelle_voti?match_id=like.${PREFISSO}-*`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
riepilogo("pagella-badge");
|
||||
}
|
||||
@@ -406,6 +406,142 @@ if (!locale) {
|
||||
assert.equal(await righeToccate(pagella), 1, "e per cancellare il voto di un altro");
|
||||
});
|
||||
|
||||
// M13: le tabelle di voto controllano anche a database chi può votare chi, non solo
|
||||
// chi firma il voto. Prima di M13 un convocato poteva votare/essere votato in un
|
||||
// evento a cui non aveva partecipato, e un voto pagella restava possibile anche a
|
||||
// `pagelle_chiuse` — entrambi filtri solo applicativi (segnalati in `badge.md`).
|
||||
// Nota: `tokenAdmin` non va usato per queste prove, la policy admin di M11 non ha il
|
||||
// controllo sui convocati (l'admin corregge anche dati fuori convocazione di
|
||||
// proposito) e farebbe passare tutto a prescindere, senza provare niente sulla nuova
|
||||
// policy. Si usa solo `tokenGiocatore` (g1), un votante non-admin vero.
|
||||
const EVENTO_SENZA_G1 = `${PREFISSO}-evento-senza-g1`;
|
||||
const EVENTO_CON_G1 = `${PREFISSO}-evento-con-g1`;
|
||||
const EVENTO_CHIUSO = `${PREFISSO}-evento-chiuso`;
|
||||
|
||||
await prova("un votante non convocato non può votare", async () => {
|
||||
const creato = await rest("eventi_app", tokenAdmin, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
id: EVENTO_SENZA_G1,
|
||||
tipo: "partita",
|
||||
titolo: "Partita senza g1 tra i convocati",
|
||||
data: "2026-01-02",
|
||||
ora: "20:00",
|
||||
luogo: "Palestra",
|
||||
convocati: ["g2", "g5"],
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(creato), 1, "l'evento con convocati si crea");
|
||||
|
||||
const votanteEscluso = await rest("pagelle_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_SENZA_G1,
|
||||
votante_id: "g1",
|
||||
votato_id: "g2",
|
||||
voto: 7,
|
||||
}),
|
||||
});
|
||||
assert.ok(!votanteEscluso.ok, `g1 non era convocato, non vota (${votanteEscluso.status})`);
|
||||
});
|
||||
|
||||
await prova("un votante convocato non può votare chi non lo era", async () => {
|
||||
const creato = await rest("eventi_app", tokenAdmin, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
id: EVENTO_CON_G1,
|
||||
tipo: "partita",
|
||||
titolo: "Partita con g1 convocato, g2 no",
|
||||
data: "2026-01-02",
|
||||
ora: "20:00",
|
||||
luogo: "Palestra",
|
||||
convocati: ["g1", "g5"],
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(creato), 1, "l'evento con convocati si crea");
|
||||
|
||||
const votatoEscluso = await rest("badge_social_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_CON_G1,
|
||||
categoria: "cuore",
|
||||
votante_id: "g1",
|
||||
votato_id: "g2",
|
||||
votato_nome: "Due",
|
||||
}),
|
||||
});
|
||||
assert.ok(
|
||||
!votatoEscluso.ok,
|
||||
`g2 non era convocato, non è votabile (${votatoEscluso.status})`,
|
||||
);
|
||||
|
||||
// Controllo positivo sullo stesso evento: g1 è convocato e vota g5, anche lui
|
||||
// convocato — senza questo, il test sopra potrebbe fallire per un altro motivo
|
||||
// (es. un evento inesistente) e sembrare comunque corretto.
|
||||
const votoValido = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_CON_G1,
|
||||
votante_id: "g1",
|
||||
votato_id: "g5",
|
||||
votato_nome: "Cinque",
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(votoValido), 1, "votante e votato convocati: il voto passa");
|
||||
});
|
||||
|
||||
await prova("pagelle_chiuse blocca anche a database, non solo in UI", async () => {
|
||||
const creato = await rest("eventi_app", tokenAdmin, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "return=representation" },
|
||||
body: JSON.stringify({
|
||||
id: EVENTO_CHIUSO,
|
||||
tipo: "partita",
|
||||
titolo: "Partita con pagelle chiuse",
|
||||
data: "2026-01-03",
|
||||
ora: "20:00",
|
||||
luogo: "Palestra",
|
||||
pagelle_chiuse: true,
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(creato), 1, "l'evento con pagelle chiuse si crea");
|
||||
|
||||
const pagellaFuoriTempo = await rest("pagelle_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_CHIUSO,
|
||||
votante_id: "g1",
|
||||
votato_id: "g5",
|
||||
voto: 7,
|
||||
}),
|
||||
});
|
||||
assert.ok(
|
||||
!pagellaFuoriTempo.ok,
|
||||
`pagelle chiuse: voto rifiutato (${pagellaFuoriTempo.status})`,
|
||||
);
|
||||
|
||||
// Il flag riguarda solo le pagelle: MVP e badge social non hanno un concetto di
|
||||
// "chiusura" (mvp.md lo segnala esplicitamente come limite noto), quindi restano
|
||||
// votabili sullo stesso evento.
|
||||
const mvpAncoraAperto = await rest("mvp_voti", tokenGiocatore, {
|
||||
method: "POST",
|
||||
headers: { Prefer: "resolution=merge-duplicates,return=representation" },
|
||||
body: JSON.stringify({
|
||||
match_id: EVENTO_CHIUSO,
|
||||
votante_id: "g1",
|
||||
votato_id: "g5",
|
||||
votato_nome: "Cinque",
|
||||
}),
|
||||
});
|
||||
assert.equal(await righeToccate(mvpAncoraAperto), 1, "l'MVP non ha un flag di chiusura");
|
||||
});
|
||||
|
||||
// Il terzo gruppo di DD-023: tabelle lasciate aperte **di proposito**, perché
|
||||
// nell'interfaccia non hanno nessun gate — il turno palloni se lo passa chiunque, e lo
|
||||
// Scout Live lo apre chiunque, con il solo lock di sessione a tenere l'ordine.
|
||||
|
||||
@@ -25,6 +25,7 @@ function g(valori: Partial<Giocatore> = {}): Giocatore {
|
||||
presenze: 0,
|
||||
mvp: 0,
|
||||
mediaVoto: 0,
|
||||
votiPagella: 0,
|
||||
palloni: 0,
|
||||
cacche: 0,
|
||||
cacchePartita: 0,
|
||||
@@ -66,6 +67,29 @@ const pagella = badgeDefs.find((b) => b.id === "pagella")!;
|
||||
assert.equal(gradoRaggiunto(pagella, 6.4), null);
|
||||
assert.equal(gradoRaggiunto(pagella, 6.5), "bronzo");
|
||||
|
||||
// Sotto i voti minimi il badge resta bloccato anche con una media altissima: un voto solo
|
||||
// non deve poter sbloccare/sfilare Pagellone.
|
||||
assert.equal(
|
||||
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 1 })).grado,
|
||||
null,
|
||||
"1 voto su 10: non basta, sotto la soglia minima",
|
||||
);
|
||||
assert.equal(
|
||||
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 4 })).grado,
|
||||
null,
|
||||
"4 voti: ancora sotto la soglia minima (5)",
|
||||
);
|
||||
assert.equal(
|
||||
statoBadge(pagella, g({ mediaVoto: 10, votiPagella: 5 })).grado,
|
||||
"oro",
|
||||
"5 voti: la soglia minima è raggiunta, la media conta",
|
||||
);
|
||||
assert.equal(
|
||||
statoBadge(pagella, g({ mediaVoto: 6.5, votiPagella: 5 })).grado,
|
||||
"bronzo",
|
||||
"sopra la soglia minima, valgono le normali soglie di grado",
|
||||
);
|
||||
|
||||
// --- badgeGiocatore ----------------------------------------------------------
|
||||
assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti");
|
||||
assert.ok(
|
||||
@@ -116,7 +140,9 @@ assert.equal(vuota.sbloccati.length, 0);
|
||||
assert.equal(vuota.inProgresso.length, badgeDefs.length);
|
||||
assert.equal(vuota.nascosti, badgeSegreti.length);
|
||||
|
||||
const piena = collezioneBadge(g({ mvp: 5, mediaVoto: 8, presenze: 30, infortuni: 3 }));
|
||||
const piena = collezioneBadge(
|
||||
g({ mvp: 5, mediaVoto: 8, votiPagella: 5, presenze: 30, infortuni: 3 }),
|
||||
);
|
||||
assert.equal(
|
||||
piena.ottenuti,
|
||||
piena.sbloccati.length + piena.segreti.length,
|
||||
@@ -133,6 +159,7 @@ assert.equal(
|
||||
g({
|
||||
mvp: 5,
|
||||
mediaVoto: 10,
|
||||
votiPagella: 5,
|
||||
palloni: 10,
|
||||
presenze: 30,
|
||||
serieAllenamenti: 10,
|
||||
|
||||
Reference in New Issue
Block a user