Sollecito presenze: un solo avviso automatico, 24 ore prima (DD-045)

La migration m30 riduce il job automatico alla sola fascia delle 24 ore:
il sollecito arriva una volta a chi non ha risposto o ha risposto «forse»,
anche per un evento creato a meno di 24 ore dall'inizio. Il pulsante
manuale «Sollecita» non cambia. I tipi sollecita_presenze_12h e _6h restano
ammessi per le notifiche già generate. Migration applicata in produzione.

Test di integrazione aggiornati; documentati DD-045, moduli notifiche,
presenze e allenatore, DATABASE, ROADMAP e changelog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-01 15:39:24 +02:00
1 parent de78431090
commit f19f0bbf8d
14 files changed
+155 -111

No files matched your search

+6 -6
View File
@@ -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) 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 | — |
| 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-M28 e M30, DD-040, DD-042, DD-043 e DD-045: palloni un solo avviso a 3 ore, sollecito uno solo a 24 ore, nessun promemoria evento) 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
+20 -32
View File
@@ -6,8 +6,7 @@
* creati con orari relativi a «adesso» e verifica:
*
* - che il promemoria a 24 e 3 ore non sia più pianificato (M28, DD-043);
* - le finestre (palloni: un solo avviso nelle 3 ore prima; solleciti 24·12·6 ore, contigue e senza
* sovrapposizione);
* - le finestre (palloni: un solo avviso nelle 3 ore prima; solleciti: un solo avviso nelle 24 ore prima, DD-045);
* - i destinatari (incaricato e turno precedente; convocati senza risposta o con «forse»; mai
* l'allenatore né chi non è più attivo);
* - una sola generazione per giocatore, evento e tipo, anche se la notifica viene eliminata;
@@ -567,7 +566,7 @@ if (!locale) {
.sort();
await prova(
"ogni evento riceve il sollecito della sua fascia, a chi deve rispondere",
"un solo sollecito, entro 24 ore dall'inizio, a chi deve rispondere (DD-045)",
async () => {
await esegui("genera_solleciti_presenze");
assert.deepEqual(
@@ -577,21 +576,18 @@ if (!locale) {
);
const senzaRisposta = (await tuttiGiocatoriAttivi()).filter((g) => g !== G4);
assert.deepEqual(
await destinatari(S12, "sollecita_presenze_12h"),
await destinatari(S12, "sollecita_presenze_24h"),
senzaRisposta,
"convocati vuoto: tutta la rosa attiva, meno chi ha risposto; g5 («forse») resta",
);
assert.deepEqual(
await destinatari(S6, "sollecita_presenze_6h"),
await destinatari(S6, "sollecita_presenze_24h"),
(await tuttiGiocatoriAttivi()).filter((g) => g !== G4),
"anche a 4 ore dall'inizio: il sollecito è uno solo, non uno per fascia",
);
assert.equal((await notifiche(S_FUORI)).length, 0, "tra 30 ore: fuori da ogni fascia");
for (const [evento, altri] of [
[S24, ["sollecita_presenze_12h", "sollecita_presenze_6h"]],
[S12, ["sollecita_presenze_24h", "sollecita_presenze_6h"]],
[S6, ["sollecita_presenze_24h", "sollecita_presenze_12h"]],
] as const) {
for (const tipo of altri) {
assert.equal((await notifiche(S_FUORI)).length, 0, "tra 30 ore: non ancora");
for (const evento of [S24, S12, S6]) {
for (const tipo of ["sollecita_presenze_12h", "sollecita_presenze_6h"]) {
assert.equal((await notifiche(evento, tipo)).length, 0, `${evento} non ha ${tipo}`);
}
}
@@ -615,8 +611,8 @@ if (!locale) {
}>("giocatori_squadra?select=id,attivo,tipo");
for (const [id, tipo] of [
[S24, "sollecita_presenze_24h"],
[S12, "sollecita_presenze_12h"],
[S6, "sollecita_presenze_6h"],
[S12, "sollecita_presenze_24h"],
[S6, "sollecita_presenze_24h"],
] as const) {
const evento = eventi.find((e) => e.id === id)!;
const risposte = await leggi<{ giocatore_id: string; stato: string }>(
@@ -656,41 +652,33 @@ if (!locale) {
});
await prova(
"chi risponde smette di riceverli, chi resta su «forse» li riceve tutti e tre",
"chi resta su «forse» riceve il sollecito una sola volta, non a ogni fascia",
async () => {
await creaEvento(S_FORSE, "evento", tra(18), { convocati: [G4, G5, G8] });
await rispondi(S_FORSE, G5, "forse");
await esegui("genera_solleciti_presenze");
assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_24h"), [G4, G5, G8]);
await rispondi(S_FORSE, G4, "presente"); // risponde dopo il primo sollecito
await spostaEvento(S_FORSE, 9); // entra nella fascia delle 12 ore
await rispondi(S_FORSE, G4, "presente");
await spostaEvento(S_FORSE, 9);
await esegui("genera_solleciti_presenze");
assert.deepEqual(
await destinatari(S_FORSE, "sollecita_presenze_12h"),
[G5, G8],
"g4 ha risposto: niente secondo sollecito; «forse» e nessuna risposta sì",
);
await rispondi(S_FORSE, G8, "assente");
await spostaEvento(S_FORSE, 4);
await esegui("genera_solleciti_presenze");
assert.deepEqual(await destinatari(S_FORSE, "sollecita_presenze_6h"), [G5]);
assert.equal(
(await notifiche(S_FORSE)).filter((r) => r.giocatore_id === G5).length,
3,
"g5, sempre «forse»: 24, 12 e 6 ore",
assert.deepEqual(
(await notifiche(S_FORSE)).map((r) => r.tipo),
["sollecita_presenze_24h", "sollecita_presenze_24h", "sollecita_presenze_24h"],
"nessun secondo sollecito avvicinandosi all'evento, nemmeno a g5 («forse»)",
);
},
);
await prova("un evento creato tardi riceve solo il sollecito della sua fascia", async () => {
await prova("un evento creato tardi riceve comunque un solo sollecito", async () => {
await creaEvento(S_TARDI, "evento", tra(5), { convocati: [G4] });
await esegui("genera_solleciti_presenze");
await esegui("genera_solleciti_presenze");
assert.deepEqual(
(await notifiche(S_TARDI)).map((r) => r.tipo),
["sollecita_presenze_6h"],
"non tre solleciti insieme",
["sollecita_presenze_24h"],
);
});