"""OpenFoodFacts proxy (spec §3.5). All OFF calls go through this router. OFF etiquette (spec §8.1 rule 10): descriptive User-Agent, timeouts, and kcal-vs-kJ normalization in exactly one module. Tests mock at the httpx boundary — never hit the real OFF API (spec §8.4). Routes: GET /api/off/product/{barcode} — lookup + normalize; 404 if not found GET /api/off/search?q= — search + normalize each hit; [] if none POST /api/off/refresh/{food_id} — re-fetch by stored barcode, update row """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from database import get_db from services.off import ( NoBarcodeError, RefreshError, fetch_product, refresh_food, search_off, ) router = APIRouter(prefix="/api/off", tags=["off"]) @router.get("/product/{barcode}") def get_product(barcode: str): """Look up a product by barcode on OpenFoodFacts and return normalized data matching our FoodCreate shape. Returns 404 when OFF has status=0, no product, or the product is unusable (no name and no computable calories). """ result = fetch_product(barcode) if result is None: raise HTTPException( status_code=404, detail=f"Product not found for barcode: {barcode}" ) return result @router.get("/search") def get_search(q: str = Query(..., min_length=1, description="Search query")): """Search OpenFoodFacts by text query. Returns a list of normalized food dicts (matching our FoodCreate shape). Empty list when nothing is found or all results are unusable. """ return search_off(q) @router.post("/refresh/{food_id}") def post_refresh(food_id: int, db: Session = Depends(get_db)): """Re-fetch a food's data from OpenFoodFacts by its stored barcode, normalize, and update the local row (nutrition, name, brand, serving, off_data; bumps updated_at). 404 — food_id not found, or OFF no longer has the product. 400 — the food has no barcode (can't refresh). """ try: return refresh_food(db, food_id) except RefreshError as e: raise HTTPException(status_code=e.status_code, detail=e.detail) except NoBarcodeError as e: raise HTTPException(status_code=e.status_code, detail=e.detail)