Quickstart — validar el portal 002 de punta a punta

Guía runnable (sin código de implementación; este se genera en /speckit.tasks + /speckit.implement). Referencias: contracts/portal.md, data-model.md, research.md.

Requisitos

Node 22, Docker + Compose, Postgres 16. Variables: DATABASE_URL, SESSION_SECRET, TZ=Europe/Madrid.

Arranque

cd tema5/citaclara
cp .env.example .env
docker compose up -d db
npm install
npx prisma migrate dev
npm run seed   # Eleva con telefono; ejemplo canónico Ana García López
npm run dev

Validación por historia (trazable a spec)

ID Escenario Comando / pasos Esperado
US3/acceso-ok Entrar con correo+teléfono de la semilla POST /api/portal/sesion correcto → abrir /portal/mis-citas entra y solo ve sus citas (SC-003)
US3/incorrecto Combinación inexistente POST /api/portal/sesion incorrecto 401 «No hemos encontrado esos datos; revísalos o llama a la clínica al …», sin citas
US3/duplicado Dos fichas con mismo correo+teléfono normalizados crear gemela → POST /api/portal/sesion 409 «Tienes varias fichas con esos datos; llama a la clínica al …», cero citas (SC-007)
US3/caducidad 30 min sin actividad entrar → avanzar reloj 31 min → GET /api/portal/citas 401 SIN_SESION, exige correo+teléfono (SC-008)
US3/cierre «Cerrar sesión» DELETE /api/portal/sesion → reintentar GET con el token anterior 401, cookie limpiada, cero restos (SC-008)
US1/secciones Ficha con historia + futuras GET /api/portal/citas enCurso/proximas (asc)/anteriores (desc), partición sin solapes, fila 08/10/2026, 10:00–10:45 · sesión fisio · María · reservada (SC-001)
US1/vacia Ficha solo con historia GET /api/portal/citas «No tienes citas en curso» y «No tienes próximas citas», historial visible, sin citas ajenas
US1/responsive 390 px y 1440 px Playwright e2e/portal-paciente.spec.ts legible sin manual ni jerga, botón pulsable sin zoom (SC-006)
US2/en-plazo reservada con inicio − ahora ≥ 24 h confirmar → PATCH /api/portal/citas/[id]/cancelar { confirmar: true } 200 aplicada, sale de Próximas, entra en Anteriores como cancelada, tramo libre y reutilizable por recepción (POST /api/citas mismo profesional/tramo, SC-005)
US2/fuera-plazo reservada con inicio − ahora < 24 h mismo PATCH 422 «Ya no se puede cancelar por internet; llama a la clínica al …», sigue reservada (SC-004)
US2/final completada/cancelada/no_asistida ver fila (sin botón) + PATCH directo sin botón; el PATCH se rechaza sin mutar (SC-003)
US2/doble Doble clic sobre la misma cita dos PATCH seguidos uno aplicada, otro YA_CANCELADA, sin dobles efectos (SC-003)
US2/en-curso reservada con inicio ≤ ahora < fin ver sección «En curso» arriba, «en curso», tramo resaltado, sin «Cancelar», con aviso de teléfono
SC-006/dinero Cualquier vista del portal inspeccionar GET y páginas cero importes visibles (FR-013 como guarda)
Greps unicidad de capas rg "update.*estado.*cancelada\|toLowerCase\|normaliz" lib app --glob '!lib/{cancelacion,identidad}.ts' y rg "now\(\)\|CURRENT_TIMESTAMP" app/api/portal lib/portal.ts única transición en lib/cancelacion.ts, única normalización en lib/identidad.ts, cero now() fuera de capturarAhora()

Suites

npm test                    # Vitest unit + integración — todo verde para merge
npm run test:e2e -- portal-paciente  # Playwright 390 px + 1440 px
npm run comprobar:es        # todo visible en es-ES, sin importes en el portal

Puerta de merge (constitución): suite verde + suite antisolape 001 en verde (el tramo liberado es reutilizable sin solapes) + cero fugas entre fichas + textos es-ES + greps de unicidad (transición/normalización/tiempo) + revisión de simplicidad (sin tablas/migraciones/endpoints fuera de data-model.md/contracts/).