from pydantic import BaseModel, Field, model_validator from typing import Optional, List, Any from uuid import UUID from datetime import date, datetime from enum import Enum class FamilyMemberRole(str, Enum): adult = "adult" child = "child" class MealType(str, Enum): breakfast = "breakfast" lunch = "lunch" dinner = "dinner" class MealPlanStatus(str, Enum): draft = "draft" pending_approval = "pending_approval" approved = "approved" locked = "locked" class MealPlanItemStatus(str, Enum): pending = "pending" approved = "approved" denied = "denied" swapped = "swapped" class DenialReason(str, Enum): too_expensive = "too_expensive" boring = "boring" disliked_ingredient = "disliked_ingredient" cultural = "cultural" other = "other" class NeverSuggestReason(str, Enum): allergy = "allergy" dislike = "dislike" tried_too_much = "tried_too_much" other = "other" class IngredientBase(BaseModel): name: str name_lower: str plural_name: Optional[str] = None aisle: Optional[str] = None typical_price: Optional[float] = None unit: Optional[str] = None season_months: Optional[List[int]] = None class IngredientResponse(IngredientBase): id: UUID created_at: Optional[datetime] = None class Config: from_attributes = True class IngredientCreate(IngredientBase): pass class FamilyMemberBase(BaseModel): name: str email: Optional[str] = None role: FamilyMemberRole likes_mushrooms: bool = False class FamilyMemberResponse(FamilyMemberBase): id: UUID family_profile_id: UUID created_at: Optional[datetime] = None updated_at: Optional[datetime] = None class Config: from_attributes = True class FamilyMemberCreate(FamilyMemberBase): pass class FamilyProfileBase(BaseModel): name: str household_size: int adult_count: int child_count: int dietary_notes: Optional[str] = None budget_per_meal: float = 50.00 class FamilyProfileResponse(FamilyProfileBase): id: UUID created_at: Optional[datetime] = None updated_at: Optional[datetime] = None members: List[FamilyMemberResponse] = [] class Config: from_attributes = True class FamilyProfileCreate(FamilyProfileBase): pass class FamilyProfileUpdate(BaseModel): name: Optional[str] = None household_size: Optional[int] = None adult_count: Optional[int] = None child_count: Optional[int] = None dietary_notes: Optional[str] = None budget_per_meal: Optional[float] = None class RecipeIngredient(BaseModel): ingredient_id: Optional[UUID] = None name: Optional[str] = None quantity: Optional[float] = None unit: Optional[str] = None is_optional: bool = False notes: Optional[str] = None @model_validator(mode="before") @classmethod def _normalize(cls, data): if not isinstance(data, dict): return data # JSONB stores qty; schema uses quantity if "qty" in data and "quantity" not in data: data["quantity"] = data.pop("qty") return data class RecipeBase(BaseModel): name: str description: Optional[str] = None image_url: Optional[str] = None image_source: Optional[str] = None prep_time_minutes: Optional[int] = None cook_time_minutes: Optional[int] = None servings: int servings_scaled: Optional[int] = None cuisine_tags: Optional[List[str]] = [] dietary_tags: Optional[List[str]] = [] protein_type: Optional[str] = None spice_level: Optional[int] = None ingredients: List[RecipeIngredient] = [] instructions: List[str] = [] source_url: Optional[str] = None is_manually_added: bool = False class RecipeResponse(RecipeBase): id: UUID family_profile_id: Optional[UUID] = None scraped_at: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None total_time_minutes: Optional[int] = None class Config: from_attributes = True class RecipeCreate(RecipeBase): pass class MealPlanItemBase(BaseModel): recipe_id: UUID day_of_week: int = Field(..., ge=1, le=7) meal_type: MealType estimated_cost: Optional[float] = None class MealPlanItemResponse(MealPlanItemBase): id: UUID meal_plan_id: UUID approval_status: MealPlanItemStatus = MealPlanItemStatus.pending denial_reason: Optional[DenialReason] = None denial_details: Optional[str] = None used_pantry_items: Optional[List[UUID]] = [] created_at: Optional[datetime] = None updated_at: Optional[datetime] = None recipe: Optional[RecipeResponse] = None class Config: from_attributes = True class MealPlanItemCreate(MealPlanItemBase): pass class MealPlanBase(BaseModel): week_start_date: date status: MealPlanStatus = MealPlanStatus.draft approval_deadline: Optional[datetime] = None notes: Optional[str] = None class MealPlanResponse(MealPlanBase): id: UUID family_profile_id: UUID total_estimated_cost: Optional[float] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None items: List[MealPlanItemResponse] = [] class Config: from_attributes = True class MealPlanCreate(MealPlanBase): items: List[MealPlanItemCreate] = [] class VoteRequest(BaseModel): vote: bool denial_reason: Optional[DenialReason] = None denial_details: Optional[str] = None class VoteResponse(BaseModel): id: UUID meal_plan_item_id: UUID family_member_id: UUID vote: bool voted_at: Optional[datetime] = None class Config: from_attributes = True class HomePantryBase(BaseModel): ingredient_id: UUID quantity: Optional[float] = None unit: Optional[str] = None expires_at: Optional[date] = None class HomePantryResponse(HomePantryBase): id: UUID family_profile_id: UUID added_at: Optional[datetime] = None created_at: Optional[datetime] = None ingredient: Optional[IngredientResponse] = None class Config: from_attributes = True class HomePantryCreate(HomePantryBase): pass class FeedbackBase(BaseModel): rating: Optional[int] = Field(None, ge=1, le=5) never_suggest: bool = False denial_reason: Optional[DenialReason] = None feedback_text: Optional[str] = None class FeedbackResponse(FeedbackBase): id: UUID family_profile_id: UUID family_member_id: Optional[UUID] = None meal_plan_item_id: UUID created_at: Optional[datetime] = None class Config: from_attributes = True class FeedbackCreate(FeedbackBase): meal_plan_item_id: UUID class ShoppingListItem(BaseModel): ingredient_id: Optional[UUID] = None name: str quantity: Optional[float] = None unit: Optional[str] = None aisle: Optional[str] = None estimated_price: Optional[float] = None is_on_sale: bool = False sale_price: Optional[float] = None in_season: bool = False in_pantry: bool = False class ShoppingListResponse(BaseModel): week_start_date: date items: List[ShoppingListItem] total_estimated_cost: float sale_items_count: int by_aisle: dict[str, List[ShoppingListItem]]