Implementa DD-050: avviso «Presenza modificata», in prova solo per gli admin
Migration m32: tipo presenza_modificata, funzione e trigger su risposte_presenze che avvisano gli admin attivi se una risposta cambia a 0-6 ore da una partita o un allenamento (push, email e centro notifiche). Gli allenatori non la ricevono finché dura la prova. Applicata in produzione il 09/10/2026. Test di integrazione e unitario; DD-050, notifiche, DATABASE, CHANGELOG, ROADMAP e PROJECT_STATE aggiornati (slot g18 ora giocatore). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0e6a3a6cc
commit
274fed93c5
11 files changed
+523
-29
No files matched your search
+6
-6
@@ -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 (calcolo e card di dettaglio), avvisi in Home, 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), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), 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 (calcolo e card di dettaglio), avvisi in Home, 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), l'avviso «Presenza modificata» agli admin con finestra di 6 ore, stati e nessun avviso a cancellazione dell'evento (`presenza-modificata`, M32, DD-050), gli avvisi di compleanno con ora, più compleanni lo stesso giorno, registro e 29 febbraio (`notifiche-compleanno`, M31, DD-047), 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
|
||||
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Avviso «Presenza modificata» (M32, DD-050): `bun test/integration/presenza-modificata.test.ts`.
|
||||
*
|
||||
* Scrive su `risposte_presenze` come farebbe l'app e verifica, sul database vero:
|
||||
*
|
||||
* - una risposta nuova, modificata o ritirata a meno di 6 ore da una partita o da un allenamento
|
||||
* avvisa gli admin attivi, con titolo e corpo del catalogo;
|
||||
* - in fase di prova (DD-050) non riceve nulla un giocatore semplice né un allenatore, e non riceve
|
||||
* nulla l'admin a cui appartiene la presenza;
|
||||
* - nessun avviso se lo stato non cambia, se l'evento è oltre le 6 ore, già iniziato, o di un tipo che
|
||||
* non è partita né allenamento, né quando una risposta sparisce perché l'evento è stato cancellato;
|
||||
* - ogni modifica è una notifica a sé (nessun raggruppamento) e ha la coda email e la coda push.
|
||||
*
|
||||
* Gira solo sullo stack locale (`npx supabase start`). Usa gli slot `g4` (chi risponde), `g5` (admin),
|
||||
* `g6` (giocatore) e `g10` (allenatore) della rosa seed, ripristinati alla fine.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { statoLocale } from "../helpers/locale";
|
||||
import { prova, riepilogo, salta } from "../helpers/prova";
|
||||
|
||||
const locale = statoLocale();
|
||||
|
||||
if (!locale) {
|
||||
salta("presenza modificata", "stack locale non attivo (npx supabase start)");
|
||||
riepilogo("presenza-modificata");
|
||||
} else {
|
||||
const { url: URL_BASE, anon: ANON, servizio: SERVIZIO } = locale;
|
||||
console.log(`presenza modificata su ${URL_BASE}`);
|
||||
|
||||
const PASSWORD = "prova-presenza-modificata-123";
|
||||
const RISPONDE = "g4";
|
||||
const ADMIN = "g5";
|
||||
const GIOCATORE = "g6";
|
||||
const ALLENATORE = "g10";
|
||||
const PREFISSO = "test-presenza-mod-";
|
||||
|
||||
const rest = (percorso: string, token: string, init?: RequestInit) =>
|
||||
fetch(`${URL_BASE}/rest/v1/${percorso}`, {
|
||||
...init,
|
||||
headers: {
|
||||
apikey: token === SERVIZIO ? SERVIZIO : ANON,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
|
||||
async function leggi<T>(percorso: string): Promise<T[]> {
|
||||
const res = await rest(percorso, SERVIZIO);
|
||||
if (!res.ok) throw new Error(`select su ${percorso}: ${res.status} ${await res.text()}`);
|
||||
return (await res.json()) as T[];
|
||||
}
|
||||
|
||||
async function scrivi(percorso: string, metodo: string, corpo?: unknown, token = SERVIZIO) {
|
||||
const res = await rest(percorso, token, {
|
||||
method: metodo,
|
||||
body: corpo === undefined ? null : JSON.stringify(corpo),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${metodo} su ${percorso}: ${res.status} ${await res.text()}`);
|
||||
}
|
||||
|
||||
async function creaUtente(email: string): Promise<{ id: string; token: string }> {
|
||||
const res = await fetch(`${URL_BASE}/auth/v1/admin/users`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: SERVIZIO,
|
||||
Authorization: `Bearer ${SERVIZIO}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, password: PASSWORD, email_confirm: true }),
|
||||
});
|
||||
const corpo = (await res.json()) as { id?: string };
|
||||
if (!corpo.id) throw new Error(`creazione utente fallita: ${JSON.stringify(corpo)}`);
|
||||
const login = await fetch(`${URL_BASE}/auth/v1/token?grant_type=password`, {
|
||||
method: "POST",
|
||||
headers: { apikey: ANON, "content-type": "application/json" },
|
||||
body: JSON.stringify({ email, password: PASSWORD }),
|
||||
});
|
||||
const t = (await login.json()) as { access_token?: string };
|
||||
if (!t.access_token) throw new Error("accesso fallito");
|
||||
return { id: corpo.id, token: t.access_token };
|
||||
}
|
||||
|
||||
/** Data (AAAA-MM-GG) e ora (HH:MM) a Roma, tra `ore` ore da adesso. */
|
||||
function traOre(ore: number): { data: string; ora: string } {
|
||||
const parti: Record<string, string> = Object.fromEntries(
|
||||
new Intl.DateTimeFormat("en-GB", {
|
||||
timeZone: "Europe/Rome",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
})
|
||||
.formatToParts(new Date(Date.now() + ore * 3_600_000))
|
||||
.map((p) => [p.type, p.value]),
|
||||
);
|
||||
return {
|
||||
data: `${parti["year"]}-${parti["month"]}-${parti["day"]}`,
|
||||
ora: `${parti["hour"]}:${parti["minute"]}`,
|
||||
};
|
||||
}
|
||||
|
||||
type Riga = { id: string; giocatore_id: string; titolo: string; corpo: string };
|
||||
|
||||
const notifiche = () =>
|
||||
leggi<Riga>(
|
||||
"notifiche_utente?tipo=eq.presenza_modificata&select=id,giocatore_id,titolo,corpo&order=creato_il",
|
||||
);
|
||||
const svuota = async () => {
|
||||
await rest("notifiche_utente?tipo=eq.presenza_modificata", SERVIZIO, { method: "DELETE" });
|
||||
};
|
||||
const risposta = (evento: string, stato: string) =>
|
||||
scrivi("risposte_presenze?on_conflict=evento_id,giocatore_id", "POST", {
|
||||
evento_id: evento,
|
||||
giocatore_id: RISPONDE,
|
||||
stato,
|
||||
});
|
||||
const aggiorna = (evento: string, stato: string, giocatore = RISPONDE) =>
|
||||
scrivi(`risposte_presenze?evento_id=eq.${evento}&giocatore_id=eq.${giocatore}`, "PATCH", {
|
||||
stato,
|
||||
});
|
||||
const ritira = (evento: string) =>
|
||||
scrivi(`risposte_presenze?evento_id=eq.${evento}&giocatore_id=eq.${RISPONDE}`, "DELETE");
|
||||
async function creaEvento(id: string, tipo: string, ore: number, titolo = `Prova ${id}`) {
|
||||
const { data, ora } = traOre(ore);
|
||||
await scrivi("eventi_app", "POST", { id, tipo, titolo, data, ora });
|
||||
return { data, ora, titolo };
|
||||
}
|
||||
|
||||
let tokenAdmin = "";
|
||||
const idUtenti: string[] = [];
|
||||
const idEventi: string[] = [];
|
||||
const originali = new Map<string, Record<string, unknown>>();
|
||||
|
||||
try {
|
||||
const admin = await creaUtente(`${PREFISSO}admin-${Date.now()}@example.test`);
|
||||
const allenatore = await creaUtente(`${PREFISSO}allenatore-${Date.now()}@example.test`);
|
||||
idUtenti.push(admin.id, allenatore.id);
|
||||
await scrivi("user_roles", "POST", { user_id: admin.id, role: "admin" });
|
||||
tokenAdmin = admin.token;
|
||||
for (const id of [RISPONDE, ADMIN, GIOCATORE, ALLENATORE]) {
|
||||
const [riga] = await leggi<Record<string, unknown>>(
|
||||
`giocatori_squadra?id=eq.${id}&select=tipo,attivo,auth_user_id`,
|
||||
);
|
||||
originali.set(id, riga ?? { tipo: "giocatore", attivo: true, auth_user_id: null });
|
||||
}
|
||||
// Il trigger di `giocatori_squadra` rifiuta la service key: serve il JWT dell'admin.
|
||||
await scrivi(
|
||||
`giocatori_squadra?id=eq.${ADMIN}`,
|
||||
"PATCH",
|
||||
{ auth_user_id: admin.id },
|
||||
tokenAdmin,
|
||||
);
|
||||
await scrivi(
|
||||
`giocatori_squadra?id=eq.${ALLENATORE}`,
|
||||
"PATCH",
|
||||
{ tipo: "allenatore", auth_user_id: allenatore.id },
|
||||
tokenAdmin,
|
||||
);
|
||||
await svuota();
|
||||
|
||||
const [chi] = await leggi<{ nome: string; cognome: string }>(
|
||||
`giocatori_squadra?id=eq.${RISPONDE}&select=nome,cognome`,
|
||||
);
|
||||
const nomeCompleto = `${chi!.nome} ${chi!.cognome}`.trim();
|
||||
|
||||
// Il seed può avere altri admin con uno slot attivo: l'elenco atteso lo calcola il test.
|
||||
const attesi = async () => {
|
||||
const ruoli = await leggi<{ user_id: string }>("user_roles?role=eq.admin&select=user_id");
|
||||
const slot = await leggi<{ id: string; auth_user_id: string }>(
|
||||
"giocatori_squadra?attivo=eq.true&auth_user_id=not.is.null&select=id,auth_user_id",
|
||||
);
|
||||
const idAdmin = new Set(ruoli.map((r) => r.user_id));
|
||||
return slot
|
||||
.filter((s) => idAdmin.has(s.auth_user_id) && s.id !== RISPONDE)
|
||||
.map((s) => s.id)
|
||||
.sort();
|
||||
};
|
||||
|
||||
// --- Partita a 3 ore ---------------------------------------------------------------------
|
||||
const ev = await creaEvento(`${PREFISSO}partita`, "partita", 3, "Partita di prova");
|
||||
idEventi.push(`${PREFISSO}partita`);
|
||||
const dataIt = `${ev.data.slice(8, 10)}/${ev.data.slice(5, 7)}/${ev.data.slice(0, 4)}`;
|
||||
|
||||
await prova("risposta nuova: avvisa gli admin, con il testo del catalogo", async () => {
|
||||
await risposta(`${PREFISSO}partita`, "presente");
|
||||
const righe = await notifiche();
|
||||
assert.deepEqual(righe.map((r) => r.giocatore_id).sort(), await attesi());
|
||||
assert.ok(
|
||||
righe.some((r) => r.giocatore_id === ADMIN),
|
||||
"l'admin di prova è tra i destinatari",
|
||||
);
|
||||
const mia = righe.find((r) => r.giocatore_id === ADMIN)!;
|
||||
assert.equal(mia.titolo, `Presenza modificata: ${nomeCompleto}`);
|
||||
assert.equal(
|
||||
mia.corpo,
|
||||
[
|
||||
`Evento: ${ev.titolo}`,
|
||||
`Data: ${dataIt}, ore ${ev.ora}`,
|
||||
"Da: nessuna risposta",
|
||||
"A: presente",
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
await prova("in prova non ricevono nulla il giocatore semplice e l'allenatore", async () => {
|
||||
const destinatari = (await notifiche()).map((r) => r.giocatore_id);
|
||||
assert.ok(!destinatari.includes(GIOCATORE));
|
||||
assert.ok(!destinatari.includes(ALLENATORE));
|
||||
assert.ok(!destinatari.includes(RISPONDE), "chi risponde non avvisa se stesso");
|
||||
});
|
||||
|
||||
await prova("la riga ha la coda email e la coda push", async () => {
|
||||
const [mia] = (await notifiche()).filter((r) => r.giocatore_id === ADMIN);
|
||||
const email = await leggi<{ notifica_id: string }>(
|
||||
`notifiche_email_coda?notifica_id=eq.${mia!.id}&select=notifica_id`,
|
||||
);
|
||||
assert.equal(email.length, 1, "email in coda");
|
||||
const push = await leggi<{ notifica_id: string }>(
|
||||
`notifiche_push_coda?notifica_id=eq.${mia!.id}&select=notifica_id`,
|
||||
);
|
||||
assert.equal(push.length, 1, "push in coda");
|
||||
});
|
||||
|
||||
await prova("modifica: ogni cambio è una notifica a sé, con Da e A giusti", async () => {
|
||||
const prima = (await notifiche()).length;
|
||||
await aggiorna(`${PREFISSO}partita`, "ritardo");
|
||||
const righe = await notifiche();
|
||||
assert.equal(righe.length, prima + (await attesi()).length);
|
||||
const ultima = righe.filter((r) => r.giocatore_id === ADMIN).at(-1)!;
|
||||
assert.match(ultima.corpo, /\nDa: presente\nA: in ritardo$/);
|
||||
});
|
||||
|
||||
await prova("stesso stato di prima: nessun avviso", async () => {
|
||||
const prima = (await notifiche()).length;
|
||||
await aggiorna(`${PREFISSO}partita`, "ritardo");
|
||||
assert.equal((await notifiche()).length, prima);
|
||||
});
|
||||
|
||||
await prova("ritiro della risposta: «nessuna risposta» come stato finale", async () => {
|
||||
await ritira(`${PREFISSO}partita`);
|
||||
const ultima = (await notifiche()).filter((r) => r.giocatore_id === ADMIN).at(-1)!;
|
||||
assert.match(ultima.corpo, /\nDa: in ritardo\nA: nessuna risposta$/);
|
||||
});
|
||||
|
||||
await prova("la presenza dell'admin non avvisa l'admin stesso", async () => {
|
||||
await svuota();
|
||||
await scrivi("risposte_presenze", "POST", {
|
||||
evento_id: `${PREFISSO}partita`,
|
||||
giocatore_id: ADMIN,
|
||||
stato: "assente",
|
||||
});
|
||||
const destinatari = (await notifiche()).map((r) => r.giocatore_id);
|
||||
assert.ok(!destinatari.includes(ADMIN));
|
||||
await scrivi(
|
||||
`risposte_presenze?evento_id=eq.${PREFISSO}partita&giocatore_id=eq.${ADMIN}`,
|
||||
"DELETE",
|
||||
);
|
||||
});
|
||||
|
||||
// --- Casi in cui non deve partire nulla --------------------------------------------------
|
||||
await prova("allenamento a 5 ore: avvisa", async () => {
|
||||
await svuota();
|
||||
await creaEvento(`${PREFISSO}allenamento`, "allenamento", 5);
|
||||
idEventi.push(`${PREFISSO}allenamento`);
|
||||
await risposta(`${PREFISSO}allenamento`, "assente");
|
||||
assert.equal((await notifiche()).length, (await attesi()).length);
|
||||
});
|
||||
|
||||
for (const [nome, tipo, ore] of [
|
||||
["oltre le 6 ore", "partita", 7],
|
||||
["già iniziato", "partita", -1],
|
||||
["un tipo che non è partita né allenamento", "evento", 3],
|
||||
] as const) {
|
||||
await prova(`nessun avviso: ${nome}`, async () => {
|
||||
await svuota();
|
||||
const id = `${PREFISSO}no-${idEventi.length}`;
|
||||
await creaEvento(id, tipo, ore);
|
||||
idEventi.push(id);
|
||||
await risposta(id, "presente");
|
||||
assert.equal((await notifiche()).length, 0);
|
||||
});
|
||||
}
|
||||
|
||||
await prova("cancellare l'evento elimina le risposte senza avvisare nessuno", async () => {
|
||||
await svuota();
|
||||
const id = `${PREFISSO}cancellato`;
|
||||
await creaEvento(id, "partita", 2);
|
||||
idEventi.push(id);
|
||||
await risposta(id, "presente");
|
||||
await svuota();
|
||||
await scrivi(`eventi_app?id=eq.${id}`, "DELETE");
|
||||
assert.equal((await notifiche()).length, 0);
|
||||
});
|
||||
|
||||
await prova("la funzione non è chiamabile dai client", async () => {
|
||||
for (const token of [ANON, admin.token]) {
|
||||
const res = await fetch(`${URL_BASE}/rest/v1/rpc/avvisa_presenza_modificata`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
apikey: ANON,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: "{}",
|
||||
});
|
||||
assert.ok(!res.ok, `risposta ${res.status}: un client non deve poterla eseguire`);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await svuota();
|
||||
for (const id of idEventi) {
|
||||
await rest(`risposte_presenze?evento_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
||||
await rest(`eventi_app?id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
||||
}
|
||||
if (tokenAdmin) {
|
||||
for (const [id, valori] of originali) {
|
||||
await rest(`giocatori_squadra?id=eq.${id}`, tokenAdmin, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(valori),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const id of idUtenti) {
|
||||
await rest(`user_roles?user_id=eq.${id}`, SERVIZIO, { method: "DELETE" });
|
||||
await fetch(`${URL_BASE}/auth/v1/admin/users/${id}`, {
|
||||
method: "DELETE",
|
||||
headers: { apikey: SERVIZIO, Authorization: `Bearer ${SERVIZIO}` },
|
||||
});
|
||||
}
|
||||
riepilogo("presenza-modificata");
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ const tipiNuovi: TipoNotifica[] = [
|
||||
"sollecita_presenze_6h",
|
||||
"compleanno",
|
||||
"compleanno_auguri",
|
||||
"presenza_modificata",
|
||||
];
|
||||
const corpoMultiriga = "Data: 01/10/2026\nOra: 21:00\nLuogo: PalaCRAP";
|
||||
for (const tipo of tipiNuovi) {
|
||||
|
||||
Reference in new issue
Block a user