from sqlalchemy import ( Column, String, Integer, Numeric, Text, Boolean, Date, DateTime, ForeignKey, CheckConstraint, UniqueConstraint, ARRAY ) from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.database import Base import uuid class FamilyProfile(Base): __tablename__ = "family_profile" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(100), nullable=False) household_size = Column(Integer, nullable=False) adult_count = Column(Integer, nullable=False) child_count = Column(Integer, nullable=False) dietary_notes = Column(Text) budget_per_meal = Column(Numeric(10, 2), default=50.00) calorie_target = Column(Integer) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) recipes = relationship("Recipe", back_populates="family_profile") meal_plans = relationship("MealPlan", back_populates="family_profile") pantry_items = relationship("HomePantry", back_populates="family_profile") feedbacks = relationship("Feedback", back_populates="family_profile") never_suggests = relationship("NeverSuggest", back_populates="family_profile") class Ingredient(Base): __tablename__ = "ingredient" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(200), nullable=False, unique=True) plural_name = Column(String(200)) aisle = Column(String(100)) typical_price = Column(Numeric(10, 2)) unit = Column(String(50)) season_months = Column(ARRAY(Integer)) created_at = Column(DateTime(timezone=True), server_default=func.now()) recipes = relationship("RecipeIngredient", back_populates="ingredient") pantry_items = relationship("HomePantry", back_populates="ingredient") never_suggests = relationship("NeverSuggest", back_populates="ingredient") class Recipe(Base): __tablename__ = "recipe" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) family_profile_id = Column(UUID(as_uuid=True), ForeignKey("family_profile.id")) name = Column(String(300), nullable=False) description = Column(Text) image_url = Column(Text) image_source = Column(String(50)) prep_time_minutes = Column(Integer) cook_time_minutes = Column(Integer) servings = Column(Integer, nullable=False) servings_scaled = Column(Integer) cuisine_tags = Column(ARRAY(String(50))) dietary_tags = Column(ARRAY(String(50))) protein_type = Column(String(50)) spice_level = Column(Integer) ingredients = Column(JSONB, nullable=False) instructions = Column(ARRAY(Text), nullable=False) source_url = Column(Text) scraped_at = Column(DateTime(timezone=True)) is_manually_added = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) family_profile = relationship("FamilyProfile", back_populates="recipes") meal_plan_items = relationship("MealPlanItem", back_populates="recipe") class MealPlan(Base): __tablename__ = "meal_plan" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) family_profile_id = Column(UUID(as_uuid=True), ForeignKey("family_profile.id")) week_start_date = Column(Date, nullable=False) status = Column(String(20), nullable=False, default="draft") approval_deadline = Column(DateTime(timezone=True)) total_estimated_cost = Column(Numeric(10, 2)) notes = Column(Text) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) __table_args__ = ( UniqueConstraint("family_profile_id", "week_start_date"), ) family_profile = relationship("FamilyProfile", back_populates="meal_plans") items = relationship("MealPlanItem", back_populates="meal_plan", cascade="all, delete-orphan") class MealPlanItem(Base): __tablename__ = "meal_plan_item" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) meal_plan_id = Column(UUID(as_uuid=True), ForeignKey("meal_plan.id", ondelete="CASCADE")) recipe_id = Column(UUID(as_uuid=True), ForeignKey("recipe.id")) day_of_week = Column(Integer, nullable=False) meal_type = Column(String(20), nullable=False) approval_status = Column(String(20), default="pending") approval_token = Column(UUID(as_uuid=True), unique=True, default=uuid.uuid4) approval_token_expires = Column(DateTime(timezone=True)) denial_reason = Column(String(50)) denial_details = Column(Text) estimated_cost = Column(Numeric(10, 2)) used_pantry_items = Column(ARRAY(UUID(as_uuid=True))) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) __table_args__ = ( UniqueConstraint("meal_plan_id", "day_of_week", "meal_type"), CheckConstraint("day_of_week BETWEEN 0 AND 6"), ) meal_plan = relationship("MealPlan", back_populates="items") recipe = relationship("Recipe", back_populates="meal_plan_items") feedback = relationship("Feedback", back_populates="meal_plan_item", uselist=False) class HomePantry(Base): __tablename__ = "home_pantry" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) family_profile_id = Column(UUID(as_uuid=True), ForeignKey("family_profile.id", ondelete="CASCADE")) ingredient_id = Column(UUID(as_uuid=True), ForeignKey("ingredient.id")) quantity = Column(Numeric(10, 2)) unit = Column(String(50)) expires_at = Column(Date) added_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now()) __table_args__ = ( UniqueConstraint("family_profile_id", "ingredient_id"), ) family_profile = relationship("FamilyProfile", back_populates="pantry_items") ingredient = relationship("Ingredient", back_populates="pantry_items") class Feedback(Base): __tablename__ = "feedback" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) family_profile_id = Column(UUID(as_uuid=True), ForeignKey("family_profile.id")) meal_plan_item_id = Column(UUID(as_uuid=True), ForeignKey("meal_plan_item.id", ondelete="CASCADE")) rating = Column(Integer) never_suggest = Column(Boolean, default=False) denial_reason = Column(String(50)) feedback_text = Column(Text) created_at = Column(DateTime(timezone=True), server_default=func.now()) __table_args__ = ( UniqueConstraint("meal_plan_item_id"), CheckConstraint("rating BETWEEN 1 AND 5"), ) family_profile = relationship("FamilyProfile", back_populates="feedbacks") meal_plan_item = relationship("MealPlanItem", back_populates="feedback") class NeverSuggest(Base): __tablename__ = "never_suggest" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) family_profile_id = Column(UUID(as_uuid=True), ForeignKey("family_profile.id", ondelete="CASCADE")) ingredient_id = Column(UUID(as_uuid=True), ForeignKey("ingredient.id")) recipe_id = Column(UUID(as_uuid=True), ForeignKey("recipe.id")) reason = Column(String(50)) notes = Column(Text) created_at = Column(DateTime(timezone=True), server_default=func.now()) family_profile = relationship("FamilyProfile", back_populates="never_suggests") ingredient = relationship("Ingredient", back_populates="never_suggests") class GroceryItem(Base): __tablename__ = "grocery_item" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String(300), nullable=False) brand = Column(String(200)) current_price = Column(Numeric(10, 2)) regular_price = Column(Numeric(10, 2)) unit = Column(String(50)) aisle = Column(String(100)) image_url = Column(Text) product_url = Column(Text) is_on_sale = Column(Boolean, default=False) sale_start_date = Column(Date) sale_end_date = Column(Date) in_season = Column(Boolean, default=False) scraped_at = Column(DateTime(timezone=True), server_default=func.now()) scraped_url = Column(Text) class ScrapeLog(Base): __tablename__ = "scrape_log" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) source = Column(String(50), nullable=False) scrape_type = Column(String(50), nullable=False) status = Column(String(20), nullable=False) items_scraped = Column(Integer, default=0) error_message = Column(Text) started_at = Column(DateTime(timezone=True), server_default=func.now()) completed_at = Column(DateTime(timezone=True)) duration_seconds = Column(Integer) class EmailLog(Base): __tablename__ = "email_log" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) recipient_email = Column(String(300), nullable=False) recipient_name = Column(String(200)) template = Column(String(100), nullable=False) meal_plan_id = Column(UUID(as_uuid=True), ForeignKey("meal_plan.id")) meal_plan_item_id = Column(UUID(as_uuid=True), ForeignKey("meal_plan_item.id")) sendgrid_message_id = Column(String(100)) status = Column(String(20), nullable=False) error_message = Column(Text) created_at = Column(DateTime(timezone=True), server_default=func.now()) delivered_at = Column(DateTime(timezone=True)) meal_plan = relationship("MealPlan") meal_plan_item = relationship("MealPlanItem") class RecipeIngredient(Base): __tablename__ = "recipe_ingredient" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) recipe_id = Column(UUID(as_uuid=True), ForeignKey("recipe.id", ondelete="CASCADE")) ingredient_id = Column(UUID(as_uuid=True), ForeignKey("ingredient.id")) quantity = Column(Numeric(10, 2)) unit = Column(String(50)) is_optional = Column(Boolean, default=False) recipe = relationship("Recipe", back_populates="recipe_ingredients") ingredient = relationship("Ingredient", back_populates="recipes")