Contracts — 003 Recordatorios de cita

1. POST /api/recordatorios/ejecutar (FR-015, disparador manual)

Auth: recepción (exigirClinica, cookie de panel). Mismo comportamiento y no-duplicidad que el cron.

  • Request: { "ejecucion"?: string } (ejecucion ISO opcional solo para pruebas con reloj controlado; en producción se ignora y se usa capturarAhora()).
  • Response 200: { "generados": number, "omitidos": number, "incidencias": number, "ejecucion": string }.
  • Errores: 401 sin sesión (heredado exigirClinica).

2. GET /api/citas/[id]/recordatorio (FR-011, señal en ficha)

Auth: recepción. Nunca expone rutas, hashes ni nombres de fichero (Q17).

  • Response 200 enviado: { "estado": "enviado", "texto": "Recordatorio enviado el 29/09/2026 a las 00:05" } (fecha = creadoEn del envío vigente, formato formatearDia + formatearHora).
  • Response 200 pendiente: { "estado": "pendiente", "texto": "Recordatorio pendiente" }.
  • Errores: 404 cita inexistente o de otra clínica; 401 sin sesión.

3. Página GET /cancelar/[id]?token= + PATCH /api/citas/[id]/cancelar {origen:'email', token} (FR-006/007/009)

Reutiliza el caso de uso único 005 y el enlace verificado 007; esta feature no define transición ni token propios.

  • GET muestra datos vigentes (clínica, tramo, profesional, servicio, precio) + botón «Confirmar cancelación». Nunca escribe (005 FR-003). Sin token válido → ENLACE_INVALIDO («Este enlace ya no es válido; entra al portal o llama a la clínica al …»).
  • Confirmar → PATCH con {origen:'email', token}. aplicada → «Tu cita ha quedado cancelada…»; PLAZO_VENCIDO → «fuera de plazo, llama a la clínica al » sin mutar; YA_CANCELADA → «Esta cita ya está cancelada»; ESTADO_FINAL → aviso de estado final con teléfono.