from __future__ import annotations from datetime import datetime from typing import Dict, List, Literal, cast from enum import Enum import ingredients import meals from api.dtos import MemberRef import recipes import shopping from common import ApiModel, Field from shopping.models import StoreEnum # Outward-facing models and mapping helpers shared by shopping API class ListIngredientItem(ApiModel): kind: Literal["ingredient"] = "ingredient" id: int = -1 ingredient_id: int person_id: int created_date: datetime # These may be present when the ingredient is part of a requested meal list_id: int | None = None meal_id: int | None = None recipe_id: int | None = None class RequestedMealItem(ApiModel): kind: Literal["requestedMeal"] = "requestedMeal" id: int = -1 person_id: int meal_id: int created_date: datetime # Input DTOs (separate from internal DB/domain models) class IngredientPurchaseItemIn(ApiModel): ingredient_id: int person_id: int created_date: datetime | None = None meal_id: int | None = None recipe_id: int | None = None class PurchaseListIn(ApiModel): store_name: StoreEnum items: List[IngredientPurchaseItemIn] # Output DTOs for purchased lists class StoreNameOut(str, Enum): woolworths = "woolworths" coles = "coles" home = "home" class ShoppingListOut(ApiModel): id: int created_date: datetime # outward-only enum values: include "home" instead of an empty string store_name: Literal["woolworths", "coles", "home"] purchased_by_id: int purchased_by: MemberRef | None = None # Make items required in the schema; callers must always send an array (possibly empty) items: List[ListIngredientItem] = Field(min_length=0, json_schema_extra={"minItems": 0}) class CurrentShoppingList(ApiModel): outstanding_items: List[ListIngredientItem] = Field( min_length=0, json_schema_extra={"minItems": 0} ) requested_meals: List[RequestedMealItem] = Field( min_length=0, json_schema_extra={"minItems": 0} ) # Make all collections required to avoid undefined/null semantics in clients purchased_items: List[ListIngredientItem] = Field( min_length=0, json_schema_extra={"minItems": 0} ) ingredients_lookup: Dict[int, ingredients.Ingredient] meals_lookup: Dict[int, meals.Meal] shopping_list_lookup: Dict[int, ShoppingListOut] recipes_lookup: Dict[int, recipes.Recipe] class PurchasedShoppingList(ApiModel): list: ShoppingListOut # Lookup maps are required to be present (may be empty) meals_lookup: Dict[int, meals.Meal] ingredients_lookup: Dict[int, ingredients.Ingredient] recipes_lookup: Dict[int, recipes.Recipe] def _to_ingredient_item(item: shopping.ShoppingListItem) -> ListIngredientItem: return ListIngredientItem( id=item.id, ingredient_id=item.ingredient_id if item.ingredient_id is not None else -1, person_id=item.person_id, created_date=item.created_date, list_id=item.list_id, meal_id=item.meal_id, recipe_id=item.recipe_id, ) def _to_meal_item(item: shopping.ShoppingListItem) -> RequestedMealItem: return RequestedMealItem( id=item.id, person_id=item.person_id, meal_id=item.meal_id if item.meal_id is not None else -1, created_date=item.created_date, ) def _to_shopping_list_out(sl: shopping.ShoppingList) -> ShoppingListOut: # Map internal enum value "" to outward-friendly "home" if sl.store_name == StoreEnum.home: outward_store: Literal["woolworths", "coles", "home"] = "home" else: # Remaining enum values are 'woolworths' or 'coles' outward_store = cast(Literal["woolworths", "coles"], sl.store_name.value) # Map purchased_by if present: expect ShoppingList.purchased_by to carry display_name if available mref = None if sl.purchased_by is not None: try: # sl.purchased_by may be a lightweight object with id/name pid = getattr(sl.purchased_by, "id", None) pname = getattr(sl.purchased_by, "name", None) or getattr( sl.purchased_by, "display_name", None ) if isinstance(pid, int) and pname: mref = MemberRef(id=pid, display_name=pname) except Exception: mref = None return ShoppingListOut( id=sl.id, created_date=sl.created_date, store_name=outward_store, purchased_by_id=sl.purchased_by_id, purchased_by=mref, items=[_to_ingredient_item(i) for i in sl.items], )