# 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 `` 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.