Implementation Plan: Identidad compartida del paciente (007)

Branch: 007-identidad-paciente | Date: 2026-09-30 | Spec: specs/007-identidad-paciente/spec.md

Input: Feature specification from specs/007-identidad-paciente/spec.md + clarificaciones 2026-09-30 (normalización, sesión, enlace, fusión).

Note: Este plan decide stack y diseño (la constitución no los impone). Toda la UI y textos en español de España.

Summary

La identidad del paciente es pacienteId en todos los canales; correo + teléfono son solo credenciales de resolución. Se implementa como lib/identidad.ts (normalización única + resolución incorrecto/duplicado + avisos únicos), lib/sesion-paciente.ts (cookie firmada, 30 min deslizantes, revocación en servidor, sin persistencia), y lib/enlace-verificado.ts (token firmado citaId + pacienteId, caducidad min(inicio, envío+72h), antiflood 10/h) que alimenta como identidad verificada al caso de uso único cancelarCita de la 005. Sin tablas nuevas, sin fusión en v1, sin redefinir tiempo (006) ni transiciones (005).

Technical Context

Language/Version: TypeScript 5.6 strict (incl. noUncheckedIndexedAccess), Node.js 22 LTS.

Primary Dependencies: Next.js 15 (App Router Route Handlers, cookies()), React 19, Prisma 6, zod (validación), node:crypto (HMAC sha256 + timingSafeEqual, mismo patrón que lib/auth.ts).

Storage: PostgreSQL 16 (única BD). Sin tablas nuevas y sin migración (FR-009: se usa Paciente de la 001 tal cual). Sesiones y antiflood viven en memoria del servidor (Map + Set con expira; ver research R3/R4). Los recordatorios no persisten identidad: comparan con la misma normalización.

Testing: Vitest (unit: matriz de normalización, resolución incorrecto/duplicado, sesión caduca/cierra, enlace caduca/antiflood; integración: portal y enlace deniegan con gemelas sin mostrar ni mutar, enlace vigente solo opera sobre su cita vía cancelarCita). Sin e2e nuevo (el contrato es de librería + Route Handlers finos; Playwright existente cubre responsive).

Target Platform: Web responsive (contenedor Node 22 + Postgres 16). Sin app nativa.

Project Type: web-application monolítica (Route Handlers = backend, Server Components = UI; lib/ = dominio compartido por portal y recordatorios).

Performance Goals: Resolución portal p95 <200 ms en local (unas decenas de fichas; filtrado en memoria); verificación de enlace <50 ms (HMAC + 1 lectura de cita); doble resolución concurrente sin fugas (SC-002 100 % denegado).

Constraints: Español de España en todo texto y aviso (VIII) con teléfono de la clínica (001 FR-001); avisos únicos FR-003/FR-004 sin variantes por canal (FR-008); tiempo solo vía 006 (capturarAhora, minuto, Europe/Madrid; prohibido redefinir); transiciones solo vía cancelarCita de la 005 (prohibido implementar transición propia); Paciente propiedad de la 001, sin fusión ni reasignación en v1 (FR-007/FR-009); sesión sin localStorage ni «recordar dispositivo» (FR-005).

Scale/Scope: 1–N clínicas; ~40 pacientes y ~500–1200 citas como en la 001; peor caso decenas de sesiones concurrentes y decenas de verificaciones de enlace por hora.

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • I. Spec First: todo comportamiento (FR-001…FR-009 + 5 clarificaciones) vive en spec.md; el plan no añade comportamiento. Propietario transversal (identidad) en specs/MAPA.md; no se tocan tablas de la 001 (sin enmienda).
  • II. Exactitud numérica y temporal: sin dinero en esta feature; tiempo delegado a 006 (caducidad 72 h / inicio de cita con precisión de minuto, Europe/Madrid; sesión 30 min de inactividad). Enlace caduca en min(inicio, envío+72h), documentado con ejemplos límite en quickstart.md.
  • III. Cero solapes: esta feature no escribe en la agenda (solo resuelve identidad y verifica; la única escritura delega en cancelarCita, que libera tramo). Tests verifican que ningún rechazo muta nada y que la suite antisolape sigue en verde.
  • IV. Simplicidad / cero alcance fantasma: sin tablas, sin migración, sin fusión, sin cola/lock externo, sin redefinir normalización/tiempo/sesión por canal; cada fichero/función traza a un FR (ver justificaciones abajo).
  • V. Datos reproducibles: semilla Eleva existente; escenarios de quickstart.md usan la ficha canónica (ana.garcia.lopez@correo.es + 612345678) y gemelas creadas en el test y limpiadas después; sin datos inventados fuera de la semilla.
  • VI. Tests con la spec: matriz test↔FR/SC en quickstart.md; suite en verde como puerta de merge; cada aviso único y cada caducidad tienen test trazable.
  • VII. Interfaz clara y moderna: avisos en lenguaje de clínica («No hemos encontrado esos datos…», «Tienes varias fichas…») con teléfono; sin jerga (firma, token, HMAC no se exponen).
  • VIII. Español de España: catálogo único es-ES con teléfono; verificación con comprobar:es.

Gates: PASS — sin violaciones que justificar. Complexity Tracking queda vacío.

Justificaciones IV (adiciones trazadas, no alcance fantasma): (1) lib/identidad.ts implementa FR-001…FR-004 + FR-008 (normalización única, resolución, avisos); (2) lib/sesion-paciente.ts implementa FR-005 (cookie firmada + revocación en servidor; estado en memoria porque FR-009 prohíbe tablas y FR-005 exige invalidación instantánea que un token stateless no da); (3) lib/enlace-verificado.ts implementa FR-006 (token + caducidad + antiflood en memoria; cierra la deuda FR-017/Q9 de la 003 sin crear tabla de envíos).

Project Structure

Documentation (this feature)

specs/007-identidad-paciente/
├── plan.md              # This file (/speckit.plan command output)
├── research.md          # Phase 0 output (/speckit.plan command)
├── data-model.md        # Phase 1 output (/speckit.plan command)
├── quickstart.md        # Phase 1 output (/speckit.plan command)
├── contracts/           # Phase 1 output (/speckit.plan command)
│   └── identidad.md     # normalizar + resolver + sesión + enlace (FR-002…FR-006, FR-008)
└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)

Source Code (repository root)

tema5/citaclara/
├── lib/
│   ├── identidad.ts          # normalizarCorreo/Telefono + resolverPaciente + AVISOS (FR-001…FR-004, FR-008)
│   ├── sesion-paciente.ts    # crear/renovar/verificar/cerrar sesión portal (FR-005)
│   ├── enlace-verificado.ts  # crear/verificar token + antiflood (FR-006)
│   ├── validacion.ts         # + esquemas de acceso portal (reutiliza ESQUEMA_EMAIL/TELEFONO_ES tras normalizar)
│   ├── cancelacion.ts        # sin cambios (consume pacienteId verificado de este plan)
│   └── auth.ts               # sin cambios (sesión de recepción intacta; firma HMAC reutilizada como patrón)
├── app/api/
│   ├── portal/sesion/route.ts        # POST (identificar) + DELETE (cerrar sesión) — fachada fina
│   └── citas/[id]/cancelar/route.ts  # + acepta token de enlace verificado (origen email)
├── tests/
│   ├── unit/test_identidad.test.ts   # matriz normalización + resolución + avisos
│   ├── unit/test_sesion_paciente.test.ts
│   ├── unit/test_enlace.test.ts
│   └── integration/test_identidad_canales.test.ts  # gemelas portal+enlace+recordatorio deniegan
└── scripts/comprobar-es.ts # + catálogo 007 (si aplica)

Structure Decision: Monolito Next.js existente (misma estructura que la 001/005). El dominio compartido vive en lib/ para que portal y recordatorios importen el mismo algoritmo (US1/SC-001); las rutas son fachadas finas. Prisma es la única capa de datos; sesiones y antiflood son memoria de proceso (sin tablas por FR-009; escala de clínica pequeña; ver research R3/R4 y su caducidad/limpieza).

Complexity Tracking

Fill ONLY if Constitution Check has violations that must be justified

Violation Why Needed Simpler Alternative Rejected Because
— — —