From a03152e51ab3c4e09952cb5c3e131c490a8e039a Mon Sep 17 00:00:00 2001
From: Peter Woolery
Date: Sat, 9 May 2026 14:47:48 -0700
Subject: [PATCH] fix: resolve ingredient names from Ingredient table in email
template
recipe.ingredients JSONB has ingredient_id but no name field; preload
names in a single bulk query before the per-member loop so ing_rows,
shopping preview, and cost lookup all render real ingredient names.
Co-Authored-By: Claude Sonnet 4.6 (1M context)
---
backend/app/services/orchestrator/steps.py | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/backend/app/services/orchestrator/steps.py b/backend/app/services/orchestrator/steps.py
index 9625779..1d979ec 100644
--- a/backend/app/services/orchestrator/steps.py
+++ b/backend/app/services/orchestrator/steps.py
@@ -12,6 +12,7 @@ from __future__ import annotations
import html
import logging
+import uuid as _uuid
from datetime import datetime, timezone
from typing import TYPE_CHECKING
@@ -122,13 +123,28 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
"days old — the Friday scrape failed and stale data was used.
"
)
+ # Preload ingredient names for all pending items
+ ing_ids: set = set()
+ for _item in plan.items:
+ if _item.approval_status == MealPlanItemStatus.PENDING and _item.recipe:
+ for _ing in (_item.recipe.ingredients or []):
+ if "ingredient_id" in _ing:
+ ing_ids.add(_ing["ingredient_id"])
+
+ ingredient_names: dict = {}
+ if ing_ids:
+ _ing_rows = db.query(Ingredient).filter(
+ Ingredient.id.in_([_uuid.UUID(str(i)) for i in ing_ids])
+ ).all()
+ ingredient_names = {str(r.id): r.name for r in _ing_rows}
+
# 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()
+ ing_name = ingredient_names.get(str(ing.get("ingredient_id", "")), ing.get("name", "unknown")).strip()
if not ing_name or ing_name in all_ingredients:
continue
match = (
@@ -199,7 +215,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
ing_rows = "".join(
f"{html.escape(str(ing.get('qty', '')))}"
f" {html.escape(str(ing.get('unit', '')))}"
- f" {html.escape(str(ing.get('name', '')))}"
+ f" {html.escape(ingredient_names.get(str(ing.get('ingredient_id', '')), ing.get('name', 'unknown')))}"
for ing in ingredients
)
ing_block = (
@@ -210,7 +226,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
# Estimated cost: sum top-confidence grocery match prices
est_cost = 0.0
for ing in ingredients:
- ing_name = ing.get("name", "").lower()
+ ing_name = ingredient_names.get(str(ing.get("ingredient_id", "")), ing.get("name", "")).lower()
match = (
db.query(IngredientGroceryMatch)
.join(Ingredient, IngredientGroceryMatch.ingredient_id == Ingredient.id)