Implementation Plan: Cancelación por el paciente (005)

Branch: 005-cancelacion-paciente | Date: 2026-09-30 | Spec: specs/005-cancelacion-paciente/spec.md

Input: Feature specification from specs/005-cancelacion-paciente/spec.md + clarificaciones 2026-09-30 (firma, lock, trazabilidad).

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

Caso de uso único cancelarCita(pacienteId, citaId, origen) como única vía por la que un paciente escribe cancelada, con catálogo único de cinco rechazos, antelación de 24 h (006 FR-005), atomicidad en transacción serializable y traza mínima (cancelacionOrigen + canceladaEn). Se implementa como lib/cancelacion.ts + PATCH /api/citas/[id]/cancelar + migración 005 (dos columnas de traza + Clinica.telefono pendiente S-01), reutilizando lib/ventanas.ts sin redefinir tiempo.

Technical Context

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

Primary Dependencies: Next.js 15 (App Router Route Handlers), React 19, Prisma 6, zod (validación), date-fns-tz + Intl (es-ES, Europe/Madrid).

Storage: PostgreSQL 16 (única BD). Enmienda a Cita (cancelacionOrigen TEXT NULL, canceladaEn TIMESTAMPTZ NULL) + Clinica.telefono TEXT NOT NULL (S-01 pendiente). Sin tablas nuevas. Migración Prisma versionada.

Testing: Vitest (unit: catálogo/bordes/matriz de rechazos; integración: aplicada, tramo libre RN1, Promise.all concurrente, carrera con recepción). Sin e2e nuevo (el contrato es de caso de uso; 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).

Performance Goals: Cancelación interactiva p95 <200 ms en local (objetivo no bloqueante); doble PATCH concurrente resuelto con exactamente una aplicada (SC-003); 100 % rechazos sin mutación (SC-002).

Constraints: Español de España en todo texto y aviso (VIII) con teléfono de la clínica (001 FR-001); tiempo solo vía 006 (capturarAhora, cancelacionEnPlazo, minuto truncado; prohibido redefinir); GET/visualización nunca escribe (FR-003); portal y recordatorios llaman al caso de uso y no implementan transición propia (FR-001/SC-004); Cita.estado propiedad de la 001 (FR-009).

Scale/Scope: 1–N clínicas; ~40 pacientes y ~500–1200 citas como en la 001; peor caso decenas de cancelaciones concurrentes sobre la misma cita.

Constitution Check

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

  • I. Spec First: todo comportamiento (FR-001…FR-009 + 4 clarificaciones) vive en spec.md; el plan no añade comportamiento. Propietario agenda y recepción (transversal) en specs/MAPA.md; la enmienda a Cita/Clinica se acuerda con el propietario 001 por el mismo dominio.
  • II. Exactitud numérica y temporal: sin dinero en esta feature; tiempo delegado a 006 (borde 24 h incluido, minuto, Europe/Madrid, tramo «DD/MM/AAAA, HH:MM–HH:MM» y ficha «…el DD/MM/AAAA a las HH:MM»).
  • III. Cero solapes: cancelar libera tramo (sale del predicado de exclusión); tests verifican reutilización sin solapes + concurrente en verde para merge.
  • IV. Simplicidad / cero alcance fantasma: sin tablas nuevas, sin cola/lock externo, sin redefinir tiempo/identidad/email; cada campo/endpoint traza a un FR (ver justificaciones abajo).
  • V. Datos reproducibles: semilla Eleva existente + telefono; regeneración bit a bit; escenarios de quickstart.md citables.
  • VI. Tests con la spec: matriz test↔FR/SC en quickstart.md; suite + concurrente en verde como puerta de merge.
  • VII. Interfaz clara y moderna: la ficha muestra el estado con lenguaje de clínica («Cancelada por el paciente desde el portal/email el …»); sin jerga.
  • 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) PATCH /api/citas/[id]/cancelar implementa FR-001 como única vía escribible por el paciente; (2) cancelacionOrigen/canceladaEn implementan FR-008 (traza visible mínima); (3) Clinica.telefono cubre FR-005 y cierra el pendiente S-01 jul2026.

Project Structure

Documentation (this feature)

specs/005-cancelacion-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)
│   └── cancelar-cita.md # Firma, Route Handler, zod, catálogo
└── tasks.md             # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)

Source Code (repository root)

tema5/citaclara/
├── app/api/citas/[id]/cancelar/route.ts  # PATCH caso de uso único (US1/US2/US3)
├── lib/
│   ├── cancelacion.ts      # decidir + ejecutar + AVISOS_CANCELACION (FR-001…FR-008)
│   ├── validacion.ts       # + ESQUEMA_CANCELACION_PACIENTE
│   ├── ventanas.ts         # reutilizado (006 FR-001/FR-005), sin cambios
│   ├── tiempo.ts           # reutilizado (formatearTramo/FechaHora), sin cambios
│   └── estados.ts          # reutilizado (transicionValida), sin cambios
├── prisma/
│   ├── schema.prisma       # + Clinica.telefono + Cita.cancelacionOrigen/canceladaEn
│   ├── migrations/XXXX_005_cancelacion_paciente/migration.sql
│   └── seed.ts             # + telefono Eleva
├── tests/
│   ├── unit/test_cancelacion.test.ts
│   └── integration/test_cancelacion_paciente.test.ts
└── scripts/comprobar-es.ts # + catálogo 005 (si aplica)

Structure Decision: Monolito Next.js existente (misma estructura que la 001). Sin split backend/frontend: el caso de uso vive en lib/ y la ruta es una fachada fina. Prisma es la única capa de datos (sin Repository extra — simplicidad IV).

Complexity Tracking

Fill ONLY if Constitution Check has violations that must be justified

Violation Why Needed Simpler Alternative Rejected Because
— — —