feat/parapharmacy-baby-products #31
@@ -20,7 +20,9 @@ VAPID_SUBJECT=mailto:admin@example.com
|
|||||||
# https://expo.dev/accounts/[username]/settings/access-tokens
|
# https://expo.dev/accounts/[username]/settings/access-tokens
|
||||||
EXPO_ACCESS_TOKEN=
|
EXPO_ACCESS_TOKEN=
|
||||||
|
|
||||||
# Open Food Facts (optional, improves rate limits)
|
# Open Food Facts
|
||||||
# Register at: https://world.openfoodfacts.org/
|
# Register at: https://world.openfoodfacts.org/
|
||||||
|
# User-Agent is REQUIRED per OFF docs (format: AppName/Version (ContactEmail))
|
||||||
|
OFF_USER_AGENT=FarmaFinder/1.0 (https://github.com/farmafinder)
|
||||||
OFF_USERNAME=
|
OFF_USERNAME=
|
||||||
OFF_PASSWORD=
|
OFF_PASSWORD=
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import redisClient from './redis-client.js';
|
import redisClient from './redis-client.js';
|
||||||
|
|
||||||
|
// OFF API v3 (recommended) with fallback to v2 for search
|
||||||
const OFF_API_BASE = 'https://world.openfoodfacts.org';
|
const OFF_API_BASE = 'https://world.openfoodfacts.org';
|
||||||
const CACHE_TTL = 3600;
|
const CACHE_TTL = 3600;
|
||||||
const MAX_RETRIES = 2;
|
const MAX_RETRIES = 2;
|
||||||
const RETRY_DELAY_MS = 1000;
|
const RETRY_DELAY_MS = 1000;
|
||||||
|
|
||||||
// Open Food Facts authentication (required for reliable access)
|
// User-Agent is REQUIRED per OFF docs: AppName/Version (ContactEmail)
|
||||||
|
const OFF_USER_AGENT = process.env.OFF_USER_AGENT || 'FarmaFinder/1.0 (https://github.com/farmafinder)';
|
||||||
|
|
||||||
|
// Open Food Facts authentication (optional, for write operations)
|
||||||
const OFF_USERNAME = process.env.OFF_USERNAME;
|
const OFF_USERNAME = process.env.OFF_USERNAME;
|
||||||
const OFF_PASSWORD = process.env.OFF_PASSWORD;
|
const OFF_PASSWORD = process.env.OFF_PASSWORD;
|
||||||
|
|
||||||
|
function getOffHeaders() {
|
||||||
|
return {
|
||||||
|
'User-Agent': OFF_USER_AGENT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getOffAuth() {
|
function getOffAuth() {
|
||||||
if (OFF_USERNAME && OFF_PASSWORD) {
|
if (OFF_USERNAME && OFF_PASSWORD) {
|
||||||
return {
|
return {
|
||||||
@@ -74,18 +84,18 @@ export async function searchBabyProducts(query) {
|
|||||||
|
|
||||||
console.log(`Fetching from OFF API: ${searchTerm}`);
|
console.log(`Fetching from OFF API: ${searchTerm}`);
|
||||||
const auth = getOffAuth();
|
const auth = getOffAuth();
|
||||||
|
const headers = getOffHeaders();
|
||||||
|
|
||||||
let lastError;
|
let lastError;
|
||||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${OFF_API_BASE}/cgi/search.pl`, {
|
// Use v2 structured search with brand filter (more reliable than /cgi/search.pl)
|
||||||
|
const response = await axios.get(`${OFF_API_BASE}/api/v2/search`, {
|
||||||
params: {
|
params: {
|
||||||
search_terms: searchTerm,
|
brands_tags: searchTerm,
|
||||||
json: true,
|
|
||||||
fields: 'product_name,brands,image_url,nutriscore_grade,ingredients_text,nova_group,ecoscore_grade,categories_tags',
|
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
action: 'process',
|
|
||||||
},
|
},
|
||||||
|
headers,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
validateStatus: (status) => status === 200,
|
validateStatus: (status) => status === 200,
|
||||||
...(auth && { auth }),
|
...(auth && { auth }),
|
||||||
@@ -150,7 +160,10 @@ export async function getBabyProductDetails(barcode) {
|
|||||||
|
|
||||||
console.log(`Fetching OFF product details: ${barcode}`);
|
console.log(`Fetching OFF product details: ${barcode}`);
|
||||||
const auth = getOffAuth();
|
const auth = getOffAuth();
|
||||||
const response = await axios.get(`${OFF_API_BASE}/api/v2/product/${barcode}.json`, {
|
const headers = getOffHeaders();
|
||||||
|
// Use v3 API (recommended) for product details
|
||||||
|
const response = await axios.get(`${OFF_API_BASE}/api/v3/product/${barcode}.json`, {
|
||||||
|
headers,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
validateStatus: (status) => status === 200,
|
validateStatus: (status) => status === 200,
|
||||||
...(auth && { auth }),
|
...(auth && { auth }),
|
||||||
|
|||||||
Reference in New Issue
Block a user