from uuid import uuid4 import httpx from fastapi import APIRouter, HTTPException, Query, status from sqlmodel import select from app.access import product_barcode_accessible from app.deps import CurrentUserDep, SessionDep from app.models import FoodLog, Product from app.schemas import ProductCreate, ProductRead, ProductSearchResult router = APIRouter(prefix="/api/products", tags=["products"]) OFF_API_URL = "https://world.openfoodfacts.org/api/v2/product/{barcode}.json" def _extract_macros(nutriments: dict) -> dict: calories = nutriments.get("energy-kcal_100g") if calories is None: energy_kj = nutriments.get("energy_100g") calories = energy_kj / 4.184 if energy_kj is not None else 0.0 return { "calories": calories, "carbs": nutriments.get("carbohydrates_100g", 0.0) or 0.0, "protein": nutriments.get("proteins_100g", 0.0) or 0.0, "fat": nutriments.get("fat_100g", 0.0) or 0.0, "sugar": nutriments.get("sugars_100g", 0.0) or 0.0, "fiber": nutriments.get("fiber_100g", 0.0) or 0.0, "saturated_fat": nutriments.get("saturated-fat_100g", 0.0) or 0.0, "salt": nutriments.get("salt_100g", 0.0) or 0.0, } async def _fetch_from_open_food_facts(barcode: str) -> Product | None: try: async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get(OFF_API_URL.format(barcode=barcode)) except httpx.RequestError: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Open Food Facts ist gerade nicht erreichbar", ) if response.status_code != 200: return None data = response.json() if data.get("status") != 1: return None product_data = data.get("product", {}) macros = _extract_macros(product_data.get("nutriments", {})) name = product_data.get("product_name") or product_data.get("generic_name") or "Unbekanntes Produkt" return Product(barcode=barcode, name=name, **macros) @router.post("", response_model=ProductRead, status_code=status.HTTP_201_CREATED) def create_product(product_in: ProductCreate, current_user: CurrentUserDep, session: SessionDep): barcode = product_in.barcode or f"manual-{uuid4().hex[:12]}" existing = session.exec(select(Product).where(Product.barcode == barcode)).first() if existing: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Barcode existiert bereits") product = Product(**{**product_in.model_dump(), "barcode": barcode}) session.add(product) session.commit() session.refresh(product) return product @router.get("/search", response_model=list[ProductSearchResult]) def search_products( current_user: CurrentUserDep, session: SessionDep, q: str = Query(default=""), limit: int = Query(default=20, ge=1, le=50), ): query = select(Product) if q: query = query.where(Product.name.ilike(f"%{q}%")) products = [ product for product in session.exec(query).all() if product_barcode_accessible(session, product.barcode, current_user.id) ] recent_logs = session.exec( select(FoodLog) .where(FoodLog.user_id == current_user.id) .order_by(FoodLog.timestamp.desc()) .limit(500) ).all() last_used: dict[str, FoodLog] = {} for log in recent_logs: if log.barcode and log.barcode not in last_used: last_used[log.barcode] = log def sort_key(product: Product): log = last_used.get(product.barcode) if log: return (0, -log.timestamp.timestamp()) return (1, product.name.lower()) products.sort(key=sort_key) results = [] for product in products[:limit]: log = last_used.get(product.barcode) results.append( ProductSearchResult( **product.model_dump(), last_amount_g=log.amount_g if log else None, is_dish=product.barcode.startswith("dish-"), ) ) return results @router.get("/{barcode}", response_model=ProductRead) async def get_product(barcode: str, current_user: CurrentUserDep, session: SessionDep): product = session.exec(select(Product).where(Product.barcode == barcode)).first() if product: if not product_barcode_accessible(session, barcode, current_user.id): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Produkt nicht gefunden") return product product = await _fetch_from_open_food_facts(barcode) if product is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Produkt nicht gefunden") session.add(product) session.commit() session.refresh(product) return product