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:
2026-09-08 14:08:52 +02:00
co-authored by Claude Sonnet 5
parent 8c9cf370e8
commit 080379cc64
14 changed files with 548 additions and 34 deletions
@@ -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)
);