Feature Specification: Caso de uso único de cancelación por el paciente (cancelarCita)

Feature Branch: 005-cancelacion-paciente

Created: 2026-09-30

Status: Draft

Input: User description: "Caso de uso único cancelarCita de CitaClara: cualquier cancelación iniciada por el propio paciente —desde el portal o desde el enlace del recordatorio— ejecuta este caso de uso y ningún otro. Entradas: pacienteId verificado, citaId, origen (portal | email). Reglas: solo actúa sobre citas en estado reservada del propio paciente; exige confirmación expresa previa (visitar el enlace o la página no cancela nada); exige antelación de 24 horas o más (inicio − ahora, borde de 24 h incluido, precisión de minuto, instante «ahora» y zona de tiempo-referencia); es idempotente (la segunda cancelación se rechaza como «ya cancelada»); al aplicarse, la cita pasa a cancelada y su tramo [inicio, fin) queda libre y reutilizable sin violar RN1. Fuera de plazo o sobre estado final, rechaza con el aviso único del catálogo (que muestra el teléfono de Clínica.telefono) y no muta nada. Comportamiento concurrente: doble clic o doble petición simultánea solo aplican una vez."

Clarifications

Session 2026-09-30 (firma, lock, trazabilidad)

  • Q1 — ¿Cuál es la firma exacta y el catálogo de avisos? → A: cancelarCita(pacienteId, citaId, origen) con origen ∈ {portal, email}, que devuelve aplicada o un rechazo del catálogo único: YA_CANCELADA, ESTADO_FINAL, CITA_AJENA, PLAZO_VENCIDO, ENLACE_INVALIDO. Cada código tiene un único texto es-ES con el teléfono de la clínica (001 FR-001).
  • Q2 — ¿Dónde vive el lock de concurrencia? → A: en la transacción: leer estado + antelación y escribir cancelada es una operación atómica. Doble clic, doble petición o cierre simultáneo de recepción: solo una gana; las demás reciben el rechazo correspondiente (YA_CANCELADA o ESTADO_FINAL). Esta es la única vía por la que un paciente escribe cancelada.
  • Q3 — ¿Qué trazabilidad queda? → A: cada ejecución registra origen, instante y resultado, visible para recepción en la ficha de la cita («Cancelada por el paciente desde el portal/email el DD/MM/AAAA a las HH:MM»). El detalle de persistencia se decide en el plan (enmienda menor en la 001 si requiere campo); nunca expone datos de otra ficha.
  • Q4 — ¿Los criterios sirven para portal y recordatorios? → A: sí. Los escenarios de aceptación de abajo son el contrato reutilizable: portal y recordatorios los referencian en vez de redactar los suyos.

User Scenarios & Testing

User Story 1 — Cancelar en plazo desde cualquier canal (Priority: P1)

Un paciente con una cita futura reservada y 24 h o más de antelación la cancela desde el portal o desde el enlace del email (previa confirmación): la cita pasa a cancelada, el tramo queda libre y ve la confirmación con el aviso único.

Why this priority: Es el ahorro directo para Sonia («la mitad de la mañana» al teléfono) en ambos canales. Sin este camino único, cada canal implementa su propia cancelación y divergen.

Independent Test: Se puede probar por completo tomando una cita reservada en plazo, ejecutando cancelarCita desde cada origen con confirmación y comprobando el paso a cancelada, la liberación del tramo (RN1) y el registro de origen.

Acceptance Scenarios:

  1. Given una cita reservada del paciente con inicio − ahora ≥ 24 h, When confirma desde el portal, Then devuelve aplicada, la cita pasa a cancelada, el tramo queda libre y la ficha muestra «Cancelada por el paciente desde el portal el …».
  2. Given la misma situación, When confirma desde el enlace vigente del email, Then devuelve aplicada con idéntico efecto y la ficha muestra «… desde el email …».
  3. Given una cita reservada en plazo, When se visita la página o el enlace sin confirmar, Then no se ejecuta nada: la cita sigue reservada y el tramo ocupado.

User Story 2 — Rechazos que nunca mutan (Priority: P1)

Todo intento fuera de plazo, sobre estado final, sobre cita ajena o duplicado se rechaza con su aviso único y no cambia nada.

Why this priority: La mitad del valor es no romper nada: un rechazo que mutara dejaría huecos fantasma o citas perdidas. Es P1 porque define la seguridad del caso de uso.

Independent Test: Se puede probar por completo con una matriz de rechazos (plazo, final, ajena, duplicada, enlace inválido) comprobando estado inmutable y aviso exacto en cada caso.

Acceptance Scenarios:

  1. Given una cita reservada con inicio − ahora de 23 h 59 min, When se intenta cancelar, Then devuelve PLAZO_VENCIDO («Ya no se puede cancelar por internet; llama a la clínica al …»), la cita sigue reservada.
  2. Given una cita en estado final (completada, cancelada, no_asistida), When se intenta cancelar, Then devuelve ESTADO_FINAL (o YA_CANCELADA si ya estaba cancelada) sin mutar nada.
  3. Given una cita reservada de otra ficha, When se intenta cancelar con el pacienteId propio, Then devuelve CITA_AJENA sin mutar nada ni revelar datos de la otra ficha.
  4. Given una cita ya cancelada por este caso de uso, When se reintenta (doble clic), Then devuelve YA_CANCELADA («Esta cita ya está cancelada») sin efectos adicionales.

User Story 3 — Concurrencia que solo aplica una vez (Priority: P2)

Doble clic, doble petición simultánea o cierre de recepción en el mismo instante: exactamente una ejecución aplica; las demás reciben el rechazo que corresponda.

Why this priority: Sin atomicidad, dos escrituras simultáneas pueden duplicar efectos o violar RN1. P2 porque presupone US1/US2 y es garantía técnica, no flujo de usuario.

Independent Test: Se puede probar por completo lanzando N ejecuciones concurrentes sobre la misma cita y comprobando que exactamente una devuelve aplicada.

Acceptance Scenarios:

  1. Given una cita cancelable, When llegan dos confirmaciones a la vez, Then una devuelve aplicada y la otra YA_CANCELADA; la cita queda cancelada una sola vez.
  2. Given una cita reservada en plazo, When recepción la marca completada mientras el paciente confirma, Then solo una de las dos transiciones aplica; la perdedora recibe ESTADO_FINAL o YA_CANCELADA sin estados intermedios.

Edge Cases

  • ¿Qué pasa si la cita está «En curso» (inicio pasado, fin futuro)? inicio − ahora < 24 h, luego PLAZO_VENCIDO: coherente con el portal (no cancelable) sin regla adicional.
  • ¿Qué pasa si inicio − ahora es exactamente 24 h? Dentro de plazo (borde incluido, 006 FR-005).
  • ¿Qué pasa si el origen no es portal ni email? Se rechaza como invocación inválida sin mutar nada; solo esos dos orígenes existen en v1.
  • ¿Qué pasa si la cita se mueve entre la visualización y la confirmación? La validación usa los datos vigentes en la transacción (006 FR-001): si ya no hay plazo, PLAZO_VENCIDO.
  • ¿Qué pasa si el paciente cancela una cita que ya tiene recordatorio enviado? El envío queda como histórico (recordatorios); el caso de uso no toca envíos.
  • ¿Qué pasa si la identidad no está verificada (enlace sin token válido)? ENLACE_INVALIDO antes de cualquier otra validación; no se revela si la cita existe.

Requirements

Functional Requirements

  • FR-001 (firma): El sistema MUST exponer cancelarCita(pacienteId, citaId, origen) con origen ∈ {portal, email} como única vía por la que un paciente escribe cancelada. Portal y recordatorios MUST llamarlo y MUST NOT implementar su propia transición. Devuelve aplicada o un código del catálogo FR-005.
  • FR-002 (identidad): El pacienteId MUST estar verificado según 007-identidad-paciente antes de invocar. Si la cita no pertenece a esa ficha MUST devolver CITA_AJENA sin mutar nada ni revelar datos ajenos.
  • FR-003 (confirmación): La ejecución MUST exigir confirmación expresa previa del paciente en su canal. Visualizar la página o el enlace MUST NOT ejecutar nada.
  • FR-004 (plazo y estado): Solo actúa sobre citas en estado reservada con inicio − ahora ≥ 24 h, computados según 006-tiempo-referencia (FR-001…FR-005 de esa spec). Esta spec MUST NOT redefinir «ahora», bordes ni precisión. Fuera de plazo → PLAZO_VENCIDO; estado final → ESTADO_FINAL (YA_CANCELADA si ya estaba cancelada).
  • FR-005 (catálogo único): Los códigos y textos son únicos para ambos canales, en español de España y con el teléfono de la clínica (001 FR-001): YA_CANCELADA («Esta cita ya está cancelada»), ESTADO_FINAL («Esta cita ya no se puede cancelar; llama a la clínica al …»), CITA_AJENA (genérico, sin revelar datos), PLAZO_VENCIDO («Ya no se puede cancelar por internet; llama a la clínica al …»), ENLACE_INVALIDO («Este enlace ya no es válido; entra al portal o llama a la clínica al …»). Ningún canal MUST redactar los suyos.
  • FR-006 (atomicidad): Leer estado + antelación y escribir cancelada MUST ser una transacción atómica. Concurrentemente, como máximo una ejecución devuelve aplicada por cita; las demás devuelven YA_CANCELADA o ESTADO_FINAL. MUST NOT quedar nunca dos efectos ni estados intermedios.
  • FR-007 (efecto): Al aplicarse, la cita MUST pasar a cancelada y su tramo [inicio, fin) MUST quedar libre y reutilizable sin violar RN1 (001 FR-007/FR-011). MUST NOT tocar envíos, métricas ni otras citas.
  • FR-008 (trazabilidad): Cada ejecución MUST registrar origen, instante y resultado, visible en la ficha como «Cancelada por el paciente desde el portal/email el DD/MM/AAAA a las HH:MM». El detalle de persistencia se decide en el plan (enmienda menor 001 si requiere campo). MUST NOT exponer datos de otra ficha.
  • FR-009 (alcance): Esta feature MUST NOT definir identidad (ver 007), tiempo (ver 006), contenido del email ni de la página del portal, y MUST NOT tocar completada ni no_asistida (solo recepción, 001 FR-011/FR-012). Escribe Cita.estado (reservada → cancelada); propietario de la tabla: 001.

Key Entities

  • Ejecución de cancelarCita: Invocación (pacienteId verificado, citaId, origen) con resultado (aplicada o código de rechazo). Atómica; idempotente; traza origen/instante/resultado.
  • Catálogo de rechazos: Cinco códigos con texto único es-ES (FR-005). Compartido por portal y email.
  • Cita (escritura delegada): Transición reservada → cancelada propiedad de la 001; este caso de uso es su único invocador por parte del paciente.

Success Criteria

Measurable Outcomes

  • SC-001: Una cita en plazo se cancela desde cada origen con confirmación y queda cancelada con tramo libre (RN1 en verde) y traza de origen, en el 100 % de los intentos.
  • SC-002: La matriz de rechazos (plazo, final, ajena, duplicada, enlace inválido) devuelve el código y texto exactos sin mutar nada, en el 100 % de los casos.
  • SC-003: N ejecuciones concurrentes sobre la misma cita producen exactamente una aplicada; cero dobles efectos y cero estados intermedios.
  • SC-004: Cero transiciones reservada → cancelada por paciente fuera de este caso de uso (revisión + grep por merge en portal y recordatorios).
  • SC-005: Todos los textos del catálogo están en español de España con el teléfono de la clínica y el formato de tramo «DD/MM/AAAA, HH:MM–HH:MM».

Assumptions

  • Identidad verificada según 007 (incluido el enlace verificado FR-006 de esa spec); el enlace sin verificar sigue siendo deuda temporal hasta adoptarlo.
  • Tiempo («ahora», bordes, precisión, zona) según 006; aquí no se redefine.
  • Clínica.telefono obligatorio (001 FR-001) para todos los avisos.
  • Recepción conserva sus transiciones (completada, no_asistida, cancelación propia) por 001 FR-011/FR-012; la carrera con el paciente la gana quien confirme primero en la transacción.
  • Confirmación expresa, textos de email/portal y métricas viven en sus specs; aquí solo el contrato del caso de uso.