longevity: API checkup accetta sessione_id per correggere (#12)
Un sessione_id nel form fa passare da correggiCheckup invece di salvaCheckup, stesso endpoint. Il client_code per validazioni e redirect si legge sempre dalla sessione nel DB, mai dal form. Refs #12 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkWhSnSLvAyRCoWBuCpXvp
This commit is contained in:
@@ -2,7 +2,7 @@ import type { APIRoute } from 'astro';
|
||||
import { getLongevityDb } from '../../../../lib/longevity/db';
|
||||
import { etaDelCliente } from '../../../../lib/longevity/anagrafica';
|
||||
import { testAttivi, type VoceRegistro } from '../../../../lib/longevity/registro';
|
||||
import { salvaCheckup } from '../../../../lib/longevity/gestionale';
|
||||
import { salvaCheckup, correggiCheckup } from '../../../../lib/longevity/gestionale';
|
||||
import type { MisuraIn } from '../../../../lib/longevity/misure';
|
||||
|
||||
export const prerender = false;
|
||||
@@ -54,22 +54,52 @@ export function misureDalForm(
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const form = await request.formData();
|
||||
const clientCode = String(form.get('client_code') ?? '').trim();
|
||||
const clientCodeForm = String(form.get('client_code') ?? '').trim();
|
||||
const data = String(form.get('data') ?? '').trim();
|
||||
// Un sessione_id nel form (issue #12) è una correzione: stesso form, stessa API, solo
|
||||
// l'hidden field in più — checkup.astro lo mette quando arriva con ?sessione=<id>.
|
||||
const sessioneIdRaw = String(form.get('sessione_id') ?? '').trim();
|
||||
const sessioneId = sessioneIdRaw !== '' ? Number(sessioneIdRaw) : null;
|
||||
|
||||
// Senza un codice valido non c'è una scheda a cui tornare: l'unico ritorno sensato è
|
||||
// l'indice del gestionale, non /cliente/undefined/checkup.
|
||||
if (!REGEX_CODE.test(clientCode)) {
|
||||
if (!REGEX_CODE.test(clientCodeForm)) {
|
||||
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Codice cliente non valido.')}`);
|
||||
}
|
||||
const erroreAllaScheda = (testo: string) =>
|
||||
redirect(`/longevity/gestionale/cliente/${clientCode}/checkup?errore=${encodeURIComponent(testo)}`);
|
||||
|
||||
const db = getLongevityDb();
|
||||
|
||||
// In correzione il client_code per validazioni e redirect è SEMPRE quello letto dalla
|
||||
// sessione nel database, mai quello del form (stesso principio di verifica.ts): un
|
||||
// sessione_id non è un fascicolo che si possa dirottare scrivendo un altro codice cliente.
|
||||
let clientCode = clientCodeForm;
|
||||
if (sessioneId !== null) {
|
||||
if (!Number.isInteger(sessioneId)) {
|
||||
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Sessione non valida.')}`);
|
||||
}
|
||||
const sessione = db.prepare(`SELECT client_code FROM sessioni WHERE id = ?`).get(sessioneId) as
|
||||
{ client_code: string } | undefined;
|
||||
if (!sessione) {
|
||||
return redirect(`/longevity/gestionale?errore=${encodeURIComponent('Sessione inesistente.')}`);
|
||||
}
|
||||
clientCode = sessione.client_code;
|
||||
if (clientCodeForm !== clientCode) {
|
||||
return redirect(
|
||||
`/longevity/gestionale/cliente/${clientCode}?errore=${encodeURIComponent('Codice cliente non coerente con la sessione.')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const erroreAllaScheda = (testo: string) => {
|
||||
const query = new URLSearchParams({ errore: testo });
|
||||
if (sessioneId !== null) query.set('sessione', String(sessioneId));
|
||||
return redirect(`/longevity/gestionale/cliente/${clientCode}/checkup?${query.toString()}`);
|
||||
};
|
||||
|
||||
if (!REGEX_DATA.test(data)) return erroreAllaScheda('Data non valida (formato AAAA-MM-GG).');
|
||||
const oggi = new Date().toISOString().slice(0, 10);
|
||||
if (data > oggi) return erroreAllaScheda('La data del check-up non può essere nel futuro.');
|
||||
|
||||
const db = getLongevityDb();
|
||||
const campiAttivi = testAttivi(db, data).filter((v) => v.test_id.startsWith('f_'));
|
||||
|
||||
const parse = misureDalForm(form, campiAttivi);
|
||||
@@ -81,6 +111,14 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const operatore = locals.user!.username;
|
||||
const eta = etaDelCliente(clientCode, data);
|
||||
|
||||
if (sessioneId !== null) {
|
||||
const esito = correggiCheckup(db, { sessioneId, data, eta, operatore, misure: parse.misure });
|
||||
if (!esito.ok) return erroreAllaScheda(esito.errore);
|
||||
const params = new URLSearchParams({ corretto: String(esito.sessioneId) });
|
||||
if (esito.fuoriRange.length > 0) params.set('fuori', String(esito.fuoriRange.length));
|
||||
return redirect(`/longevity/gestionale/cliente/${clientCode}?${params.toString()}`);
|
||||
}
|
||||
|
||||
const esito = salvaCheckup(db, { client_code: clientCode, data, eta, operatore, misure: parse.misure });
|
||||
if (!esito.ok) return erroreAllaScheda(esito.errore);
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Issue #12: la correzione di un check-up passa dalla stessa API di salvaCheckup, distinta
|
||||
// da un sessione_id nel form. File separato da gestionale.test.ts apposta: l'endpoint usa il
|
||||
// singleton getLongevityDb() (db.ts), e gestionale.test.ts ha già un test (apriFascicolo)
|
||||
// che lo inizializza — nello stesso file la variabile d'ambiente impostata qui arriverebbe
|
||||
// troppo tardi. Stesso trucco di questionario-pagina.test.ts: LONGEVITY_DB_PATH va impostata
|
||||
// PRIMA che il singleton apra il suo longevity.db. Il setup usa lo STESSO getLongevityDb()
|
||||
// che poi chiamerà POST (una sola connessione, non due sullo stesso file): così si può
|
||||
// chiuderla nel finally, altrimenti rmSync fallisce con EPERM su Windows a handle aperto.
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { getLongevityDb } from '../../src/lib/longevity/db';
|
||||
import { seedRegistro, seedPesi, MODEL_VERSION } from '../../src/lib/longevity/registro';
|
||||
import { salvaCheckup } from '../../src/lib/longevity/gestionale';
|
||||
|
||||
describe('API checkup.ts, correzione (#12): il client_code del redirect e delle validazioni viene sempre dalla sessione', () => {
|
||||
it('un sessione_id di un altro cliente rispetto al client_code del form: errore, niente scritto', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'lg-api-checkup-test-'));
|
||||
const lgPath = join(dir, 'longevity.db');
|
||||
const primaLg = process.env.LONGEVITY_DB_PATH;
|
||||
let lg: ReturnType<typeof getLongevityDb> | undefined;
|
||||
try {
|
||||
process.env.LONGEVITY_DB_PATH = lgPath;
|
||||
|
||||
lg = getLongevityDb();
|
||||
seedRegistro(lg);
|
||||
seedPesi(lg, MODEL_VERSION);
|
||||
lg.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run();
|
||||
lg.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0002', 'M')`).run();
|
||||
|
||||
const esito = salvaCheckup(lg, {
|
||||
client_code: 'ISL-0001', data: '2026-09-26', eta: 38, operatore: 'nicola',
|
||||
misure: [{ test_id: 'f_peso_kg', valore_num: 80 }],
|
||||
});
|
||||
expect(esito.ok).toBe(true);
|
||||
if (!esito.ok) throw new Error(esito.errore);
|
||||
|
||||
const { POST } = await import('../../src/pages/api/longevity/gestionale/checkup');
|
||||
const form = new FormData();
|
||||
form.set('sessione_id', String(esito.sessioneId));
|
||||
form.set('client_code', 'ISL-0002'); // la sessione è di ISL-0001: incoerente
|
||||
form.set('data', '2026-09-26');
|
||||
form.set('f_peso_kg', '999'); // se scrivesse, sarebbe questo il valore
|
||||
const richiesta = new Request('http://localhost/api/longevity/gestionale/checkup', { method: 'POST', body: form });
|
||||
|
||||
const res = await POST({ request: richiesta, locals: { user: { username: 'donata' } } } as any);
|
||||
expect(res.status).toBe(303);
|
||||
expect(res.headers.get('Location')).toMatch(/^\/longevity\/gestionale\/cliente\/ISL-0001\?/); // redirect sul cliente VERO, non su quello del form
|
||||
expect(res.headers.get('Location')).toMatch(/errore=/);
|
||||
|
||||
const riga = lg.prepare(
|
||||
`SELECT valore_num FROM misure WHERE sessione_id = ? AND test_id = 'f_peso_kg'`
|
||||
).get(esito.sessioneId) as { valore_num: number };
|
||||
expect(riga.valore_num).toBe(80); // invariato: nessuna scrittura
|
||||
} finally {
|
||||
lg?.close(); // libera l'handle sul file: senza, rmSync fallisce con EPERM su Windows
|
||||
if (primaLg === undefined) delete process.env.LONGEVITY_DB_PATH; else process.env.LONGEVITY_DB_PATH = primaLg;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user