kiwi/app/services/meal_plan/prep_scheduler.py

86 lines
2.6 KiB
Python

# app/services/meal_plan/prep_scheduler.py
"""Sequence prep tasks for a batch cooking session.
Pure function — no DB or network calls. Sorts tasks by equipment priority
(oven first to maximise oven utilisation) then assigns sequence_order.
"""
from __future__ import annotations
from dataclasses import dataclass, field
_EQUIPMENT_PRIORITY = {"oven": 0, "stovetop": 1, "cold": 2, "no-heat": 3}
_DEFAULT_PRIORITY = 4
@dataclass
class PrepTask:
recipe_id: int | None
slot_id: int | None
task_label: str
duration_minutes: int | None
sequence_order: int
equipment: str | None
is_parallel: bool = False
notes: str | None = None
user_edited: bool = False
def _total_minutes(recipe: dict) -> int | None:
prep = recipe.get("prep_time")
cook = recipe.get("cook_time")
if prep is None and cook is None:
return None
return (prep or 0) + (cook or 0)
def _equipment(recipe: dict) -> str | None:
# Corpus recipes don't have an explicit equipment field; use test helper
# field if present, otherwise infer from cook_time (long = oven heuristic).
if "_equipment" in recipe:
return recipe["_equipment"]
minutes = _total_minutes(recipe)
if minutes and minutes >= 45:
return "oven"
return "stovetop"
def build_prep_tasks(slots: list[dict], recipes: list[dict]) -> list[PrepTask]:
"""Return a sequenced list of PrepTask objects from plan slots + recipe rows.
Algorithm:
1. Build a recipe_id → recipe dict lookup.
2. Create one task per slot that has a recipe assigned.
3. Sort by equipment priority (oven first).
4. Assign contiguous sequence_order starting at 1.
"""
if not slots or not recipes:
return []
recipe_map: dict[int, dict] = {r["id"]: r for r in recipes}
raw_tasks: list[tuple[int, PrepTask]] = [] # (priority, task)
for slot in slots:
recipe_id = slot.get("recipe_id")
if not recipe_id:
continue
recipe = recipe_map.get(recipe_id)
if not recipe:
continue
eq = _equipment(recipe)
priority = _EQUIPMENT_PRIORITY.get(eq or "", _DEFAULT_PRIORITY)
task = PrepTask(
recipe_id=recipe_id,
slot_id=slot.get("id"),
task_label=recipe.get("name", f"Recipe {recipe_id}"),
duration_minutes=_total_minutes(recipe),
sequence_order=0, # filled below
equipment=eq,
)
raw_tasks.append((priority, task))
raw_tasks.sort(key=lambda t: t[0])
for i, (_, task) in enumerate(raw_tasks, 1):
task.sequence_order = i
return [t for _, t in raw_tasks]