diff --git a/backend/app/services/orchestrator/steps.py b/backend/app/services/orchestrator/steps.py index 75728a9..9625779 100644 --- a/backend/app/services/orchestrator/steps.py +++ b/backend/app/services/orchestrator/steps.py @@ -16,7 +16,7 @@ from datetime import datetime, timezone from typing import TYPE_CHECKING from app.config import settings -from app.models import FamilyMember, FamilyProfile, MealPlan, MealPlanItemStatus, MealPlanVote +from app.models import FamilyMember, FamilyProfile, Ingredient, IngredientGroceryMatch, MealPlan, MealPlanItemStatus, MealPlanVote from app.services.approval import issue_token from app.services.email import get_email_backend from app.services.orchestrator.alerts import send_admin_alert @@ -122,6 +122,57 @@ def step_email(run: "WeeklyRun", db: "Session") -> None: "days old — the Friday scrape failed and stale data was used.
" ) + # Build shopping list preview once (shared across all member emails) + all_ingredients: dict[str, tuple[str, str, str, str]] = {} + for item in plan.items: + if item.approval_status != MealPlanItemStatus.PENDING: + continue + for ing in (item.recipe.ingredients or [] if item.recipe else []): + ing_name = ing.get("name", "").strip() + if not ing_name or ing_name in all_ingredients: + continue + match = ( + db.query(IngredientGroceryMatch) + .join(Ingredient, IngredientGroceryMatch.ingredient_id == Ingredient.id) + .filter(Ingredient.name_lower == ing_name.lower()) + .order_by(IngredientGroceryMatch.confidence.desc()) + .first() + ) + grocery_name = match.grocery_item.name if match and match.grocery_item else "—" + price = ( + f"${float(match.grocery_item.current_price):.2f}" + if match and match.grocery_item and match.grocery_item.current_price + else "—" + ) + all_ingredients[ing_name] = ( + ing.get("qty", ""), + ing.get("unit", ""), + grocery_name, + price, + ) + + if all_ingredients: + shop_rows = "".join( + f"| Ingredient | " + f"Qty | " + f"At Lucky | " + f"Price | " + f"
|---|
Est. cost: ~${est_cost:.2f}
" + if est_cost > 0 else "" + ) + item_html_parts.append( - f'Hi {html.escape(member.name)}, please vote on this week's meals by Fri 17:00 PT:
" - f"Silence = approved. Any denial removes that meal.
" + f"Hi {html.escape(member.name)}, please vote on this week's meals by Fri 17:00 PT. " + f"Silence = approved. Any denial removes that meal.
" + f"{''.join(item_html_parts)}" + f"{shopping_preview}" + f"