19 KiB
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/eios/: 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_URLesta definido - Redis para cache de CIMA
express-sessionpara sesion de usuariobcryptpara hash de contrasenasweb-pushpara notificaciones push webexpress-rate-limitpara 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 principalbackend/: API principal y scripts de mantenimientoAPI/: libreria de importacion de farmacias desde fuentes externasscraper/: utilidades de scraping con Puppeteerandroid/yios/: proyectos nativos Capacitordocker-compose.yml: entorno completo con backend, frontend, Redis y PostgreSQL
4. Arquitectura funcional
Flujo publico
- El usuario escribe un medicamento en el buscador.
- El frontend llama a
GET /api/medicines/search. - El backend consulta CIMA, filtra resultados y los cachea en Redis.
- Al seleccionar un medicamento, el frontend llama a
GET /api/medicines/:id/pharmacies. - El backend devuelve farmacias enlazadas en SQLite/PG con precio, stock, coordenadas y horario.
- La UI permite ordenar por distancia si el usuario tiene ubicacion guardada o concede geolocalizacion.
Flujo de administracion
- Un admin inicia sesion.
- El panel permite gestionar farmacias, buscar medicamentos en CIMA e importar farmacias externas.
- El admin enlaza medicamentos a farmacias con precio y stock.
- Al crear un nuevo enlace de farmacia-medicamento, el backend dispara notificaciones push a usuarios suscritos.
Flujo de perfil y ubicacion
- Un usuario autenticado puede editar su perfil.
- Puede guardar direccion y coordenadas manualmente.
- Tambien puede geocodificar su direccion via Nominatim o usar la geolocalizacion del navegador.
- Esa ubicacion se usa en el ordenamiento por distancia en la vista publica.
5. Frontend
Punto de entrada
frontend/src/main.jsxmonta<App />en#root.- Se inicializa el shell nativo con
initNativeShell().
Componente raiz
frontend/src/App.jsx controla tres vistas:
publicadminprofile
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.jsxfrontend/src/components/MedicineResults.jsxfrontend/src/components/PharmacyList.jsxfrontend/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.jsxfrontend/src/components/admin/MedicineManagement.jsxfrontend/src/components/admin/PharmacyMedicineLink.jsxfrontend/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 proxyconfigurable conTRUST_PROXY
9. Modelo de datos
pharmacies
Campos:
idnameaddressphonelatitudelongitudeopening_hours
pharmacy_medicines
Relacion farmacia-medicamento.
Campos:
idpharmacy_idmedicine_nregistromedicine_namepricestock
Restriccion:
UNIQUE(pharmacy_id, medicine_nregistro)
users
Campos actuales:
idusernamepassword_hashis_adminaddresslatitudelongitudecreated_at
push_subscriptions
Subscripciones push globales por medicamento.
Campos:
idmedicine_nregistromedicine_nameendpointp256dhauthuser_idcreated_at
push_subscriptions_pharmacy
Subscripciones push por medicamento y farmacia.
Campos:
idmedicine_nregistromedicine_namepharmacy_idendpointp256dhauthuser_idcreated_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
- devuelve detalle de un medicamento por
GET /api/medicines/:medicineId/pharmacies- devuelve farmacias que tienen ese medicamento
Farmacias
GET /api/pharmacies- lista completa de farmacias
Autenticacion
POST /api/auth/loginPOST /api/auth/registerPOST /api/auth/logoutGET /api/auth/check
Perfil
GET /api/users/mePUT /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/pharmaciesPUT /api/admin/pharmacies/:idDELETE /api/admin/pharmacies/:idPOST /api/admin/pharmacies/import-webhookPOST /api/admin/pharmacies/import-external
Admin: medicamentos y relaciones
GET /api/admin/medicinesGET /api/admin/pharmacies/:pharmacyId/medicinesPOST /api/admin/pharmacy-medicinesPUT /api/admin/pharmacy-medicines/:idDELETE /api/admin/pharmacy-medicines/:id
Notificaciones
GET /api/notifications/vapid-public-keyPOST /api/notifications/subscribeDELETE /api/notifications/unsubscribeGET /api/notifications/mineDELETE /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:
idnregistronameactive_ingredientdosageformformSimplifiedlaboratoryprescriptioncommercializedgenericphotosdocs
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_KEYyVAPID_PRIVATE_KEYexisten - 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
404o410, 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 notificationspermite 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.jsonandroid/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 paralelonpm run start: backend + preview frontendnpm run install:all: instala dependencias raiz, backend y frontendnpm run build:web: build de frontendnpm run cap:sync: build web + sync Capacitor
Backend
npm startnpm run devnpm run seednpm run create-adminnpm run migratenpm run import-farmaciasnpm test
Frontend
npm run devnpm run buildnpm run previewnpm run test
16. Docker
docker-compose.yml levanta:
redispostgresbackendfrontend
Puertos:
- frontend:
3000 - backend:
3001
Persistencia:
backend_datapara la DB SQLite del backendpostgres_datapara PostgreSQL
17. Variables de entorno
Backend
PORTSESSION_SECRETCORS_ORIGINFARMACIAS_WEBHOOK_URLREDIS_HOSTREDIS_PORTREDIS_PASSWORDDATABASE_PATHPG_URLTRUST_PROXYVAPID_PUBLIC_KEYVAPID_PRIVATE_KEYVAPID_SUBJECTNODE_ENV
App
- en produccion, el frontend debe poder resolver la URL del backend segun despliegue
- en native shell, el manual
NATIVE.mdindica 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
- usuario:
backend/migrate.js
- migracion para el cambio historico de
medicine_idamedicine_nregistro - la tabla
medicinesantigua 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
- El backend mezcla SQLite y PostgreSQL segun entorno. Esto funciona, pero introduce dos rutas de persistencia para usuarios y sesiones.
- Los medicamentos no se guardan localmente como catalogo propio; se consumen desde CIMA en tiempo real y se cachean en Redis.
- Los enlaces farmacia-medicamento dependen de
nregistrode CIMA, no de un ID interno de medicamentos. - Las notificaciones push son web push; en shells nativos siguen la logica del navegador/PWA, no una integracion FCM/APNs independiente.
- La importacion de farmacias depende de la calidad de la fuente externa y de la normalizacion de campos.
- 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
- El usuario abre el escaner de TSI desde la app.
- La app lee la tarjeta y extrae los identificadores necesarios.
- El backend valida la informacion y la vincula al usuario autenticado.
- Se obtiene la lista de medicamentos asociados o derivados del contexto del usuario.
- El sistema cruza esa lista con:
- farmacias cercanas
- stock disponible
- precio
- horario
- 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.