Data Model — 005 Cancelación por el paciente
Fuente: spec.md (FR-001…FR-009). Propietario de la tabla: 001 (esta feature solo escribe por delegación). Zona Europe/Madrid, idioma es-ES, dinero intacto.
Sin tablas nuevas (principio IV)
Esta feature no crea tablas ni endpoints de lectura propios. Reutiliza Cita, Paciente y Clinica de la 001.
Enmienda menor a la 001 (dos columnas + teléfono pendiente)
Migración Prisma: prisma/migrations/XXXX_005_cancelacion_paciente/migration.sql (ADD COLUMN con DEFAULT '910123456' para Clinica.telefono + UPDATE de backfill en filas existentes antes de SET NOT NULL; semilla Eleva actualizada).
Ejecución de cancelarCita (no persistente como entidad)
Orden de validación dentro de la transacción (tras capturarAhora())
- Identidad no verificada →
ENLACE_INVALIDO(antes de leer la cita). - Cita inexistente en la clínica →
ENLACE_INVALIDOsi el origen es email (no revelar existencia),CITA_AJENAnunca revela datos de otra ficha; en portal con sesión válida la cita inexistente se trata igual sin enumerar. cita.pacienteId !== pacienteId→CITA_AJENA.estado === 'cancelada'→YA_CANCELADA;estado ∈ {completada, no_asistida}→ESTADO_FINAL.cancelacionEnPlazo(inicio, ahora) === false→PLAZO_VENCIDO(006 FR-005, borde 24 h incluido, minuto).- En otro caso →
UPDATE … SET estado='cancelada', cancelacionOrigen, canceladaEn=ahora WHERE id AND estado='reservada'; sicount === 0(carrera perdida) → releer y devolverYA_CANCELADA/ESTADO_FINAL.
Reglas de negocio en BD + aplicación
- Atomicidad (FR-006): transacción
serializable, reintento ≤3 anteP2034; escritura condicional como árbitro de la carrera. La exclusión RN1 (Cita_sin_solapes) no interviene: pasar acanceladasale del predicado y libera el tramo[inicio, fin)para RN1 (FR-007). - Idempotencia: la segunda ejecución sobre la misma cita lee
canceladay devuelveYA_CANCELADA. - Trazabilidad (FR-008): ficha muestra «Cancelada por el paciente desde el portal/email el DD/MM/AAAA a las HH:MM» (
cancelacionOrigen+formatearFechaHora(canceladaEn)). Los rechazos solo van al log del servidor (origen, instante, código).
Diagrama (texto)
Portal / Email --cancelarCita(pacienteId verificado, citaId, origen)--> lib/cancelacion.ts
--tx serializable--> Cita(reservada -> cancelada + origen + instante)
--rechazo--> mismo estado, aviso único con Clinica.telefono
Cita cancelada --sale del predicado--> RN1 libre para POST /api/citas