Contracts — API de agenda (Route Handlers Next.js)

Base: /api. Todos los cuerpos y errores en español de España. Errores: { "error": "mensaje claro", "codigo": "HUECO_OCUPADO | PASADO | ... " }.

GET /api/agenda?profesionalId=<uuid>&fecha=YYYY-MM-DD → 200 (US1, FR-013/FR-014)

Respuesta:

{
  "profesional": { "id": "uuid", "nombre": "María", "especialidad": "fisioterapia" },
  "fecha": "2026-10-05",
  "jornada": { "inicio": "09:00", "fin": "20:00" },
  "citas": [
    {
      "id": "uuid",
      "inicio": "2026-10-05T10:00:00+02:00",
      "fin": "2026-10-05T10:45:00+02:00",
      "servicio": "sesión fisio",
      "paciente": "Ana García",
      "estado": "reservada",
      "inicioTexto": "05/10/2026, 10:00",
      "tramoTexto": "05/10/2026, 10:00–10:45",
      "precioTexto": "40,00 €"
    }
  ],
  "huecosLibres": [{ "inicio": "2026-10-05T10:45:00+02:00", "fin": "2026-10-05T11:30:00+02:00" }]
}

Contrato: ordenadas por inicio; incluye vigentes y finales del día; resto de jornada como libre. 400 si fecha inválida.

POST /api/citas → 201 | 409 | 422 (US2, RN1+RN2)

Petición:

{
  "profesionalId": "uuid",
  "servicioId": "uuid",
  "pacienteId": "uuid",
  "inicio": "2026-10-05T10:00:00+02:00"
}
  • 201: { "id": "uuid", "inicio": "...", "fin": "...+30min", "estado": "reservada", "tramoTexto": "05/10/2026, 10:00–10:30" }.
  • 409 HUECO_OCUPADO: Ese tramo ya está ocupado (solape de rangos [inicio, fin) con reservada/completada mismo profesional, incl. carrera concurrente; entradas truncadas a minuto — FR-007).
  • 422 PASADO / FUERA_JORNADA / DIA_PARTIDO: No se pueden crear citas en el pasado / Fuera de la jornada 09:00–20:00 / La cita debe pertenecer a un único día (FR-009 / FR-013 jornada-bounds).
  • 404: profesional/servicio/paciente inexistente. fin lo calcula el servidor (FR-006).

PATCH /api/citas/[id]/estado → 200 | 409 | 422 (US3)

Petición: { "estado": "completada" | "cancelada" | "no_asistida" }.

  • 200: cita en estado final. cancelada libera tramo.
  • 409 TRANSICION_INVALIDA: cualquier cambio no reservada → final, entre finales, o no_asistida sin incomparecencia desde reservada, con aviso es-ES.

Fichas y acceso (US4 + gestión básica)

  • POST /api/auth/panel { "clave": "…" } → 200 con cookie de sesión de clínica o 401 Clave incorrecta (genérico, sin bloqueo — Q4/A).
  • GET/POST /api/profesionales|/servicios|/pacientes y PATCH correspondiente → CRUD básico; DELETE de ficha con citas ⇒ 409 FICHA_CON_CITAS (No se puede eliminar: tiene citas asociadas); crear servicio duplicado ⇒ 409 SERVICIO_DUPLICADO; paciente sin/inválido ⇒ 422 (Q1/C, Q3/A, Q5/B).
  • GET /api/semilla { "version": 1, "resumen": { "profesionales": 3, "servicios": 4, "pacientes": "38–42" } } para SC-005. Endpoint auxiliar de verificabilidad justificado por Constitución V (no es funcionalidad de clínica); alternativa seed:verificar también válida.

OpenAPI simplificado y esquemas zod viven en lib/validacion.ts; estos .md son la referencia para contracts/ y los tests de contrato Vitest.