longevity: verificaMisura, misureDaVerificare, endpoint di verifica dal gestionale
misure.ts: misureDaVerificare(db, clientCode) legge le misure fuori range mai
verificate; verificaMisura(db, {misuraId, esito, operatore}) conferma o scarta.
La conferma ricalcola e ricongela lo score (salvaScore + calcolaSessione) per
la sessione della misura e ogni sessione successiva dello stesso cliente, che
puo' averla ereditata via misureValideAlla; lo scarto non tocca score. Rifiuta
misura inesistente, gia' nel range, o gia' verificata.
pages/api/longevity/gestionale/verifica.ts: POST da form (misura_id, esito),
operatore dalla sessione (locals.user.username). Il client_code del redirect
si legge dalla misura nel database, mai dal form.
tests/longevity/verifica-misure.test.ts: migrazione (ALTER TABLE su un
longevity.db vecchio, idempotente), conferma che ricalcola la sessione e le
successive con sessioniRicalcolate, scarto che non scrive in score, i rifiuti
(doppia verifica, misura nel range, id inesistente), canAccessPath per
cliente/trainer sulla nuova rotta.
Suite: 219 passed (212 + 7 nuovi). tsc --noEmit: 0 errori.
Refs #7
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type Database from 'better-sqlite3';
|
import type Database from 'better-sqlite3';
|
||||||
import { esisteTest } from './registro';
|
import { esisteTest } from './registro';
|
||||||
|
import { calcolaSessione, salvaScore } from './motore';
|
||||||
|
|
||||||
export type Fonte = 'manuale' | 'questionario' | 'wellness_tower' | 'vald' | 'calibre' | 'stress_index';
|
export type Fonte = 'manuale' | 'questionario' | 'wellness_tower' | 'vald' | 'calibre' | 'stress_index';
|
||||||
|
|
||||||
@@ -72,3 +73,105 @@ export function registraMisure(
|
|||||||
|
|
||||||
return { scritte: misure.length, fuoriRange };
|
return { scritte: misure.length, fuoriRange };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Una misura fuori range in attesa che il trainer decida se e' un dato vero o uno
|
||||||
|
* strumento/protocollo sbagliato (§9 della spec). L'etichetta e il range vengono dal
|
||||||
|
* registro (dato di prodotto), non duplicati qui; `data` e' quella della SESSIONE in cui
|
||||||
|
* la misura e' stata presa, utile a ordinare la lista senza un altro giro sul database. */
|
||||||
|
export type MisuraDaVerificare = {
|
||||||
|
id: number;
|
||||||
|
test_id: string;
|
||||||
|
etichetta: string;
|
||||||
|
valore_num: number | null;
|
||||||
|
unita: string | null;
|
||||||
|
range_min: number | null;
|
||||||
|
range_max: number | null;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Le misure di un cliente ancora da verificare: fuori range (`fuori_range = 1`) e mai
|
||||||
|
* verificate (`verifica IS NULL`) — una gia' confermata o scartata non torna qui, la
|
||||||
|
* decisione del trainer e' definitiva (vedi `verificaMisura`). Ordine per data della
|
||||||
|
* sessione decrescente: la piu' recente prima, come le altre liste del gestionale.
|
||||||
|
*/
|
||||||
|
export function misureDaVerificare(db: Database.Database, clientCode: string): MisuraDaVerificare[] {
|
||||||
|
return db.prepare(
|
||||||
|
`SELECT m.id AS id, m.test_id AS test_id, r.etichetta AS etichetta,
|
||||||
|
m.valore_num AS valore_num, m.unita AS unita,
|
||||||
|
r.range_min AS range_min, r.range_max AS range_max,
|
||||||
|
s.data AS data
|
||||||
|
FROM misure m
|
||||||
|
JOIN registro_test r ON r.test_id = m.test_id
|
||||||
|
JOIN sessioni s ON s.id = m.sessione_id
|
||||||
|
WHERE m.client_code = ? AND m.fuori_range = 1 AND m.verifica IS NULL
|
||||||
|
ORDER BY s.data DESC, s.id DESC, m.id DESC`
|
||||||
|
).all(clientCode) as MisuraDaVerificare[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EsitoVerifica =
|
||||||
|
| { ok: true; sessioniRicalcolate: number[] }
|
||||||
|
| { ok: false; errore: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La decisione del trainer su una misura fuori range: confermarla (e' un dato vero, deve
|
||||||
|
* contare) o scartarla (e' uno strumento/protocollo sbagliato, resta fuori per sempre).
|
||||||
|
* Rifiuta una misura inesistente, una gia' dentro il range (non c'era niente da
|
||||||
|
* verificare) o gia' verificata (la decisione non si cambia scrivendoci sopra — chi
|
||||||
|
* sbaglia riapre una nuova misura, non riscrive la storia di questa).
|
||||||
|
*
|
||||||
|
* Solo la CONFERMA ricalcola: da quel momento `misureValideAlla` la lascia passare, quindi
|
||||||
|
* ogni punteggio congelato che l'ha ignorata e' cambiato sotto di lui. Si ricongela la
|
||||||
|
* sessione della misura stessa e ogni sessione SUCCESSIVA dello stesso cliente (stesso
|
||||||
|
* criterio data/id di `misureValideAlla`): anche quelle possono aver ereditato il valore
|
||||||
|
* — o la sua assenza — tramite la propagazione in avanti. Una sessione precedente non puo'
|
||||||
|
* mai vedere una misura futura, quindi non ha bisogno di ricalcolo. Lo SCARTO non tocca
|
||||||
|
* `score`: la misura restava esclusa anche prima, niente e' cambiato per il motore.
|
||||||
|
*/
|
||||||
|
export function verificaMisura(
|
||||||
|
db: Database.Database,
|
||||||
|
input: { misuraId: number; esito: 'confermata' | 'scartata'; operatore: string }
|
||||||
|
): EsitoVerifica {
|
||||||
|
const misura = db.prepare(
|
||||||
|
`SELECT m.id AS id, m.client_code AS client_code, m.sessione_id AS sessione_id,
|
||||||
|
m.fuori_range AS fuori_range, m.verifica AS verifica, s.data AS data
|
||||||
|
FROM misure m
|
||||||
|
JOIN sessioni s ON s.id = m.sessione_id
|
||||||
|
WHERE m.id = ?`
|
||||||
|
).get(input.misuraId) as
|
||||||
|
{ id: number; client_code: string; sessione_id: number; fuori_range: number; verifica: string | null; data: string }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!misura) return { ok: false, errore: `misura ${input.misuraId} inesistente` };
|
||||||
|
if (misura.fuori_range === 0) return { ok: false, errore: 'la misura e nel range: non c e niente da verificare' };
|
||||||
|
if (misura.verifica !== null) return { ok: false, errore: 'misura gia verificata' };
|
||||||
|
|
||||||
|
const sessioniRicalcolate: number[] = [];
|
||||||
|
|
||||||
|
const tx = db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE misure SET verifica = ?, verificata_da = ?, verificata_at = datetime('now') WHERE id = ?`
|
||||||
|
).run(input.esito, input.operatore, misura.id);
|
||||||
|
|
||||||
|
if (input.esito === 'confermata') {
|
||||||
|
// La sessione della misura stessa (id = sessione_id) piu' ogni sessione successiva
|
||||||
|
// dello stesso cliente (data > D, o stessa data e id maggiore) — stesso confronto
|
||||||
|
// di misureValideAlla, cosi' la platea dei ricalcoli e' esattamente quella dei
|
||||||
|
// punteggi che possono essere cambiati da questa conferma.
|
||||||
|
const sessioni = db.prepare(
|
||||||
|
`SELECT id FROM sessioni
|
||||||
|
WHERE client_code = ?
|
||||||
|
AND (id = ? OR data > ? OR (data = ? AND id > ?))
|
||||||
|
ORDER BY data ASC, id ASC`
|
||||||
|
).all(misura.client_code, misura.sessione_id, misura.data, misura.data, misura.sessione_id) as { id: number }[];
|
||||||
|
|
||||||
|
for (const s of sessioni) {
|
||||||
|
salvaScore(db, s.id, calcolaSessione(db, s.id));
|
||||||
|
sessioniRicalcolate.push(s.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
|
|
||||||
|
return { ok: true, sessioniRicalcolate };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { getLongevityDb } from '../../../../lib/longevity/db';
|
||||||
|
import { verificaMisura } from '../../../../lib/longevity/misure';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
const json = (status: number, body: object) =>
|
||||||
|
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La decisione del trainer su una misura fuori range (issue #7): confermarla o scartarla,
|
||||||
|
* dalla scheda cliente del gestionale (le pagine arrivano con #2, questo endpoint no).
|
||||||
|
* `canAccessPath` (auth.ts) gia' chiude /api/longevity/gestionale/** a chi non e' trainer
|
||||||
|
* o admin: qui non c'e' nessun controllo di ruolo da ripetere.
|
||||||
|
*
|
||||||
|
* Il codice cliente per il redirect si legge SEMPRE dalla misura nel database, mai da un
|
||||||
|
* campo del form — stesso principio di questionario.ts (il client_code non si fida di chi
|
||||||
|
* scrive la richiesta), applicato qui al lato trainer: un id di misura non e' un fascicolo
|
||||||
|
* che si possa dirottare scrivendo un altro codice nel form.
|
||||||
|
*/
|
||||||
|
export const POST: APIRoute = async ({ request, locals }) => {
|
||||||
|
const form = await request.formData();
|
||||||
|
|
||||||
|
const misuraIdRaw = form.get('misura_id');
|
||||||
|
const misuraId = typeof misuraIdRaw === 'string' ? Number(misuraIdRaw) : NaN;
|
||||||
|
if (!Number.isInteger(misuraId)) return json(404, { error: 'misura inesistente' });
|
||||||
|
|
||||||
|
const db = getLongevityDb();
|
||||||
|
|
||||||
|
// Si legge il client_code PRIMA di chiamare verificaMisura: serve per il redirect anche
|
||||||
|
// quando la verifica viene rifiutata (gia' verificata, nel range...) — l'unico caso senza
|
||||||
|
// una scheda cliente su cui tornare e' proprio l'id che non esiste.
|
||||||
|
const riga = db.prepare(`SELECT client_code FROM misure WHERE id = ?`).get(misuraId) as
|
||||||
|
{ client_code: string } | undefined;
|
||||||
|
if (!riga) return json(404, { error: `misura ${misuraId} inesistente` });
|
||||||
|
|
||||||
|
const tornaA = (query: string): Response => new Response(null, {
|
||||||
|
status: 303,
|
||||||
|
headers: { Location: `/longevity/gestionale/cliente/${riga.client_code}${query}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
const esitoRaw = form.get('esito');
|
||||||
|
const esito: 'confermata' | 'scartata' | null =
|
||||||
|
esitoRaw === 'confermata' || esitoRaw === 'scartata' ? esitoRaw : null;
|
||||||
|
if (esito === null) {
|
||||||
|
return tornaA(`?errore=${encodeURIComponent('Esito non valido: deve essere confermata o scartata.')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const risultato = verificaMisura(db, { misuraId, esito, operatore: locals.user!.username });
|
||||||
|
if (!risultato.ok) return tornaA(`?errore=${encodeURIComponent(risultato.errore)}`);
|
||||||
|
return tornaA(`?verificata=${esito}`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
// Issue #7: verifica delle misure fuori range (confermata/scartata), migrazione dello
|
||||||
|
// schema, e ricalcolo dei punteggi congelati che una conferma cambia.
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import Database from 'better-sqlite3';
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { createLongevityDb } from '../../src/lib/longevity/db';
|
||||||
|
import { seedRegistro } from '../../src/lib/longevity/registro';
|
||||||
|
import { apriSessione, registraMisure, verificaMisura } from '../../src/lib/longevity/misure';
|
||||||
|
import { leggiScore, calcolaSessione, salvaScore } from '../../src/lib/longevity/motore';
|
||||||
|
import { canAccessPath } from '../../src/lib/auth';
|
||||||
|
|
||||||
|
function dbPronto() {
|
||||||
|
const db = createLongevityDb(':memory:');
|
||||||
|
seedRegistro(db);
|
||||||
|
db.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001', 'F')`).run();
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('migrazione: un longevity.db esistente riceve le tre colonne di verifica', () => {
|
||||||
|
it('aggiunge verifica/verificata_da/verificata_at via ALTER TABLE, senza perdere le righe', () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'lg-migra-test-'));
|
||||||
|
const path = join(dir, 'longevity.db');
|
||||||
|
try {
|
||||||
|
// Schema PRIMA di questa issue: la tabella misure senza le colonne di verifica —
|
||||||
|
// lo stesso longevity.db che gira in produzione dal 03/09/2026 (vedi commento su
|
||||||
|
// migra() in db.ts). Costruito a mano, non con createLongevityDb, che oggi crea
|
||||||
|
// gia' le colonne nuove.
|
||||||
|
// Le altre tabelle sono quelle vere (SCHEMA_LONGEVITY in db.ts, invariate da questa
|
||||||
|
// issue): solo `misure` qui e' la versione vecchia, senza le tre colonne. Con uno
|
||||||
|
// schema ridotto a mano seedRegistro/seedPesi (chiamati da createLongevityDb subito
|
||||||
|
// dopo la migrazione) fallirebbero su colonne che si aspettano e non trovano.
|
||||||
|
const vecchio = new Database(path);
|
||||||
|
vecchio.pragma('foreign_keys = ON');
|
||||||
|
vecchio.exec(`
|
||||||
|
CREATE TABLE soggetti (
|
||||||
|
client_code TEXT PRIMARY KEY,
|
||||||
|
sesso TEXT NOT NULL CHECK (sesso IN ('M','F')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE registro_test (
|
||||||
|
test_id TEXT PRIMARY KEY,
|
||||||
|
etichetta TEXT NOT NULL,
|
||||||
|
unita TEXT,
|
||||||
|
tipo_valore TEXT NOT NULL CHECK (tipo_valore IN ('num','txt')),
|
||||||
|
curva TEXT,
|
||||||
|
params TEXT,
|
||||||
|
asse TEXT,
|
||||||
|
sotto_dominio TEXT,
|
||||||
|
range_min REAL,
|
||||||
|
range_max REAL,
|
||||||
|
attivo_da TEXT NOT NULL,
|
||||||
|
attivo_a TEXT,
|
||||||
|
note TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE sessioni (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
||||||
|
data TEXT NOT NULL,
|
||||||
|
tipo TEXT NOT NULL CHECK (tipo IN ('checkup','questionario')),
|
||||||
|
eta_alla_data INTEGER,
|
||||||
|
operatore TEXT,
|
||||||
|
note TEXT,
|
||||||
|
quest_version TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
CREATE TABLE misure (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
sessione_id INTEGER NOT NULL REFERENCES sessioni(id) ON DELETE CASCADE,
|
||||||
|
client_code TEXT NOT NULL REFERENCES soggetti(client_code),
|
||||||
|
test_id TEXT NOT NULL REFERENCES registro_test(test_id),
|
||||||
|
valore_num REAL,
|
||||||
|
valore_txt TEXT,
|
||||||
|
unita TEXT,
|
||||||
|
fonte TEXT NOT NULL CHECK (fonte IN
|
||||||
|
('manuale','questionario','wellness_tower','vald','calibre','stress_index')),
|
||||||
|
fuori_range INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
vecchio.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES ('ISL-0001','F')`).run();
|
||||||
|
vecchio.prepare(
|
||||||
|
`INSERT INTO registro_test (test_id, etichetta, tipo_valore, attivo_da) VALUES ('q_ore_sonno','Ore di sonno','num','2026-01-01')`
|
||||||
|
).run();
|
||||||
|
vecchio.prepare(`INSERT INTO sessioni (client_code, data, tipo) VALUES ('ISL-0001','2026-08-21','questionario')`).run();
|
||||||
|
vecchio.prepare(
|
||||||
|
`INSERT INTO misure (sessione_id, client_code, test_id, valore_num, fonte, fuori_range)
|
||||||
|
VALUES (1, 'ISL-0001', 'q_ore_sonno', 26, 'questionario', 1)`
|
||||||
|
).run();
|
||||||
|
vecchio.close();
|
||||||
|
|
||||||
|
const db = createLongevityDb(path);
|
||||||
|
const colonne = (db.prepare(`PRAGMA table_info(misure)`).all() as { name: string }[]).map((c) => c.name);
|
||||||
|
expect(colonne).toEqual(expect.arrayContaining(['verifica', 'verificata_da', 'verificata_at']));
|
||||||
|
|
||||||
|
const riga = db.prepare(
|
||||||
|
`SELECT valore_num, fuori_range, verifica FROM misure WHERE test_id = 'q_ore_sonno'`
|
||||||
|
).get() as { valore_num: number; fuori_range: number; verifica: string | null };
|
||||||
|
expect(riga.valore_num).toBe(26);
|
||||||
|
expect(riga.fuori_range).toBe(1);
|
||||||
|
expect(riga.verifica).toBeNull();
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
// Riaprirla una seconda volta (le colonne ci sono gia') non deve fallire: migra()
|
||||||
|
// deve saltare l'ALTER TABLE quando non serve, non ripeterlo alla cieca.
|
||||||
|
const db2 = createLongevityDb(path);
|
||||||
|
expect(() => db2.prepare(`SELECT verifica, verificata_da, verificata_at FROM misure`).all()).not.toThrow();
|
||||||
|
db2.close();
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verificaMisura: conferma', () => {
|
||||||
|
it('una misura fuori range confermata entra nello score, e la sessione successiva che la eredita viene ricalcolata', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
|
||||||
|
const s1 = apriSessione(db, { client_code: 'ISL-0001', data: '2026-08-01', tipo: 'questionario' });
|
||||||
|
registraMisure(db, s1, 'questionario', [{ test_id: 'q_ore_sonno', valore_num: 26 }]); // fuori range (0-14)
|
||||||
|
salvaScore(db, s1, calcolaSessione(db, s1));
|
||||||
|
|
||||||
|
const s2 = apriSessione(db, { client_code: 'ISL-0001', data: '2026-08-15', tipo: 'questionario' });
|
||||||
|
salvaScore(db, s2, calcolaSessione(db, s2));
|
||||||
|
|
||||||
|
// Baseline: 'questionario_sonno' pesa 0.20 nell'asse 'Recupero & Sistema Nervoso'
|
||||||
|
// (registro.ts, PESI_ASSE) e non e' coperto ne' da s1 (misura esclusa, fuori range non
|
||||||
|
// confermato) ne' da s2 (che la erediterebbe da s1 se fosse valida).
|
||||||
|
const asse = 'Recupero & Sistema Nervoso';
|
||||||
|
expect(leggiScore(db, s1).assi[asse].copertura).toBe(0);
|
||||||
|
expect(leggiScore(db, s2).assi[asse].copertura).toBe(0);
|
||||||
|
|
||||||
|
const misura = db.prepare(`SELECT id FROM misure WHERE test_id = 'q_ore_sonno'`).get() as { id: number };
|
||||||
|
const esito = verificaMisura(db, { misuraId: misura.id, esito: 'confermata', operatore: 'nicola' });
|
||||||
|
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
if (esito.ok) expect(esito.sessioniRicalcolate.sort((a, b) => a - b)).toEqual([s1, s2].sort((a, b) => a - b));
|
||||||
|
|
||||||
|
const rigaMisura = db.prepare(`SELECT verifica, verificata_da FROM misure WHERE id = ?`).get(misura.id) as
|
||||||
|
{ verifica: string; verificata_da: string };
|
||||||
|
expect(rigaMisura.verifica).toBe('confermata');
|
||||||
|
expect(rigaMisura.verificata_da).toBe('nicola');
|
||||||
|
|
||||||
|
// Dopo la conferma la copertura sale a 0.20 (il peso di 'questionario_sonno') sia
|
||||||
|
// nella sessione della misura sia in quella successiva, che la eredita da
|
||||||
|
// misureValideAlla — senza aver ripetuto il test.
|
||||||
|
expect(leggiScore(db, s1).assi[asse].copertura).toBe(0.2);
|
||||||
|
expect(leggiScore(db, s2).assi[asse].copertura).toBe(0.2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verificaMisura: scarto', () => {
|
||||||
|
it('una misura scartata resta esclusa, e non scrive nessuna riga nuova in score', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const s1 = apriSessione(db, { client_code: 'ISL-0001', data: '2026-08-01', tipo: 'questionario' });
|
||||||
|
registraMisure(db, s1, 'questionario', [{ test_id: 'q_ore_sonno', valore_num: 26 }]);
|
||||||
|
|
||||||
|
const primaN = (db.prepare(`SELECT COUNT(*) n FROM score`).get() as { n: number }).n;
|
||||||
|
|
||||||
|
const misura = db.prepare(`SELECT id FROM misure WHERE test_id = 'q_ore_sonno'`).get() as { id: number };
|
||||||
|
const esito = verificaMisura(db, { misuraId: misura.id, esito: 'scartata', operatore: 'nicola' });
|
||||||
|
|
||||||
|
expect(esito.ok).toBe(true);
|
||||||
|
if (esito.ok) expect(esito.sessioniRicalcolate).toEqual([]);
|
||||||
|
|
||||||
|
const dopoN = (db.prepare(`SELECT COUNT(*) n FROM score`).get() as { n: number }).n;
|
||||||
|
expect(dopoN).toBe(primaN); // nessun ricalcolo per uno scarto
|
||||||
|
|
||||||
|
const rigaMisura = db.prepare(`SELECT verifica FROM misure WHERE id = ?`).get(misura.id) as { verifica: string };
|
||||||
|
expect(rigaMisura.verifica).toBe('scartata');
|
||||||
|
|
||||||
|
// Ancora esclusa da misureValideAlla, esattamente come prima della verifica.
|
||||||
|
const dominio = calcolaSessione(db, s1);
|
||||||
|
expect(dominio.assi['Recupero & Sistema Nervoso'].copertura).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verificaMisura: rifiuti', () => {
|
||||||
|
it('rifiuta una seconda verifica sulla stessa misura', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const s1 = apriSessione(db, { client_code: 'ISL-0001', data: '2026-08-01', tipo: 'questionario' });
|
||||||
|
registraMisure(db, s1, 'questionario', [{ test_id: 'q_ore_sonno', valore_num: 26 }]);
|
||||||
|
const misura = db.prepare(`SELECT id FROM misure WHERE test_id = 'q_ore_sonno'`).get() as { id: number };
|
||||||
|
|
||||||
|
const prima = verificaMisura(db, { misuraId: misura.id, esito: 'confermata', operatore: 'nicola' });
|
||||||
|
expect(prima.ok).toBe(true);
|
||||||
|
|
||||||
|
const seconda = verificaMisura(db, { misuraId: misura.id, esito: 'scartata', operatore: 'donata' });
|
||||||
|
expect(seconda.ok).toBe(false);
|
||||||
|
if (!seconda.ok) expect(seconda.errore).toMatch(/gia.*verificat/i);
|
||||||
|
|
||||||
|
// La seconda chiamata non ha scritto niente: resta la decisione della prima.
|
||||||
|
const riga = db.prepare(`SELECT verifica, verificata_da FROM misure WHERE id = ?`).get(misura.id) as
|
||||||
|
{ verifica: string; verificata_da: string };
|
||||||
|
expect(riga.verifica).toBe('confermata');
|
||||||
|
expect(riga.verificata_da).toBe('nicola');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rifiuta una misura che e gia nel range: non c e niente da verificare', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const s1 = apriSessione(db, { client_code: 'ISL-0001', data: '2026-08-01', tipo: 'questionario' });
|
||||||
|
registraMisure(db, s1, 'questionario', [{ test_id: 'q_ore_sonno', valore_num: 7.5 }]); // nel range 0-14
|
||||||
|
const misura = db.prepare(`SELECT id FROM misure WHERE test_id = 'q_ore_sonno'`).get() as { id: number };
|
||||||
|
|
||||||
|
const esito = verificaMisura(db, { misuraId: misura.id, esito: 'confermata', operatore: 'nicola' });
|
||||||
|
expect(esito.ok).toBe(false);
|
||||||
|
|
||||||
|
const riga = db.prepare(`SELECT verifica FROM misure WHERE id = ?`).get(misura.id) as { verifica: string | null };
|
||||||
|
expect(riga.verifica).toBeNull(); // nessuna scrittura
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rifiuta un id di misura inesistente', () => {
|
||||||
|
const db = dbPronto();
|
||||||
|
const esito = verificaMisura(db, { misuraId: 999999, esito: 'confermata', operatore: 'nicola' });
|
||||||
|
expect(esito.ok).toBe(false);
|
||||||
|
if (!esito.ok) expect(esito.errore).toMatch(/inesistente/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('accesso: la verifica e roba da trainer, non da cliente', () => {
|
||||||
|
it('canAccessPath chiude /api/longevity/gestionale/verifica al cliente e la apre al trainer', () => {
|
||||||
|
expect(canAccessPath('cliente', '/api/longevity/gestionale/verifica')).toBe(false);
|
||||||
|
expect(canAccessPath('trainer', '/api/longevity/gestionale/verifica')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user