Files
FarmaFinder/docs/PLAN-HORARIOS.md
T
Antoni Nuñez Romeu d68463548f
Run Tests on Branches / Detect Changes (push) Successful in 19s
Run Tests on Branches / Backend Tests (push) Successful in 2m46s
Run Tests on Branches / PIP Platform Tests (push) Has been skipped
Run Tests on Branches / Frontend Tests (push) Successful in 2m24s
Run Tests on Branches / Frontend Mobile Tests (push) Has been skipped
Run Tests on Branches / Parapharmacy API Tests (push) Has been skipped
Patentes internacionales + Forgot Pwd
2026-08-18 11:01:54 +02:00

482 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Plan — Horarios de farmacias, filtro "Abiertas Ahora" y farmacias 24h
> **TL;DR**
> - **El parser OSM ya existe** (`apps/API/opening-hours-osm.js`) y cubre `24/7`, rangos, splits, wrap-around y días cerrados. Su output (`["00:00", "24:00"]` para 24/7) ya entra en BD; lo que falta es **interpretar bien `24:00`**, centralizar el cálculo "abierto/24h" en el backend, exponer un endpoint "isOpenNow" y añadir el filtro en la UI.
> - Tres pasos incrementales, cada uno verificable de forma aislada. Ninguno rompe lo que ya hay: el campo `opening_hours` se conserva como JSON string y el admin ya lo edita.
> - **TDD obligatorio** (red-green-refactor, ≥80% cobertura en lo que se añada) por la regla global de testing.
---
## Estado actual (lo que ya tenemos)
| Pieza | Ubicación | Estado |
|-------|-----------|--------|
| Parser OSM → JSON `{day:[open,close] \| null}` | `apps/API/opening-hours-osm.js` | Hecho + tests en `apps/backend/__tests__/opening-hours-osm.test.js` |
| Almacenamiento `pharmacies.opening_hours TEXT` | `apps/backend/server.js` (`initDatabase`, `serializeOpeningHours`) | Hecho (PG + SQLite con migración) |
| Ingestión desde OSM, n8n webhook y URL openData | `apps/API/*`, `apps/backend/farmacias-webhook-import.js` | Hecho — los tres caminos ya parsean horarios |
| `getOpenStatus()` frontend | `apps/frontend/src/utils/hours.js` | Parcial: NO trata `24:00` como 24h (lo trata como "medianoche", farmacia cerrada); usa idioma hardcoded (es) en labels |
| Editor de horarios en admin | `apps/frontend/src/components/admin/PharmacyManagement.jsx` | Hecho (filas Lun–Dom, open/close, toggle "Cerrado") |
| Badge "Abierto / Cerrado" en lista | `apps/frontend/src/components/PharmacyList.jsx` | Hecho vía `getOpenStatus()` |
| Filtro "Abiertas Ahora" | — | **No existe** |
| Etiqueta "24h" en lista/mapa | — | **No existe** |
| Endpoint que devuelva estado calculado en servidor | — | **No existe** (todo se calcula en cliente) |
**Diagnóstico clave:**
1. `parseOsmOpeningHours('24/7')` produce `["00:00", "24:00"]` para todos los días. `getOpenStatus()` en frontend llama `parseHM("24:00")` → `24*60+0 = 1440`. Como `nowMins < 1440` casi siempre es `true`, nunca se evalúa el caso 24h. Necesita un caso explícito.
2. `getOpenStatus()` calcula "ahora" en el navegador. Si el usuario cambia de zona horaria o el servidor de test no está en hora local, el resultado puede no coincidir con el esperado.
3. `opening_hours` se devuelve crudo en `/api/medicines/:nregistro/pharmacies` y `/api/pharmacies`. El frontend debe parsearlo cada vez (ya lo hace, pero sin tests frontend).
---
## Enfoque general
1. **Mover el cálculo de "está abierta / es 24h" al backend**, exponerlo en un único helper puro y testable. Devolver ese dato precalculado en los endpoints públicos para que el frontend no tenga que duplicar la lógica ni parchear el caso `24:00`.
2. **Filtrar en cliente** (no en servidor): el dataset de farmacias por búsqueda es pequeño (decenas, no miles), y el filtro debe combinarse con "ordenar por distancia". Mantener la lógica en un solo reducer/hook.
3. **24h como estado derivado** del JSON, no como columna nueva: si todos los días son `["00:00", "24:00"]`, la farmacia es 24h. Evita migración, evita sincronización.
4. **i18n primero**: mover los strings hardcoded de `hours.js` y `PharmacyList.jsx` a los locales (`es.js`, `ca.js`).
---
## Paso 1 — Helper de estado abierto/24h en el backend (TDD)
> **Objetivo:** un solo módulo `apps/backend/src/hours.js` con `isOpenNow(rawHours, now)` y `isAlwaysOpen(rawHours)` que sea la fuente de verdad. Tests Jest con casos explícitos para `24:00`, medianoche cruzando, splits (no usados en almacenamiento, pero defensivo), días cerrados.
### 1.1 Tests primero (RED)
**`apps/backend/__tests__/hours.test.js`** (nuevo)
```
describe('isOpenNow')
✓ returns null when raw is null/empty
✓ returns null when JSON is malformed
✓ open window covers now → { isOpen: true, kind: 'open', closesAt: '21:00' }
✓ before opening today → { isOpen: false, kind: 'before-open', opensAt: '09:00' }
✓ after closing today but opens tomorrow → { isOpen: false, kind: 'after-close', nextOpen: { day: 'mon', time: '09:00' } }
✓ closed all week → { isOpen: false, kind: 'closed', nextOpen: null }
✓ 24/7 (["00:00","24:00"]) → { isOpen: true, kind: '24h' }
✓ 24:00 close at 23:59:59 → open; at 24:00:00 → close, nextOpen: tomorrow
✓ midnight-crossing range: mon ["22:00","02:00"] — at 01:00 monday → open (special-case: subtract 1 day from now, check sunday range)
— implement: if closeMins <= openMins, treat the range as [open, close+24h]; check nowMins against that
✓ all days closed → kind 'closed'
describe('isAlwaysOpen')
✓ all 7 days ["00:00","24:00"] → true
✓ one day different → false
✓ one day null → false
✓ null input → false
```
**Aceptación:** `npm test --workspace=farma-clic-backend` corre los nuevos tests y todos pasan **antes** de tocar otros archivos.
### 1.2 Implementación (GREEN)
**`apps/backend/src/hours.js`** (nuevo)
```js
const DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
function parse(raw) {
if (!raw) return null;
if (typeof raw === 'object') return raw;
try { return JSON.parse(raw); } catch { return null; }
}
function toMin(hm) {
// accept "24:00" as 1440 only when minutes == 0
const [h, m] = String(hm).split(':').map(Number);
if (!Number.isFinite(h) || !Number.isFinite(m)) return null;
if (h === 24 && m === 0) return 1440;
if (h < 0 || h > 24 || m < 0 || m >= 60) return null;
return h * 60 + m;
}
export function isAlwaysOpen(rawHours) {
const h = parse(rawHours);
if (!h) return false;
for (const d of DAYS) {
const r = h[d];
if (!Array.isArray(r) || r.length !== 2) return false;
if (r[0] !== '00:00' || r[1] !== '24:00') return false;
}
return true;
}
export function isOpenNow(rawHours, now = new Date()) {
const h = parse(rawHours);
if (!h) return null;
if (isAlwaysOpen(h)) {
return { isOpen: true, kind: '24h' };
}
const day = DAYS[now.getDay()];
const range = h[day];
if (!Array.isArray(range) || range.length !== 2) {
return { isOpen: false, kind: 'closed', nextOpen: findNextOpen(h, now) };
}
const openM = toMin(range[0]);
const closeM = toMin(range[1]);
if (openM == null || closeM == null) return null;
const nowM = now.getHours() * 60 + now.getMinutes();
// Midnight-crossing: close <= open means the range spans past 24:00.
if (closeM <= openM) {
if (nowM >= openM) return { isOpen: true, kind: 'open', closesAt: range[1] };
// before open today — maybe it's open from yesterday's range
const yestKey = DAYS[(now.getDay() + 6) % 7];
const yest = h[yestKey];
if (Array.isArray(yest)) {
const yOpen = toMin(yest[0]);
const yClose = toMin(yest[1]);
if (yOpen != null && yClose != null && yClose <= yOpen && nowM < yClose) {
return { isOpen: true, kind: 'open', closesAt: yest[1] };
}
}
return { isOpen: false, kind: 'before-open', opensAt: range[0], nextOpen: null };
}
if (nowM < openM) {
return { isOpen: false, kind: 'before-open', opensAt: range[0], nextOpen: null };
}
if (nowM >= closeM) {
return { isOpen: false, kind: 'after-close', nextOpen: findNextOpen(h, now) };
}
return { isOpen: true, kind: 'open', closesAt: range[1] };
}
function findNextOpen(h, now) {
for (let off = 1; off <= 7; off++) {
const key = DAYS[(now.getDay() + off) % 7];
const r = h[key];
if (Array.isArray(r) && r.length === 2) {
return { day: key, time: r[0] };
}
}
return null;
}
```
### 1.3 Refactor (IMPROVE)
- Extraer `findNextOpen` como `export` y reusarlo en `apps/frontend/src/utils/hours.js` (vía copia local por ahora — son proyectos independientes). No mover al `apps/API/` para no introducir regresiones en el parser OSM, que ya está en producción.
- Añadir `coverage` config al `jest.config.js` para exigir 100% en `src/hours.js` (es un módulo puro, sin dependencias).
**Verificación (regla de evidence):**
```bash
npm test --workspace=farma-clic-backend -- --testPathPattern=hours
# Esperado: 10/10 tests pasados, coverage del nuevo módulo = 100%
```
---
## Paso 2 — Exponer el estado calculado en los endpoints públicos
> **Objetivo:** que `/api/medicines/:nregistro/pharmacies` y `/api/pharmacies` devuelvan `is_open` y `is_24h` precalculados, basados en la hora del **servidor**. Esto fija una única fuente de verdad y elimina el bug del frontend con `24:00`.
### 2.1 Tests primero (RED)
**Ampliar `apps/backend/__tests__/server.test.js`** (o crear `pharmacy-hours-endpoint.test.js`):
```
GET /api/medicines/:nregistro/pharmacies
✓ when DB has pharmacy with opening_hours = 24/7 JSON
→ response[0].is_open === true, is_24h === true
✓ when DB has pharmacy with closed day
→ response[0].is_open === false, is_24h === false
✓ when DB has pharmacy with null opening_hours
→ response[0].is_open === null, is_24h === false
```
Mockear `userDbAll` o sembrar una BD en memoria antes de cada test (ya se hace en `server.test.js` — seguir el mismo patrón).
### 2.2 Implementación (GREEN)
En `apps/backend/server.js`, en ambos endpoints (`/api/medicines/:id/pharmacies` y `/api/products/:source/:id/pharmacies` y `/api/pharmacies`), mapear las filas para añadir dos campos:
```js
import { isOpenNow, isAlwaysOpen } from './src/hours.js';
// tras obtener `pharmacies`:
const enriched = pharmacies.map(p => {
let isOpen = null;
let is24h = false;
if (p.opening_hours) {
const status = isOpenNow(p.opening_hours);
isOpen = status ? status.isOpen : null;
is24h = isAlwaysOpen(p.opening_hours);
}
return { ...p, is_open: isOpen, is_24h: is24h };
});
res.json(enriched);
```
Crear un helper local `enrichPharmacy(row)` para no duplicarlo tres veces.
### 2.3 Refactor (IMPROVE)
- Si el rendimiento fuera problema (no lo es con <1000 filas), cachear `isOpenNow` con un TTL de 60s por minuto actual. Por ahora se calcula en cada request; dejar un comentario `// TODO(cache): si el dataset crece, cache por minuto`.
**Verificación:**
```bash
npm test --workspace=farma-clic-backend
# Esperado: tests existentes verdes + los nuevos verdes
# Manual: curl a /api/medicines/<nregistro>/pharmacies y verificar is_open en JSON
```
---
## Paso 3 — Soporte nativo de farmacias 24h en admin y selector de horarios
> **Objetivo:** que el admin pueda marcar una farmacia como "24h" con un toggle que rellene los 7 días con `00:00–24:00`, y que el editor manual respete el shape exacto del parser OSM.
### 3.1 Tests primero (RED)
**`apps/frontend/src/components/admin/PharmacyManagement.test.jsx`** (nuevo, con `@testing-library/react` si está disponible, si no: tests de unidad sobre los helpers puros)
Tests del helper puro `hoursToDraft(raw)` y `draftToHours(draft)` (extraídos de `PharmacyManagement.jsx`):
```
hoursToDraft
✓ 24/7 JSON → all 7 days with closed: false, open: '00:00', close: '24:00'
✓ normal hours → mirror
✓ null → all days with closed: true, open: '09:00', close: '21:00' (default)
draftToHours
✓ 24/7 draft (all days closed:false, 00:00, 24:00) → all 7 days ["00:00","24:00"]
✓ mixed → matches input
✓ all closed → null
```
### 3.2 Refactor del admin (IMPROVE para testabilidad)
1. Mover `emptyHoursDraft`, `hoursToDraft`, `draftToHours` a `apps/frontend/src/utils/hours.js` (ya existe) y exportarlos.
2. En `PharmacyManagement.jsx`, importar desde el helper. Esto los hace testables sin renderizar el componente.
3. Añadir **toggle "24 horas"** encima del fieldset de horarios. Estado: `is24h`. Cuando se active, poblar `hoursDraft` con `00:00/24:00` en todos los días y `closed: false`. Cuando se desactive, volver a `emptyHoursDraft()` (pregunte `confirm()` antes de descartar).
4. CSS: el toggle debe ser visible y mostrar el estado actual. Estilo coherente con el resto del admin (`.hours-row--always-open`).
### 3.3 Verificación
```bash
npm test --workspace=farma-clic-frontend
# Esperado: tests verdes del helper + sin regresión en PharmacyList
# Manual: crear farmacia 24h desde admin → ver en /api/pharmacies que is_24h=true
```
---
## Paso 4 — Filtro "Abiertas Ahora" en la vista pública
> **Objetivo:** un toggle justo al lado del botón "Ordenar por distancia". Cuando esté activo, la lista y el mapa solo muestran farmacias con `is_open === true` (o `is_24h === true`). Si una farmacia no tiene horarios, se muestra siempre (no se penaliza al admin por no haber editado).
### 4.1 Tests primero (RED)
**`apps/frontend/src/views/PublicView.test.jsx`** (nuevo si no existe) o tests sobre un hook puro extraído.
Refactor sugerido: extraer un hook `usePharmacyFilters(pharmacies, { sortByDistance, userPosition, openNow })` que devuelva la lista filtrada y ordenada. Eso permite testearlo sin renderizar.
```
usePharmacyFilters
✓ openNow=false → todas las farmacias
✓ openNow=true → solo las con is_open === true
✓ openNow=true + pharmacy con opening_hours=null → incluida (no se filtra)
✓ openNow=true + is_24h=true → incluida
✓ sortByDistance=true → orden por km ascendente
✓ sortByDistance=true sin userPosition → orden estable (no rompe)
✓ sortByDistance=true + pharmacy sin lat/lng → al final
```
### 4.2 Implementación (GREEN)
**`apps/frontend/src/views/PublicView.jsx`**
1. Nuevo state: `const [openNow, setOpenNow] = useState(false);`
2. Nuevo botón en `.pharmacy-controls` (donde está "Ordenar por distancia"):
```jsx
<button
className={`open-now-toggle ${openNow ? 'active' : ''}`}
onClick={() => setOpenNow(o => !o)}
aria-pressed={openNow}
>
{openNow ? '🟢 Abiertas ahora' : '⏱ Mostrar solo abiertas ahora'}
</button>
```
3. Mover la lógica de `displayedPharmacies` al nuevo hook `usePharmacyFilters(pharmacies, { sortByDistance, userPosition, openNow })`.
4. Pasar `displayedPharmacies` a `<PharmacyMap />` y `<PharmacyList />` igual que ahora.
5. Si el filtro deja 0 resultados: mostrar mensaje "Ninguna farmacia abierta ahora — desactiva el filtro para ver todas".
### 4.3 Visualización del badge 24h
En `apps/frontend/src/components/PharmacyList.jsx`, dentro de `PharmacyCard`, añadir (cuando `pharmacy.is_24h === true`):
```jsx
<span className="pharmacy-badge pharmacy-badge--24h" aria-label="Abierta 24 horas">24h</span>
```
al lado del nombre, antes de la distancia.
En el popup de `PharmacyMap.jsx` (`apps/frontend/src/components/PharmacyMap.jsx`):
```jsx
{pharmacy.is_24h && <span className="map-badge-24h">24h</span>}
```
### 4.4 Verificación
```bash
npm test --workspace=farma-clic-frontend
# Manual:
# 1. Crear 2 farmacias: una 9-14h (cerrada ahora si son las 17h), otra 24h
# 2. Buscar un medicamento enlazado a las dos
# 3. Pulsar "Abiertas ahora" → solo aparece la 24h
# 4. Desactivar filtro → aparecen las dos
```
---
## Paso 5 — i18n de los labels de horario
> **Objetivo:** mover los strings hardcoded de `apps/frontend/src/utils/hours.js` ("Abierto · Cierra a las X", "Cerrado · Abre mañana…") a los locales `es.js` y `ca.js`. Idem para el nuevo "Abiertas ahora".
### 5.1 Tests / Tareas
1. Añadir claves en `apps/frontend/src/i18n/locales/es.js`:
```js
'pharmacy.openNow': 'Abierto · Cierra a las {{time}}',
'pharmacy.closedAllDay': 'Cerrado',
'pharmacy.opensAt': 'Cerrado · Abre a las {{time}}',
'pharmacy.opensTomorrow': 'Cerrado · Abre mañana a las {{time}}',
'pharmacy.opensDay': 'Cerrado · Abre el {{day}} a las {{time}}',
'pharmacy.alwaysOpen': 'Abierto 24h',
'pharmacy.filterOpenNow': 'Mostrar solo abiertas ahora',
'pharmacy.filterOpenNowActive': 'Solo abiertas ahora',
'pharmacy.badge24h': '24h',
'pharmacy.filterNoResults': 'Ninguna farmacia abierta ahora. Desactiva el filtro para ver todas.'
```
2. Replicar en `ca.js` con las traducciones catalanas correspondientes.
3. Refactor de `getOpenStatus()` para que devuelva `{ status, labelKey, labelParams }` en vez de strings listos, y que `PharmacyList.jsx` los pase por `t()`. Mantener compatibilidad: si no se puede traducir, devolver el string por defecto (defensivo).
### 5.2 Verificación
```bash
npm test --workspace=farma-clic-frontend
# Manual: cambiar idioma a catalán (ca) y verificar badges
```
---
## Paso 6 — Verificación end-to-end (cierre del plan)
> Antes de declarar el plan completo, ejecutar el set de verificaciones del repo (regla "evidence-based completion" — sin evidencia, no se afirma éxito).
### 6.1 Backend
```bash
# Linter
cd apps/backend && npx eslint .
# Tests
npm test --workspace=farma-clic-backend
# Cobertura ≥ 80% en modules nuevos:
# apps/backend/src/hours.js → 100%
# apps/backend/__tests__/hours.test.js → 100%
# endpoints pharmacy hours tests → ≥ 80%
```
### 6.2 Frontend
```bash
# Linter
cd apps/frontend && npx eslint .
# Tests
npm test --workspace=farma-clic-frontend
# Cobertura ≥ 80% en:
# apps/frontend/src/utils/hours.js (tras mover helpers de admin) → 100%
# apps/frontend/src/views/PublicView.jsx → ≥ 80%
# apps/frontend/src/components/PharmacyList.jsx → ≥ 80%
# apps/frontend/src/components/admin/PharmacyManagement.jsx → ≥ 80%
```
### 6.3 Integración real (Docker)
```bash
docker compose up --build -d
docker compose exec backend npm run seed # crea 3 farmacias: 24h, 9-21h, fines de semana
curl -s http://localhost:3001/api/pharmacies | jq '.[0] | {name, is_open, is_24h}'
# Esperado: 24h farmacia con is_open=true, is_24h=true
```
### 6.4 E2E manual (smoke)
1. Abrir `http://localhost:4000`, buscar "ibuprofeno", seleccionar.
2. Verificar: las 3 farmacias aparecen con su badge de estado correcto.
3. Pulsar "Mostrar solo abiertas ahora" → solo las abiertas.
4. Pulsar "Ordenar por distancia" → reordena, badge 24h visible.
5. Abrir el popup del mapa de la 24h → etiqueta "24h" visible.
### 6.5 Regresión
- `apps/backend/__tests__/opening-hours-osm.test.js` → sigue verde (no tocamos el parser).
- `apps/backend/__tests__/server.test.js` → sigue verde; los nuevos endpoints se añaden sin romper los existentes.
- `apps/frontend/src/components/PharmacyList.jsx` actual: si `is_24h` es `undefined` (respuesta legacy de la BD sin enriquecer), debe renderizarse igual que antes (compatibilidad).
---
## Orden de implementación recomendado
| Paso | Tiempo estimado | Bloquea |
|------|----------------|---------|
| 1. Helper backend `src/hours.js` (TDD) | 1.5 h | Pasos 2 y 4 |
| 2. Enriquecer endpoints públicos (TDD) | 1 h | Paso 4 (frontend necesita el campo) |
| 3. Admin 24h (refactor + test) | 1 h | — (paralelo a 4) |
| 4. Filtro "Abiertas ahora" + badge 24h (TDD) | 2 h | — |
| 5. i18n (es + ca) | 0.5 h | — |
| 6. Verificación + cobertura | 0.5 h | Cierre |
| **Total** | **~6.5 h** | |
Pasos 1 y 3 pueden hacerse en paralelo. Paso 5 puede entramparse con 4.
---
## Riesgos y mitigaciones
| Riesgo | Mitigación |
|--------|-----------|
| `parseOsmOpeningHours` cambia en producción y rompe el shape | Tests existentes en `opening-hours-osm.test.js` ya cubren los casos. No tocar. |
| Hora del servidor difiere del navegador del usuario | Devolver `server_now` en la respuesta (`{ is_open, server_now, server_tz }`) para que la UI pueda mostrar "estado según la hora del servidor". Si en el futuro se quiere client-side, aceptar `?at=<iso>` y calcular contra ese timestamp. |
| Performance: 1000 farmacias × 7 días de parseo por request | Trivial. Si crece, cachear por minuto (ver 2.3). |
| `24:00` no es `HH:mm` válido ISO y rompe parsers terceros | Mantener el shape interno pero, al exportar, escribir un campo derivado `is_24h` (booleano) en el JSON público. El shape interno sigue siendo el mismo. |
| i18n incompleta en ca.js | Mantener strings en `es.js` como fallback. Si una clave falta en `ca.js`, el `useTranslation` ya cae al idioma por defecto. |
---
## Archivos a crear / modificar (resumen)
**Crear:**
- `apps/backend/src/hours.js`
- `apps/backend/__tests__/hours.test.js`
- `apps/backend/__tests__/pharmacy-hours-endpoint.test.js`
- `apps/frontend/src/views/PublicView.test.jsx` (o `usePharmacyFilters.test.js` si se extrae el hook)
- `apps/frontend/src/components/admin/PharmacyManagement.test.jsx` (si testing-library está disponible; si no, tests sobre los helpers puros)
**Modificar:**
- `apps/backend/server.js` — enriquecer `pharmacies` en 3 endpoints; importar `src/hours.js`
- `apps/frontend/src/views/PublicView.jsx` — state `openNow`, hook de filtros, botón en `.pharmacy-controls`
- `apps/frontend/src/components/PharmacyList.jsx` — badge 24h, pasar por i18n
- `apps/frontend/src/components/PharmacyMap.jsx` — badge 24h en popup
- `apps/frontend/src/components/admin/PharmacyManagement.jsx` — toggle 24h, importar helpers
- `apps/frontend/src/utils/hours.js` — mover `emptyHoursDraft`/`hoursToDraft`/`draftToHours` desde admin; añadir `isAlwaysOpen`; cambiar `getOpenStatus` para devolver labelKey/params
- `apps/frontend/src/i18n/locales/es.js` — nuevas claves
- `apps/frontend/src/i18n/locales/ca.js` — nuevas claves
**No tocar:**
- `apps/API/opening-hours-osm.js` (parser OSM en producción)
- `apps/backend/farmacias-webhook-import.js` (ya parsea correctamente)
---
## Preguntas abiertas (a resolver antes de implementar)
1. **¿Soportar farmacias que cruzan medianoche?** (ej. `mon ["22:00","02:00"]`). El shape actual lo permite pero `getOpenStatus` actual no lo trata. **Propuesta:** sí, implementar el caso en el helper del backend (tests ya incluidos en 1.1).
2. **¿Borrar `getOpenStatus` del frontend o mantenerlo como fallback?** **Propuesta:** mantenerlo, pero hacer que la UI use `pharmacy.is_open` y `pharmacy.is_24h` cuando estén presentes (del backend) y solo caiga al cálculo local si faltan. Sin duplicación de lógica.
3. **¿Mostrar farmacia sin horarios cuando el filtro "abiertas ahora" está activo?** **Propuesta:** sí, mostrarlas (asumimos que no sabemos = no filtramos). Documentarlo en la UI.
4. **¿Refrescar el estado cada minuto?** (por si la lista queda abierta mucho tiempo y cambia el estado). **Propuesta:** no por ahora; si la lista se refresca al cambiar de medicamento, basta. Si se observa uso prolongado sin refresco, añadir `setInterval` de 60s para recalcular.
Cuando confirmes el plan (o pidas ajustes) lo paso a implementación siguiendo TDD estricto.