Data Model — 007 Identidad compartida del paciente
Fuente: spec.md (FR-001…FR-009). Propietario de las tablas: 001 (esta feature no crea ni migra nada). Zona Europe/Madrid, idioma es-ES.
Sin tablas nuevas ni migraciones (principios IV + FR-009)
Esta feature reutiliza Paciente (id, clinicaId, nombre, telefono, email), Cita (id, pacienteId, inicio, estado) y Clinica (id, telefono) tal cual. No hay ALTER TABLE, no hay backfill, no hay fusión ni reasignación en v1 (FR-007).
Entidades lógicas (no persistentes)
Normalización documentada (FR-002, algoritmo único)
normalizarCorreo(x) = minusculas(eliminarEspaciosInteriores(recortar(x)))
normalizarTelefono(x) = digitos9(eliminar([espacio, '-', '.', '(', ')'], x)
con '+34'/'0034' -> 9 digitos nacionales)
validarTrasNormalizar = ESQUEMA_EMAIL(zod) para correo
AND /^[6-9]\d{8}$/ para telefono (forma normalizada
de ESQUEMA_TELEFONO_ES de la 001)
Ejemplos: Ana.Garcia.Lopez@correo.es → ana.garcia.lopez@correo.es; +34 612 345 678, 0034-612-345-678, (612) 345.678 → 612345678. 9 dígitos que no empiezan por 6/7/8/9 → «incorrecto».
Resolución (orden vinculante, sin fugas)
- Normalizar + validar → si falla,
incorrecto(antes de distinguir). - Comparar en memoria sobre fichas candidatas con igualdad normalizada.
- 0 →
incorrecto(aviso FR-003 + teléfono clínica). ≥2 →duplicado(aviso FR-004 + teléfono; recordatorios: sin envío + incidenciaduplicado). 1 →ok(pacienteId). - Con
ok, todos los accesos posteriores filtran porpacienteId(portal:cita.pacienteId === sesion.pacienteId; email:citaId del token+pacienteId del tokenvíacancelarCita, que devuelveCITA_AJENAsi no coincide).
Sesión y enlace (estado en memoria, caducidad)
- Sesión:
Map<nonce, { pacienteId, expira }>; limpieza perezosa de expirados; firma HMAC-sha256 conSESSION_SECRETverificada contimingSafeEqual(patrónlib/auth.ts). - Enlace:
Map<hashToken, number[]>(instantes de intento, ventana 1 h, tope 10); firma HMAC-sha256 conSESSION_SECRET;expira = min(inicioCita, emitidoEn + 72 h)a minuto (primitivas 006).
Diagrama (texto)
Portal: credenciales --normalizar--> resolver --> ok(pacienteId)
--> crearSesion (cookie firmada 30min) --> citas WHERE pacienteId
--> incorrecto/duplicado --> aviso unico, sin citas
Email: cita --crearEnlace(citaId+pacienteId, min(inicio, +72h))--> token
--> verificarEnlace (firma+caducidad+antiflood) --> cancelarCita(token)
Recordatorios: destinatario --normalizar--> resolver --> duplicado? no enviar + incidencia
Reglas de negocio que esta feature NO toca
Transiciones (reservada → cancelada en 005), ventanas y «ahora» (006), métricas/envíos/auth de recepción (FR-009). La fusión de fichas queda fuera para spec posterior (FR-007).