longevity: API del gestionale (clienti, check-up) e test dell'unità 2a

pages/api/longevity/gestionale/clienti.ts: POST del nuovo fascicolo (nome,
cognome, sesso obbligatori; data di nascita/email/telefono facoltativi) →
apriFascicolo → 303 alla scheda cliente, o 303 al modulo con ?errore=.
pages/api/longevity/gestionale/checkup.ts: POST del check-up manuale, con
misureDalForm (funzione pura, esportata e testata) per il parsing del form —
virgola decimale accettata, campo vuoto = non misurato, valore non numerico
ferma tutto, chiavi che non sono campi f_* attivi ignorate. operatore sempre
da locals.user (mai dal corpo, stesso principio del client_code in
questionario.ts); eta da etaDelCliente. Entrambi riservati a admin+trainer da
RULES in auth.ts (già in vigore, verificato dal test su canAccessPath).

tests/longevity/gestionale.test.ts: 17 test — salvaCheckup e il congelamento
dei punteggi, il referto che combina check-up e questionario di sessioni
diverse (#1), i rifiuti prima della scrittura, le misure fuori range, le
versioni parametrizzate dell'anagrafica, apriFascicolo sul singleton (stesso
trucco env-var di questionario-pagina.test.ts), misureDalForm, canAccessPath.

vitest: 229 passed (212 di partenza + 17). tsc --noEmit: 0 errori.

Refs #2
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 12:53:46 +00:00
parent 758463bd26
commit 95ffa08197
3 changed files with 427 additions and 0 deletions
@@ -0,0 +1,56 @@
import type { APIRoute } from 'astro';
import { apriFascicolo } from '../../../../lib/longevity/anagrafica';
export const prerender = false;
// Riservato a admin+trainer da src/lib/auth.ts (RULES, /^\/api\/longevity\/gestionale(\/|$)/):
// il middleware nega prima che questo file veda la richiesta.
const REGEX_DATA = /^\d{4}-\d{2}-\d{2}$/;
const redirect = (location: string) => new Response(null, { status: 303, headers: { Location: location } });
/** Sempre la stessa pagina di partenza: questo endpoint ha un solo modulo che lo chiama
* (pages/longevity/gestionale/nuovo.astro, unità 2b), quindi il ritorno in caso di errore
* non serve dedurlo da un Referer — si scrive qui, una volta. */
const erroreAlModulo = (testo: string) =>
redirect(`/longevity/gestionale/nuovo?errore=${encodeURIComponent(testo)}`);
function isSesso(v: string): v is 'M' | 'F' {
return v === 'M' || v === 'F';
}
export const POST: APIRoute = async ({ request }) => {
const form = await request.formData();
const leggi = (chiave: string) => String(form.get(chiave) ?? '').trim();
const nome = leggi('nome');
const cognome = leggi('cognome');
const sesso = leggi('sesso');
const dataNascita = leggi('data_nascita');
const email = leggi('email');
const telefono = leggi('telefono');
if (!nome) return erroreAlModulo('Il nome è obbligatorio.');
if (!cognome) return erroreAlModulo('Il cognome è obbligatorio.');
if (!isSesso(sesso)) return erroreAlModulo('Il sesso deve essere M o F.');
if (dataNascita) {
if (!REGEX_DATA.test(dataNascita)) {
return erroreAlModulo('Data di nascita non valida (formato AAAA-MM-GG).');
}
// La data del server, non quella (manomettibile) del form: stesso criterio di
// pages/api/longevity/questionario.ts per "oggi".
const oggi = new Date().toISOString().slice(0, 10);
if (dataNascita > oggi) return erroreAlModulo('La data di nascita non può essere nel futuro.');
}
const code = apriFascicolo({
nome, cognome, sesso,
data_nascita: dataNascita || undefined,
email: email || undefined,
telefono: telefono || undefined,
});
return redirect(`/longevity/gestionale/cliente/${code}`);
};