from __future__ import annotations import datetime from typing import List, Optional import aiosqlite from fastapi import APIRouter, Depends, Query, Request, Response import meals import shopping from common import ProblemDetails, ApiModel from api.deps import error_response from api.deps import get_db, get_household_from_slug router = APIRouter(prefix="/households/{householdSlug}/meals", tags=["meals-v2"]) class MarkConsumedBody(ApiModel): consumed_date: Optional[datetime.datetime] = None @router.get( "/upcoming", operation_id="getUpcomingMealsV2", summary="List upcoming meals in a date range (scoped)", ) async def get_upcoming_meals_scoped( household=Depends(get_household_from_slug), date_from: datetime.datetime = Query(..., alias="from"), to: datetime.datetime = Query(...), conn: aiosqlite.Connection = Depends(get_db), ) -> List[meals.Meal]: hid = household["id"] # Use a scoped repository function, falling back to filtering in SQL until repository is fully scoped try: async for _ in meals.find_upcoming_meals_by_date_range_scoped(conn, date_from, to, hid): # if function exists, break immediately to use it break use_scoped = True except AttributeError: use_scoped = False result: List[meals.Meal] = [] if use_scoped: async for meal in meals.find_upcoming_meals_by_date_range_scoped(conn, date_from, to, hid): result.append(meal) else: # Temporary path: direct query with household_id filter async with conn.execute( f""" SELECT {",".join(meals.Meal.KEYS)} FROM Meal WHERE suggested_date >= ? AND suggested_date <= ? AND consumed_date IS NULL AND deleted_date IS NULL AND household_id = ? """, (date_from, to, hid), ) as cursor: async for row in cursor: result.append(meals.Meal(**{k: v for k, v in zip(meals.Meal.KEYS, row)})) if not result: return result # Load relateds similar to v1 await meals.bulk_load_participants(conn, result) for meal in result: await meals.load_recipes(conn, meal) await meals.load_extra_ingredients(conn, meal) return result @router.get( "/{meal_id}", operation_id="getMealV2", summary="Get a meal by id (scoped)", response_model=meals.Meal, responses={404: {"model": ProblemDetails}}, ) async def get_meal_scoped( meal_id: int, request: Request, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), ) -> meals.Meal | Response: hid = household["id"] meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid) if not meal: return error_response(request, 404, "Meal not found") return meal @router.post( "/{meal_id}/consumed", operation_id="markMealConsumedV2", summary="Mark a meal as consumed (scoped)", response_model=meals.Meal, responses={ 400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}, }, ) async def mark_meal_consumed_scoped( meal_id: int, request: Request, body: Optional[MarkConsumedBody] = None, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), ) -> meals.Meal | Response: hid = household["id"] consumed_date: Optional[datetime.datetime] = None if body is not None: # Model aliasing handles consumedDate -> consumed_date consumed_date = getattr(body, "consumed_date", None) if consumed_date is not None and not getattr(consumed_date, "tzinfo", None): return error_response(request, 400, "Consumed date must include timezone") meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid) if not meal: return error_response(request, 404, "Meal not found") await meals.mark_consumed(conn, meal, consumed_date or datetime.datetime.now().astimezone()) # Clear any outstanding meal request entries for this meal await shopping.remove_request(conn, person=None, meal=meal) return meal