Patentes internacionales + Forgot Pwd
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

This commit is contained in:
Antoni Nuñez Romeu
2026-08-18 11:01:54 +02:00
parent 87df61ab15
commit d68463548f
31 changed files with 4134 additions and 3 deletions
+481
View File
@@ -0,0 +1,481 @@
# 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.