Feature Specification: Referencia temporal compartida de CitaClara

Feature Branch: 006-tiempo-referencia

Created: 2026-09-30

Status: Draft

Input: User description: "Referencia temporal compartida de CitaClara: una única definición de instante «ahora» (instante del sistema en el momento de validar, precisión de minuto), zona canónica Europe/Madrid, jornada configurable por clínica (defecto 09:00–20:00) y calendario laborable (qué días cuentan como jornada). Reglas: bordes incluidos y su semántica exacta («a las 24 h justas está dentro»); cómo se evalúan las ventanas derivadas (RN2 sin pasado, antelación de cancelación de 24 h, ventana de recordatorios 24–48 h, 8 semanas cerradas de analítica con semana en curso fuera); cómo se muestra cualquier fecha/hora (día, mes, año, hora y minutos, 24 h) y cómo se comporta el cambio de hora estacional. El denominador de ocupación de la analítica se deriva de la jornada vigente real, nunca de una constante de 5 días."

Clarifications

Session 2026-09-30 (origen del «ahora» y calendario laborable)

  • Q1 — ¿De dónde sale el instante «ahora»: reloj del servidor o reloj de la BD, y con qué tolerancia? → A: el «ahora» se captura una vez al inicio de cada operación desde el reloj del servidor de aplicación y se propaga a todas las validaciones de esa operación (incluida la BD, que usa el valor propagado y no su propio now()). Tolerancia cero: la comparación es exacta con precisión de minuto (truncado a minuto, sin redondeo).
  • Q2 — ¿Qué días cuentan como jornada laborable y dónde se configura? → A: cada clínica configura sus días de apertura (defecto lunes a viernes); vive junto a la jornada en la entidad Clínica de la 001 (enmienda menor S-06: campo diasLaborables). Los festivos quedan fuera de v1: un festivo se trata como día abierto salvo configuración explícita.
  • Q3 — ¿Las fórmulas derivadas se repiten en cada spec? → A: no. Esta spec las escribe una sola vez (FR-004…FR-007) y las demás solo las referencian; redefinir un borde o una ventana fuera de aquí es defecto.
  • Q4 — ¿Cómo se etiqueta una semana y un tramo para que no haya ambigüedad? → A: semanas como «S-N: DD/MM/AAAA–DD/MM/AAAA» derivadas de la ventana calculada, nunca constantes; tramos como «DD/MM/AAAA, HH:MM–HH:MM» en 24 h.

User Scenarios & Testing

User Story 1 — Un único «ahora» para todas las validaciones (Priority: P1)

El sistema valida «pasado», antelaciones y ventanas contra un único instante capturado al inicio de cada operación, de modo que recepción, portal, recordatorios y analítica nunca discrepan sobre si una cita está en el pasado o dentro de plazo.

Why this priority: Sin un «ahora» único, la misma cita puede ser «futura» para el portal y «pasada» para recepción. Es la base de todas las ventanas; sin ella nada más es verificable.

Independent Test: Se puede probar por completo fijando el reloj del sistema, ejecutando una alta en el pasado límite, una cancelación al borde de 24 h y una selección de ventana 24–48 h, y comprobando que las tres usan el mismo instante con precisión de minuto.

Acceptance Scenarios:

  1. Given el reloj del sistema en 30/09/2026 10:00, When se valida una cita con inicio 30/09/2026 09:59, Then se considera pasado en todas las features (alta, portal, recordatorios).
  2. Given el mismo instante, When se valida una cita con inicio 30/09/2026 10:00, Then no se considera pasado (el borde es estricto: solo lo anterior a «ahora» es pasado).
  3. Given una operación que valida varias reglas (p. ej. alta + antelación), When el reloj avanza durante la operación, Then todas las reglas usan el instante capturado al inicio, no el instante de cada chequeo.

User Story 2 — Ventanas derivadas con bordes escritos una sola vez (Priority: P1)

Cada spec consumidora (001, portal, recordatorios, analítica) aplica su ventana con la fórmula y el borde definidos aquí, sin copiarlos ni reinterpretarlos.

Why this priority: Es el ahorro directo: cuatro implementaciones que hoy calculan «24 h» por su cuenta pasan a una sola definición. Depende de US1 (necesita el «ahora» único).

Independent Test: Se puede probar por completo comprobando cada ventana en sus bordes exactos (RN2, 24 h justas, 24 h y 48 h de recordatorios, S-1…S-8) contra esta spec.

Acceptance Scenarios:

  1. Given una cancelación de paciente con inicio − ahora de exactamente 24 h 00 min, When se valida, Then está dentro de plazo (el borde de 24 h está incluido).
  2. Given una cancelación con inicio − ahora de 23 h 59 min, When se valida, Then está fuera de plazo.
  3. Given el proceso de recordatorios ejecutado a las 00:00, When hay citas a exactamente 24 h y exactamente 48 h, Then ambas entran en ventana (bordes incluidos).
  4. Given la analítica abierta un miércoles, When se calcula la evolución, Then la semana en curso queda fuera y S-1…S-8 son las 8 semanas naturales cerradas anteriores.

User Story 3 — Jornada y calendario configurables con denominador honesto (Priority: P2)

La clínica configura jornada y días de apertura; la ocupación de la analítica deriva su denominador de esa configuración vigente y lo muestra junto al porcentaje.

Why this priority: Sin esto, una clínica que abre sábados ve ocupaciones infladas sin saber por qué. Es P2 porque solo afecta a la analítica y presupone US1/US2.

Independent Test: Se puede probar por completo reconfigurando la jornada o los días laborables y comprobando que el denominador de ocupación cambia y se declara.

Acceptance Scenarios:

  1. Given la jornada por defecto 09:00–20:00 y laborables Lun–Vie, When se calcula la ocupación de 8 semanas, Then el denominador es 26.400 min por profesional y se muestra «09:00–20:00, Lun–Vie».
  2. Given una clínica que abre también los sábados, When se calcula la ocupación, Then el denominador incluye los sábados y el valor no se compara con el de una clínica Lun–Vie.
  3. Given cualquier porcentaje de ocupación visible, When se lee, Then la jornada y los días usados como denominador aparecen junto a él.

Edge Cases

  • ¿Qué pasa si una operación cruza un cambio de hora estacional (p. ej. se valida a las 02:30 el día del cambio)? Todas las comparaciones usan Europe/Madrid con día, mes, año, hora y minutos sin ambigüedad; la hora ambigua se resuelve con el desplazamiento vigente en el instante «ahora» capturado.
  • ¿Qué pasa si el reloj del servidor y el de la BD difieren? No importa: la BD usa el «ahora» propagado por la aplicación, nunca su propio now() para reglas de negocio.
  • ¿Qué pasa si la cita cae exactamente en «ahora» (mismo minuto)? No es pasado (RN2 exige estrictamente anterior) pero tampoco tiene antelación suficiente para cancelar (24 h no cumplidas): cada fórmula decide con su propio borde, ambas escritas en FR-004…FR-007.
  • ¿Qué pasa si la clínica reconfigura su jornada a mitad de las 8 semanas? La ocupación usa la jornada vigente en el momento del cálculo y lo declara; no se reconstruye jornada histórica en v1.
  • ¿Qué pasa si una spec necesita una ventana nueva (p. ej. «últimos 30 días»)? La define aquí primero como fórmula derivada y luego la referencia; definirla localmente es defecto.
  • ¿Qué pasa si un festivo cae en martes? En v1 se trata como día laborable salvo que la clínica lo excluya en su configuración de días de apertura.

Requirements

Functional Requirements

  • FR-001 (instante «ahora»): El sistema MUST capturar un único instante «ahora» al inicio de cada operación (alta, desenlace, cancelación de paciente, selección de recordatorios, cálculo de analítica) desde el reloj del servidor de aplicación, y MUST usar ese mismo instante en todas las validaciones temporales de esa operación. La BD MUST usar el valor propagado y MUST NOT usar su propio now() para reglas de negocio.
  • FR-002 (precisión): Todas las comparaciones temporales MUST truncar a precisión de minuto (sin redondeo). Dos instantes del mismo minuto son el mismo minuto a efectos de todas las reglas.
  • FR-003 (zona y formato): La zona canónica MUST ser Europe/Madrid en todas las specs y en todo el código; escribir «España (península)» u otro nombre para la misma zona es defecto. Todo instante visible MUST mostrarse como «DD/MM/AAAA, HH:MM» en 24 h y todo tramo como «DD/MM/AAAA, HH:MM–HH:MM» en 24 h, sin ambigüedad.
  • FR-004 (RN2 — pasado): Una cita está en el pasado si y solo si su inicio es estrictamente anterior al «ahora» (inicio < ahora). El instante igual a «ahora» no es pasado. Consumidores: 001 FR-009.
  • FR-005 (antelación de cancelación): Una cancelación de paciente está en plazo si y solo si inicio − ahora ≥ 24 h (borde de 24 h incluido, precisión de minuto). Por debajo de 24 h está fuera de plazo. Sin tope superior. Consumidores: caso de uso único cancelarCita (005), portal, recordatorios.
  • FR-006 (ventana de recordatorios): Una cita entra en ventana si y solo si 24 h ≤ inicio − ejecucion ≤ 48 h (ambos bordes incluidos, precisión de minuto, Europe/Madrid). Consumidor: recordatorios FR-001. El horario de ejecución por defecto (00:00, configurable) vive en la spec de recordatorios; solo consume esta fórmula.
  • FR-007 (ventana de analítica): La «semana en curso» es la semana natural lunes–domingo que contiene la fecha de hoy en Europe/Madrid, recalculada en cada llamada a semanasAnalitica(hoy). El marco semanal es siempre lunes–domingo con independencia de diasLaborables; diasLaborables solo parametriza el denominador de ocupación (FR-008), nunca los bordes de semana. La historia son las 8 semanas naturales cerradas anteriores (S-8 la más antigua … S-1 la más reciente); la semana en curso MUST NOT entrar. Consumidor: analítica FR-005.
  • FR-008 (jornada y calendario): Cada clínica MUST poder configurar su jornada (jornadaInicio/jornadaFin, defecto 09:00/20:00) y sus días de apertura (diasLaborables, defecto Lun–Vie; enmienda menor en la 001). En v1 la configuración vive en semilla/migración (prisma/seed.ts + columna Clinica.diasLaborables), sin UI ni endpoint. El denominador de ocupación MUST derivarse siempre por cálculo (minutosJornada × nºDíasLaborables × 8 semanas) con la configuración vigente y MUST mostrarse junto a cada porcentaje. El sistema MUST NOT hardcodear la constante 26.400 min en ningún caso (con defecto se calcula igual: 660 × 5 × 8).
  • FR-009 (etiquetado): Cada fila de evolución MUST mostrar «S-N: DD/MM/AAAA–DD/MM/AAAA» derivada de la ventana calculada en FR-007, nunca una constante. La jornada y los días usados como denominador MUST indicarse visiblemente junto a los porcentajes de ocupación.
  • FR-010 (prohibición de redefinir): Ninguna spec consumidora MUST redefinir «ahora», zona, precisión, bordes ni ventanas. Toda regla temporal local MUST citar la fórmula de esta spec que aplica. Una definición temporal duplicada fuera de aquí se considera defecto.
  • FR-011 (alcance): Esta feature MUST NOT implementar métricas, envíos, altas ni transiciones. Solo define el vocabulario temporal que las demás consumen; no crea tablas ni endpoints.

Key Entities

  • Instante «ahora»: Valor temporal capturado una vez por operación desde el reloj del servidor; única fuente de verdad para pasado, antelaciones y ventanas de esa operación. Sin persistencia propia.
  • Jornada y calendario de la clínica: Configuración propiedad de la 001 (jornadaInicio, jornadaFin, diasLaborables); defecto 09:00–20:00 Lun–Vie. Determina la agenda del día y el denominador de ocupación.
  • Ventana derivada: Fórmula con nombre (RN2, antelación 24 h, 24–48 h, S-8…S-1) definida una sola vez aquí y referenciada por nombre desde cada spec consumidora.

Success Criteria

Measurable Outcomes

  • SC-001: El 100 % de las validaciones temporales de una misma operación usan el mismo «ahora» capturado al inicio; cero discrepancias pasado/futuro entre features para la misma cita y el mismo instante.
  • SC-002: El 100 % de los casos de borde (RN2 en el minuto exacto, 24 h justas dentro, 23:59 fuera, 24 h y 48 h de recordatorios dentro, semana en curso fuera) se resuelve según FR-004…FR-007 en pruebas automatizadas con reloj controlado.
  • SC-003: Con jornada no estándar (p. ej. con sábados), el denominador de ocupación difiere de 26.400 min y la jornada/días usados aparecen junto al porcentaje en el 100 % de las vistas.
  • SC-004: Cero definiciones temporales duplicadas fuera de esta spec: ninguna otra spec contiene su propia definición de «ahora», zona, precisión o bordes (verificación por revisión + grep en cada merge).

Assumptions

  • El reloj del servidor está sincronizado (NTP); la deriva entre nodos no se trata en v1.
  • Festivos fuera de v1: se tratan como día abierto salvo exclusión explícita en diasLaborables.
  • La enmienda menor diasLaborables en la entidad Clínica de la 001 se aplica junto a esta spec (propietario 001, Antonio Natusch).
  • Formatos de visualización de importes y textos es-ES siguen en sus specs (001 FR-017, analítica FR-007); aquí solo vive el tiempo.