Public Access
feat(backend): implement unit conversion for cost calculation
- Add UnitConverter (normalization, within-family, density tables) - Update cost.py to convert recipe qty to grocery price unit - Update generate.py _load_match_index to fetch ingredient name + unit - Fix orchestrator email/shopping-list cost loops to use conversion - Fix missing Ingredient import in generate.py - Add 19 unit tests
This commit is contained in:
@@ -4,27 +4,24 @@ Inputs:
|
||||
ingredients: list[dict] from recipe.ingredients JSONB
|
||||
match_index: dict[ingredient_id, list[match_dict]] — pre-fetched, sorted by confidence DESC
|
||||
pantry_ingredient_ids: set of ingredient_ids in home_pantry
|
||||
ingredient_name_index: optional dict[ingredient_id, name_lower] for unit conversion
|
||||
|
||||
Strategy:
|
||||
For each ingredient in the recipe, take the top-confidence match
|
||||
(or skip if none). Cost = current_price * qty (best-effort scaling
|
||||
that ignores unit conversion — see Limitations below).
|
||||
Savings = max(regular - current, 0) * qty.
|
||||
|
||||
Limitations:
|
||||
Unit conversion (lb vs oz, cup vs ml) is NOT implemented in this
|
||||
pass. The qty multiplier is treated as dimensionless. This produces
|
||||
a biased-but-monotonic ranking signal: recipes that use more of an
|
||||
expensive ingredient still rank as more expensive, which is what
|
||||
the planner needs. Real dollar accuracy can come later.
|
||||
(or skip if none). Cost = current_price × converted_qty.
|
||||
Unit conversion is attempted via app.utils.units.convert_qty; if
|
||||
the units are incommensurable, falls back to dimensionless qty to
|
||||
preserve the monotonic ranking signal.
|
||||
Savings = max(regular - current, 0) × converted_qty.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Dict, Iterable, List, Set
|
||||
from typing import Dict, Iterable, List, Optional, Set
|
||||
from uuid import UUID
|
||||
|
||||
from app.services.planner.types import IngredientCost, RecipeCost
|
||||
from app.utils.units import convert_qty
|
||||
|
||||
|
||||
def _decimal(v) -> Decimal:
|
||||
@@ -87,8 +84,16 @@ def compute_recipe_cost(
|
||||
current = _decimal(best.get("current_price"))
|
||||
regular = _decimal(best.get("regular_price"))
|
||||
is_on_sale = bool(best.get("is_on_sale"))
|
||||
line_cost = _scale(current, qty)
|
||||
line_savings = _scale(max(regular - current, Decimal("0")), qty)
|
||||
converted_qty = float(
|
||||
convert_qty(
|
||||
qty,
|
||||
unit,
|
||||
best.get("grocery_unit"),
|
||||
ingredient_name_lower=best.get("ingredient_name"),
|
||||
)
|
||||
)
|
||||
line_cost = _scale(current, converted_qty)
|
||||
line_savings = _scale(max(regular - current, Decimal("0")), converted_qty)
|
||||
|
||||
matched_count += 1
|
||||
if is_on_sale:
|
||||
|
||||
Reference in New Issue
Block a user