Rende dinamica anche la scadenza di "Tutti rispondono alle convocazioni"

La scadenza era una data fissa (2026-09-30), stesso problema già risolto per
"presenze del mese" e "evento di squadra al mese": ora segue l'ultimo giorno
del mese corrente. Aggiunti unit test e integration test end-to-end contro il
database locale (isolando gli eventi di test, perché questo obiettivo aggrega
su tutti gli eventi indipendentemente dal mese).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 11:14:57 +02:00
co-authored by Claude Sonnet 5
parent 2b30dd4708
commit e13722eae9
4 changed files with 93 additions and 8 deletions
+11 -5
View File
@@ -59,8 +59,14 @@ smart (`notifiche-smart.ts`).
"90% presenze del mese" e "1 evento di squadra al mese" si azzerano automaticamente a ogni "90% presenze del mese" e "1 evento di squadra al mese" si azzerano automaticamente a ogni
cambio mese: il mese di riferimento è calcolato dalla data corrente (fuso Europe/Rome), non più cambio mese: il mese di riferimento è calcolato dalla data corrente (fuso Europe/Rome), non più
una costante fissa. Per il primo, titolo ("90% di presenze ad agosto" / "a settembre" / ...) e una costante fissa. Per il primo, titolo ("90% di presenze ad agosto" / "a settembre" / ...) e
scadenza (ultimo giorno del mese) seguono di conseguenza. Per i test, `obiettiviSquadra`/ scadenza (ultimo giorno del mese) seguono di conseguenza.
`obiettiviOrdinati` accettano un terzo parametro opzionale `oggi: Date` per iniettare una data
deterministica. Coperti sia da unit test (`test/unit/obiettivi.test.ts`, funzione pura) sia da "Tutti rispondono alle convocazioni" non si azzera (aggrega su tutti gli eventi in programma,
un integration test end-to-end (`test/integration/obiettivi.test.ts`, scrive/rilegge righe vere non solo quelli del mese corrente), ma la sua `scadenza` mostrata in interfaccia era anch'essa
su `eventi_app` e `risposte_presenze` sullo stack Supabase locale). una data fissa (`"2026-09-30"`): ora è anch'essa l'ultimo giorno del mese corrente.
Per i test, `obiettiviSquadra`/`obiettiviOrdinati` accettano un terzo parametro opzionale
`oggi: Date` per iniettare una data deterministica. Coperti sia da unit test
(`test/unit/obiettivi.test.ts`, funzione pura) sia da un integration test end-to-end
(`test/integration/obiettivi.test.ts`, scrive/rilegge righe vere su `eventi_app` e
`risposte_presenze` sullo stack Supabase locale).
+1 -1
View File
@@ -102,7 +102,7 @@ export function obiettiviSquadra(
valore: percentualeRisposte(ctx, rosa.length), valore: percentualeRisposte(ctx, rosa.length),
target: 90, target: 90,
unita: "%", unita: "%",
scadenza: "2026-09-30", scadenza: fineMese(oggi),
emoji: "⚡", emoji: "⚡",
impatto: "Bastano pochi tap per far quadrare i conti a chi organizza.", impatto: "Bastano pochi tap per far quadrare i conti a chi organizza.",
}, },
+74 -2
View File
@@ -1,6 +1,7 @@
/** /**
* Obiettivi mensili di squadra ("presenze del mese" e "evento di squadra al mese") * Obiettivi di squadra con scadenza/mese dinamici ("presenze del mese", "evento di squadra al
* end-to-end contro il database locale: `bun test/integration/obiettivi.test.ts`. * mese" e "tutti rispondono alle convocazioni") 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 * 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 * pura, con un `ContestoObiettivi` costruito a mano. Qui invece si scrivono righe vere su
@@ -207,6 +208,77 @@ if (!locale) {
); );
}, },
); );
await prova(
"o2 legge dal database risposte reali ed esclude i compleanni, con scadenza dinamica",
async () => {
// o2 (percentualeRisposte) aggrega su TUTTI gli eventi non-compleanno, senza filtro di
// mese: per un'asserzione deterministica isoliamo dal risultato reale solo i due eventi
// di questo test, invece di dipendere dal numero di eventi già presenti nel database
// (seed incluso).
const partitaId = `${PREFISSO}-o2-partita`;
const compleannoId = `${PREFISSO}-o2-compleanno`;
const inseritaPartita = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: partitaId,
tipo: "partita",
titolo: "Test obiettivi o2",
data: `${MESE_TEST}-05`,
}),
});
if (!inseritaPartita.ok) {
throw new Error(`inserimento evento fallito: ${await inseritaPartita.text()}`);
}
const inseritoCompleanno = await rest("eventi_app", {
method: "POST",
body: JSON.stringify({
id: compleannoId,
tipo: "compleanno",
titolo: "Test obiettivi o2 - compleanno",
data: `${MESE_TEST}-06`,
}),
});
if (!inseritoCompleanno.ok) {
throw new Error(`inserimento evento fallito: ${await inseritoCompleanno.text()}`);
}
// Solo 12 giocatori su tutta la rosa rispondono alla partita; nessuno "risponde" al
// compleanno, perché non richiede risposta.
const rispondenti = giocatori.slice(0, 12);
const righe = rispondenti.map((g, i) => ({
evento_id: partitaId,
giocatore_id: g.id,
stato: i % 2 === 0 ? "presente" : "forse",
}));
const inseriteRisposte = await rest("risposte_presenze", {
method: "POST",
body: JSON.stringify(righe),
});
if (!inseriteRisposte.ok) {
throw new Error(`inserimento presenze fallito: ${await inseriteRisposte.text()}`);
}
const eventiReali = (await leggiEventi()).filter(
(e) => e.id === partitaId || e.id === compleannoId,
);
const presenzeReali = await leggiPresenze(partitaId);
const o2 = obiettiviSquadra(
giocatori,
{ eventi: eventiReali, presenze: presenzeReali, pagelle: [] },
OGGI,
).find((o) => o.id === "o2")!;
assert.equal(
o2.valore,
Math.round((12 / giocatori.length) * 100),
"12 risposte reali su un solo evento che le richiede (il compleanno è escluso)",
);
assert.equal(o2.scadenza, "2099-03-31", "scadenza o2 = ultimo giorno del mese iniettato");
},
);
} finally { } finally {
await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" }); await rest(`risposte_presenze?evento_id=like.${PREFISSO}*`, { method: "DELETE" });
await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" }); await rest(`eventi_app?id=like.${PREFISSO}*`, { method: "DELETE" });
+7
View File
@@ -127,6 +127,13 @@ assert.equal(trova(obiettiviSquadra(giocatori, soloCompleanni, OGGI_AGOSTO), "o2
const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1"); const o1Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o1");
assert.ok(o1Settembre.titolo.includes("settembre"), "titolo o1 riflette il mese iniettato (settembre)"); 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)"); assert.equal(o1Settembre.scadenza, "2026-09-30", "scadenza = ultimo giorno di settembre (30 gg)");
// La scadenza di o2 ("Tutti rispondono alle convocazioni") era una data fissa
// ("2026-09-30"): ora segue lo stesso mese dinamico di o1.
const o2Agosto = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_AGOSTO), "o2");
assert.equal(o2Agosto.scadenza, "2026-08-31", "scadenza o2 = ultimo giorno del mese iniettato");
const o2Settembre = trova(obiettiviSquadra(giocatori, contestoVuoto, OGGI_SETTEMBRE), "o2");
assert.equal(o2Settembre.scadenza, "2026-09-30", "scadenza o2 cambia con il mese iniettato");
} }
// --- o6: evento di squadra al mese, si azzera come o1 ------------------------ // --- o6: evento di squadra al mese, si azzera come o1 ------------------------