Rende dinamico il mese degli obiettivi di squadra invece di una costante fissa

"Presenze del mese" e "evento di squadra al mese" erano ancorati a una costante
MESE = "2026-08": passato agosto restavano congelati sul mese scorso invece di
azzerarsi a ogni cambio mese. Ora il mese di riferimento (e per il primo anche
titolo e scadenza) segue la data corrente, con un parametro `oggi` iniettabile
per test deterministici. Aggiunti unit test e un integration test end-to-end
contro il database locale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 11:00:31 +02:00
co-authored by Claude Sonnet 5
parent 440bf0cd56
commit 2b30dd4708
4 changed files with 339 additions and 24 deletions
+215
View File
@@ -0,0 +1,215 @@
/**
* Obiettivi mensili di squadra ("presenze del mese" e "evento di squadra al mese")
* end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`.
*
* I test unitari (`test/unit/obiettivi.test.ts`) verificano `obiettiviSquadra()` come funzione
* pura, con un `ContestoObiettivi` costruito a mano. Qui invece si scrivono righe vere su
* `eventi_app` e `risposte_presenze`, si rileggono con `leggiEventi()` (la stessa funzione che
* usa l'app lato server) e una query REST equivalente a `fetchPresenze()`, e si verifica che il
* risultato che arriva a `obiettiviSquadra()` sia quello atteso: se una colonna cambia nome o
* la mappatura si rompe, qui il valore torna sbagliato (o NaN) anche se i test unitari restano
* verdi, perché quelli non toccano mai il database.
*
* Il mese di test è marzo 2099: non collide con gli eventi del seed (tutti in agosto 2026), così
* questi due obiettivi — che ora aggregano su TUTTI gli eventi del mese corrente, non solo
* quelli del test — restano prevedibili senza dover filtrare gli eventi letti dal database.
*
* Gira solo sullo stack locale (`npx supabase start`) e cancella le proprie righe alla fine:
* usa id con il prefisso `test-obiettivi`, che nessun dato vero può avere.
*/
import assert from "node:assert/strict";
import { giocatori } from "@/lib/crapp-data";
import { obiettiviSquadra } from "@/lib/obiettivi";
import type { MappaPresenze } from "@/lib/presenze";
import { statoLocale } from "../helpers/locale";
import { prova, riepilogo, salta } from "../helpers/prova";
const locale = statoLocale();
if (!locale) {
salta("obiettivi (o1) sul database", "stack locale non attivo (npx supabase start)");
riepilogo("obiettivi");
} else {
const { url: URL_BASE, servizio: SERVIZIO } = locale;
console.log(`obiettivi su ${URL_BASE}`);
// Prima di qualsiasi import dei moduli server: è da qui che nasce `supabaseAdmin`.
process.env["SUPABASE_URL"] = URL_BASE;
process.env["SUPABASE_SERVICE_ROLE_KEY"] = SERVIZIO;
const { leggiEventi } = await import("@/lib/eventi.server");
const PREFISSO = "test-obiettivi";
const MESE_TEST = "2099-03";
const OGGI = new Date("2099-03-15T10:00:00Z");
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 ?? {}),
},
});
/** Stessa query di `fetchPresenze()` (`src/lib/presenze.ts`), raggruppata a mano. */
async function leggiPresenze(eventoId: string): Promise<MappaPresenze> {
const res = await rest(
`risposte_presenze?evento_id=eq.${eventoId}&select=evento_id,giocatore_id,stato`,
);
const righe = (await res.json()) as Array<{
evento_id: string;
giocatore_id: string;
stato: string;
}>;
const mappa: MappaPresenze = {};
for (const r of righe) {
(mappa[r.evento_id] ??= {})[r.giocatore_id] = r.stato as never;
}
return mappa;
}
try {
const eventoId = `${PREFISSO}-e1`;
await prova("prepara evento e presenze reali su Supabase locale", async () => {
const inserito = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: eventoId,
tipo: "allenamento",
titolo: "Test obiettivi o1",
data: `${MESE_TEST}-10`,
}),
});
if (!inserito.ok) throw new Error(`inserimento evento fallito: ${await inserito.text()}`);
// 9 presenti su 17 giocatori del roster reale (`crapp-data.ts`, id g1..g17).
const presenti = giocatori.slice(0, 9).map((g) => g.id);
const righe = giocatori.map((g) => ({
evento_id: eventoId,
giocatore_id: g.id,
stato: presenti.includes(g.id) ? "presente" : "assente",
}));
const inseritePresenze = await rest("risposte_presenze", { method: "POST", body: JSON.stringify(righe) });
if (!inseritePresenze.ok) {
throw new Error(`inserimento presenze fallito: ${await inseritePresenze.text()}`);
}
});
await prova(
"o1 legge l'evento e le presenze vere dal database e calcola la percentuale del mese",
async () => {
const eventiReali = await leggiEventi();
assert.ok(
eventiReali.some((e) => e.id === eventoId),
"leggiEventi() include l'evento appena inserito",
);
const presenzeReali = await leggiPresenze(eventoId);
const obiettivi = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
OGGI,
);
const o1 = obiettivi.find((o) => o.id === "o1")!;
assert.equal(
o1.valore,
Math.round((9 / giocatori.length) * 100),
"9 presenti su tutta la rosa, letti dal database",
);
assert.equal(o1.titolo, "90% di presenze a marzo", "titolo segue il mese iniettato");
assert.equal(o1.scadenza, "2099-03-31", "scadenza = ultimo giorno di marzo");
},
);
await prova("un evento fuori dal mese di test non sposta o1", async () => {
const eventoFuoriMese = `${PREFISSO}-e2`;
const fuori = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: eventoFuoriMese,
tipo: "allenamento",
titolo: "Test obiettivi o1 - fuori mese",
data: "2099-04-01",
}),
});
if (!fuori.ok) throw new Error(`inserimento evento fallito: ${await fuori.text()}`);
try {
const presenzeFuoriMese = giocatori.map((g) => ({
evento_id: eventoFuoriMese,
giocatore_id: g.id,
stato: "presente",
}));
const scritte = await rest("risposte_presenze", {
method: "POST",
body: JSON.stringify(presenzeFuoriMese),
});
if (!scritte.ok) throw new Error(`inserimento presenze fallito: ${await scritte.text()}`);
const eventiReali = await leggiEventi();
const presenzeReali = {
...(await leggiPresenze(eventoId)),
...(await leggiPresenze(eventoFuoriMese)),
};
const o1 = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
OGGI,
).find((o) => o.id === "o1")!;
assert.equal(
o1.valore,
Math.round((9 / giocatori.length) * 100),
"l'evento di aprile non entra nel calcolo di marzo",
);
} finally {
await rest(`eventi_app?id=eq.${eventoFuoriMese}`, { method: "DELETE" });
await rest(`risposte_presenze?evento_id=eq.${eventoFuoriMese}`, { method: "DELETE" });
}
});
await prova(
"o6 legge dal database l'evento sociale del mese, azzerandosi come o1",
async () => {
const pizzataId = `${PREFISSO}-pizzata`;
const inserita = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: pizzataId,
tipo: "evento",
titolo: "Test obiettivi o6",
data: `${MESE_TEST}-20`,
}),
});
if (!inserita.ok) throw new Error(`inserimento evento fallito: ${await inserita.text()}`);
const eventiReali = await leggiEventi();
const o6InMese = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: {}, pagelle: [] },
OGGI,
).find((o) => o.id === "o6")!;
assert.equal(o6InMese.valore, 1, "l'evento sociale di marzo conta letto dal database");
const OGGI_MESE_DOPO = new Date("2099-04-15T10:00:00Z");
const o6MeseDopo = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: {}, pagelle: [] },
OGGI_MESE_DOPO,
).find((o) => o.id === "o6")!;
assert.equal(
o6MeseDopo.valore,
0,
"lo stesso evento non conta più il mese successivo (si azzera)",
);
},
);
} finally {
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
riepilogo("obiettivi");
}
}
+79 -7
View File
@@ -30,8 +30,12 @@ const evento = (id: string, data: string, tipo: Evento["tipo"]): Evento => ({
const trova = (lista: ObiettivoSquadra[], id: string) => lista.find((o) => o.id === id)!;
// Data di riferimento fissa: o1 (presenze del mese) ora dipende dal mese corrente,
// quindi va iniettata esplicitamente per avere test deterministici.
const OGGI_AGOSTO = new Date("2026-08-15T10:00:00Z");
// --- contesto vuoto: nessuna divisione per zero ------------------------------
const vuoti = obiettiviSquadra(giocatori, contestoVuoto);
const vuoti = obiettiviSquadra(giocatori, contestoVuoto, OGGI_AGOSTO);
assert.equal(trova(vuoti, "o1").valore, 0, "nessun evento nel mese: 0%, non NaN");
assert.equal(trova(vuoti, "o2").valore, 0);
assert.equal(trova(vuoti, "o12").valore, 0, "nessuna pagella: media 0");
@@ -49,25 +53,32 @@ const ctx: ContestoObiettivi = {
presenze: tuttiPresenti,
pagelle: [],
};
assert.equal(trova(obiettiviSquadra(giocatori, ctx), "o1").valore, 100, "rosa al completo = 100%");
assert.equal(
trova(obiettiviSquadra(giocatori, ctx, OGGI_AGOSTO), "o1").valore,
100,
"rosa al completo = 100%",
);
const metaRosa: MappaPresenze = {
a1: Object.fromEntries(
giocatori.map((g, i) => [g.id, i % 2 === 0 ? ("presente" as const) : ("assente" as const)]),
),
};
const percentuale = trova(obiettiviSquadra(giocatori, { ...ctx, presenze: metaRosa }), "o1").valore;
const percentuale = trova(
obiettiviSquadra(giocatori, { ...ctx, presenze: metaRosa }, OGGI_AGOSTO),
"o1",
).valore;
assert.ok(percentuale > 40 && percentuale < 60, `metà rosa presente ≈ 50%, era ${percentuale}`);
// Il ritardo conta come presenza, il "forse" no.
const conRitardo: MappaPresenze = { a1: { g1: "ritardo", g2: "forse" } };
assert.equal(
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }), "o1").valore,
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }, OGGI_AGOSTO), "o1").valore,
Math.round((1 / giocatori.length) * 100),
"solo il ritardo conta come presente",
);
assert.equal(
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }), "o2").valore,
trova(obiettiviSquadra(giocatori, { ...ctx, presenze: conRitardo }, OGGI_AGOSTO), "o2").valore,
Math.round((2 / giocatori.length) * 100),
"per le risposte anche il forse conta",
);
@@ -78,7 +89,7 @@ const fuoriMese: ContestoObiettivi = {
presenze: { s1: { g1: "presente" } },
pagelle: [],
};
assert.equal(trova(obiettiviSquadra(giocatori, fuoriMese), "o1").valore, 0);
assert.equal(trova(obiettiviSquadra(giocatori, fuoriMese, OGGI_AGOSTO), "o1").valore, 0);
// I compleanni non richiedono risposta.
const soloCompleanni: ContestoObiettivi = {
@@ -86,7 +97,68 @@ const soloCompleanni: ContestoObiettivi = {
presenze: {},
pagelle: [],
};
assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni), "o2").valore, 0);
assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2").valore, 0);
// --- o1: si azzera a ogni cambio mese, in base agli eventi a calendario ------
{
// Stesso evento/presenze: "in mese" a settembre, "fuori mese" se letto da agosto.
const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z");
const eventoSettembre: ContestoObiettivi = {
eventi: [evento("s2", "2026-09-04", "allenamento")],
presenze: { s2: Object.fromEntries(giocatori.map((g) => [g.id, "presente" as const])) },
pagelle: [],
};
assert.equal(
trova(obiettiviSquadra(giocatori, eventoSettembre, OGGI_SETTEMBRE), "o1").valore,
100,
"evento di settembre conta se oggi è settembre",
);
assert.equal(
trova(obiettiviSquadra(giocatori, eventoSettembre, OGGI_AGOSTO), "o1").valore,
0,
"lo stesso evento non conta se oggi è agosto",
);
// Titolo e scadenza seguono il mese corrente, non più una stagione fissa.
const o1Agosto = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_AGOSTO), "o1");
assert.equal(o1Agosto.titolo, "90% di presenze ad agosto", "elisione 'ad' davanti a vocale");
assert.equal(o1Agosto.scadenza, "2026-08-31", "scadenza = ultimo giorno del mese");
const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1");
assert.ok(o1Settembre.titolo.includes("settembre"), "titolo o1 riflette il mese iniettato (settembre)");
assert.equal(o1Settembre.scadenza, "2026-09-30", "scadenza = ultimo giorno di settembre (30 gg)");
}
// --- o6: evento di squadra al mese, si azzera come o1 ------------------------
{
const OGGI_SETTEMBRE = new Date("2026-09-05T10:00:00Z");
const pizzataSettembre: ContestoObiettivi = {
eventi: [evento("p1", "2026-09-12", "evento")],
presenze: {},
pagelle: [],
};
assert.equal(
trova(obiettiviSquadra(giocatori, pizzataSettembre, OGGI_SETTEMBRE), "o6").valore,
1,
"l'evento sociale di settembre conta se oggi è settembre",
);
assert.equal(
trova(obiettiviSquadra(giocatori, pizzataSettembre, OGGI_AGOSTO), "o6").valore,
0,
"lo stesso evento non conta se oggi è agosto",
);
const allenamentoNelMese: ContestoObiettivi = {
eventi: [evento("al1", "2026-08-12", "allenamento")],
presenze: {},
pagelle: [],
};
assert.equal(
trova(obiettiviSquadra(giocatori, allenamentoNelMese, OGGI_AGOSTO), "o6").valore,
0,
"un allenamento nel mese non è un evento sociale",
);
}
// --- somme sulla rosa --------------------------------------------------------
const sommaPresenze = giocatori.reduce((s, g) => s + g.presenze, 0);