diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index bb9cba9..cadcb38 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -15,6 +15,11 @@ versioni `0.y.z` erano pre-release.
### Rimosso
+- **Foto tessera del profilo** — il giocatore non la carica più e l'admin non la vede nella scheda; il
+ completamento del profilo si calcola su dati personali (34%), documento (33%) e certificato (33%). Migration
+ `m29_senza_foto_tessera` (elimina la colonna `foto_path`); i file già caricati sono stati cancellati dal
+ bucket `profili-giocatore` (DD-044). Applicata in produzione il 01/10/2026, con la cancellazione dei 4 file già caricati.
+
- **Promemoria evento a 24 e 3 ore** — non partono più per nessun evento (allenamenti, partite, extra-campo):
niente notifica, push né email. Migration `m28_niente_promemoria_evento` (tolti i due job `pg_cron`;
funzione, tipi e notifiche già generate restano). Applicata in produzione il 01/10/2026 (DD-043).
diff --git a/docs/DATABASE.md b/docs/DATABASE.md
index 0aa1b87..819063e 100644
--- a/docs/DATABASE.md
+++ b/docs/DATABASE.md
@@ -66,10 +66,10 @@ Colonne di `giocatori_squadra` che meritano una nota:
## Storage
-| Bucket | Scopo | Note |
-| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `profili-giocatore` | Documento d'identità, certificato medico e foto tessera, in cartelle per giocatore (`/.`). | **Privato e destinato a restare tale**: documenti e dati sanitari non devono mai avere URL pubblici (DD-016 regola 4). Il giocatore gestisce solo la propria cartella; l'admin scarica tutto con signed URL a scadenza breve. |
-| `avatar-giocatori` | Foto profilo (`/avatar.jpg`). | **Pubblico**: foto informali. Qualsiasi autenticato può caricare, sostituire o eliminare un file (nessun controllo per proprietario: la maggior parte dei giocatori non ha ancora `auth_user_id`, DD-018). Letto da `src/lib/avatar-store.ts`. |
+| Bucket | Scopo | Note |
+| ------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `profili-giocatore` | Documento d'identità e certificato medico, in cartelle per giocatore (`/.`). | **Privato e destinato a restare tale**: documenti e dati sanitari non devono mai avere URL pubblici (DD-016 regola 4). Il giocatore gestisce solo la propria cartella; l'admin scarica tutto con signed URL a scadenza breve. |
+| `avatar-giocatori` | Foto profilo (`/avatar.jpg`). | **Pubblico**: foto informali. Qualsiasi autenticato può caricare, sostituire o eliminare un file (nessun controllo per proprietario: la maggior parte dei giocatori non ha ancora `auth_user_id`, DD-018). Letto da `src/lib/avatar-store.ts`. |
## Eventi e presenze
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 1afcd3e..58e9318 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -27,7 +27,7 @@ Tutto quello che è rilasciato, fino alla versione 1.0.0 (vedi `CHANGELOG.md`).
- [x] Dashboard amministratore
- [x] Download CSV dati
- [x] Profilo Giocatore — dati personali, documento d'identità, certificato medico
- (caricamento, scadenza, stato, download) e foto tessera; lo storico dei certificati
+ (caricamento, scadenza, stato, download); la foto tessera è stata tolta (DD-044); lo storico dei certificati
resta un'estensione futura
- [x] Gestione tesseramenti CSI — raccolta dati, export CSV e tracciamento di chi è già
tesserato (numero e data di tessera)
diff --git a/docs/decisions/DD-016.md b/docs/decisions/DD-016.md
index e01726c..66a09c0 100644
--- a/docs/decisions/DD-016.md
+++ b/docs/decisions/DD-016.md
@@ -16,7 +16,7 @@ Regole vincolanti:
1. **`giocatori_squadra` diventa progressivamente la source of truth** per l’anagrafica squadra. Durante la transizione, `crapp-data.ts` resta come **fallback** se il database non è disponibile o i dati non sono ancora migrati.
2. L’associazione **`auth_user_id` ↔ giocatore** è un’operazione **controllata e atomica** (es. al primo accesso da `/benvenuto`, con `UPDATE … WHERE auth_user_id IS NULL`). Il giocatore **non può modificare liberamente** `auth_user_id`; solo un admin può resettarlo in casi eccezionali.
-3. I file (documento identità, certificato, foto tessera) vivono nel bucket Storage **`profili-giocatore`**, configurato come **privato**.
+3. I file (documento identità, certificato; la foto tessera è stata tolta da DD-044) vivono nel bucket Storage **`profili-giocatore`**, configurato come **privato**.
4. Documenti personali e sanitari **non devono mai essere esposti tramite URL pubblici**. Accesso solo tramite client autenticato con policy RLS, o signed URL a scadenza breve per download admin.
5. Le **tabelle preesistenti non vengono modificate** (`eventi_app`, `risposte_presenze`, voti, palloni, scout, push, ecc.). Il profilo si aggancia agli ID `g1`…`g17` già in uso, senza migrare verso UUID per ora (coerente con DD-012).
6. La tabella `giocatori` (UUID) resta **invariata e non usata** dal modulo profilo.
diff --git a/docs/decisions/DD-017.md b/docs/decisions/DD-017.md
index 451bf9a..4ceee4f 100644
--- a/docs/decisions/DD-017.md
+++ b/docs/decisions/DD-017.md
@@ -15,7 +15,7 @@ Dalla dashboard amministratore, un admin può:
Restano fuori, e non cambiano:
-- i **file** (documento, certificato, foto): l'admin li scarica ma non li carica né li sostituisce. Un documento d'identità lo produce il suo titolare, e la catena di responsabilità deve restare leggibile;
+- i **file** (documento, certificato; la foto tessera è stata tolta da DD-044): l'admin li scarica ma non li carica né li sostituisce. Un documento d'identità lo produce il suo titolare, e la catena di responsabilità deve restare leggibile;
- il **giocatore**, che continua a non poter toccare i propri dati squadra.
**Alternative scartate**
diff --git a/docs/decisions/DD-044.md b/docs/decisions/DD-044.md
new file mode 100644
index 0000000..a82236f
--- /dev/null
+++ b/docs/decisions/DD-044.md
@@ -0,0 +1,38 @@
+# DD-044 — Via la foto tessera dal profilo
+
+**Data:** 1 ottobre 2026
+**Stato:** Accettata (migration `m29`, applicata in produzione il 1 ottobre 2026)
+
+**Contesto**
+Il profilo amministrativo chiedeva al giocatore una foto formato tessera (DD-016), per il
+tesseramento CSI. Non serve più: non viene richiesta nel tesseramento e resta un dato personale
+da custodire.
+
+**Decisione**
+
+1. Il profilo **non ha più la foto tessera**: sparisce il campo di caricamento dal Profilo e la
+ voce «Foto» dalla scheda dell'admin.
+2. **Completamento del profilo** su tre sezioni: dati personali 34%, documento 33%, certificato 33%
+ (prima 30/30/30/10). Il totale resta 100 e il widget «Completa il tuo profilo» sparisce con le
+ tre sezioni complete.
+3. **Dati:** la migration `m29` elimina la colonna `profili_giocatore.foto_path`. I file
+ `/foto.*` del bucket privato `profili-giocatore` vanno cancellati con l'API
+ Storage (da SQL Storage lo vieta) prima della migration, perché dopo i path non sono più
+ leggibili. **Non toccati:** l'avatar (bucket `avatar-giocatori`), il documento e il certificato.
+4. Ordine in produzione: **prima il rilascio del codice** (la versione precedente seleziona
+ `foto_path` e darebbe errore), poi cancellazione dei file, poi `m29`.
+
+**Alternative scartate**
+
+- Lasciare la colonna e nascondere solo l'interfaccia → scartata: resterebbe un dato personale
+ senza uso e senza un campo da cui aggiornarlo.
+- Tenere i pesi 30/30/30 senza rinormalizzare → scartata: nessun profilo arriverebbe a 100%.
+
+**Conseguenze**
+
+- Cancellazione definitiva: le foto già caricate non si recuperano.
+- Il 1 ottobre 2026 sono state cancellate in produzione le 4 foto caricate, poi la colonna.
+- Le percentuali di completamento cambiano per tutti (chi aveva il 90% senza foto passa al 100%).
+
+**Riesame**
+Se il comitato CSI dovesse richiedere di nuovo la foto, reintrodurla con una nuova decisione.
diff --git a/docs/decisions/README.md b/docs/decisions/README.md
index 09bb665..abe752a 100644
--- a/docs/decisions/README.md
+++ b/docs/decisions/README.md
@@ -86,6 +86,7 @@ Una decisione sostituita non si cancella: si cambia il suo **Stato** e si rimand
| [DD-038](DD-038.md) | Tre canali per ogni notifica |
| [DD-039](DD-039.md) | Niente promemoria all'allenatore |
| [DD-040](DD-040.md) | Palloni e solleciti automatici |
+| [DD-044](DD-044.md) | Via la foto tessera dal profilo |
| [DD-043](DD-043.md) | Niente promemoria evento a 24 e 3 ore |
| [DD-042](DD-042.md) | Turno palloni: un solo avviso a 3 ore |
| [DD-041](DD-041.md) | Avviso certificati: giocatore a 30 giorni |
diff --git a/docs/modules/allenatore.md b/docs/modules/allenatore.md
index 91ea452..ee15632 100644
--- a/docs/modules/allenatore.md
+++ b/docs/modules/allenatore.md
@@ -55,8 +55,8 @@ Un link diretto a `/profilo?tab=stagione` o `?tab=badge` apre Documenti.
| Telefono | `profili_giocatore.telefono` | |
| Foto profilo | bucket `avatar-giocatori` | stesso cerchio avatar e stesso upload dei giocatori |
-**Non** si mostrano: indirizzo di residenza, documento d'identità, certificato medico, foto
-tessera, tesseramento CSI.
+**Non** si mostrano: indirizzo di residenza, documento d'identità, certificato medico,
+tesseramento CSI.
### Completamento profilo
@@ -164,7 +164,7 @@ giocatore).
| ------------------------------------------------------------------- | ---------- | ------------------ | ----------------------------------- |
| 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 |
+| Documento, certificato | 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ì |
diff --git a/docs/modules/profilo-giocatore.md b/docs/modules/profilo-giocatore.md
index 0e00435..4dec796 100644
--- a/docs/modules/profilo-giocatore.md
+++ b/docs/modules/profilo-giocatore.md
@@ -44,7 +44,7 @@ Può:
riga non viene eliminata, così presenze, voti, pagelle e badge della stagione restano
agganciati al suo id
-Non può caricare o sostituire i file altrui: documento, certificato e foto restano
+Non può caricare o sostituire i file altrui: documento e certificato restano
responsabilità del giocatore che li fornisce.
## Flusso utente
@@ -73,7 +73,6 @@ Viene mostrata una barra di avanzamento (esempio: _Profilo completato — 85%_),
- Dati personali
- Documento di identità
- Certificato medico
-- Foto tessera
Quando tutte le sezioni sono complete il widget scompare automaticamente.
@@ -273,9 +272,8 @@ Lo storico non viene mantenuto nella prima versione.
### Foto tessera
-Upload di una fotografia formato tessera.
-
-Utilizzata dagli amministratori per il tesseramento CSI.
+**Rimossa** (DD-044, migration `m29`): il profilo non chiede più la foto tessera e la colonna
+`foto_path` non esiste più. I file già caricati sono stati cancellati dal bucket.
### Statistiche
@@ -328,7 +326,6 @@ Per ogni giocatore, nella tab Profili, vengono mostrati.
- Stato del profilo
- Certificato medico
- Documento di identità
-- Foto tessera
- Stato tesseramento CSI (tesserato / da tesserare)
Azioni disponibili.
@@ -336,7 +333,6 @@ Azioni disponibili.
- Visualizza profilo (la scheda si apre in linea nell'elenco: nessuna schermata separata)
- Scarica certificato
- Scarica documento
-- Scarica foto tessera
- Modifica dati squadra e dati personali del giocatore (DD-017)
- Registra numero e data della tessera CSI, una volta arrivata dal comitato
- Scollega account, per liberare uno slot assegnato per errore
@@ -377,10 +373,9 @@ Ogni sezione contribuisce alla percentuale di completamento.
| Sezione | Peso |
| --------------------- | ---- |
-| Dati personali | 30% |
-| Documento di identità | 30% |
-| Certificato medico | 30% |
-| Foto tessera | 10% |
+| Dati personali | 34% |
+| Documento di identità | 33% |
+| Certificato medico | 33% |
Quando tutte le sezioni risultano complete il profilo raggiunge il 100%.
@@ -402,7 +397,6 @@ nessun altro ne vede (vedi [Avviso certificati](#avviso-certificati)).
- Gestione dati personali
- Documento di identità
- Certificato medico
-- Foto tessera
- Dashboard amministratore
- Esportazione CSV CSI
diff --git a/src/components/crapp/ProfiloAmministrativo.tsx b/src/components/crapp/ProfiloAmministrativo.tsx
index a41c8ea..17cfdee 100644
--- a/src/components/crapp/ProfiloAmministrativo.tsx
+++ b/src/components/crapp/ProfiloAmministrativo.tsx
@@ -120,14 +120,12 @@ export function CampiProfilo({
sezioni,
fileDocumento,
fileCertificato,
- fileFoto,
}: {
corrente: Profilo;
aggiorna: (patch: Partial) => void;
sezioni: Record;
fileDocumento?: React.ReactNode;
fileCertificato?: React.ReactNode;
- fileFoto?: React.ReactNode;
}) {
return (
<>
@@ -241,14 +239,13 @@ export function CampiProfilo({
/>
{fileCertificato}
- {fileFoto}
>
);
}
/**
* I dati personali dell'allenatore (DD-034): gli stessi campi di `profili_giocatore`, meno
- * indirizzo, documento, certificato e foto tessera. Condivisi tra Profilo e dashboard admin.
+ * indirizzo, documento e certificato. Condivisi tra Profilo e dashboard admin.
*/
export function CampiAllenatore({
corrente,
@@ -469,18 +466,6 @@ export function ProfiloAmministrativo({
onCaricato={caricato("certificatoPath")}
/>
}
- fileFoto={
- <>
-
-
- >
- }
/>
diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts
index 899b26b..251a58c 100644
--- a/src/integrations/supabase/types.ts
+++ b/src/integrations/supabase/types.ts
@@ -375,7 +375,6 @@ export type Database = {
documento_scadenza: string | null;
documento_tipo: string | null;
email: string | null;
- foto_path: string | null;
giocatore_id: string;
indirizzo: string | null;
luogo_nascita: string | null;
@@ -395,7 +394,6 @@ export type Database = {
documento_scadenza?: string | null;
documento_tipo?: string | null;
email?: string | null;
- foto_path?: string | null;
giocatore_id: string;
indirizzo?: string | null;
luogo_nascita?: string | null;
@@ -415,7 +413,6 @@ export type Database = {
documento_scadenza?: string | null;
documento_tipo?: string | null;
email?: string | null;
- foto_path?: string | null;
giocatore_id?: string;
indirizzo?: string | null;
luogo_nascita?: string | null;
diff --git a/src/lib/profili-core.ts b/src/lib/profili-core.ts
index 0841b58..5799b83 100644
--- a/src/lib/profili-core.ts
+++ b/src/lib/profili-core.ts
@@ -21,7 +21,6 @@ export type Profilo = {
documentoRetroPath: string | null;
certificatoScadenza: string | null;
certificatoPath: string | null;
- fotoPath: string | null;
};
export type RigaProfilo = {
@@ -40,11 +39,10 @@ export type RigaProfilo = {
documento_retro_path: string | null;
certificato_scadenza: string | null;
certificato_path: string | null;
- foto_path: string | null;
};
export const COLONNE_PROFILO =
- "giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email, documento_tipo, documento_numero, documento_rilasciato_da, documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path, certificato_scadenza, certificato_path, foto_path";
+ "giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email, documento_tipo, documento_numero, documento_rilasciato_da, documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path, certificato_scadenza, certificato_path";
export function profiloVuoto(giocatoreId: string): Profilo {
return {
@@ -63,7 +61,6 @@ export function profiloVuoto(giocatoreId: string): Profilo {
documentoRetroPath: null,
certificatoScadenza: null,
certificatoPath: null,
- fotoPath: null,
};
}
@@ -84,7 +81,6 @@ export function daRigaProfilo(r: RigaProfilo): Profilo {
documentoRetroPath: r.documento_retro_path,
certificatoScadenza: r.certificato_scadenza,
certificatoPath: r.certificato_path,
- fotoPath: r.foto_path,
};
}
@@ -111,12 +107,11 @@ export function aRigaProfilo(p: Profilo): RigaProfilo {
documento_retro_path: oNull(p.documentoRetroPath),
certificato_scadenza: oNull(p.certificatoScadenza),
certificato_path: oNull(p.certificatoPath),
- foto_path: oNull(p.fotoPath),
};
}
/** Pesi delle sezioni del profilo (docs/modules/profilo-giocatore.md). */
-export const PESI = { dati: 30, documento: 30, certificato: 30, foto: 10 } as const;
+export const PESI = { dati: 34, documento: 33, certificato: 33 } as const;
export type Sezione = keyof typeof PESI;
@@ -131,7 +126,6 @@ export function sezioniComplete(p: Profilo | null | undefined): Record
- }
- label="Foto"
- stato={sezioni.foto ? "presente" : "assente"}
- path={profilo?.fotoPath ?? null}
- />
/foto.*` nel bucket
+-- privato `profili-giocatore` NON si cancellano da SQL (Storage lo vieta con un trigger): vanno
+-- rimossi con l'API Storage, prima di questa migration, mentre i path sono ancora leggibili.
+-- Da applicare DOPO il rilascio del codice che non legge più `foto_path`: l'app precedente
+-- la seleziona esplicitamente e darebbe errore.
+
+ALTER TABLE public.profili_giocatore DROP COLUMN IF EXISTS foto_path;
diff --git a/supabase/seed.sql b/supabase/seed.sql
index d74ccac..d197b16 100644
--- a/supabase/seed.sql
+++ b/supabase/seed.sql
@@ -15,24 +15,24 @@ INSERT INTO public.profili_giocatore
(giocatore_id, data_nascita, luogo_nascita, indirizzo, telefono, email,
documento_tipo, documento_numero, documento_rilasciato_da,
documento_emissione, documento_scadenza, documento_fronte_path, documento_retro_path,
- certificato_scadenza, certificato_path, foto_path)
+ certificato_scadenza, certificato_path)
VALUES
-- Profilo completo al 100%.
('g1', '1997-08-30', 'Bologna', 'Via Roma 1', '3330000001', 'g1@example.test',
'Carta d''identità', 'CA1000001', 'Comune di Bologna',
'2021-03-01', '2031-03-01', 'g1/documento-fronte.jpg', 'g1/documento-retro.jpg',
- '2027-06-30', 'g1/certificato.pdf', 'g1/foto.jpg'),
+ '2027-06-30', 'g1/certificato.pdf'),
-- Certificato scaduto: in dashboard deve comparire rosso.
('g4', '1995-05-01', 'Bologna', 'Via Verdi 2', '3330000004', 'g4@example.test',
'Carta d''identità', 'CA1000004', 'Comune di Bologna',
'2019-05-01', '2029-05-01', 'g4/documento-fronte.jpg', 'g4/documento-retro.jpg',
- '2025-01-01', 'g4/certificato.pdf', 'g4/foto.jpg'),
+ '2025-01-01', 'g4/certificato.pdf'),
- -- Profilo a metà: dati personali sì, documento no, certificato sì, foto no.
+ -- Profilo a metà: dati personali sì, documento no, certificato sì.
('g2', '1996-12-07', 'Modena', 'Via Bianchi 3', '3330000002', 'g2@example.test',
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
- '2027-09-15', 'g2/certificato.pdf', NULL)
+ '2027-09-15', 'g2/certificato.pdf')
ON CONFLICT (giocatore_id) DO NOTHING;
ALTER TABLE public.giocatori_squadra ENABLE TRIGGER enforce_giocatori_squadra_update;
diff --git a/test/README.md b/test/README.md
index 5b7a5e2..03d6bcb 100644
--- a/test/README.md
+++ b/test/README.md
@@ -24,12 +24,12 @@ la consegna effettiva a schermo bloccato richiede un telefono e il servizio push
## Struttura
-| Cartella | Cosa verifica | Serve rete? |
-| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
-| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
-| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26 e M27, DD-040 e DD-042: palloni un solo avviso a 3 ore) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì |
-| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
-| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
+| Cartella | Cosa verifica | Serve rete? |
+| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
+| `unit/` | Logica di dominio pura: badge, serie, palloni, pagelle, MVP, cacche, scout, obiettivi, notifiche, parsing CSI, dati della rosa. Più le funzioni pure isolabili nei moduli con hook/rete (validazione upload, guardie push, JWT VAPID, cattura errori, avatar, guardia admin delle route, lettura del `.env` ed elenco dei bucket dello script di backup, logica del worker in `mailer/` (email e push dei promemoria), preferenza «Email» e avviso del sondaggio) | No |
+| `integration/` | Le route `/api/public/*` sul server di sviluppo: risposte, cache, validazione degli input. Più schema e permessi del Profilo Giocatore (`schema-profili`) contro il database configurato; permessi per ruolo, incluse le policy di M11, le deroghe dell'amministratore e le tabelle lasciate aperte di proposito (`permessi`), accesso alle route di notifica (`permessi-route`), semantica degli upsert e vincoli (`scritture`), le letture lato server delle route push (`lettori-server`) e la pulizia a cascata dei dati collegati alla cancellazione di un evento (`pulizia-evento`, M14) la funzione di bonifica delle righe orfane (`bonifica-evento`, M16), il centro notifiche in-app (`notifiche-utente`, M17), la coda delle email e le sue funzioni RPC (`notifiche-email`, M22), i promemoria evento che non ricompaiono se eliminati (`promemoria-eventi`, M23), la coda delle push dei promemoria (`push-promemoria`, M24), i job automatici di turno palloni e sollecito presenze con revoca, finestre e testi identici al codice (`notifiche-automatiche`, M26 e M27, DD-040 e DD-042: palloni un solo avviso a 3 ore) la colonna `foto_path` eliminata (`foto-tessera-rimossa`, M29, DD-044) e il sondaggio in-app/email (`sondaggio-notifiche`, M24), i destinatari delle notifiche per ruolo (`destinatari-notifiche`: promemoria, messaggio dello staff, sondaggio, sollecito e turno palloni con allenatore e giocatore), la nascita pubblica sincronizzata dal profilo (`nascita-pubblica`, M18) e il suo backfill per i profili preesistenti (`backfill-nascita-esistenti`, M19 — unico test che esegue SQL diretto via `docker exec` nel container Postgres, perché PostgREST non espone quell'update multi-tabella come RPC) sul database locale | Sì |
+| `e2e/` | Percorsi completi sull'app servita: schermate, dati CSI fino alla pagina, file PWA, 404 | Sì |
+| `helpers/` | Avvio del server di test e mini-harness condiviso | — |
## Server di sviluppo con Node vecchio
diff --git a/test/integration/foto-tessera-rimossa.test.ts b/test/integration/foto-tessera-rimossa.test.ts
new file mode 100644
index 0000000..598148a
--- /dev/null
+++ b/test/integration/foto-tessera-rimossa.test.ts
@@ -0,0 +1,37 @@
+/**
+ * Via la foto tessera dal profilo (M29, DD-044): `bun test/integration/foto-tessera-rimossa.test.ts`.
+ *
+ * Gira solo sullo stack locale (`npx supabase start`): verifica che la colonna `foto_path` non ci
+ * sia più e che le colonne lette dall'app (`COLONNE_PROFILO`) esistano tutte. Non guarda il
+ * database di `.env`, che può essere ancora quello di produzione prima di applicare la M29.
+ */
+import assert from "node:assert/strict";
+import { COLONNE_PROFILO } from "@/lib/profili-core";
+import { statoLocale } from "../helpers/locale";
+import { prova, riepilogo, salta } from "../helpers/prova";
+
+const locale = statoLocale();
+
+if (!locale) {
+ salta("foto tessera rimossa", "stack locale non attivo (npx supabase start)");
+ riepilogo("foto-tessera-rimossa");
+} else {
+ const leggi = (colonne: string) =>
+ fetch(`${locale.url}/rest/v1/profili_giocatore?select=${encodeURIComponent(colonne)}&limit=1`, {
+ headers: { apikey: locale.servizio, Authorization: `Bearer ${locale.servizio}` },
+ });
+
+ await prova("la colonna foto_path non esiste più", async () => {
+ const res = await leggi("foto_path");
+ assert.equal(res.status, 400, "PostgREST rifiuta una colonna che non c'è");
+ assert.match(await res.text(), /foto_path/);
+ });
+
+ await prova("le colonne lette dall'app esistono tutte", async () => {
+ assert.ok(!COLONNE_PROFILO.includes("foto"), "COLONNE_PROFILO non nomina la foto");
+ const res = await leggi(COLONNE_PROFILO);
+ assert.equal(res.status, 200, await res.text());
+ });
+
+ riepilogo("foto-tessera-rimossa");
+}
diff --git a/test/unit/profili-core.test.ts b/test/unit/profili-core.test.ts
index 7657399..1d2eda8 100644
--- a/test/unit/profili-core.test.ts
+++ b/test/unit/profili-core.test.ts
@@ -8,6 +8,7 @@ import {
completamento,
completamentoAllenatore,
csvTesseramento,
+ PESI,
daRigaProfilo,
formatDataBreve,
giorniAllaScadenza,
@@ -41,7 +42,6 @@ const vuoto: Profilo = {
documentoRetroPath: null,
certificatoScadenza: null,
certificatoPath: null,
- fotoPath: null,
};
const completo: Profilo = {
@@ -60,18 +60,22 @@ const completo: Profilo = {
documentoRetroPath: "g1/documento-retro.jpg",
certificatoScadenza: "2027-06-30",
certificatoPath: "g1/certificato.pdf",
- fotoPath: "g1/foto.jpg",
};
// --- completamento -----------------------------------------------------------
assert.equal(completamento(null), 0, "profilo inesistente = 0%");
assert.equal(completamento(vuoto), 0);
assert.equal(completamento(completo), 100, "tutte le sezioni piene = 100%");
-assert.equal(completamento({ ...completo, fotoPath: null }), 90, "la foto pesa 10");
-assert.equal(completamento({ ...completo, certificatoPath: null }), 70, "il certificato pesa 30");
+assert.equal(
+ Object.values(PESI).reduce((somma, peso) => somma + peso, 0),
+ 100,
+ "senza foto tessera (DD-044) i pesi di dati, documento e certificato fanno 100",
+);
+assert.deepEqual(Object.keys(sezioniComplete(completo)), ["dati", "documento", "certificato"]);
+assert.equal(completamento({ ...completo, certificatoPath: null }), 67, "il certificato pesa 33");
assert.equal(
completamento({ ...completo, email: null }),
- 70,
+ 66,
"i dati personali sono completi solo tutti insieme",
);
@@ -216,7 +220,7 @@ assert.equal(
email: "a@b.it",
}),
100,
- "bastano i quattro dati personali: niente indirizzo, documento, certificato, foto",
+ "bastano i quattro dati personali: niente indirizzo, documento, certificato",
);
assert.equal(completamentoAllenatore({ ...vuoto, telefono: " " }), 0, "spazi soli non contano");
diff --git a/test/unit/profili.test.ts b/test/unit/profili.test.ts
index c24586f..bee7879 100644
--- a/test/unit/profili.test.ts
+++ b/test/unit/profili.test.ts
@@ -14,11 +14,11 @@ await assert.rejects(
);
// --- file troppo grande: rifiutato anche con un formato valido ---------------
-const troppoGrande = new File([new Uint8Array(8 * 1024 * 1024 + 1)], "foto.jpg", {
+const troppoGrande = new File([new Uint8Array(8 * 1024 * 1024 + 1)], "certificato.jpg", {
type: "image/jpeg",
});
await assert.rejects(
- () => caricaFile("g1", "foto", troppoGrande),
+ () => caricaFile("g1", "certificato", troppoGrande),
/File troppo grande: massimo 8 MB\./,
);
diff --git a/test/unit/profilo-campi.test.ts b/test/unit/profilo-campi.test.ts
new file mode 100644
index 0000000..07ed53c
--- /dev/null
+++ b/test/unit/profilo-campi.test.ts
@@ -0,0 +1,28 @@
+/** Check dei campi del profilo: `bun test/unit/profilo-campi.test.ts` (DD-044: niente foto tessera). */
+import assert from "node:assert/strict";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { CampiProfilo } from "@/components/crapp/ProfiloAmministrativo";
+import { profiloVuoto, sezioniComplete } from "@/lib/profili-core";
+
+const corrente = profiloVuoto("g1");
+const html = renderToStaticMarkup(
+ createElement(CampiProfilo, {
+ corrente,
+ aggiorna: () => {},
+ sezioni: sezioniComplete(corrente),
+ fileDocumento: createElement("p", null, "slot-documento"),
+ fileCertificato: createElement("p", null, "slot-certificato"),
+ }),
+);
+
+// --- le sezioni rimaste ci sono ----------------------------------------------
+assert.ok(html.includes("Dati personali"));
+assert.ok(html.includes("slot-documento"), "lo slot del documento è mostrato");
+assert.ok(html.includes("slot-certificato"), "lo slot del certificato è mostrato");
+
+// --- la foto tessera non c'è più, né come titolo né come campo ----------------
+assert.ok(!/foto tessera/i.test(html), "nessun riferimento alla foto tessera");
+assert.ok(!html.includes("fotoPath"));
+
+console.log("profilo-campi: ok");