feat/parapharmacy-baby-products #31
@@ -3,6 +3,7 @@ import { jest } from '@jest/globals'
|
||||
jest.unstable_mockModule('../cima-service.js', () => ({
|
||||
searchMedicines: jest.fn(async () => []),
|
||||
getMedicineDetails: jest.fn(async () => null),
|
||||
searchOTC: jest.fn(async () => []),
|
||||
}))
|
||||
|
||||
jest.unstable_mockModule('../farmacias-webhook-import.js', () => ({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,226 @@
|
||||
# Parafarmacia + Productos de Bebé — Design Spec
|
||||
|
||||
**Date:** 2026-07-13
|
||||
**Status:** Approved
|
||||
**Author:** MiMoCode
|
||||
|
||||
## Problem
|
||||
|
||||
FarmaFinder currently only searches medications via the CIMA API. Users need to find parapharmacy products (OTC medications, vitamins, topical treatments) and baby products (formula milk, baby food, cereals) — categories that pharmacies sell but CIMA doesn't cover comprehensively.
|
||||
|
||||
## Goal
|
||||
|
||||
Extend FarmaFinder's search to include:
|
||||
1. **OTC medications** from CIMA (filtered by `cpresc = "Sin Receta"`)
|
||||
2. **Baby products** (formula milk, baby food, cereals) from Open Food Facts API
|
||||
|
||||
Results should appear in a unified search alongside existing prescription medication results.
|
||||
|
||||
## Data Sources
|
||||
|
||||
### CIMA API (existing, extended)
|
||||
|
||||
- **Base URL:** `https://cima.aemps.es/cima/rest`
|
||||
- **No authentication required**
|
||||
- **New filter:** `cpresc=Sin+Receta` to get only OTC products
|
||||
- **Endpoints used:**
|
||||
- `GET /medicamentos?nombre={query}&cpresc=Sin+Receta` — search OTC products
|
||||
- `GET /medicamento/{nregistro}` — get OTC product details
|
||||
- **Rate limits:** None documented; existing 5s timeout per request
|
||||
- **Data fields:** nregistro, nombre, labtitular, cpresc, formaFarmaceutica, vtm, dosis, fotos, docs
|
||||
|
||||
### Open Food Facts API (new)
|
||||
|
||||
- **Base URL:** `https://world.openfoodfacts.org/api/v2`
|
||||
- **No authentication required**
|
||||
- **Endpoints used:**
|
||||
- `GET /search?categories_tags=baby-food&search_terms={query}&json=true` — search baby products
|
||||
- `GET /product/{barcode}.json` — get product details
|
||||
- **Rate limits:** Be polite (< 10 req/s)
|
||||
- **Data fields:** product_name, brands, image_url, nutriscore, ingredients_text, categories_tags
|
||||
- **Product categories to search:**
|
||||
- `baby-foods` — general baby food
|
||||
- `baby-milks` — formula milk
|
||||
- `cereals-for-babies` — baby cereals
|
||||
- `snacks-and-desserts-for-babies` — baby snacks
|
||||
|
||||
## Architecture
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User searches "leche"
|
||||
→ Frontend: GET /api/products/search?q=leche
|
||||
→ Backend orchestrator:
|
||||
1. cimaService.searchOTC('leche') — CIMA OTC search
|
||||
2. offService.searchBaby('leche') — Open Food Facts search
|
||||
3. Merge results with source tag
|
||||
4. Cache in Redis (1h TTL)
|
||||
→ Return unified result list
|
||||
```
|
||||
|
||||
### Unified Product Model
|
||||
|
||||
```typescript
|
||||
interface Product {
|
||||
id: string; // nregistro (CIMA) or _id (OFF)
|
||||
source: 'cima' | 'openfoodfacts';
|
||||
name: string;
|
||||
brand: string; // labtitular (CIMA) or brands (OFF)
|
||||
category: 'otc' | 'baby_food' | 'baby_milk' | 'baby_cereal';
|
||||
image_url: string | null; // fotos[0].url (CIMA) or image_url (OFF)
|
||||
|
||||
// CIMA-specific fields
|
||||
active_ingredient?: string; // vtm.nombre
|
||||
dosage?: string; // dosis
|
||||
form?: string; // formaFarmaceutica.nombre
|
||||
prescription?: string; // cpresc
|
||||
commercialized?: boolean; // comerc
|
||||
photos?: string[];
|
||||
docs?: { tipo: number; url: string }[];
|
||||
|
||||
// OFF-specific fields
|
||||
nutriscore?: string; // nutriscore_grade
|
||||
ingredients?: string; // ingredients_text
|
||||
nova_group?: number; // nova_group
|
||||
eco_score?: string; // ecoscore_grade
|
||||
}
|
||||
```
|
||||
|
||||
## Backend Changes
|
||||
|
||||
### 1. New file: `apps/backend/off-service.js`
|
||||
|
||||
Open Food Facts API client:
|
||||
|
||||
```javascript
|
||||
const OFF_API_BASE = 'https://world.openfoodfacts.org/api/v2';
|
||||
|
||||
async function searchBabyProducts(query) {
|
||||
// Search in baby-food categories
|
||||
const url = `${OFF_API_BASE}/search?categories_tags=baby-food&search_terms=${encodeURIComponent(query)}&json=true&fields=product_name,brands,image_url,nutriscore_grade,ingredients_text,nova_group,ecoscore_grade,categories_tags&page_size=20`;
|
||||
// Cache key: off:baby:{query}, TTL: 1 hour
|
||||
// Transform to unified Product model
|
||||
}
|
||||
|
||||
async function getBabyProductDetails(barcode) {
|
||||
const url = `${OFF_API_BASE}/product/${barcode}.json`;
|
||||
// Cache key: off:product:{barcode}, TTL: 24 hours
|
||||
// Transform to unified Product model
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Modify: `apps/backend/cima-service.js`
|
||||
|
||||
Add OTC-specific search function:
|
||||
|
||||
```javascript
|
||||
async function searchOTC(query) {
|
||||
// Same as searchMedicines but adds cpresc=Sin+Receta filter
|
||||
// Cache key: cima:otc:{query}, TTL: 1 hour
|
||||
// Transform to unified Product model (add source: 'cima', category: 'otc')
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Modify: `apps/backend/server.js`
|
||||
|
||||
New unified search route:
|
||||
|
||||
```javascript
|
||||
app.get('/api/products/search', async (req, res) => {
|
||||
const { q } = req.query;
|
||||
// Launch parallel searches:
|
||||
const [cimaResults, offResults] = await Promise.allSettled([
|
||||
searchOTC(q),
|
||||
searchBabyProducts(q)
|
||||
]);
|
||||
// Merge, deduplicate, sort by relevance
|
||||
// Return unified results
|
||||
});
|
||||
|
||||
app.get('/api/products/:source/:id', async (req, res) => {
|
||||
const { source, id } = req.params;
|
||||
if (source === 'cima') return getMedicineDetails(id);
|
||||
if (source === 'openfoodfacts') return getBabyProductDetails(id);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Redis Cache Strategy
|
||||
|
||||
| Key Pattern | TTL | Source |
|
||||
|-------------|-----|--------|
|
||||
| `products:search:{query}` | 1 hour | Merged results |
|
||||
| `cima:otc:{query}` | 1 hour | CIMA OTC only |
|
||||
| `off:baby:{query}` | 1 hour | OFF baby only |
|
||||
| `off:product:{barcode}` | 24 hours | OFF product detail |
|
||||
|
||||
## Frontend Changes
|
||||
|
||||
### 1. New component: `ProductResults.jsx`
|
||||
|
||||
Displays unified search results with:
|
||||
- Product card with image, name, brand
|
||||
- Source badge: "CIMA" or "Open Food Facts"
|
||||
- Category badge: "OTC", "Baby Food", "Formula"
|
||||
- Click navigates to `/product/{source}/{id}`
|
||||
|
||||
### 2. Modify: `PublicView.jsx`
|
||||
|
||||
- Update search to call `/api/products/search?q=`
|
||||
- Show unified results alongside existing medicine results
|
||||
- Add category filter tabs: "All" | "Medications" | "OTC" | "Baby"
|
||||
|
||||
### 3. Modify: `MedicineResults.jsx`
|
||||
|
||||
- Accept mixed product types
|
||||
- Conditionally render fields based on `source`
|
||||
- Show CIMA-specific fields (active ingredient, dosage) for CIMA products
|
||||
- Show OFF-specific fields (nutriscore, ingredients) for OFF products
|
||||
|
||||
### 4. New route: `/product/:source/:id`
|
||||
|
||||
- Detail page for any product type
|
||||
- CIMA products: show full medication info + pharmacies
|
||||
- OFF products: show nutritional info + "Available at pharmacies" section
|
||||
|
||||
## Mobile Changes
|
||||
|
||||
### 1. `apps/frontend-mobile/services/products.ts`
|
||||
|
||||
```typescript
|
||||
export async function searchProducts(query: string): Promise<Product[]> {
|
||||
const { data } = await api.get('/products/search', { params: { q: query } });
|
||||
return data.results;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `apps/frontend-mobile/app/(tabs)/search.tsx`
|
||||
|
||||
- Add product search alongside medicine search
|
||||
- Display unified results with source/category badges
|
||||
|
||||
### 3. `apps/frontend-mobile/app/product/[source]/[id].tsx`
|
||||
|
||||
- New detail screen for non-medication products
|
||||
- Adapt layout based on product source
|
||||
|
||||
## Error Handling
|
||||
|
||||
- CIMA API down: return OFF results only (graceful degradation)
|
||||
- OFF API down: return CIMA results only
|
||||
- Both down: return cached results if available, else empty
|
||||
- OFF rate limit: implement 100ms delay between requests
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Unit tests:** off-service.js search/transform functions
|
||||
2. **Integration tests:** `/api/products/search` endpoint with mocked APIs
|
||||
3. **Manual test:** Search "leche" and verify both CIMA OTC + OFF baby results appear
|
||||
4. **Edge cases:** Empty results, API timeout, malformed OFF responses
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Homeopathy (not reliably available in either API)
|
||||
- Commercial parapharmacy APIs (paid, not needed for OTC + baby)
|
||||
- Product price/stock for OFF products (pharmacies don't stock these in the current model)
|
||||
- User reviews/ratings for products
|
||||
Reference in New Issue
Block a user