From e22eae1ecdc2d2763a160828fc3168ae2557d407 Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Sun, 24 May 2026 20:20:11 -0700 Subject: [PATCH] feat(backend): persist plan scores on MealPlanItem - Migration 0012 adds score (float) and components (jsonb) to meal_plan_item - generate.py: populates score and components at create time - schemas/MealPlanItemResponse: include score + components fields - GET /api/meal-plans/{id}: returns persisted values instead of zeros --- .../versions/0012_meal_plan_item_scores.py | 22 +++++++++++++++++++ backend/app/api/meal_plans.py | 7 +++--- backend/app/models/__init__.py | 4 +++- backend/app/schemas/__init__.py | 2 ++ backend/app/services/planner/generate.py | 2 ++ 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 backend/alembic/versions/0012_meal_plan_item_scores.py diff --git a/backend/alembic/versions/0012_meal_plan_item_scores.py b/backend/alembic/versions/0012_meal_plan_item_scores.py new file mode 100644 index 0000000..d40f5b1 --- /dev/null +++ b/backend/alembic/versions/0012_meal_plan_item_scores.py @@ -0,0 +1,22 @@ +"""Persist score and components on MealPlanItem.""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision = "0012" +down_revision = "0011" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("meal_plan_item", sa.Column("score", sa.Float, nullable=True)) + op.add_column( + "meal_plan_item", + sa.Column("components", sa.dialects.postgresql.JSONB, nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("meal_plan_item", "components") + op.drop_column("meal_plan_item", "score") diff --git a/backend/app/api/meal_plans.py b/backend/app/api/meal_plans.py index 4760c41..79bcb2c 100644 --- a/backend/app/api/meal_plans.py +++ b/backend/app/api/meal_plans.py @@ -125,8 +125,7 @@ def get_plan(plan_id: UUID, db: Session = Depends(get_db)) -> GenerationResponse .order_by(MealPlanItem.day_of_week) .all() ) - # Read-after-create: scores not stored on MealPlanItem, so we return zeros - # for score/components. The original generate response is authoritative. + # Read-after-create: scores now stored on MealPlanItem return GenerationResponse( meal_plan_id=plan.id, week_start_date=plan.week_start_date, @@ -135,8 +134,8 @@ def get_plan(plan_id: UUID, db: Session = Depends(get_db)) -> GenerationResponse recipe_id=it.recipe_id, day_of_week=it.day_of_week, estimated_cost=it.estimated_cost or 0, - score=0.0, - components={}, + score=it.score or 0.0, + components={k: float(v) for k, v in (it.components.items() if it.components else [])}, ) for it in items ], diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index c409cce..133bc91 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -1,5 +1,5 @@ from sqlalchemy import ( - Column, String, Integer, Numeric, Text, Boolean, Date, DateTime, + Column, String, Integer, Numeric, Float, Text, Boolean, Date, DateTime, ForeignKey, CheckConstraint, UniqueConstraint, Enum as SQLEnum ) from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY @@ -228,6 +228,8 @@ class MealPlanItem(Base): denial_reason = Column(SQLEnum(DenialReason, name="denial_reason_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj])) denial_details = Column(Text) estimated_cost = Column(Numeric(10, 2)) + score = Column(Float) + components = Column(JSONB) 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()) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 4b6cc0d..42f0512 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -188,6 +188,8 @@ class MealPlanItemResponse(MealPlanItemBase): denial_reason: Optional[DenialReason] = None denial_details: Optional[str] = None used_pantry_items: Optional[List[UUID]] = [] + score: Optional[float] = None + components: Optional[Dict[str, float]] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None recipe: Optional[RecipeResponse] = None diff --git a/backend/app/services/planner/generate.py b/backend/app/services/planner/generate.py index de675f8..7512d20 100644 --- a/backend/app/services/planner/generate.py +++ b/backend/app/services/planner/generate.py @@ -196,6 +196,8 @@ def generate_meal_plan( meal_type=meal_type, approval_status=MealPlanItemStatus.pending, estimated_cost=scored_recipe.cost.total_cost, + score=scored_recipe.score, + components=scored_recipe.components, ) db.add(item)