Files
FarmaFinder/Patentes/MEMORIA_TECNICA.md
T
Antoni Nuñez Romeu 2c1b3cfca6
Build & Push Docker Images / test-backend (push) Successful in 24s
Build & Push Docker Images / test-frontend (push) Successful in 23s
Build & Push Docker Images / build-backend (push) Successful in 2m22s
Build & Push Docker Images / build-frontend (push) Successful in 57s
Documentos + Homepage rediseñada
2026-06-26 16:59:20 +02:00

710 lines
19 KiB
Markdown

# Memoria tecnica FarmaFinder
## 1. Resumen ejecutivo
FarmaFinder es una aplicacion web/PWA para buscar medicamentos en la base oficial de CIMA (AEMPS), localizar farmacias que los comercializan y administrar el catalogo de farmacias y sus relaciones con medicamentos. La aplicacion tambien incluye perfil de usuario con ubicacion guardada, geocodificacion, ordenacion por distancia, estados de apertura de farmacias, notificaciones push para medicamentos y un panel de administracion para altas/bajas/importacion de farmacias.
La base funcional actual esta repartida en cuatro capas principales:
- `frontend/`: interfaz React + Vite, tambien preparada para PWA y empaquetado Capacitor.
- `backend/`: API Express con sesion, autenticacion, persistencia SQLite/PG y notificaciones push.
- `API/`: helpers de importacion y normalizacion de farmacias desde fuentes externas.
- `android/` e `ios/`: shells nativos generados por Capacitor.
## 2. Stack tecnologico
### Frontend
- React 18
- Vite 5
- Leaflet + React Leaflet para el mapa
- Vitest + Testing Library + JSDOM para tests de UI
- PWA con `vite-plugin-pwa`
- Capacitor 6 para envoltorio nativo
### Backend
- Node.js en modo ESM
- Express 4
- SQLite como almacenamiento base local
- PostgreSQL opcional para usuarios y sesiones cuando `PG_URL` esta definido
- Redis para cache de CIMA
- `express-session` para sesion de usuario
- `bcrypt` para hash de contrasenas
- `web-push` para notificaciones push web
- `express-rate-limit` para proteccion de endpoints sensibles
### Integraciones externas
- CIMA API de la AEMPS para busqueda y detalle de medicamentos
- Nominatim / OpenStreetMap para geocodificacion
- OpenStreetMap Overpass u otras fuentes JSON para importacion de farmacias
- Webhooks externos tipo n8n para importacion de farmacias
## 3. Estructura del repositorio
- `frontend/`: SPA principal
- `backend/`: API principal y scripts de mantenimiento
- `API/`: libreria de importacion de farmacias desde fuentes externas
- `scraper/`: utilidades de scraping con Puppeteer
- `android/` y `ios/`: proyectos nativos Capacitor
- `docker-compose.yml`: entorno completo con backend, frontend, Redis y PostgreSQL
## 4. Arquitectura funcional
### Flujo publico
1. El usuario escribe un medicamento en el buscador.
2. El frontend llama a `GET /api/medicines/search`.
3. El backend consulta CIMA, filtra resultados y los cachea en Redis.
4. Al seleccionar un medicamento, el frontend llama a `GET /api/medicines/:id/pharmacies`.
5. El backend devuelve farmacias enlazadas en SQLite/PG con precio, stock, coordenadas y horario.
6. La UI permite ordenar por distancia si el usuario tiene ubicacion guardada o concede geolocalizacion.
### Flujo de administracion
1. Un admin inicia sesion.
2. El panel permite gestionar farmacias, buscar medicamentos en CIMA e importar farmacias externas.
3. El admin enlaza medicamentos a farmacias con precio y stock.
4. Al crear un nuevo enlace de farmacia-medicamento, el backend dispara notificaciones push a usuarios suscritos.
### Flujo de perfil y ubicacion
1. Un usuario autenticado puede editar su perfil.
2. Puede guardar direccion y coordenadas manualmente.
3. Tambien puede geocodificar su direccion via Nominatim o usar la geolocalizacion del navegador.
4. Esa ubicacion se usa en el ordenamiento por distancia en la vista publica.
## 5. Frontend
### Punto de entrada
- `frontend/src/main.jsx` monta `<App />` en `#root`.
- Se inicializa el shell nativo con `initNativeShell()`.
### Componente raiz
`frontend/src/App.jsx` controla tres vistas:
- `public`
- `admin`
- `profile`
Tambien gestiona:
- comprobacion de sesion con `GET /api/auth/check`
- apertura/cierre del modal de login
- cierre de sesion con `POST /api/auth/logout`
- panel de notificaciones guardadas
### Vistas principales
#### PublicView
`frontend/src/views/PublicView.jsx`
Responsabilidades:
- busqueda de medicamentos con debounce
- consulta de farmacias asociadas al medicamento
- mapa de farmacias
- lista de farmacias con precio, stock, horario y notificaciones
- ordenacion por distancia usando ubicacion guardada o geolocalizacion del navegador
Caracteristicas clave:
- Si el usuario tiene coordenadas en el perfil, se priorizan sobre la geolocalizacion del navegador.
- La lista y el mapa usan la misma ordenacion.
- Si no hay coordenadas o el navegador falla, se muestra un mensaje de error contextual.
#### AdminView
`frontend/src/views/AdminView.jsx`
Responsabilidades:
- autenticar acceso al panel
- mostrar tabs de:
- farmacias
- medicamentos
- enlace farmacia-medicamento
- logout de admin
#### ProfileView
`frontend/src/views/ProfileView.jsx`
Responsabilidades:
- editar direccion y coordenadas del usuario
- geocodificar la direccion con `/api/geocode`
- obtener ubicacion actual del dispositivo
- abrir la vista de notificaciones guardadas
- cerrar sesion
## 6. Componentes de UI
### Busqueda y resultados
- `frontend/src/components/SearchBar.jsx`
- `frontend/src/components/MedicineResults.jsx`
- `frontend/src/components/PharmacyList.jsx`
- `frontend/src/components/PharmacyMap.jsx`
#### MedicineResults
- Lista resultados de CIMA.
- Cada card incluye boton de notificaciones para ese medicamento.
- Al pulsar la card se abre el detalle de farmacias asociadas.
#### PharmacyList
- Muestra farmacias enlazadas al medicamento.
- Incluye:
- distancia al usuario si existe ubicacion
- estado de apertura calculado a partir de `opening_hours`
- direccion
- telefono
- precio
- stock
- boton de notificacion por farmacia cuando el stock esta agotado
#### PharmacyMap
- Muestra farmacias sobre Leaflet.
- Usa coordenadas de cada farmacia para ubicarlas en el mapa.
### Admin components
- `frontend/src/components/admin/PharmacyManagement.jsx`
- `frontend/src/components/admin/MedicineManagement.jsx`
- `frontend/src/components/admin/PharmacyMedicineLink.jsx`
- `frontend/src/components/admin/LoginForm.jsx`
#### PharmacyManagement
- Lista farmacias existentes.
- Permite crear, editar y borrar farmacias.
- Integra importacion por:
- webhook externo
- OpenStreetMap / Overpass
- URL de open data JSON/GeoJSON
- Permite filtrar farmacias por radio geografico.
- Permite geocodificar una ciudad para obtener lat/lon/radio de trabajo.
- Gestiona horario semanal estructurado.
#### MedicineManagement
- Busca medicamentos en CIMA desde el panel admin.
- Solo sirve para exploracion y seleccion previa al enlace.
#### PharmacyMedicineLink
- Permite seleccionar farmacia y medicamento.
- Crea o actualiza el enlace con precio y stock.
- Permite editar y eliminar relaciones ya existentes.
## 7. Utilidades de frontend
### Geolocalizacion
`frontend/src/utils/geo.js`
- `haversineKm()`: calcula distancia entre dos puntos GPS.
- `getUserPosition()`: obtiene ubicacion del navegador con validaciones de contexto seguro.
- `formatDistance()`: da formato a metros/kilometros.
### Horarios
`frontend/src/utils/hours.js`
- `getOpenStatus()`: interpreta el horario semanal y devuelve un estado legible.
- Soporta:
- abierto
- cerrado
- abre a una hora concreta
- abre el siguiente dia habil
- El formato almacenado es JSON con claves `sun`, `mon`, `tue`, `wed`, `thu`, `fri`, `sat`.
### Notificaciones
`frontend/src/utils/notifications.js`
- comprueba soporte de Push API y Service Worker
- obtiene la VAPID public key del backend
- registra suscripciones push del navegador
- guarda una huella local en `localStorage`
- permite suscripcion global por medicamento o por medicamento + farmacia
### Service worker
`frontend/src/sw.js`
- precache de assets PWA
- muestra notificaciones al recibir eventos `push`
- navega a la URL asociada al click en la notificacion
## 8. Backend
### Arranque
`backend/server.js` es el nucleo de la API. Expone:
- la aplicacion Express
- `initDatabase()`
- la conexion `db`
Si `NODE_ENV !== test`, inicializa DB y levanta el servidor en `PORT` o `3001`.
### Configuracion basica
- CORS con credenciales
- `express.json()`
- session middleware
- rate limiting en:
- busqueda
- login
- registro
- geocoding
### Persistencia
#### SQLite
Se usa como base local principal:
- farmacias
- relaciones farmacia-medicamento
- subscripciones push
- fallback de usuarios cuando no hay PostgreSQL
#### PostgreSQL opcional
Si existe `PG_URL`, la tabla `users` y el store de sesiones se montan sobre PostgreSQL.
Esto genera una arquitectura mixta:
- SQLite para catalogo/relaciones
- PostgreSQL para usuarios y sesiones, si se configura
### Capa de sesion
- `express-session`
- store SQLite o PostgreSQL segun entorno
- cookie httpOnly con maxAge de 24h
- `trust proxy` configurable con `TRUST_PROXY`
## 9. Modelo de datos
### `pharmacies`
Campos:
- `id`
- `name`
- `address`
- `phone`
- `latitude`
- `longitude`
- `opening_hours`
### `pharmacy_medicines`
Relacion farmacia-medicamento.
Campos:
- `id`
- `pharmacy_id`
- `medicine_nregistro`
- `medicine_name`
- `price`
- `stock`
Restriccion:
- `UNIQUE(pharmacy_id, medicine_nregistro)`
### `users`
Campos actuales:
- `id`
- `username`
- `password_hash`
- `is_admin`
- `address`
- `latitude`
- `longitude`
- `created_at`
### `push_subscriptions`
Subscripciones push globales por medicamento.
Campos:
- `id`
- `medicine_nregistro`
- `medicine_name`
- `endpoint`
- `p256dh`
- `auth`
- `user_id`
- `created_at`
### `push_subscriptions_pharmacy`
Subscripciones push por medicamento y farmacia.
Campos:
- `id`
- `medicine_nregistro`
- `medicine_name`
- `pharmacy_id`
- `endpoint`
- `p256dh`
- `auth`
- `user_id`
- `created_at`
## 10. API backend
### Medicamentos
- `GET /api/medicines/search?q=...`
- busca en CIMA con cache Redis
- devuelve una lista normalizada
- `GET /api/medicines/:medicineId`
- devuelve detalle de un medicamento por `nregistro`
- `GET /api/medicines/:medicineId/pharmacies`
- devuelve farmacias que tienen ese medicamento
### Farmacias
- `GET /api/pharmacies`
- lista completa de farmacias
### Autenticacion
- `POST /api/auth/login`
- `POST /api/auth/register`
- `POST /api/auth/logout`
- `GET /api/auth/check`
### Perfil
- `GET /api/users/me`
- `PUT /api/users/me`
### Geocoding
- `GET /api/geocode?q=...`
- autenticado
- usa Nominatim
- devuelve lat/lon y texto de resultado
- `GET /api/admin/geocode?q=...`
- solo admin
- ademas calcula radio sugerido
### Admin: farmacias
- `POST /api/admin/pharmacies`
- `PUT /api/admin/pharmacies/:id`
- `DELETE /api/admin/pharmacies/:id`
- `POST /api/admin/pharmacies/import-webhook`
- `POST /api/admin/pharmacies/import-external`
### Admin: medicamentos y relaciones
- `GET /api/admin/medicines`
- `GET /api/admin/pharmacies/:pharmacyId/medicines`
- `POST /api/admin/pharmacy-medicines`
- `PUT /api/admin/pharmacy-medicines/:id`
- `DELETE /api/admin/pharmacy-medicines/:id`
### Notificaciones
- `GET /api/notifications/vapid-public-key`
- `POST /api/notifications/subscribe`
- `DELETE /api/notifications/unsubscribe`
- `GET /api/notifications/mine`
- `DELETE /api/notifications/mine`
## 11. Capa de importacion externa
La carpeta `API/` no expone un servidor HTTP propio, sino funciones reutilizables para importar farmacias desde distintas fuentes.
### `API/index.js`
- `fetchPharmaciesExternal(opts)`
- `source = 'osm'`
- `source = 'openData'`
- decide el extractor adecuado segun la fuente
### `API/normalize.js`
- `buildAddressFromOsmTags()`
- `osmElementToPharmacy()`
- normaliza nodos Overpass a un formato comun de farmacia
- parsea horarios OSM si existen
### `API/opening-hours-osm.js`
- parser de un subconjunto de `opening_hours`
- soporta:
- `24/7`
- rangos semanales
- dias multiples
- `off` / `closed`
- franjas partidas resumidas
### `backend/farmacias-webhook-import.js`
- importa datos desde un webhook HTTP
- normaliza campos en espanol o ingles
- acepta ubicacion en distintas variantes
- extrae horarios si vienen en formato OSM
- inserta o salta duplicados con estadisticas
## 12. CIMA y cache
`backend/cima-service.js` implementa la integracion con CIMA.
### Busqueda
- consulta `https://cima.aemps.es/cima/rest/medicamentos`
- usa el parametro `nombre`
- normaliza el resultado a campos internos:
- `id`
- `nregistro`
- `name`
- `active_ingredient`
- `dosage`
- `form`
- `formSimplified`
- `laboratory`
- `prescription`
- `commercialized`
- `generic`
- `photos`
- `docs`
### Detalle
- consulta `GET /medicamento/:nregistro`
- cachea por mas tiempo que la busqueda
### Cache
- Redis guarda resultados de busqueda y detalle
- si CIMA falla, se intenta devolver cache obsoleto si existe
## 13. Notificaciones push
### Estado actual
La app ya soporta notificaciones push web completas.
### Backend
- usa VAPID si `VAPID_PUBLIC_KEY` y `VAPID_PRIVATE_KEY` existen
- si faltan, push queda desactivado con respuesta `503`
- al crear una relacion nueva farmacia-medicamento, se ejecuta `sendPushForMedicine()`
- se deduplican endpoints para evitar notificacion doble global + farmacia
- si el endpoint devuelve `404` o `410`, la suscripcion se limpia
### Frontend
- permite suscripcion global por medicamento
- permite suscripcion por medicamento + farmacia
- el icono de campana refleja el estado local
- la vista `Saved notifications` permite ver y borrar suscripciones guardadas del usuario
## 14. PWA y capa nativa
### PWA
- existe `frontend/src/sw.js`
- hay assets publicos de icono
- la app esta pensada para funcionar como PWA instalable
### Capacitor
- `capacitor.config.json`
- `android/`
- `ios/`
El proyecto esta preparado para empaquetarse como app nativa. La documentacion de `NATIVE.md` indica que no hay una base de codigo separada para Android e iOS; la UI real vive en `frontend/`.
## 15. Scripts y comandos
### Raiz
- `npm run dev`: backend + frontend en paralelo
- `npm run start`: backend + preview frontend
- `npm run install:all`: instala dependencias raiz, backend y frontend
- `npm run build:web`: build de frontend
- `npm run cap:sync`: build web + sync Capacitor
### Backend
- `npm start`
- `npm run dev`
- `npm run seed`
- `npm run create-admin`
- `npm run migrate`
- `npm run import-farmacias`
- `npm test`
### Frontend
- `npm run dev`
- `npm run build`
- `npm run preview`
- `npm run test`
## 16. Docker
`docker-compose.yml` levanta:
- `redis`
- `postgres`
- `backend`
- `frontend`
Puertos:
- frontend: `3000`
- backend: `3001`
Persistencia:
- `backend_data` para la DB SQLite del backend
- `postgres_data` para PostgreSQL
## 17. Variables de entorno
### Backend
- `PORT`
- `SESSION_SECRET`
- `CORS_ORIGIN`
- `FARMACIAS_WEBHOOK_URL`
- `REDIS_HOST`
- `REDIS_PORT`
- `REDIS_PASSWORD`
- `DATABASE_PATH`
- `PG_URL`
- `TRUST_PROXY`
- `VAPID_PUBLIC_KEY`
- `VAPID_PRIVATE_KEY`
- `VAPID_SUBJECT`
- `NODE_ENV`
### App
- en produccion, el frontend debe poder resolver la URL del backend segun despliegue
- en native shell, el manual `NATIVE.md` indica que conviene usar URL absoluta o servir el PWA desplegado
## 18. Seeds, migraciones y administracion
### `backend/seed.js`
- crea tablas basicas
- inserta farmacias y medicamentos de ejemplo
- genera relaciones ficticias usando `EXAMPLE_...`
- actualmente es util para demos, no como dato real
### `backend/create-admin.js`
- crea usuario admin en SQLite o PostgreSQL segun `PG_URL`
- por defecto:
- usuario: `admin`
- password: `admin123`
### `backend/migrate.js`
- migracion para el cambio historico de `medicine_id` a `medicine_nregistro`
- la tabla `medicines` antigua se conserva como referencia si existe
## 19. Tests
### Backend
- `backend/__tests__/server.test.js`
- valida busqueda vacia
- valida login incorrecto
- valida proteccion de rutas admin
- valida creacion de farmacia autenticado
- `backend/__tests__/opening-hours-osm.test.js`
- cubre parser de horarios OSM
### Frontend
- existe configuracion de Vitest
- hay `frontend/src/App.test.jsx`
- la cobertura visible en el repo parece enfocada a smoke tests y utilidades
## 20. Observaciones tecnicas y limites actuales
1. El backend mezcla SQLite y PostgreSQL segun entorno. Esto funciona, pero introduce dos rutas de persistencia para usuarios y sesiones.
2. Los medicamentos no se guardan localmente como catalogo propio; se consumen desde CIMA en tiempo real y se cachean en Redis.
3. Los enlaces farmacia-medicamento dependen de `nregistro` de CIMA, no de un ID interno de medicamentos.
4. Las notificaciones push son web push; en shells nativos siguen la logica del navegador/PWA, no una integracion FCM/APNs independiente.
5. La importacion de farmacias depende de la calidad de la fuente externa y de la normalizacion de campos.
6. El seed actual contiene datos de ejemplo y relaciones ficticias; no representa datos de produccion.
## 21. Conclusiones
El estado actual de FarmaFinder es el de una plataforma funcional orientada a consulta y administracion de farmacias y medicamentos, con un backend ya bastante consolidado: autentica usuarios, controla roles, persiste relaciones, geocodifica, importa fuentes externas, cachea CIMA y dispara push notifications. La parte de frontend acompana ese backend con un flujo publico de busqueda, un perfil con ubicacion y un panel admin operativo.
Si quieres, el siguiente paso puede ser convertir esta memoria en una version formal de entrega, con estilo academico o empresarial, o bien en un documento mas tecnico con diagramas de arquitectura y secuencia.
## 22. Next steps propuestos
El siguiente paso funcional natural para FarmaFinder seria incorporar el escaneo de la TSI, de forma que el usuario pueda identificar rapidamente su perfil asistencial y, a partir de ahi, cruzar su necesidad con el stock real disponible en farmacias.
### Objetivo
- Capturar la informacion de la tarjeta sanitaria individual mediante escaneo.
- Asociar esa informacion al perfil del usuario de manera segura.
- Recuperar o inferir los medicamentos relevantes para el usuario.
- Contrastar esos medicamentos con el stock de farmacias cercanas.
- Permitir actuar si no hay stock disponible, por ejemplo:
- reservar una farmacia concreta
- generar una solicitud de pedido
- iniciar un flujo de aviso o sustitucion
### Flujo funcional propuesto
1. El usuario abre el escaner de TSI desde la app.
2. La app lee la tarjeta y extrae los identificadores necesarios.
3. El backend valida la informacion y la vincula al usuario autenticado.
4. Se obtiene la lista de medicamentos asociados o derivados del contexto del usuario.
5. El sistema cruza esa lista con:
- farmacias cercanas
- stock disponible
- precio
- horario
6. Si no existe stock suficiente, la app muestra alternativas y acciones posibles.
### Consideraciones tecnicas
- Esta funcionalidad requiere definir el formato exacto de lectura de la TSI y las fuentes de datos sanitarias a consultar.
- Debe tratarse como una funcionalidad sensible, con control de acceso estricto y minimizacion de datos.
- El escaneo deberia integrarse con el mismo modelo de ubicacion, autenticacion y notificaciones que ya existe.
- La logica de contraste stock/medicamento encaja bien con el modelo actual de `pharmacy_medicines`, pero probablemente exigiria ampliar la capa de negocio para soportar reservas, pedidos o estados intermedios.
### Riesgos y dependencias
- Dependencia de la integracion real con sistemas sanitarios externos.
- Posible necesidad de cumplimiento legal adicional por el tipo de dato tratado.
- Definicion de si el sistema solo informa de disponibilidad o si tambien permite operar pedidos/reservas.
- Necesidad de auditar trazabilidad, consentimiento y control de uso de datos.
### Resultado esperado
Esta ampliacion convertiria FarmaFinder de un buscador de disponibilidad a un asistente operativo de acceso al medicamento, capaz de ayudar al usuario a pasar de la consulta del stock a la accion cuando no exista disponibilidad inmediata.