Feature Specification: Identidad compartida del paciente de CitaClara

Feature Branch: 007-identidad-paciente

Created: 2026-09-30

Status: Draft

Input: User description: "Identidad compartida del paciente de CitaClara: la identidad es pacienteId (001), nunca correo + teléfono. Define una única normalización documentada de correo y teléfono (mayúsculas, espacios, prefijo +34, formato ES) usada por el portal y por los recordatorios; una única regla de resolución de duplicados (qué pasa cuando correo + teléfono coincide con más de una ficha: denegación genérica con derivación al teléfono de la clínica, sin enumerar fichas); la sesión del portal (cookie firmada, 30 min de inactividad, «Cerrar sesión», sin persistencia); y el camino de cierre de la deuda del enlace (el enlace pasa a verificarse contra esta identidad ejecutando cancelarCita). Ningún paciente ve ni cancela citas de otra ficha en ningún canal. Incluye si se permite fusionar fichas duplicadas y quién puede hacerlo."

Clarifications

Session 2026-09-30 (normalización, sesión, enlace y fusión)

  • Q1 — ¿Cuál es el algoritmo exacto de normalización? → A: correo: recortar espacios extremos, eliminar espacios interiores y pasar a minúsculas; teléfono: eliminar espacios, guiones, puntos y paréntesis, convertir prefijo +34 o 0034 a formato nacional de 9 dígitos. Tras normalizar, se aplica la validación estricta ES heredada de la 001 (FR-004); lo que no la cumple es «incorrecto».
  • Q2 — ¿Cómo se firma y se cierra la sesión del portal? → A: cookie firmada con secreto del servidor, caducidad deslizante de 30 min de inactividad renovada con cada uso; «Cerrar sesión» la invalida en servidor de inmediato; sin persistencia más allá de la cookie (nada en localStorage, sin «recordar dispositivo»); tras caducidad o cierre se exige reidentificación completa y no quedan datos residuales.
  • Q3 — ¿Cómo es el enlace verificado que cierra la deuda del email? → A: token firmado atado a citaId + pacienteId, caduca en el inicio de la cita o a las 72 h desde el envío (lo que ocurra antes); reutilizable para ver la confirmación pero la acción es idempotente vía cancelarCita; antiflood de 10 intentos/hora por enlace.
  • Q4 — ¿Se pueden fusionar fichas duplicadas? → A: en v1 no hay fusión (ni automática ni manual): la duplicidad se resuelve denegando con derivación a la clínica. Las citas y los envíos históricos nunca se reasignan solos. La fusión queda fuera de alcance para una spec posterior.
  • Q5 — ¿Qué avisos exactos se muestran? → A: los del portal, únicos para todos los canales: «No hemos encontrado esos datos; revísalos o llama a la clínica» (incorrecto) y «Tienes varias fichas con esos datos; llama a la clínica» (duplicado), ambos con el teléfono de la clínica (001 FR-001).

User Scenarios & Testing

User Story 1 — Una sola normalización para todos los canales (Priority: P1)

Recepción corrige un teléfono con espacios y +34, el paciente entra al portal con otra variante equivalente y el recordatorio llega al mismo destinatario: las tres comparaciones usan el mismo algoritmo y dan el mismo resultado.

Why this priority: Hoy el portal normaliza y los recordatorios no: el mismo paciente puede entrar pero no recibir (o viceversa). Sin algoritmo único no hay identidad compartida.

Independent Test: Se puede probar por completo con una matriz de variantes (mayúsculas, espacios, +34, 0034, guiones) sobre una ficha conocida y comprobando que todas las variantes equivalentes resuelven a la misma ficha en portal y en recordatorios.

Acceptance Scenarios:

  1. Given la ficha con ana.garcia.lopez@correo.es y 612345678, When el paciente teclea Ana.Garcia.Lopez@correo.es y +34 612 345 678, Then ambas variantes resuelven a la misma ficha.
  2. Given una variante que tras normalizar no cumple el formato ES estricto, When se usa para entrar o como destinatario, Then se trata como «incorrecto» con el aviso único, sin revelar si existe.
  3. Given la misma variante equivalente, When se usa en el portal y en la comparación de destinatario de recordatorios, Then el resultado es idéntico en ambos canales.

User Story 2 — Duplicados que nunca filtran entre fichas (Priority: P1)

Cuando correo + teléfono coincide con más de una ficha, ningún canal muestra ni cancela nada: se deniega con aviso genérico y se deriva al teléfono de la clínica.

Why this priority: Es la garantía «cero fugas entre fichas» del portal, extendida al email. Sin ella, la colisión que bloquea el portal sigue enviando recordatorios al destinatario ambiguo.

Independent Test: Se puede probar por completo creando dos fichas gemelas (mismo correo + teléfono normalizados) y comprobando que portal y enlace deniegan sin mostrar citas de ninguna.

Acceptance Scenarios:

  1. Given dos fichas con el mismo correo + teléfono normalizados, When se intenta entrar al portal con ellos, Then se deniega con «Tienes varias fichas con esos datos; llama a la clínica» (+ teléfono) y no se muestra ninguna cita.
  2. Given las mismas fichas gemelas, When el proceso de recordatorios las evalúa, Then no genera envío a ese destinatario ambiguo y lo anota en el informe de incidencias como duplicado.
  3. Given cualquier denegación por duplicado, When se lee el aviso, Then no enumera ni distingue las fichas coincidentes.

User Story 3 — Sesión que caduca y cierra de verdad (Priority: P2)

La sesión del portal vive 30 min de inactividad, se renueva con cada uso y «Cerrar sesión» la invalida al instante sin dejar restos en el navegador.

Why this priority: Es la puerta del portal; sin cierre real, un móvil compartido expone citas ajenas. P2 porque presupone la identidad (US1/US2).

Independent Test: Se puede probar por completo entrando, esperando 30 min sin actividad, y comprobando que la siguiente petición exige reidentificación; y cerrando sesión y comprobando que el token anterior ya no vale.

Acceptance Scenarios:

  1. Given un paciente identificado, When pasan 30 minutos sin actividad, Then la siguiente petición se rechaza y exige de nuevo correo y teléfono.
  2. Given un paciente identificado, When pulsa «Cerrar sesión», Then el acceso se cierra al instante y reutilizar el token anterior se rechaza.
  3. Given una sesión cerrada o caducada, When se inspecciona el navegador, Then no quedan datos de identificación (ni cookie válida ni localStorage).

User Story 4 — Enlace verificado que cierra la deuda (Priority: P2)

El enlace del recordatorio pasa a ser un token firmado atado a la cita y al paciente, con caducidad y antiflood, y ejecuta cancelarCita contra esta identidad.

Why this priority: Cierra la deuda consciente FR-017/Q9 de recordatorios: hoy quien reenvía el email puede cancelar citas ajenas. P2 porque requiere US1–US3 y cambia el email.

Independent Test: Se puede probar por completo generando el enlace, usándolo desde otro navegador (sin sesión), y comprobando que solo opera sobre su cita; y reenviándolo tras su caducidad y comprobando el rechazo.

Acceptance Scenarios:

  1. Given un enlace vigente de una cita reservada, When se abre sin sesión de portal, Then muestra solo esa cita y permite confirmar su cancelación vía cancelarCita.
  2. Given un enlace caducado (pasado el inicio o 72 h), When se usa, Then se rechaza con aviso claro sin mutar nada.
  3. Given un enlace sometido a más de 10 intentos/hora, When sigue intentándose, Then se bloquea temporalmente con aviso claro.

Edge Cases

  • ¿Qué pasa si el paciente tiene dos fichas con el mismo nombre pero distinto correo/teléfono? Cada ficha es una identidad distinta (pacienteId); cada una ve solo sus citas y nunca se mezclan.
  • ¿Qué pasa si recepción corrige el email a uno que usa otra ficha? La corrección crea una colisión: esa combinación pasa a tratarse como duplicado (US2) hasta que recepción la resuelva por teléfono; no se fusiona nada solo.
  • ¿Qué pasa si el token del enlace se usa después de que recepción marque la cita como completada? Se rechaza por estado final vía cancelarCita; la identidad no cambia las reglas de estados.
  • ¿Qué pasa si el paciente comparte dispositivo? «Cerrar sesión» + caducidad de 30 min limitan la exposición; no hay «recordar dispositivo» en v1.
  • ¿Qué pasa si el correo contiene + o mayúsculas legítimas? La normalización solo recorta espacios y pasa a minúsculas; el resto del local-part se conserva tal cual.
  • ¿Qué pasa si el teléfono tiene 9 dígitos pero no empieza por 6/7/9? Tras normalizar no cumple el formato ES estricto y se trata como «incorrecto».

Requirements

Functional Requirements

  • FR-001 (identidad): La identidad del paciente MUST ser pacienteId (ficha de la 001) en todos los canales. Ningún canal MUST usar correo + teléfono como identidad; solo como credenciales de resolución hacia pacienteId según FR-002…FR-004.
  • FR-002 (normalización): El sistema MUST normalizar correo (recortar espacios extremos, eliminar interiores, minúsculas) y teléfono (eliminar espacios, guiones, puntos, paréntesis; +34/0034 → 9 dígitos nacionales) antes de comparar. Tras normalizar MUST aplicar la validación estricta ES de la 001 (FR-004). Portal y recordatorios MUST usar este algoritmo y MUST NOT definir el suyo.
  • FR-003 (incorrecto): Una combinación que no resuelve a ninguna ficha (o no normaliza a formato válido) MUST denegarse con «No hemos encontrado esos datos; revísalos o llama a la clínica» (+ teléfono de la clínica), MUST NOT revelar si correo o teléfono existen y MUST NOT mostrar ninguna cita.
  • FR-004 (duplicados): Si correo + teléfono normalizados resuelven a más de una ficha, el sistema MUST denegar en todos los canales con «Tienes varias fichas con esos datos; llama a la clínica» (+ teléfono), MUST NOT enumerar ni distinguir fichas, MUST NOT generar recordatorio a ese destinatario (se anota como duplicado en el informe de incidencias) y MUST NOT mostrar ni cancelar ninguna cita.
  • FR-005 (sesión): La sesión del portal MUST sostenerse en cookie firmada con secreto del servidor, caducidad deslizante de 30 min de inactividad renovada con cada uso y acción «Cerrar sesión» que la invalida en servidor al instante. MUST NOT persistir identificación más allá de la cookie (nada en localStorage, sin «recordar dispositivo»). Tras caducidad o cierre MUST exigir reidentificación completa.
  • FR-006 (enlace verificado): El enlace de cancelación MUST ser un token firmado atado a citaId + pacienteId, que caduca en el inicio de la cita o a las 72 h del envío (lo antes posible). MUST permitir ver la confirmación sin sesión pero solo opera sobre su cita vía cancelarCita (005). Superados 10 intentos/hora por enlace MUST bloquearse temporalmente con aviso claro. Sustituye a la deuda FR-017/Q9 de recordatorios.
  • FR-007 (sin fusión en v1): El sistema MUST NOT fusionar fichas (ni auto ni manual) en v1. Las citas y envíos históricos MUST NOT reasignarse solos. La fusión queda fuera de alcance para spec posterior.
  • FR-008 (avisos únicos): Los textos de FR-003/FR-004 (con teléfono de la clínica, 001 FR-001) son el catálogo único para todos los canales; ningún canal MUST redactar los suyos. Todos en español de España.
  • FR-009 (alcance): Esta feature MUST NOT definir transiciones (ver 005), métricas, envíos ni auth de recepción. No crea tablas: usa Paciente de la 001.

Key Entities

  • Identidad (Paciente.id): Ficha de la 001 (nombre, teléfono, email). Única fuente de «quién es quién»; correo + teléfono son credenciales de resolución, no identidad.
  • Normalización: Función documentada (FR-002) compartida por portal y recordatorios. Sin estado propio.
  • Sesión del portal: Cookie firmada + revocación en servidor; 30 min deslizantes; cierre instantáneo; sin restos.
  • Enlace verificado: Token firmado (citaId + pacienteId, caducidad, antiflood) que autoriza una sola cita vía cancelarCita.

Success Criteria

Measurable Outcomes

  • SC-001: El 100 % de las variantes equivalentes de una matriz (mayúsculas, espacios, +34/0034, guiones) resuelve a la misma ficha en portal y en recordatorios; las no válidas se tratan como «incorrecto» en ambos.
  • SC-002: El 100 % de los intentos con fichas gemelas (portal, enlace, reintento) se deniega sin mostrar ni mutar citas de ninguna ficha; el caso de recordatorios queda en incidencias como duplicado.
  • SC-003: Toda sesión con 30 min sin actividad exige reidentificación; toda sesión cerrada invalida su token al instante; cero restos en navegador tras cierre/caducidad.
  • SC-004: Un enlace vigente solo opera sobre su cita y sin sesión; uno caducado o con flood se rechaza sin mutar nada, en el 100 % de los intentos.
  • SC-005: Cero definiciones propias de normalización, sesión o duplicados fuera de esta spec (revisión + grep por merge).

Assumptions

  • Sin cuentas ni contraseñas; la clínica no gestiona altas (Sara 2026-09-29, portal Q1).
  • El secreto de firma vive en el servidor y rota sin invalidar sesiones más allá de su caducidad natural; el mecanismo exacto de firma/rotación se decide en el plan.
  • La validación estricta ES de fondo es la de la 001 (FR-004); aquí solo se añade la normalización previa.
  • La deuda del enlace sin verificar (recordatorios FR-017/Q9) se cierra adoptando FR-006; el aviso de no reenviar se mantiene hasta entonces.
  • Fusión de fichas, auth de recepción y transiciones quedan fuera (ver 005 y specs propias).