feat: orchestrator step_generate

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 06:30:50 -07:00
co-authored by Claude Sonnet 4.6
parent b2cbdd1533
commit 0a097da18e
2 changed files with 413 additions and 0 deletions
+195
View File
@@ -60,3 +60,198 @@ def step_scrape(run: "WeeklyRun", db: "Session") -> None:
),
)
logger.warning("step_scrape: both attempts failed; marked stale")
def step_generate(run: "WeeklyRun", db: "Session") -> None:
if run.generated_at is not None:
logger.info("step_generate: already done for %s", run.week_start_date)
return
try:
result = generate_meal_plan(
db,
family_id=run.family_id,
week_start_date=run.week_start_date,
)
run.generated_at = datetime.now(timezone.utc)
db.commit()
logger.info("step_generate: meal_plan %s created", result.meal_plan_id)
except Exception as exc:
run.error_step = "generate"
run.error_message = str(exc)
db.commit()
send_admin_alert(
subject=f"Generate failed week {run.week_start_date}",
body=str(exc),
)
raise
def step_email(run: "WeeklyRun", db: "Session") -> None:
if run.emailed_at is not None:
logger.info("step_email: already done for %s", run.week_start_date)
return
plan = (
db.query(MealPlan)
.filter(
MealPlan.family_profile_id == run.family_id,
MealPlan.week_start_date == run.week_start_date,
)
.first()
)
if plan is None:
raise RuntimeError(
f"No MealPlan for family {run.family_id} week {run.week_start_date}"
)
members = (
db.query(FamilyMember)
.filter(
FamilyMember.family_profile_id == run.family_id,
FamilyMember.email.isnot(None),
)
.all()
)
stale_banner = ""
if run.used_stale_data:
stale_banner = (
"<p><strong>Note:</strong> Grocery prices in this plan may be a few "
"days old — the Friday scrape failed and stale data was used.</p>"
)
backend = get_email_backend()
for member in members:
item_html_parts = []
for item in plan.items:
if item.approval_status != MealPlanItemStatus.PENDING:
continue
token = issue_token(item.id, member.id)
vote_url = (
f"{settings.APP_BASE_URL}/api/meals/vote/{item.id}?token={token}"
)
recipe_name = item.recipe.name if item.recipe else str(item.recipe_id)
item_html_parts.append(
f'<li>{recipe_name} — <a href="{vote_url}">Vote</a></li>'
)
if not item_html_parts:
continue
html = (
f"<h2>This week's meal suggestions</h2>"
f"{stale_banner}"
f"<p>Hi {member.name}, please vote on this week's meals by Fri 17:00 PT:</p>"
f"<ul>{''.join(item_html_parts)}</ul>"
f"<p>Silence = approved. Any denial removes that meal.</p>"
)
backend.send(
to=member.email,
subject=f"Meal plan for week of {run.week_start_date}",
html=html,
)
logger.info("step_email: sent to %s", member.email)
run.emailed_at = datetime.now(timezone.utc)
db.commit()
def step_deadline(run: "WeeklyRun", db: "Session") -> None:
if run.deadline_passed_at is not None:
logger.info("step_deadline: already done for %s", run.week_start_date)
return
plan = (
db.query(MealPlan)
.filter(
MealPlan.family_profile_id == run.family_id,
MealPlan.week_start_date == run.week_start_date,
)
.first()
)
if plan is None:
run.deadline_passed_at = datetime.now(timezone.utc)
db.commit()
return
family = db.query(FamilyProfile).filter(FamilyProfile.id == run.family_id).first()
policy = getattr(family, "pending_approval_policy", "approve") if family else "approve"
resolved = 0
for item in plan.items:
if item.approval_status == MealPlanItemStatus.PENDING:
item.approval_status = (
MealPlanItemStatus.APPROVED
if policy == "approve"
else MealPlanItemStatus.DENIED
)
resolved += 1
run.deadline_passed_at = datetime.now(timezone.utc)
db.commit()
logger.info(
"step_deadline: resolved %d pending items with policy=%s", resolved, policy
)
def step_finalize(run: "WeeklyRun", db: "Session") -> None:
if run.finalized_at is not None:
logger.info("step_finalize: already done for %s", run.week_start_date)
return
plan = (
db.query(MealPlan)
.filter(
MealPlan.family_profile_id == run.family_id,
MealPlan.week_start_date == run.week_start_date,
)
.first()
)
approved_items = [
item
for item in (plan.items if plan else [])
if item.approval_status == MealPlanItemStatus.APPROVED
]
members = (
db.query(FamilyMember)
.filter(
FamilyMember.family_profile_id == run.family_id,
FamilyMember.email.isnot(None),
)
.all()
)
if approved_items:
rows_html = "".join(
f"<tr><td>{item.recipe.name}</td>"
f"<td>{ing.get('name', '')}</td>"
f"<td>{ing.get('qty', '')} {ing.get('unit', '')}</td></tr>"
for item in approved_items
if item.recipe
for ing in (item.recipe.ingredients or [])
)
html = (
f"<h2>Shopping list — week of {run.week_start_date}</h2>"
f"<p>{len(approved_items)} meal(s) approved.</p>"
f"<table><thead><tr><th>Recipe</th><th>Ingredient</th><th>Qty</th></tr></thead>"
f"<tbody>{rows_html}</tbody></table>"
)
else:
html = f"<p>No meals were approved for week of {run.week_start_date}.</p>"
backend = get_email_backend()
for member in members:
backend.send(
to=member.email,
subject=f"Shopping list — week of {run.week_start_date}",
html=html,
)
logger.info("step_finalize: shopping list sent to %s", member.email)
run.finalized_at = datetime.now(timezone.utc)
run.status = "completed"
db.commit()
logger.info("step_finalize: done, %d approved meals", len(approved_items))