92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
"""Meals router (spec §3.2). Thin handlers — business logic in services/meals.py."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
|
|
from database import get_db
|
|
from schemas import (
|
|
LogEntryRead,
|
|
MealComponentsUpdateRequest,
|
|
MealFromLogRequest,
|
|
MealFromLogResponse,
|
|
MealRead,
|
|
MealUnpackRequest,
|
|
MealUnpackResponse,
|
|
)
|
|
from services import meals as svc
|
|
|
|
router = APIRouter(prefix="/api/meals", tags=["meals"])
|
|
|
|
|
|
@router.post("/from-log", response_model=MealFromLogResponse, status_code=201)
|
|
def create_meal_from_log(data: MealFromLogRequest, db: Session = Depends(get_db)):
|
|
"""Create a meal food from selected daily log entries (§4.3).
|
|
|
|
Runs as one transaction: creates the meal + components, deletes source
|
|
entries, inserts one replacement entry. Rolls back entirely on failure.
|
|
"""
|
|
try:
|
|
meal, entry = svc.create_meal_from_log(
|
|
db, data.date, data.entry_ids, data.name,
|
|
)
|
|
except svc.EntryNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except svc.EntryDateMismatchError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
return MealFromLogResponse(meal=meal, entry=entry)
|
|
|
|
|
|
@router.post("/{meal_id}/unpack", response_model=MealUnpackResponse)
|
|
def unpack_meal(
|
|
meal_id: int,
|
|
data: MealUnpackRequest,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Replace a logged meal entry with its leaf component entries (§4.4).
|
|
|
|
Components are resolved recursively — nested meals are flattened to
|
|
leaf foods with their scaling factors multiplied down.
|
|
|
|
Runs as one transaction. If multiple entries match the meal on this
|
|
date, specify ``entry_id`` to disambiguate.
|
|
"""
|
|
try:
|
|
entries = svc.unpack_meal(db, meal_id, data.date, data.entry_id)
|
|
except svc.EntryNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except svc.AmbiguousMealEntryError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
return MealUnpackResponse(entries=entries)
|
|
|
|
|
|
@router.put("/{meal_id}/components", response_model=MealRead)
|
|
def update_meal_components(
|
|
meal_id: int,
|
|
data: MealComponentsUpdateRequest,
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Replace a meal's component list (§3.2 PUT). Cycle-checked.
|
|
|
|
Components are validated for cycles (direct or transitive self-reference).
|
|
A 422 is returned when the update would create a cycle.
|
|
|
|
This is a full replacement — all existing components are removed and
|
|
replaced with the provided list.
|
|
"""
|
|
comps = [{"food_id": c.food_id, "quantity": c.quantity} for c in data.components]
|
|
|
|
try:
|
|
return svc.update_meal_components(db, meal_id, comps)
|
|
except svc.MealNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except svc.ComponentFoodNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except svc.MealCycleError as e:
|
|
raise HTTPException(status_code=422, detail=str(e))
|