From 229e1d81066e9e661ecde011ae5e3b8d4b51c817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Mon, 13 Jul 2026 15:23:43 +0200 Subject: [PATCH] Add unified product search routes (CIMA OTC + Open Food Facts) - GET /api/products/search?q={query} - parallel search across both sources - GET /api/products/:source/:id - product detail by source (cima/openfoodfacts) - Uses Promise.allSettled for fault-tolerant parallel search - Added imports for searchOTC and OFF service functions --- apps/backend/server.js | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/backend/server.js b/apps/backend/server.js index 19d4270..5136f0b 100644 --- a/apps/backend/server.js +++ b/apps/backend/server.js @@ -19,7 +19,8 @@ import pinoHttp from 'pino-http'; import multer from 'multer'; import { createWorker } from 'tesseract.js'; import { BarcodeDetector } from 'barcode-detector'; -import { searchMedicines, getMedicineDetails } from './cima-service.js'; +import { searchMedicines, getMedicineDetails, searchOTC } from './cima-service.js'; +import { searchBabyProducts, getBabyProductDetails } from './off-service.js'; import { runFarmaciaWebhookImport, DEFAULT_FARMACIAS_WEBHOOK, importPharmaciesFromRows } from './farmacias-webhook-import.js'; import { fetchPharmaciesExternal } from '../API/index.js'; @@ -598,6 +599,54 @@ app.get('/api/medicines/:medicineId', async (req, res) => { } }); +// ========== UNIFIED PRODUCT SEARCH ========== + +app.get('/api/products/search', async (req, res) => { + try { + const { q } = req.query; + if (!q || q.trim().length < 2) { + return res.json({ results: [], total: 0 }); + } + const searchTerm = q.trim(); + const [cimaResults, offResults] = await Promise.allSettled([ + searchOTC(searchTerm), + searchBabyProducts(searchTerm) + ]); + const cimaProducts = cimaResults.status === 'fulfilled' ? cimaResults.value : []; + const offProducts = offResults.status === 'fulfilled' ? offResults.value : []; + const allProducts = [...cimaProducts, ...offProducts] + .sort((a, b) => a.name.localeCompare(b.name)); + res.json({ + results: allProducts, + total: allProducts.length, + sources: { cima: cimaProducts.length, openfoodfacts: offProducts.length } + }); + } catch (err) { + console.error('[Products] Search error:', err); + res.status(500).json({ error: 'Error searching products' }); + } +}); + +app.get('/api/products/:source/:id', async (req, res) => { + try { + const { source, id } = req.params; + if (source === 'cima') { + const product = await getMedicineDetails(id); + if (!product) return res.status(404).json({ error: 'Product not found' }); + return res.json({ ...product, source: 'cima', category: 'otc' }); + } + if (source === 'openfoodfacts') { + const product = await getBabyProductDetails(id); + if (!product) return res.status(404).json({ error: 'Product not found' }); + return res.json(product); + } + res.status(400).json({ error: 'Invalid source' }); + } catch (err) { + console.error('[Products] Detail error:', err); + res.status(500).json({ error: 'Error fetching product details' }); + } +}); + // ========== AUTHENTICATION MIDDLEWARE ========== // Middleware to check if user is authenticated