diff --git a/src/lib/longevity/anagrafica.ts b/src/lib/longevity/anagrafica.ts new file mode 100644 index 0000000..4e26c63 --- /dev/null +++ b/src/lib/longevity/anagrafica.ts @@ -0,0 +1,47 @@ +// +// QUESTO è l'unico modulo autorizzato ad aprire insieme identity e longevity. +// La separazione dei due database è la garanzia che chi legge i dati clinici non +// risalga alla persona (richiesta del cliente, 21/08): un secondo punto di giunzione +// la annullerebbe in silenzio. Un test in tests/longevity/anagrafica.test.ts lo verifica. +import type Database from 'better-sqlite3'; + +export function creaCliente( + identity: Database.Database, + longevity: Database.Database, + dati: { + nome: string; cognome: string; sesso: 'M' | 'F'; + data_nascita?: string; email?: string; telefono?: string; user_id?: number; + } +): string { + const ultimo = identity.prepare( + `SELECT client_code FROM clienti ORDER BY client_code DESC LIMIT 1` + ).get() as { client_code: string } | undefined; + const n = ultimo ? Number(ultimo.client_code.slice(4)) + 1 : 1; + const code = `ISL-${String(n).padStart(4, '0')}`; + + identity.prepare( + `INSERT INTO clienti (client_code, user_id, nome, cognome, data_nascita, email, telefono) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ).run(code, dati.user_id ?? null, dati.nome, dati.cognome, + dati.data_nascita ?? null, dati.email ?? null, dati.telefono ?? null); + + longevity.prepare(`INSERT INTO soggetti (client_code, sesso) VALUES (?, ?)`).run(code, dati.sesso); + + return code; +} + +export function codicePerUtente(identity: Database.Database, userId: number): string | null { + const r = identity.prepare(`SELECT client_code FROM clienti WHERE user_id = ?`).get(userId) as + { client_code: string } | undefined; + return r?.client_code ?? null; +} + +/** L'eta' al momento della misura: è ciò che serve al motore, e non è un quasi-identificatore. */ +export function etaAllaData(dataNascita: string, alla: string): number { + const n = new Date(dataNascita); + const d = new Date(alla); + let eta = d.getFullYear() - n.getFullYear(); + const m = d.getMonth() - n.getMonth(); + if (m < 0 || (m === 0 && d.getDate() < n.getDate())) eta--; + return eta; +} diff --git a/tests/longevity/anagrafica.test.ts b/tests/longevity/anagrafica.test.ts new file mode 100644 index 0000000..1114c7c --- /dev/null +++ b/tests/longevity/anagrafica.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { createLongevityDb, createIdentityDb } from '../../src/lib/longevity/db'; +import { creaCliente, codicePerUtente, etaAllaData } from '../../src/lib/longevity/anagrafica'; + +describe('anagrafica pseudonimizzata', () => { + it('il nome sta in identity, il sesso in longevity, e i due non si mescolano', () => { + const id = createIdentityDb(':memory:'); + const lg = createLongevityDb(':memory:'); + const code = creaCliente(id, lg, { nome: 'Mario', cognome: 'Rossi', sesso: 'M', data_nascita: '1988-03-04' }); + + const inIdentity = id.prepare(`SELECT nome, cognome FROM clienti WHERE client_code = ?`).get(code) as + { nome: string; cognome: string }; + expect(inIdentity.nome).toBe('Mario'); + + const inLongevity = lg.prepare(`SELECT sesso FROM soggetti WHERE client_code = ?`).get(code) as { sesso: string }; + expect(inLongevity.sesso).toBe('M'); + + // in longevity non deve esistere nessuna colonna che contenga il nome + const dump = JSON.stringify(lg.prepare(`SELECT * FROM soggetti`).all()); + expect(dump).not.toContain('Mario'); + expect(dump).not.toContain('Rossi'); + expect(dump).not.toContain('1988-03-04'); + }); + + it('assegna codici progressivi e non riusa i vecchi', () => { + const id = createIdentityDb(':memory:'); + const lg = createLongevityDb(':memory:'); + const a = creaCliente(id, lg, { nome: 'A', cognome: 'A', sesso: 'F' }); + const b = creaCliente(id, lg, { nome: 'B', cognome: 'B', sesso: 'M' }); + expect(a).toMatch(/^ISL-\d{4}$/); + expect(b).not.toBe(a); + }); + + it('ritrova il codice a partire dall utente del sito', () => { + const id = createIdentityDb(':memory:'); + const lg = createLongevityDb(':memory:'); + const code = creaCliente(id, lg, { nome: 'C', cognome: 'C', sesso: 'F', user_id: 42 }); + expect(codicePerUtente(id, 42)).toBe(code); + expect(codicePerUtente(id, 99)).toBeNull(); + }); + + it('calcola l eta alla data della sessione', () => { + expect(etaAllaData('1988-03-04', '2026-08-21')).toBe(38); + expect(etaAllaData('1988-12-31', '2026-08-21')).toBe(37); // compleanno non ancora passato + }); + + it('nessun altro modulo apre entrambe le connessioni', () => { + const dir = join(process.cwd(), 'src/lib/longevity'); + const colpevoli: string[] = []; + for (const f of readdirSync(dir)) { + if (!f.endsWith('.ts') || f === 'anagrafica.ts' || f === 'db.ts') continue; + const src = readFileSync(join(dir, f), 'utf8'); + if (src.includes('createIdentityDb') && src.includes('createLongevityDb')) colpevoli.push(f); + } + expect(colpevoli).toEqual([]); + }); +});