From bfb768fb5a4784bfa9242c12d9036270d312cf23 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Tue, 6 Oct 2026 13:21:14 +0200 Subject: [PATCH] =?UTF-8?q?Rimuove=20la=20serie=20=C2=ABConferme=2024h?= =?UTF-8?q?=C2=BB=20e=20i=20badge=20collegati?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toglie la serie dal profilo, dalla Home e dalle notifiche di traguardo, insieme ai badge «Risposta lampo» e «Mai un forfait» e al calcolo di serieConferme. La colonna risposto_il e il suo trigger restano nel database, inutilizzati. Test e documentazione allineati. Co-Authored-By: Claude Sonnet 5.5 --- docs/CHANGELOG.md | 6 + docs/DATABASE.md | 2 +- docs/GLOSSARIO.md | 2 +- docs/modules/badge.md | 62 +++-- docs/modules/serie-presenze.md | 55 ++--- src/lib/badges.ts | 23 -- src/lib/crapp-data.ts | 2 - src/lib/presenze.ts | 27 +-- src/lib/rosa.ts | 17 +- src/lib/serie.ts | 12 +- test/integration/s-mai-forfait-badge.test.ts | 204 ---------------- test/integration/scritture.test.ts | 4 +- test/integration/serie-conferme-badge.test.ts | 229 ------------------ test/unit/badges.test.ts | 42 ---- test/unit/notifiche-smart.test.ts | 3 +- test/unit/presenze.test.ts | 85 +------ test/unit/serie.test.ts | 12 +- 17 files changed, 69 insertions(+), 718 deletions(-) delete mode 100644 test/integration/s-mai-forfait-badge.test.ts delete mode 100644 test/integration/serie-conferme-badge.test.ts diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b577df6..f344ec3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,12 @@ versioni `0.y.z` erano pre-release. ## [Non rilasciato] +### Rimosso + +- **Serie «Conferme 24h» e badge collegati** — la serie non compare più nel profilo, in Home né nelle notifiche di + traguardo; restano Allenamenti e Partite. Rimossi anche il badge «Risposta lampo» e il segreto «Mai un forfait», che + si basavano su di essa. Nessuna migrazione: la colonna `risposto_il` resta, inutilizzata. + ## [1.3.0] - 2026-10-06 Gli auguri di compleanno arrivano da soli, gli obiettivi di squadra si possono toccare per capire come funzionano diff --git a/docs/DATABASE.md b/docs/DATABASE.md index cd6de2f..d96ce5d 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -77,7 +77,7 @@ Colonne di `giocatori_squadra` che meritano una nota: | Tabella | Scopo | Note | | ------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `eventi_app` | Eventi gestionali usati dall'app. | Modello in uso. Cancellare un evento pulisce a cascata, via trigger, tutte le tabelle collegate: `risposte_presenze`, `cacche_partita`, `mvp_voti`, `pagelle_voti`, `badge_social_voti`, `turni_palloni`, `scout_sessioni`, `scout_live`, `scout_partite` (DD-029). `bonifica_dati_evento_orfani()` (solo service role) ripulisce eventuali righe orfane. | -| `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: confrontato con `eventi_app.creato_il` dà la serie «Conferme 24h». `aggiornato_il` è l'ultima modifica. | +| `risposte_presenze` | Risposte dei giocatori agli eventi. | `risposto_il` è l'istante della **prima** risposta e un trigger lo rende immutabile: oggi nessuna statistica lo usa (la serie «Conferme 24h» è stata rimossa il 2026-10-06). `aggiornato_il` è l'ultima modifica. | | `cacche_partita` | Sondaggio prepartita. | Usato per statistiche e badge segreti. | | `eventi` | Calendario generale: allenamenti, partite, eventi della squadra. | Modello «nuovo» con autenticazione e vincoli, non adottato (DD-014). | | `presenze` | Presenze agli eventi. | Come sopra (DD-014). | diff --git a/docs/GLOSSARIO.md b/docs/GLOSSARIO.md index 28565bd..c4e38cc 100644 --- a/docs/GLOSSARIO.md +++ b/docs/GLOSSARIO.md @@ -12,7 +12,7 @@ il campionato. Dove serve il dettaglio c'è il rimando al modulo. | **Evento** | Riga di `eventi_app`: `partita`, `allenamento` o `evento` extra-campo (es. cena). I compleanni compaiono in calendario ma non sono eventi di campo. | | **Convocati** | Giocatori chiamati a un evento. **`convocati` vuoto = tutta la rosa attiva**: è la convenzione di `convocatiEvento()` e di `evento_permette_voto()`. | | **Presenza / risposta** | Risposta di un giocatore a un evento (`risposte_presenze`): `presente`, `ritardo`, `assente`, `forse`, `infortunato`. Vedi [modules/presenze.md](modules/presenze.md). | -| **Serie** | Streak di comportamenti positivi consecutivi (allenamenti, partite, conferme entro 24 ore). Vedi [modules/serie-presenze.md](modules/serie-presenze.md). | +| **Serie** | Streak di comportamenti positivi consecutivi (allenamenti e partite). Vedi [modules/serie-presenze.md](modules/serie-presenze.md). | | **Pagelle** | Voti 1-10 anonimi ai compagni dopo una partita; solo i convocati votano, finché `pagelle_chiuse = false` (DD-027). Vedi [modules/pagelle.md](modules/pagelle.md). | | **MVP** | Voto al migliore in campo a fine partita, uno per votante (DD-027). Vedi [modules/mvp.md](modules/mvp.md). | | **Badge** | Riconoscimenti calcolati a runtime, mai salvati (DD-007): normali (bronzo/argento/oro), segreti, social (votati dai compagni). Vedi [modules/badge.md](modules/badge.md). | diff --git a/docs/modules/badge.md b/docs/modules/badge.md index bcc7170..d09a6b8 100644 --- a/docs/modules/badge.md +++ b/docs/modules/badge.md @@ -76,7 +76,7 @@ badge assegnati per voto dai compagni. ## Elenco badge -Riferimento completo per chi lavora sul codice. **In app i 5 badge segreti restano nascosti +Riferimento completo per chi lavora sul codice. **In app i 4 badge segreti restano nascosti finché non sbloccati** (fanno parte della sorpresa per i giocatori): elencarli qui, con le condizioni esatte, è una scelta deliberata per la documentazione tecnica, non una fuga di informazioni verso l'interfaccia. @@ -93,7 +93,6 @@ soglia raggiunta o superata (soglie inclusive), oltre l'oro resta oro. | `palloni` | Sherpa dei palloni | quante volte hai confermato il turno palloni (`g.palloni`) — le proposte automatiche non ancora confermate non contano | 3 / 6 / 10 | | `presenze` | Presenza fissa | totale presenze (presente o ritardo) a eventi/partite di sempre, non solo della stagione in corso (`g.presenze`) | 5 / 15 / 30 | | `serie-allenamenti` | Sempre in palestra | allenamenti consecutivi presenti (`g.serieAllenamenti`); un infortunio non spezza la serie, un'assenza sì | 3 / 6 / 10 | -| `serie-conferme` | Risposta lampo | conferme di presenza consecutive date entro 24h dalla convocazione (`g.serieConferme`) | 3 / 8 / 15 | ### Badge segreti (booleani, nascosti finché non sbloccati) @@ -104,13 +103,12 @@ grado effettivo che risulta una volta sbloccato è sempre **`"oro"`** (l'ultimo `for` sovrascrive), mai `"bronzo"` — l'unica cosa che conta davvero per questi badge è `grado !== null`, non il suo valore, ed è così che li legge `badgeSegretiSbloccati()`. -| id | nome | condizione esatta | -| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `s-tiebreak` | Uomo tie-break | almeno 2 MVP **e** media pagella ≥ 8, sopra la soglia minima di voti di Pagellone (`g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8`) | -| `s-mai-forfait` | Mai un forfait | almeno 10 conferme rapide consecutive **e** almeno 15 presenze (`g.serieConferme >= 10 && g.presenze >= 15`) | -| `s-infermeria` | Cliente VIP dell'Infermeria | almeno 3 eventi saltati per infortunio (`g.infortuni >= 3`) | -| `s-ritardi` | Aspettate, arrivo! | almeno 5 ritardi a eventi (`g.ritardi >= 5`) | -| `s-cacche` | Trono di ferro | almeno 3 partite (campionato o amichevole) con 3 o più cacche pre-gara dichiarate (`g.cacche >= 3`) | +| id | nome | condizione esatta | +| -------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `s-tiebreak` | Uomo tie-break | almeno 2 MVP **e** media pagella ≥ 8, sopra la soglia minima di voti di Pagellone (`g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8`) | +| `s-infermeria` | Cliente VIP dell'Infermeria | almeno 3 eventi saltati per infortunio (`g.infortuni >= 3`) | +| `s-ritardi` | Aspettate, arrivo! | almeno 5 ritardi a eventi (`g.ritardi >= 5`) | +| `s-cacche` | Trono di ferro | almeno 3 partite (campionato o amichevole) con 3 o più cacche pre-gara dichiarate (`g.cacche >= 3`) | ### Badge social (votati dai compagni, 5 categorie per partita) @@ -149,24 +147,22 @@ dicono i file, non questa pagina: leggili prima di cambiare una soglia. ### Riepilogo per badge -| # | id | tipo | test unit | test integration | -| --- | ------------------- | ------- | ----------------------------- | --------------------------------------------- | -| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) | -| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) | -| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) | -| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) | -| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) | -| 6 | `serie-conferme` | normale | ✅ (vedi Limiti noti) | ✅ (`serie-conferme-badge`) | -| 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) | -| 8 | `s-mai-forfait` | segreto | ✅ | ✅ (`s-mai-forfait-badge`) | -| 9 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) | -| 10 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) | -| 11 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) | -| 12 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) | -| 13 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | -| 14 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | -| 15 | `meme` | social | ✅ | ✅ (`badge-social`) | -| 16 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) | +| # | id | tipo | test unit | test integration | +| --- | ------------------- | ------------ | ----------------------------- | --------------------------------------------- | +| 1 | `mvp` | normale | ✅ | ✅ (`scritture`, `permessi`, `mvp-badge`) | +| 2 | `pagella` | normale | ✅ (incl. soglia minima voti) | ✅ (`scritture`, `permessi`, `pagella-badge`) | +| 3 | `palloni` | normale | ✅ | ✅ (`scritture`, `palloni-badge`) | +| 4 | `presenze` | normale | ✅ | ✅ (`obiettivi`, `presenze-badge`) | +| 5 | `serie-allenamenti` | normale | ✅ | ✅ (`serie-allenamenti-badge`) | +| 6 | 7 | `s-tiebreak` | segreto | ✅ (soglia minima voti) | ✅ (`s-tiebreak-badge`) | +| 6 | `s-infermeria` | segreto | ✅ | ✅ (`s-infermeria-badge`) | +| 7 | `s-ritardi` | segreto | ✅ | ✅ (`s-ritardi-badge`) | +| 8 | `s-cacche` | segreto | ✅ | ✅ (`s-cacche-badge`) | +| 9 | `affidabile` | social | ✅ | ✅ (`scritture`, `permessi`, `badge-social`) | +| 10 | `spirito` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | +| 11 | `fairplay` | social | ✅ (meccanismo generico) | ✅ (meccanismo generico, `badge-social`) | +| 12 | `meme` | social | ✅ | ✅ (`badge-social`) | +| 13 | `cuore` | social | ✅ | ✅ (autovoto, `badge-social`) | --- @@ -193,11 +189,9 @@ dicono i file, non questa pagina: leggili prima di cambiare una soglia. ## Limiti noti -- **Dipendenza dal modulo [Serie](serie-presenze.md)**: i badge "Sempre in palestra", - "Risposta lampo" e il segreto "Mai un forfait" si muovono solo se cambiano le serie. Le - serie sono calcolate sui dati reali dalla migration `m9` in avanti, ma "Risposta lampo" e - "Mai un forfait" dipendono da `serieConferme`, e `risposto_il` non è ricostruibile per le - risposte precedenti a `m9`: su quelle righe la serie è un'approssimazione. +- **Dipendenza dal modulo [Serie](serie-presenze.md)**: il badge «Sempre in palestra» si muove solo se cambia la serie + Allenamenti. Il badge «Risposta lampo» e il segreto «Mai un forfait», basati sulla serie «Conferme 24h», sono stati + rimossi il 2026-10-06. - Nessuno storico dei badge sbloccati: se cambiano le soglie o i dati sorgente, un badge già "ottenuto" può sparire o apparire retroattivamente. - Notifiche "nuovo badge" solo locali al dispositivo (localStorage), si ripetono cambiando @@ -213,7 +207,7 @@ UI di rotazione (`TurnoPalloni.tsx`, `PromemoriaPalloni.tsx`), mai per il conteg Dimostrato con dati veri in `palloni-badge.test.ts`. [palloni.md](palloni.md) aggiornato di conseguenza. -**Risolto (audit completo dei 16 badge)**: `s-tiebreak` (`badges.ts:139`) usava `g.mediaVoto` +**Risolto (audit completo dei badge (all'epoca 16))**: `s-tiebreak` (`badges.ts:139`) usava `g.mediaVoto` senza applicare `VOTI_MINIMI_PAGELLA`, a differenza del badge normale `pagella` che usa lo stesso campo — un giocatore con un solo voto pagella altissimo e 2 MVP poteva sbloccare il segreto senza che la media fosse statisticamente significativa. Ora `s-tiebreak` richiede anche @@ -238,7 +232,7 @@ ancora correggere un voto anche fuori convocazione o dopo la chiusura. ## Evoluzioni possibili - Sincronizzare lo stato "visto" su Supabase invece che solo in localStorage. -- Verificare sui dati di stagione che i tre badge legati alle serie si sblocchino davvero, +- Verificare sui dati di stagione che il badge legato alla serie si sblocchino davvero, ora che le serie sono calcolate. - Rimuovere `badgeSbloccati()` (codice morto) o documentarne lo scopo. - Aggiungere un vincolo (CHECK o FK) sulla colonna `categoria` di `badge_social_voti`. diff --git a/docs/modules/serie-presenze.md b/docs/modules/serie-presenze.md index 14bf69d..76c5250 100644 --- a/docs/modules/serie-presenze.md +++ b/docs/modules/serie-presenze.md @@ -1,12 +1,12 @@ # Modulo — Serie di presenze -**Stato:** implementato — tutte e tre le serie calcolate sui dati reali +**Stato:** implementato — le due serie (allenamenti e partite) calcolate sui dati reali **File principali:** `src/lib/serie.ts`, `src/lib/presenze.ts`, `src/lib/rosa.ts`, `src/components/crapp/SerieCard.tsx` **Migration collegata:** `m9_risposte_presenze_risposto_il` **Test:** `test/unit/serie.test.ts`, `test/unit/presenze.test.ts`, `test/integration/scritture.test.ts` (il trigger che congela `risposto_il`), -`test/integration/serie-allenamenti-badge.test.ts`, `test/integration/serie-conferme-badge.test.ts` +`test/integration/serie-allenamenti-badge.test.ts` --- @@ -19,15 +19,18 @@ requisiti di sblocco di alcuni [badge](badge.md) e di un [obiettivo di squadra]( --- -## Le tre serie in sintesi +## Le due serie in sintesi -| Tipo | Campo `Giocatore` | Cosa conta | Traguardi | -| ------------- | ------------------ | ------------------------------------------------------------ | ------------ | -| `allenamenti` | `serieAllenamenti` | Allenamenti passati consecutivi con presenza | 3, 6, 10, 15 | -| `partite` | `seriePartite` | Partite passate consecutive con presenza | 2, 5, 8, 12 | -| `conferme` | `serieConferme` | Eventi consecutivi con risposta entro 24h dalla convocazione | 3, 8, 15, 20 | +| Tipo | Campo `Giocatore` | Cosa conta | Traguardi | +| ------------- | ------------------ | -------------------------------------------- | ------------ | +| `allenamenti` | `serieAllenamenti` | Allenamenti passati consecutivi con presenza | 3, 6, 10, 15 | +| `partite` | `seriePartite` | Partite passate consecutive con presenza | 2, 5, 8, 12 | -Esiste un quarto contatore fuori da questo modulo, `Giocatore.streak`: la stessa regola delle +La serie «Conferme 24h», con i badge «Risposta lampo» e «Mai un forfait» che ne dipendevano, è stata rimossa il +2026-10-06: restano `Giocatore.streak` e le due serie sopra. La colonna `risposto_il` e il trigger che la congela +restano nel database, ma oggi nessuna statistica li usa. + +Esiste un terzo contatore fuori da questo modulo, `Giocatore.streak`: la stessa regola delle presenze ma **su partite e allenamenti insieme**. Non ha card né traguardi, compare come "presenze consecutive" in `src/routes/index.tsx`, `src/routes/squadra.tsx` e `src/routes/profilo.tsx`. @@ -67,10 +70,10 @@ il cronometro»), che riscrive la risposta provando a riscrivere anche `risposto che il database abbia tenuto la prima: se qualcuno togliesse il trigger, quel test diventa rosso. Il test precedente guardava `aggiornato_il` e passava anche senza trigger. -| Colonna | Cosa registra | Chi la usa | -| --------------- | --------------------- | ----------------------- | -| `risposto_il` | la **prima** risposta | la serie "Conferme 24h" | -| `aggiornato_il` | l'**ultima** modifica | nessuna statistica | +| Colonna | Cosa registra | Chi la usa | +| --------------- | --------------------- | -------------------------------------- | +| `risposto_il` | la **prima** risposta | nessuna statistica (ex «Conferme 24h») | +| `aggiornato_il` | l'**ultima** modifica | nessuna statistica | Cancellare la risposta (`stato: null` → DELETE) elimina anche `risposto_il`: se il giocatore risponde di nuovo, riparte il cronometro. È voluto — ha ritirato la risposta. @@ -147,26 +150,6 @@ Conseguenze da conoscere prima di cambiare qualcosa: significa che eventi storici importati senza presenze schiacciano a zero le serie di tutti. - Senza `tipo` conta partite e allenamenti insieme: è così che si ottiene `streak`. -### `serieConferme()` — conferme entro 24 ore - -```ts -serieConferme(giocatoreId, eventi, tempi, oggi?) -``` - -Onorato = esiste una risposta **e** `risposto_il − creato_il ≤ 24h` (confronto inclusivo, -costante `ORE_24`, entrambi gli istanti passati da `Date.parse`). - -- Conta **partite e allenamenti insieme**, non c'è una versione per tipo. -- **Lo stato non conta**: anche un "assente" dato in fretta tiene viva la serie. È una serie - sulla reattività, non sulla presenza. -- **Gli eventi senza `creatoIl` vengono saltati e non spezzano la serie.** Sono gli eventi - costruiti dal client e mai salvati a database — i compleanni di `compleanniEventi()` e la - bozza di `eventoVuoto()`. Senza istante di convocazione la domanda "ha risposto in fretta?" - non ha risposta, e trattarli come un buco punirebbe il giocatore per un dettaglio tecnico. -- **Le 24 ore partono dalla creazione dell'evento**, non da un invio di notifica: oggi un - momento di "convocazione mandata" distinto non esiste. Se un domani ci sarà, è quello - l'istante giusto da confrontare. - ### Lettura e cache `fetchPresenze()` fa **una sola query** e costruisce due mappe: @@ -263,8 +246,6 @@ Toccare la regola di calcolo muove anche questi, che non hanno logica propria: | Dove | Cosa | Soglie | | ------------------------------- | -------------------------------------------------------------- | --------------------------- | | `badges.ts` `serie-allenamenti` | "Sempre in palestra", su `serieAllenamenti` | bronzo 3, argento 6, oro 10 | -| `badges.ts` `serie-conferme` | "Risposta lampo", su `serieConferme` | bronzo 3, argento 8, oro 15 | -| `badges.ts` `s-mai-forfait` | Badge segreto: `serieConferme >= 10` **e** `presenze >= 15` | — | | `obiettivi.ts` `o11` | "Continuità di squadra": giocatori con `serieAllenamenti >= 3` | target 12 | --- @@ -320,8 +301,8 @@ risultato finale, ma va sistemato se un giorno serve l'ordine esatto. **`oggi` è sempre in fuso Italia.** `dataOggi()` (`src/lib/scout-live.ts`) usa `Intl.DateTimeFormat` con `timeZone: "Europe/Rome"`, non i getter locali di `Date` né `toISOString()`: il cambio ora legale/solare lo gestisce il database IANA dei fusi, non un -offset scritto a mano. È lo stesso `oggi` di `serieConsecutiva()`, `serieConferme()` e del -conteggio presenze — prima `serieConsecutiva()`/`serieConferme()` calcolavano `oggi` con +offset scritto a mano. È lo stesso `oggi` di `serieConsecutiva()` e del +conteggio presenze — prima `serieConsecutiva()` e `serieConferme()` (poi rimossa) calcolavano `oggi` con `toISOString()` (sempre UTC) mentre il conteggio presenze usava i getter locali di `Date` (corretti solo se il processo gira già in fuso italiano): nelle prime ore della giornata italiana potevano non essere d'accordo su cosa fosse "oggi". diff --git a/src/lib/badges.ts b/src/lib/badges.ts index 9d0ff67..178a070 100644 --- a/src/lib/badges.ts +++ b/src/lib/badges.ts @@ -3,9 +3,7 @@ import { Repeat, Lock, Ghost, - Zap, Rocket, - Anchor, Stethoscope, AlarmClock, ClipboardCheck, @@ -114,15 +112,6 @@ export const badgeDefs: BadgeDef[] = [ soglie: { bronzo: 3, argento: 6, oro: 10 }, valore: (g) => g.serieAllenamenti, }, - { - id: "serie-conferme", - nome: "Risposta lampo", - descrizione: "Conferme rapide agli eventi: la squadra sa subito che ci sei.", - unita: "conferme entro 24h", - icon: Zap, - soglie: { bronzo: 3, argento: 8, oro: 15 }, - valore: (g) => g.serieConferme, - }, ]; /** Badge segreti: non compaiono nella UI finché non vengono sbloccati. */ @@ -141,18 +130,6 @@ export const badgeSegreti: BadgeDef[] = [ valore: (g) => (g.mvp >= 2 && g.votiPagella >= VOTI_MINIMI_PAGELLA && g.mediaVoto >= 8 ? 1 : 0), celebrazione: "Nei momenti caldi ci sei sempre.", }, - { - id: "s-mai-forfait", - nome: "Mai un forfait", - descrizione: - "Sbloccato con 10 conferme rapide consecutive e 15 presenze: su di te la squadra può contare a occhi chiusi.", - unita: "requisito nascosto", - icon: Anchor, - segreto: true, - soglie: { bronzo: 1, argento: 1, oro: 1 }, - valore: (g) => (g.serieConferme >= 10 && g.presenze >= 15 ? 1 : 0), - celebrazione: "Su di te la squadra può contare a occhi chiusi.", - }, { id: "s-infermeria", nome: "Cliente VIP dell'Infermeria", diff --git a/src/lib/crapp-data.ts b/src/lib/crapp-data.ts index 8e9d91d..e934b4f 100644 --- a/src/lib/crapp-data.ts +++ b/src/lib/crapp-data.ts @@ -34,7 +34,6 @@ export type Giocatore = { /** Serie consecutive per tipo: si azzerano in modo indipendente. */ serieAllenamenti: number; seriePartite: number; - serieConferme: number; /** MVP eletti dalla squadra. */ mvp: number; /** Media delle pagelle ricevute dai compagni (1-10). */ @@ -109,7 +108,6 @@ export const giocatori: Giocatore[] = rosaCSI streak: 0, serieAllenamenti: 0, seriePartite: 0, - serieConferme: 0, mvp: 0, mediaVoto: 0, votiPagella: 0, diff --git a/src/lib/presenze.ts b/src/lib/presenze.ts index 0ee85eb..d4f578e 100644 --- a/src/lib/presenze.ts +++ b/src/lib/presenze.ts @@ -155,31 +155,6 @@ export function serieConsecutiva( ); } -const ORE_24 = 24 * 60 * 60 * 1000; - -/** - * Serie di conferme rapide: risposte arrivate entro 24 ore dalla convocazione - * (`creatoIl` dell'evento). Gli eventi senza istante di creazione — quelli generati - * dal client, non salvati a database — non spezzano la serie: vengono saltati. - */ -export function serieConferme( - giocatoreId: string, - eventi: Evento[], - tempi: MappaTempiRisposta, - oggi: string = dataOggi(), -): number { - return serieSu( - giocatoreId, - eventi.filter((e) => e.creatoIl), - oggi, - undefined, - (e) => { - const risposto = tempi[e.id]?.[giocatoreId]; - return risposto !== undefined && Date.parse(risposto) - Date.parse(e.creatoIl!) <= ORE_24; - }, - ); -} - /** Scorre gli eventi già passati in ordine di data applicando la regola delle serie. */ function serieSu( giocatoreId: string, @@ -228,7 +203,7 @@ export function usePresenzeEvento(eventoId: string) { * * - l'istante si scrive **solo se manca** (`??=`), come fa il database, dove `risposto_il` * non viene inviato sull'upsert e un trigger lo congela: è la prima risposta, non l'ultima, - * e un ripensamento non deve far ripartire il cronometro della serie "Conferme 24h"; + * e un ripensamento non deve cambiare l'istante della prima risposta; * - cancellare la risposta (`stato: null`) elimina **anche** l'istante, così se il giocatore * risponde di nuovo il cronometro riparte davvero — ha ritirato la risposta. */ diff --git a/src/lib/rosa.ts b/src/lib/rosa.ts index 6e8ae58..62d4f5d 100644 --- a/src/lib/rosa.ts +++ b/src/lib/rosa.ts @@ -12,7 +12,6 @@ import { useEventi } from "./eventi"; import { contaPartiteGiocate, contaPresenzeGiocatore, - serieConferme, serieConsecutiva, totaliEventiGiocatore, useRispostePresenze, @@ -68,7 +67,7 @@ export function useRosa(): Giocatore[] { const { salvati: turniSalvati } = useTurniPalloni(); const { infortuni, ritardi } = useInfortuniERitardi(); const { eventi } = useEventi(); - const { presenze: mappaPresenze, tempi } = useRispostePresenze(); + const { presenze: mappaPresenze } = useRispostePresenze(); const votiMvp = voti.data; @@ -94,7 +93,6 @@ export function useRosa(): Giocatore[] { streak: serieConsecutiva(g.id, eventi, mappaPresenze), serieAllenamenti: serieConsecutiva(g.id, eventi, mappaPresenze, "allenamento"), seriePartite: serieConsecutiva(g.id, eventi, mappaPresenze, "partita"), - serieConferme: serieConferme(g.id, eventi, tempi), mvp: mvpVinti[g.id] ?? 0, mediaVoto: medie[g.id]?.media ?? 0, votiPagella: medie[g.id]?.voti ?? 0, @@ -105,18 +103,7 @@ export function useRosa(): Giocatore[] { infortuni: infortuni[g.id] ?? 0, ritardi: ritardi[g.id] ?? 0, })); - }, [ - squadra, - votiMvp, - pagelle, - cacche, - turniSalvati, - infortuni, - ritardi, - eventi, - mappaPresenze, - tempi, - ]); + }, [squadra, votiMvp, pagelle, cacche, turniSalvati, infortuni, ritardi, eventi, mappaPresenze]); } /** Criteri di ordinamento della classifica interna di Squadra. */ diff --git a/src/lib/serie.ts b/src/lib/serie.ts index 3abcbbd..940d9a3 100644 --- a/src/lib/serie.ts +++ b/src/lib/serie.ts @@ -1,8 +1,8 @@ -import { Dumbbell, Swords, Zap } from "lucide-react"; +import { Dumbbell, Swords } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import type { Giocatore } from "./crapp-data"; -export type SerieTipo = "allenamenti" | "partite" | "conferme"; +export type SerieTipo = "allenamenti" | "partite"; export type SerieDef = { tipo: SerieTipo; @@ -31,14 +31,6 @@ export const serieDefs: SerieDef[] = [ traguardi: [2, 5, 8, 12], valore: (g) => g.seriePartite, }, - { - tipo: "conferme", - label: "Conferme 24h", - descrizione: "Risposte date entro 24 ore dalla convocazione", - icon: Zap, - traguardi: [3, 8, 15, 20], - valore: (g) => g.serieConferme, - }, ]; export type SerieStato = { diff --git a/test/integration/s-mai-forfait-badge.test.ts b/test/integration/s-mai-forfait-badge.test.ts deleted file mode 100644 index dbf804a..0000000 --- a/test/integration/s-mai-forfait-badge.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Badge segreto Mai un forfait end-to-end contro il database locale: - * `bun test/integration/s-mai-forfait-badge.test.ts`. - * - * I test unitari (`test/unit/presenze.test.ts`, `test/unit/badges.test.ts`) verificano - * `serieConferme()`, `contaPresenzeGiocatore()` e `statoBadge()` come funzioni pure. Qui invece - * si scrivono eventi con `creato_il` esplicito e risposte con `risposto_il` esplicito su - * `eventi_app`/`risposte_presenze`, si rileggono via REST e si passa il risultato attraverso - * entrambe le funzioni fino a `statoBadge()` sul badge segreto `s-mai-forfait`, che è l'unico a - * combinare due statistiche indipendenti (serie di conferme rapide **e** presenze totali). - * - * Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine: - * usa id con il prefisso `test-s-mai-forfait-badge`, che nessun dato vero può avere. - */ -import assert from "node:assert/strict"; -import { badgeSegreti, statoBadge } from "@/lib/badges"; -import { serieConferme, contaPresenzeGiocatore } from "@/lib/presenze"; -import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi"; -import { giocatori, type Giocatore } from "@/lib/crapp-data"; -import { statoLocale } from "../helpers/locale"; -import { prova, riepilogo, salta } from "../helpers/prova"; -import type { Stato } from "@/lib/crapp-data"; -import type { MappaPresenze } from "@/lib/presenze"; - -const locale = statoLocale(); - -if (!locale) { - salta("badge Mai un forfait sul database", "stack locale non attivo (npx supabase start)"); - riepilogo("s-mai-forfait-badge"); -} else { - const { url: URL_BASE, servizio: SERVIZIO } = locale; - console.log(`badge Mai un forfait su ${URL_BASE}`); - - const PREFISSO = "test-s-mai-forfait-badge"; - const def = badgeSegreti.find((b) => b.id === "s-mai-forfait")!; - const OGGI = "2099-01-01"; - - 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 ?? {}), - }, - }); - - const dataEvento = (i: number) => { - const d = new Date(Date.UTC(2020, 0, 1)); - d.setUTCDate(d.getUTCDate() + i - 1); - return d.toISOString().slice(0, 10); - }; - - async function creaEvento(id: string, i: number) { - const res = await rest("eventi_app", { - method: "POST", - headers: { Prefer: "resolution=merge-duplicates,return=representation" }, - body: JSON.stringify({ - id, - tipo: "allenamento", - titolo: `Evento ${id}`, - data: dataEvento(i), - creato_il: `${dataEvento(i)}T08:00:00Z`, - }), - }); - if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`); - } - - async function rispondi(eventoId: string, giocatoreId: string, i: number) { - const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", { - method: "POST", - headers: { Prefer: "resolution=merge-duplicates,return=representation" }, - body: JSON.stringify({ - evento_id: eventoId, - giocatore_id: giocatoreId, - stato: "presente", - risposto_il: `${dataEvento(i)}T08:30:00Z`, - }), - }); - if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`); - } - - async function leggiEventi(): Promise { - const res = await rest( - "eventi_app?id=like." + - PREFISSO + - "*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il", - ); - const righe = (await res.json()) as RigaEvento[]; - return righe.map(daRiga); - } - - async function leggiPresenzeETempi(): Promise<{ - presenze: MappaPresenze; - tempi: Record>; - }> { - const res = await rest( - `risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,stato,risposto_il`, - ); - const righe = (await res.json()) as Array<{ - evento_id: string; - giocatore_id: string; - stato: Stato; - risposto_il: string; - }>; - const presenze: MappaPresenze = {}; - const tempi: Record> = {}; - for (const r of righe) { - (presenze[r.evento_id] ??= {})[r.giocatore_id] = r.stato; - (tempi[r.evento_id] ??= {})[r.giocatore_id] = r.risposto_il; - } - return { presenze, tempi }; - } - - function giocatoreAzzerato(serieConferme: number, presenze: number): Giocatore { - return { - ...giocatori[0]!, - serieConferme, - presenze, - mvp: 0, - mediaVoto: 0, - votiPagella: 0, - palloni: 0, - }; - } - - try { - await prova( - "il segreto si sblocca solo quando entrambe le condizioni sono vere insieme", - async () => { - // "mf1": 15 eventi passati, presente e confermato in fretta a tutti — soddisfa - // ampiamente sia la serie di conferme (>=10) sia le presenze (>=15). - for (let i = 1; i <= 15; i += 1) { - const id = `${PREFISSO}-a${String(i).padStart(2, "0")}`; - await creaEvento(id, i); - await rispondi(id, "mf1", i); - } - - const eventi15 = await leggiEventi(); - const { presenze: p15, tempi: t15 } = await leggiPresenzeETempi(); - - // Solo 9 eventi: serie di conferme appena sotto soglia, presenze sotto soglia. - const eventi9 = eventi15.slice(0, 9); - const serie9 = serieConferme("mf1", eventi9, t15, OGGI); - const pres9 = contaPresenzeGiocatore("mf1", eventi9, p15, OGGI); - assert.equal(serie9, 9); - assert.equal(pres9, 9); - assert.equal( - statoBadge(def, giocatoreAzzerato(serie9, pres9)).grado, - null, - "9 conferme e 9 presenze: nessuna delle due soglie raggiunta", - ); - - const serie15 = serieConferme("mf1", eventi15, t15, OGGI); - const pres15 = contaPresenzeGiocatore("mf1", eventi15, p15, OGGI); - assert.equal(serie15, 15); - assert.equal(pres15, 15); - assert.equal( - statoBadge(def, giocatoreAzzerato(serie15, pres15)).grado, - "oro", - "serie conferme >=10 e presenze >=15: il segreto si sblocca", - ); - }, - ); - - await prova("una risposta lenta rompe la serie ma non le presenze già accumulate", async () => { - // Un sedicesimo evento con risposta arrivata oltre le 24h: la serie conferme torna a 0, - // ma le presenze (indipendenti) restano a 16 — il segreto deve richiudersi, non serve - // ripartire da zero anche sulle presenze. - const idLento = `${PREFISSO}-a16`; - await creaEvento(idLento, 16); - const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", { - method: "POST", - headers: { Prefer: "resolution=merge-duplicates,return=representation" }, - body: JSON.stringify({ - evento_id: idLento, - giocatore_id: "mf1", - stato: "presente", - risposto_il: `${dataEvento(18)}T08:00:00Z`, // 2 giorni dopo la convocazione - }), - }); - if (!res.ok) - throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`); - - const eventi = await leggiEventi(); - const { presenze, tempi } = await leggiPresenzeETempi(); - const serie = serieConferme("mf1", eventi, tempi, OGGI); - const conto = contaPresenzeGiocatore("mf1", eventi, presenze, OGGI); - assert.equal(serie, 0, "la risposta lenta azzera la serie di conferme"); - assert.equal(conto, 16, "le presenze restano quelle di sempre, indipendenti dalla serie"); - assert.equal( - statoBadge(def, giocatoreAzzerato(serie, conto)).grado, - null, - "presenze abbondanti ma serie azzerata: il segreto si richiude", - ); - }); - } finally { - await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" }); - await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" }); - } - - riepilogo("s-mai-forfait-badge"); -} diff --git a/test/integration/scritture.test.ts b/test/integration/scritture.test.ts index e470703..8767d78 100644 --- a/test/integration/scritture.test.ts +++ b/test/integration/scritture.test.ts @@ -254,8 +254,8 @@ if (!locale) { // --- presenze: la risposta si cambia, il cronometro no -------------------------- // Due colonne che sembrano la stessa cosa e non lo sono: `risposto_il` è la PRIMA - // risposta e alimenta la serie "Conferme 24h", `aggiornato_il` è l'ultima modifica e non - // alimenta niente. Il trigger `risposte_presenze_risposto_il_immutabile` (M9) tiene ferma + // risposta, `aggiornato_il` è l'ultima modifica; oggi nessuna statistica le usa (la serie + // «Conferme 24h» è stata rimossa). Il trigger `risposte_presenze_risposto_il_immutabile` (M9) tiene ferma // la prima: senza, chi risponde subito e ci ripensa una settimana dopo risulterebbe lento. await prova( "la risposta di presenza si aggiorna senza far ripartire il cronometro", diff --git a/test/integration/serie-conferme-badge.test.ts b/test/integration/serie-conferme-badge.test.ts deleted file mode 100644 index 8b66782..0000000 --- a/test/integration/serie-conferme-badge.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Badge Risposta lampo end-to-end contro il database locale: - * `bun test/integration/serie-conferme-badge.test.ts`. - * - * I test unitari (`test/unit/presenze.test.ts`, `test/unit/badges.test.ts`) verificano - * `serieConferme()` e `statoBadge()` come funzioni pure, con eventi e risposte costruiti a - * mano. Qui invece si scrivono allenamenti/partite ed eventi veri su `eventi_app`, e risposte - * vere su `risposte_presenze` con `creato_il`/`risposto_il` espliciti, si rileggono via REST - * con la stessa forma di `daRiga()`/`fetchPresenze()`, e si passa il risultato attraverso - * `serieConferme()` fino a `statoBadge()` sul badge `serie-conferme`: se una colonna cambia - * nome, o la mappatura `creato_il`/`risposto_il` -> `creatoIl`/`tempi` si rompe, qui il grado - * del badge torna sbagliato anche se i test unitari restano verdi, perché quelli non toccano - * mai il database. - * - * Copre due comportamenti non ovvi con dati veri, non solo con numeri scelti a mano: - * - una risposta arrivata oltre le 24h dalla convocazione azzera la serie, anche dopo una - * striscia lunga; - * - partite e allenamenti contano nella stessa serie, senza bisogno di un filtro per tipo. - * - * Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine: - * usa id con il prefisso `test-serie-conferme-badge`, che nessun dato vero può avere. - */ -import assert from "node:assert/strict"; -import { badgeDefs, statoBadge } from "@/lib/badges"; -import { serieConferme } from "@/lib/presenze"; -import { daRiga, type RigaEvento, type Evento } from "@/lib/eventi"; -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 Risposta lampo sul database", "stack locale non attivo (npx supabase start)"); - riepilogo("serie-conferme-badge"); -} else { - const { url: URL_BASE, servizio: SERVIZIO } = locale; - console.log(`badge Risposta lampo su ${URL_BASE}`); - - const PREFISSO = "test-serie-conferme-badge"; - const serieDef = badgeDefs.find((b) => b.id === "serie-conferme")!; - // "Oggi" fissato nel futuro: gli eventi scritti sotto sono datati nel passato remoto, così - // restano "passati" (quindi contati) a prescindere da quando gira il test. - const OGGI = "2099-01-01"; - - 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 ?? {}), - }, - }); - - /** `2020-01-01` per i=1, avanzando di un giorno per ogni evento: niente collisioni di data. */ - const dataEvento = (i: number) => { - const d = new Date(Date.UTC(2020, 0, 1)); - d.setUTCDate(d.getUTCDate() + i - 1); - return d.toISOString().slice(0, 10); - }; - - /** Istante di convocazione: le 8 del mattino del giorno dell'evento. */ - const creazioneEvento = (i: number) => `${dataEvento(i)}T08:00:00Z`; - - async function creaEvento( - id: string, - i: number, - tipo: "allenamento" | "partita" = "allenamento", - ) { - const res = await rest("eventi_app", { - method: "POST", - headers: { Prefer: "resolution=merge-duplicates,return=representation" }, - body: JSON.stringify({ - id, - tipo, - titolo: `Evento ${id}`, - data: dataEvento(i), - creato_il: creazioneEvento(i), - }), - }); - if (!res.ok) throw new Error(`creazione evento ${id}: ${res.status} ${await res.text()}`); - } - - async function rispondi( - eventoId: string, - giocatoreId: string, - stato: string, - rispostoIl: string, - ) { - const res = await rest("risposte_presenze?on_conflict=evento_id,giocatore_id", { - method: "POST", - headers: { Prefer: "resolution=merge-duplicates,return=representation" }, - body: JSON.stringify({ - evento_id: eventoId, - giocatore_id: giocatoreId, - stato, - risposto_il: rispostoIl, - }), - }); - if (!res.ok) throw new Error(`upsert su risposte_presenze: ${res.status} ${await res.text()}`); - } - - async function leggiEventi(): Promise { - const res = await rest( - "eventi_app?id=like." + - PREFISSO + - "*&select=id,tipo,titolo,luogo,data,ora,note,convocati,campionato,casa,pagelle_chiuse,creato_il", - ); - const righe = (await res.json()) as RigaEvento[]; - return righe.map(daRiga); - } - - /** Stessa forma di `fetchPresenze()`: eventoId -> giocatoreId -> istante della risposta. */ - async function leggiTempi(): Promise>> { - const res = await rest( - `risposte_presenze?evento_id=like.${PREFISSO}*&select=evento_id,giocatore_id,risposto_il`, - ); - const righe = (await res.json()) as Array<{ - evento_id: string; - giocatore_id: string; - risposto_il: string; - }>; - const mappa: Record> = {}; - for (const r of righe) (mappa[r.evento_id] ??= {})[r.giocatore_id] = r.risposto_il; - return mappa; - } - - function giocatoreAzzerato(serieConferme: number): Giocatore { - return { - ...giocatori[0]!, - serieConferme, - mvp: 0, - mediaVoto: 0, - votiPagella: 0, - palloni: 0, - presenze: 0, - }; - } - - try { - await prova( - "il badge passa da bronzo a oro con conferme rapide vere, e una risposta lenta lo azzera", - async () => { - // 15 eventi passati, tutti confermati mezz'ora dopo la convocazione per "sc1": - // bronzo (3), poi argento (8), poi oro (15) man mano che si rileggono i dati. - for (let i = 1; i <= 15; i += 1) { - const id = `${PREFISSO}-a${String(i).padStart(2, "0")}`; - await creaEvento(id, i); - await rispondi(id, "sc1", "presente", `${dataEvento(i)}T08:30:00Z`); - } - const eventi15 = await leggiEventi(); - const tempi15 = await leggiTempi(); - - const serieA3 = serieConferme("sc1", eventi15.slice(0, 3), tempi15, OGGI); - assert.equal(serieA3, 3); - assert.equal( - statoBadge(serieDef, giocatoreAzzerato(serieA3)).grado, - "bronzo", - "3 conferme rapide di fila: soglia bronzo", - ); - - const serieA8 = serieConferme("sc1", eventi15.slice(0, 8), tempi15, OGGI); - assert.equal(serieA8, 8); - assert.equal( - statoBadge(serieDef, giocatoreAzzerato(serieA8)).grado, - "argento", - "8 conferme rapide di fila: soglia argento", - ); - - const serieCompleta = serieConferme("sc1", eventi15, tempi15, OGGI); - assert.equal(serieCompleta, 15, "confermato in fretta a tutti e 15, nessun buco"); - assert.equal( - statoBadge(serieDef, giocatoreAzzerato(serieCompleta)).grado, - "oro", - "15 conferme rapide di fila: soglia oro", - ); - - // Un sedicesimo evento con una risposta arrivata dopo 2 giorni azzera la serie. - const idLento = `${PREFISSO}-a16`; - await creaEvento(idLento, 16); - await rispondi(idLento, "sc1", "presente", `${dataEvento(18)}T08:00:00Z`); - const eventiConRitardo = await leggiEventi(); - const tempiConRitardo = await leggiTempi(); - const serieDopoRitardo = serieConferme("sc1", eventiConRitardo, tempiConRitardo, OGGI); - assert.equal( - serieDopoRitardo, - 0, - "una risposta oltre le 24h azzera la serie, anche dopo 15 conferme di fila", - ); - assert.equal( - statoBadge(serieDef, giocatoreAzzerato(serieDopoRitardo)).grado, - null, - "serie azzerata: nessun grado, si riparte da zero", - ); - }, - ); - - await prova( - "partite e allenamenti contano nella stessa serie, senza filtro per tipo", - async () => { - // "sc2": conferma rapida su un allenamento, poi su una partita, poi su un altro - // allenamento: la serie sale a 3 senza bisogno che siano tutti dello stesso tipo. - await creaEvento(`${PREFISSO}-b01`, 30, "allenamento"); - await creaEvento(`${PREFISSO}-b02`, 31, "partita"); - await creaEvento(`${PREFISSO}-b03`, 32, "allenamento"); - await rispondi(`${PREFISSO}-b01`, "sc2", "presente", `${dataEvento(30)}T08:30:00Z`); - await rispondi(`${PREFISSO}-b02`, "sc2", "presente", `${dataEvento(31)}T08:30:00Z`); - await rispondi(`${PREFISSO}-b03`, "sc2", "presente", `${dataEvento(32)}T08:30:00Z`); - - const eventiLetti = await leggiEventi(); - const tempiLetti = await leggiTempi(); - const serie = serieConferme("sc2", eventiLetti, tempiLetti, OGGI); - assert.equal(serie, 3, "partita in mezzo a due allenamenti: la serie resta a 3"); - assert.equal( - statoBadge(serieDef, giocatoreAzzerato(serie)).grado, - "bronzo", - "3 conferme rapide di fila, tipo misto: soglia bronzo", - ); - }, - ); - } finally { - await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" }); - await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" }); - } - - riepilogo("serie-conferme-badge"); -} diff --git a/test/unit/badges.test.ts b/test/unit/badges.test.ts index aeaf417..1e3d19f 100644 --- a/test/unit/badges.test.ts +++ b/test/unit/badges.test.ts @@ -34,7 +34,6 @@ function g(valori: Partial = {}): Giocatore { streak: 0, serieAllenamenti: 0, seriePartite: 0, - serieConferme: 0, ...valori, }; } @@ -146,16 +145,6 @@ assert.equal(gradoRaggiunto(serieAllenamentiDef, 9), "argento"); assert.equal(gradoRaggiunto(serieAllenamentiDef, 10), "oro"); assert.equal(gradoRaggiunto(serieAllenamentiDef, 50), "oro", "oltre l'oro resta oro"); -// --- serie-conferme: soglie 3 / 8 / 15 ---------------------------------------- -const serieConfermeDef = badgeDefs.find((b) => b.id === "serie-conferme")!; -assert.equal(gradoRaggiunto(serieConfermeDef, 2), null, "sotto la prima soglia nessun grado"); -assert.equal(gradoRaggiunto(serieConfermeDef, 3), "bronzo", "la soglia è inclusiva"); -assert.equal(gradoRaggiunto(serieConfermeDef, 7), "bronzo"); -assert.equal(gradoRaggiunto(serieConfermeDef, 8), "argento"); -assert.equal(gradoRaggiunto(serieConfermeDef, 14), "argento"); -assert.equal(gradoRaggiunto(serieConfermeDef, 15), "oro"); -assert.equal(gradoRaggiunto(serieConfermeDef, 50), "oro", "oltre l'oro resta oro"); - // --- badgeGiocatore ---------------------------------------------------------- assert.equal(badgeGiocatore(g()).length, badgeDefs.length, "i badge normali sono sempre tutti"); assert.ok( @@ -200,36 +189,6 @@ assert.deepEqual( ["s-cacche"], ); assert.equal(badgeSegretiSbloccati(g({ cacche: 2 })).length, 0, "2 cacche non bastano"); -assert.deepEqual( - badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 15 })).map((b) => b.def.id), - ["s-mai-forfait"], -); -assert.equal( - badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 14 })).length, - 0, - "servono entrambe le condizioni", -); -// Confini isolati: ogni soglia testata da sola, con l'altra abbondantemente sopra. -assert.equal( - badgeSegretiSbloccati(g({ serieConferme: 9, presenze: 30 })).length, - 0, - "serieConferme appena sotto soglia, presenze abbondanti: non basta", -); -assert.deepEqual( - badgeSegretiSbloccati(g({ serieConferme: 10, presenze: 30 })).map((b) => b.def.id), - ["s-mai-forfait"], - "serieConferme esattamente al confine, presenze abbondanti: sblocca", -); -assert.equal( - badgeSegretiSbloccati(g({ serieConferme: 30, presenze: 14 })).length, - 0, - "presenze appena sotto soglia, serieConferme abbondante: non basta", -); -assert.deepEqual( - badgeSegretiSbloccati(g({ serieConferme: 30, presenze: 15 })).map((b) => b.def.id), - ["s-mai-forfait"], - "presenze esattamente al confine, serieConferme abbondante: sblocca", -); // --- collezioneBadge --------------------------------------------------------- const vuota = collezioneBadge(g()); @@ -262,7 +221,6 @@ assert.equal( palloni: 10, presenze: 30, serieAllenamenti: 10, - serieConferme: 15, }), ), null, diff --git a/test/unit/notifiche-smart.test.ts b/test/unit/notifiche-smart.test.ts index f84f8fb..764e484 100644 --- a/test/unit/notifiche-smart.test.ts +++ b/test/unit/notifiche-smart.test.ts @@ -19,7 +19,6 @@ function g(valori: Partial = {}): Giocatore { streak: 0, serieAllenamenti: 0, seriePartite: 0, - serieConferme: 0, ...valori, }; } @@ -57,7 +56,7 @@ assert.ok( // --- serie: solo sul traguardo esatto ---------------------------------------- assert.ok(ids(g({ serieAllenamenti: 3 })).includes("serie:allenamenti:3")); assert.ok(!ids(g({ serieAllenamenti: 4 })).includes("serie:allenamenti:4"), "4 non è un traguardo"); -assert.ok(ids(g({ serieConferme: 8 })).includes("serie:conferme:8")); +assert.ok(ids(g({ seriePartite: 5 })).includes("serie:partite:5")); // --- obiettivi di squadra: uno solo, tra il 90% e il 99% -------------------- const obiettivo = (id: string, valore: number): ObiettivoSquadra => ({ diff --git a/test/unit/presenze.test.ts b/test/unit/presenze.test.ts index e931132..0ba147b 100644 --- a/test/unit/presenze.test.ts +++ b/test/unit/presenze.test.ts @@ -7,7 +7,6 @@ import { contaPresenzeGiocatore, destinatariSollecito, includeInfortunato, - serieConferme, serieConsecutiva, testoSollecito, totaliEventiGiocatore, @@ -218,91 +217,9 @@ assert.ok( "senza luogo la riga manca", ); -// --- serie conferme: risposta entro 24h dalla convocazione -------------------- -const convocati = (id: string, data: string, creatoIl?: string): Evento => ({ - ...ev(id, "allenamento", data), - ...(creatoIl === undefined ? {} : { creatoIl }), -}); - -const conConvocazione: Evento[] = [ - convocati("c1", "2026-08-06", "2026-08-01T10:00:00Z"), - convocati("c2", "2026-08-13", "2026-08-08T10:00:00Z"), - convocati("c3", "2026-08-20", "2026-08-15T10:00:00Z"), - convocati("c4", "2026-08-27"), // evento generato dal client: nessuna convocazione tracciata - convocati("c5", "2026-09-10", "2026-09-05T10:00:00Z"), // futuro -]; - -const tempi: MappaTempiRisposta = { - c1: { g1: "2026-08-01T11:00:00Z" }, // un'ora dopo - c2: { g1: "2026-08-10T10:00:00Z" }, // due giorni dopo: buco - c3: { g1: "2026-08-16T09:59:00Z" }, // appena dentro le 24h - c5: { g1: "2026-09-05T10:30:00Z" }, -}; - -assert.equal(serieConferme("g1", conConvocazione, tempi, OGGI), 1, "il ritardo su c2 azzera"); -assert.equal( - serieConferme("g1", conConvocazione.slice(0, 1), tempi, OGGI), - 1, - "una risposta rapida vale 1", -); -assert.equal( - serieConferme("g1", [conConvocazione[0]!, conConvocazione[3]!, conConvocazione[2]!], tempi, OGGI), - 2, - "un evento senza istante di convocazione viene saltato, non spezza la serie", -); -assert.equal(serieConferme("g2", conConvocazione, tempi, OGGI), 0, "chi non risponde è a zero"); - -// Chi non è convocato non spezza la serie conferme di nessun altro (stesso filtro condiviso -// con `serieConsecutiva`, vedi sopra). -const c6Ristretto: Evento = { - ...convocati("c6", "2026-08-08", "2026-08-03T10:00:00Z"), - convocati: ["g9"], -}; -assert.equal( - serieConferme("g1", [conConvocazione[0]!, c6Ristretto], tempi, OGGI), - 1, - "un evento convocato solo per un altro giocatore resta fuori, la serie di g1 non ne risente", -); - -// Le conferme contano partite e allenamenti insieme: nessuna versione per tipo. -const mistiTipo: Evento[] = [ - convocati("m1", "2026-08-06", "2026-08-01T10:00:00Z"), - { ...convocati("m2", "2026-08-13", "2026-08-08T10:00:00Z"), tipo: "partita" }, -]; -const tempiMisti: MappaTempiRisposta = { - m1: { g1: "2026-08-01T11:00:00Z" }, // un'ora dopo - m2: { g1: "2026-08-08T11:00:00Z" }, // un'ora dopo, ma è una partita -}; -assert.equal( - serieConferme("g1", mistiTipo, tempiMisti, OGGI), - 2, - "partite e allenamenti si sommano nella stessa serie", -); - -// Il confronto con le 24h è inclusivo: esattamente al bordo conta, un secondo oltre azzera. -const bordo: Evento[] = [convocati("b1", "2026-08-06", "2026-08-01T10:00:00Z")]; -assert.equal( - serieConferme("g1", bordo, { b1: { g1: "2026-08-02T10:00:00Z" } }, OGGI), - 1, - "esattamente 24h dopo: il confronto è <=, quindi conta", -); -assert.equal( - serieConferme("g1", bordo, { b1: { g1: "2026-08-02T10:00:01Z" } }, OGGI), - 0, - "un secondo oltre le 24h azzera", -); - -// Un evento futuro non conta ancora, anche con una risposta rapidissima già registrata. -const futuro: Evento[] = [convocati("f1", "2026-09-10", "2026-09-05T10:00:00Z")]; -assert.equal( - serieConferme("g1", futuro, { f1: { g1: "2026-09-05T10:05:00Z" } }, OGGI), - 0, - "l'evento di domani non è ancora passato, non entra nel calcolo", -); - // --- cache locale dopo una risposta: il cronometro non riparte ---------------- // Stessa regola del database: `risposto_il` è la PRIMA risposta e un trigger la congela. -// Qui la cache deve imitarla, altrimenti la serie "Conferme 24h" mente fino al refresh. +// Qui la cache deve imitarla, altrimenti la cache mente fino al refresh. const PRIMA = "2026-08-01T10:00:00Z"; const POI = "2026-08-08T10:00:00Z"; diff --git a/test/unit/serie.test.ts b/test/unit/serie.test.ts index edf476e..f7c84aa 100644 --- a/test/unit/serie.test.ts +++ b/test/unit/serie.test.ts @@ -4,7 +4,7 @@ import { giocatori, type Giocatore } from "@/lib/crapp-data"; import { aggiornaSerie, serieDefs, serieGiocatore, serieMigliore, statoSerie } from "@/lib/serie"; function g(valori: Partial = {}): Giocatore { - return { ...giocatori[0]!, serieAllenamenti: 0, seriePartite: 0, serieConferme: 0, ...valori }; + return { ...giocatori[0]!, serieAllenamenti: 0, seriePartite: 0, ...valori }; } const allenamenti = serieDefs.find((d) => d.tipo === "allenamenti")!; @@ -45,16 +45,16 @@ assert.deepEqual([fuoriScala.prossimo, fuoriScala.manca, fuoriScala.progresso], assert.equal(fuoriScala.messaggio, "Serie leggendaria: sei fuori scala!"); // --- serieGiocatore / serieMigliore ------------------------------------------ -const tutte = serieGiocatore(g({ serieAllenamenti: 4, seriePartite: 1, serieConferme: 9 })); -assert.equal(tutte.length, serieDefs.length, "tre serie indipendenti"); +const tutte = serieGiocatore(g({ serieAllenamenti: 4, seriePartite: 6 })); +assert.equal(tutte.length, serieDefs.length, "due serie indipendenti"); assert.deepEqual( tutte.map((s) => s.valore), - [4, 1, 9], + [4, 6], "ogni serie legge il proprio contatore", ); -const migliore = serieMigliore(g({ serieAllenamenti: 4, seriePartite: 1, serieConferme: 9 })); -assert.equal(migliore.def.tipo, "conferme", "in home si mostra la serie più lunga"); +const migliore = serieMigliore(g({ serieAllenamenti: 4, seriePartite: 6 })); +assert.equal(migliore.def.tipo, "partite", "in home si mostra la serie più lunga"); assert.ok(serieMigliore(g()), "anche a zero c'è sempre una serie da mostrare"); // --- invarianti sulle definizioni --------------------------------------------