From 9e566ad7b1fb8ae3ba9212ae9cbce112cdceb765 Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Sun, 27 Sep 2026 22:12:00 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01TkWhSnSLvAyRCoWBuCpXvp --- .../pages/api/longevity/gestionale/checkup.ts | 50 +++++++++++++-- .../longevity/correzione-checkup-api.test.ts | 62 +++++++++++++++++++ 2 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 apps/platforms/tests/longevity/correzione-checkup-api.test.ts diff --git a/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts b/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts index 8ca5c92..e507cc0 100644 --- a/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts +++ b/apps/platforms/src/pages/api/longevity/gestionale/checkup.ts @@ -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=. + 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); diff --git a/apps/platforms/tests/longevity/correzione-checkup-api.test.ts b/apps/platforms/tests/longevity/correzione-checkup-api.test.ts new file mode 100644 index 0000000..dcf85e5 --- /dev/null +++ b/apps/platforms/tests/longevity/correzione-checkup-api.test.ts @@ -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 | 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 }); + } + }); +});