feat: wire dietary constraints into secondary use filter on all inventory endpoints

_user_constraints() loads dietary_constraints from user_settings once per
request. All 7 _enrich_item call sites now pass constraints so wine (and
any future alcohol-containing entries) are suppressed for halal/alcohol-free
users at the API response layer.
This commit is contained in:
pyr0ball 2026-04-24 17:12:39 -07:00
parent e45b07c203
commit 3463aa1e17

View file

@ -43,6 +43,12 @@ router = APIRouter()
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────────────
def _user_constraints(store) -> list[str]:
"""Load active dietary constraints from user settings (comma-separated string)."""
raw = store.get_setting("dietary_constraints") or ""
return [c.strip() for c in raw.split(",") if c.strip()]
def _enrich_item(item: dict, user_constraints: list[str] | None = None) -> dict: def _enrich_item(item: dict, user_constraints: list[str] | None = None) -> dict:
"""Attach computed fields: opened_expiry_date, secondary_state/uses/warning/discard_signs.""" """Attach computed fields: opened_expiry_date, secondary_state/uses/warning/discard_signs."""
from datetime import date, timedelta from datetime import date, timedelta
@ -215,13 +221,15 @@ async def list_inventory_items(
store: Store = Depends(get_store), store: Store = Depends(get_store),
): ):
items = await asyncio.to_thread(store.list_inventory, location, item_status) items = await asyncio.to_thread(store.list_inventory, location, item_status)
return [InventoryItemResponse.model_validate(_enrich_item(i)) for i in items] constraints = await asyncio.to_thread(_user_constraints, store)
return [InventoryItemResponse.model_validate(_enrich_item(i, constraints)) for i in items]
@router.get("/items/expiring", response_model=List[InventoryItemResponse]) @router.get("/items/expiring", response_model=List[InventoryItemResponse])
async def get_expiring_items(days: int = 7, store: Store = Depends(get_store)): async def get_expiring_items(days: int = 7, store: Store = Depends(get_store)):
items = await asyncio.to_thread(store.expiring_soon, days) items = await asyncio.to_thread(store.expiring_soon, days)
return [InventoryItemResponse.model_validate(_enrich_item(i)) for i in items] constraints = await asyncio.to_thread(_user_constraints, store)
return [InventoryItemResponse.model_validate(_enrich_item(i, constraints)) for i in items]
@router.get("/items/{item_id}", response_model=InventoryItemResponse) @router.get("/items/{item_id}", response_model=InventoryItemResponse)
@ -229,7 +237,8 @@ async def get_inventory_item(item_id: int, store: Store = Depends(get_store)):
item = await asyncio.to_thread(store.get_inventory_item, item_id) item = await asyncio.to_thread(store.get_inventory_item, item_id)
if not item: if not item:
raise HTTPException(status_code=404, detail="Inventory item not found") raise HTTPException(status_code=404, detail="Inventory item not found")
return InventoryItemResponse.model_validate(_enrich_item(item)) constraints = await asyncio.to_thread(_user_constraints, store)
return InventoryItemResponse.model_validate(_enrich_item(item, constraints))
@router.patch("/items/{item_id}", response_model=InventoryItemResponse) @router.patch("/items/{item_id}", response_model=InventoryItemResponse)
@ -246,7 +255,8 @@ async def update_inventory_item(
item = await asyncio.to_thread(store.update_inventory_item, item_id, **updates) item = await asyncio.to_thread(store.update_inventory_item, item_id, **updates)
if not item: if not item:
raise HTTPException(status_code=404, detail="Inventory item not found") raise HTTPException(status_code=404, detail="Inventory item not found")
return InventoryItemResponse.model_validate(_enrich_item(item)) constraints = await asyncio.to_thread(_user_constraints, store)
return InventoryItemResponse.model_validate(_enrich_item(item, constraints))
@router.post("/items/{item_id}/open", response_model=InventoryItemResponse) @router.post("/items/{item_id}/open", response_model=InventoryItemResponse)
@ -260,7 +270,8 @@ async def mark_item_opened(item_id: int, store: Store = Depends(get_store)):
) )
if not item: if not item:
raise HTTPException(status_code=404, detail="Inventory item not found") raise HTTPException(status_code=404, detail="Inventory item not found")
return InventoryItemResponse.model_validate(_enrich_item(item)) constraints = await asyncio.to_thread(_user_constraints, store)
return InventoryItemResponse.model_validate(_enrich_item(item, constraints))
@router.post("/items/{item_id}/consume", response_model=InventoryItemResponse) @router.post("/items/{item_id}/consume", response_model=InventoryItemResponse)
@ -289,7 +300,8 @@ async def consume_item(
) )
if not item: if not item:
raise HTTPException(status_code=404, detail="Inventory item not found") raise HTTPException(status_code=404, detail="Inventory item not found")
return InventoryItemResponse.model_validate(_enrich_item(item)) constraints = await asyncio.to_thread(_user_constraints, store)
return InventoryItemResponse.model_validate(_enrich_item(item, constraints))
@router.post("/items/{item_id}/discard", response_model=InventoryItemResponse) @router.post("/items/{item_id}/discard", response_model=InventoryItemResponse)
@ -313,7 +325,8 @@ async def discard_item(
) )
if not item: if not item:
raise HTTPException(status_code=404, detail="Inventory item not found") raise HTTPException(status_code=404, detail="Inventory item not found")
return InventoryItemResponse.model_validate(_enrich_item(item)) constraints = await asyncio.to_thread(_user_constraints, store)
return InventoryItemResponse.model_validate(_enrich_item(item, constraints))
@router.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)