"""Authenticated catalog search and user-owned foods / mappings.""" from __future__ import annotations from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from auth import require_auth from data_layer.food_mapping import ( apply_mapping_to_items, clear_mapping_from_items, normalize_food_name, suggest_catalog_foods, upsert_food_mapping, ) from data_layer.nutrition_items import dates_for_normalized_name, rebuild_daily_nutrients from db import get_cursor, get_db, r2d from routers.profiles import get_pid router = APIRouter(prefix="/api/bls", tags=["bls"]) class UserFoodCreate(BaseModel): name_de: str name_en: Optional[str] = None macros_per_100g: Optional[dict] = None class MappingUpsert(BaseModel): source_name: str food_id: str source_system: str = "fddb" @router.get("/foods") def search_foods( q: str = "", limit: int = 20, session: dict = Depends(require_auth), ): pid = session["profile_id"] with get_db() as conn: cur = get_cursor(conn) return suggest_catalog_foods(cur, q, pid, limit=min(max(limit, 1), 50)) @router.post("/foods/manual") def create_user_food(body: UserFoodCreate, session: dict = Depends(require_auth)): from routers.admin_bls import _write_manual_macros pid = session["profile_id"] name = (body.name_de or "").strip() if not name: raise HTTPException(400, "Name fehlt") with get_db() as conn: cur = get_cursor(conn) cur.execute( """ INSERT INTO food_catalog (name_de, name_en, catalog_kind, owner_profile_id, source, external_key) VALUES (%s, %s, 'manual_user', %s, 'manual', %s) RETURNING * """, (name, body.name_en, pid, f"man-user-{normalize_food_name(name)[:40]}"), ) food = r2d(cur.fetchone()) _write_manual_macros(cur, food["id"], body.macros_per_100g) return food @router.get("/mappings") def list_my_mappings(session: dict = Depends(require_auth)): pid = session["profile_id"] with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT m.id, m.source_name_raw, m.source_name_normalized, m.food_id, m.source, f.name_de AS food_name_de, f.bls_code, f.catalog_kind FROM food_name_mappings m JOIN food_catalog f ON f.id = m.food_id WHERE m.profile_id = %s ORDER BY m.source_name_normalized """, (pid,), ) return [r2d(r) for r in cur.fetchall()] @router.post("/mappings") def upsert_my_mapping(body: MappingUpsert, session: dict = Depends(require_auth)): pid = session["profile_id"] with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT id FROM food_catalog WHERE id = %s AND is_active = true AND (owner_profile_id IS NULL OR owner_profile_id = %s) """, (body.food_id, pid), ) if not cur.fetchone(): raise HTTPException(404, "Lebensmittel nicht gefunden") mid = upsert_food_mapping( cur, source_name_raw=body.source_name, food_id=body.food_id, profile_id=pid, source="bulk", source_system=body.source_system, ) norm = normalize_food_name(body.source_name) n = apply_mapping_to_items(cur, pid, norm, body.food_id, mid) for d in dates_for_normalized_name(cur, pid, norm): rebuild_daily_nutrients(cur, pid, d) return {"mapping_id": mid, "items_updated": n, "source_name_normalized": norm} @router.delete("/mappings/{mapping_id}") def delete_my_mapping(mapping_id: int, session: dict = Depends(require_auth)): pid = session["profile_id"] with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT source_name_normalized FROM food_name_mappings WHERE id = %s AND profile_id = %s """, (mapping_id, pid), ) row = cur.fetchone() if not row: raise HTTPException(404, "Mapping nicht gefunden") norm = row["source_name_normalized"] dates = dates_for_normalized_name(cur, pid, norm) clear_mapping_from_items(cur, pid, norm) cur.execute("DELETE FROM food_name_mappings WHERE id = %s AND profile_id = %s", (mapping_id, pid)) for d in dates: rebuild_daily_nutrients(cur, pid, d) return {"ok": True} # keep get_pid imported for consistency with other routers _ = get_pid