131 lines
4 KiB
Python
131 lines
4 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime
|
||
|
|
from typing import Dict, List, Literal, cast
|
||
|
|
from enum import Enum
|
||
|
|
|
||
|
|
import ingredients
|
||
|
|
import meals
|
||
|
|
import persons
|
||
|
|
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: persons.Person | 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)
|
||
|
|
return ShoppingListOut(
|
||
|
|
id=sl.id,
|
||
|
|
created_date=sl.created_date,
|
||
|
|
store_name=outward_store,
|
||
|
|
purchased_by_id=sl.purchased_by_id,
|
||
|
|
purchased_by=sl.purchased_by,
|
||
|
|
items=[_to_ingredient_item(i) for i in sl.items],
|
||
|
|
)
|