From aeed2a4dc0d3d3f605ace14f5bde1b68f5252cbb Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Sun, 10 May 2026 09:55:52 -0700 Subject: [PATCH] feat: add cooking instructions to vote email; shopping list grouped by meal; fix current_price field - Vote email: recipe cards now include a collapsible
block with numbered cooking steps (recipe.instructions ARRAY) - Shopping list email: ingredients now grouped under each meal heading instead of a flat deduplicated list - step_finalize: fix grocery price lookup (.price -> .current_price) Co-Authored-By: Claude Sonnet 4.6 (1M context) --- backend/app/services/orchestrator/steps.py | 50 +++++++++++++++------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/backend/app/services/orchestrator/steps.py b/backend/app/services/orchestrator/steps.py index 04cfc96..1c411af 100644 --- a/backend/app/services/orchestrator/steps.py +++ b/backend/app/services/orchestrator/steps.py @@ -223,6 +223,21 @@ def step_email(run: "WeeklyRun", db: "Session") -> None: if ing_rows else "" ) + # Cooking instructions (collapsed by default) + instructions = item.recipe.instructions or [] if item.recipe else [] + instr_rows = "".join( + f"
  • {html.escape(str(step))}
  • " + for step in instructions + ) + instructions_block = ( + f"
    " + f"" + f"Cooking steps ({len(instructions)})" + f"
      {instr_rows}
    " + f"
    " + if instr_rows else "" + ) + # Estimated cost: sum top-confidence grocery match prices est_cost = 0.0 for ing in ingredients: @@ -247,6 +262,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None: f'{img_block}' f'

    {recipe_name}

    ' f'{ing_block}' + f'{instructions_block}' f'{cost_block}' f'' @@ -363,21 +379,20 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None: if approved_items: total_cost = 0.0 - rows_html = "" - seen_ingredients: set = set() + sections_html = "" for item in approved_items: if not item.recipe: continue + recipe_name = html.escape(item.recipe.name) + rows_html = "" for ing in (item.recipe.ingredients or []): ing_id = str(ing.get("ingredient_id", "")) ing_name = _fin_names.get(ing_id, "") - if not ing_name or ing_name in seen_ingredients: + if not ing_name: continue - seen_ingredients.add(ing_name) qty = ing.get("qty", "") unit = ing.get("unit", "") - # Find Lucky CA match match = ( db.query(IngredientGroceryMatch) .join(Ingredient, IngredientGroceryMatch.ingredient_id == Ingredient.id) @@ -387,7 +402,7 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None: ) if match and match.grocery_item: lucky_name = html.escape(match.grocery_item.name or "") - price = float(match.grocery_item.price or 0) + price = float(match.grocery_item.current_price or 0) total_cost += price price_str = f"${price:.2f}" else: @@ -403,19 +418,24 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None: f"" ) + sections_html += ( + f"

    {recipe_name}

    " + f"" + f"" + f"" + f"" + f"" + f"" + f"" + f"{rows_html}" + f"
    IngredientQtyAt LuckyPrice
    " + ) + email_html = ( f"
    " f"

    Shopping list — week of {run.week_start_date}

    " f"

    {len(approved_items)} meal(s) approved.

    " - f"" - f"" - f"" - f"" - f"" - f"" - f"" - f"{rows_html}" - f"
    IngredientQtyAt LuckyPrice
    " + f"{sections_html}" f"

    Estimated total: ${total_cost:.2f}

    " f"
    " )