Public Access
Backend (FastAPI): - docker-compose with all 4 services - FastAPI app with health endpoints - SQLAlchemy models for all tables - Placeholder API endpoints for all routes - Config and database modules - requirements.txt with all dependencies Frontend (React): - package.json with React, Tailwind, React Query, React Router - Vite config with API proxy - Tailwind and TypeScript configs - Basic App with routing skeleton - Placeholder pages (Dashboard, MealDetail, Pantry) Infrastructure: - nginx config for reverse proxy - Dockerfile for backend and frontend
249 lines
10 KiB
Python
249 lines
10 KiB
Python
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")
|