Phase 5 cleanup
This commit is contained in:
parent
5536acd67d
commit
76a516a417
136 changed files with 6322 additions and 1985 deletions
|
|
@ -50,13 +50,21 @@ export interface Recipe {
|
|||
export interface RecipeIngredient {
|
||||
productId: string; // Reference to Product
|
||||
productName: string; // Denormalized for display
|
||||
quantity: number;
|
||||
unit: ServingUnit;
|
||||
quantity: number; // stored in metric (g | ml) or as a discrete count
|
||||
unit: RecipeUnit; // metric/discrete only after normalization
|
||||
originalQuantity?: number; // optional, preserved from import (e.g. 1)
|
||||
originalUnit?: ImperialUnit | RecipeUnit; // optional, preserved from import (e.g. 'cup')
|
||||
preparation?: string; // e.g., 'diced', 'minced', 'melted'
|
||||
isOptional: boolean;
|
||||
nutritionContribution: NutritionInfo; // Per-ingredient computed nutrition
|
||||
}
|
||||
|
||||
// Storage units for recipe ingredients — same set as `ServingUnit` in Phase 5.
|
||||
export type RecipeUnit = 'g' | 'ml' | 'piece' | 'slice';
|
||||
|
||||
// Accepted at input/import only; converted to `RecipeUnit` before persistence.
|
||||
export type ImperialUnit = 'oz' | 'lb' | 'cup' | 'tbsp' | 'tsp' | 'fl_oz';
|
||||
|
||||
export interface RecipeStep {
|
||||
order: number;
|
||||
instruction: string;
|
||||
|
|
@ -143,7 +151,7 @@ class NutritionCalculatorService {
|
|||
/**
|
||||
* For each ingredient:
|
||||
* 1. Lookup the product by productId
|
||||
* 2. Convert ingredient quantity/unit to product's servingUnit
|
||||
* 2. Convert ingredient quantity/unit to product's servingUnit (already metric)
|
||||
* 3. Calculate nutrition proportionally: (ingredient_qty / serving_size) * nutrition_per_serving
|
||||
* 4. Sum across all ingredients → totalNutrition
|
||||
* 5. Divide by servings → perServingNutrition
|
||||
|
|
@ -160,17 +168,26 @@ class NutritionCalculatorService {
|
|||
}
|
||||
```
|
||||
|
||||
- Unit conversion helper: handle common conversions (g ↔ oz, ml ↔ cups, etc.)
|
||||
- Not all conversions are possible (density-dependent) — log warning, use best approximation
|
||||
- This is explicitly **informative, not clinical-grade accurate**
|
||||
- Because products are stored in metric (`g | ml | piece | slice`), the calculator only needs to bridge metric ↔ metric and discrete ↔ metric (via `Product.servingSize`).
|
||||
- Imperial input handling lives in `UnitConversionService` (see 6.2a) and runs **before** the calculator at create/update/import time.
|
||||
|
||||
### 6.2a — Unit Conversion Service
|
||||
|
||||
- `UnitConversionService.toMetric(quantity, unit, product)` returns `{ quantity, unit: RecipeUnit }`.
|
||||
- Mass conversions (exact): `oz → g` (× 28.3495), `lb → g` (× 453.592).
|
||||
- Volume conversions (exact, US customary): `tsp → ml` (× 4.92892), `tbsp → ml` (× 14.7868), `fl_oz → ml` (× 29.5735), `cup → ml` (× 236.588).
|
||||
- Mass ↔ volume conversions require `product.densityGPerMl`. If absent, the service returns an error tagged `MISSING_DENSITY` and the route returns 422 with the offending ingredient so the user can either supply a density on the product or restate the quantity in the product's native unit.
|
||||
- Discrete units (`piece`, `slice`) cannot be converted from imperial — reject at validation.
|
||||
- This service is **input-side only**: ingredients persisted on a recipe are always already in `RecipeUnit`.
|
||||
|
||||
### 6.3 — Recipe CRUD with Auto-Calculation
|
||||
|
||||
- On `POST /recipes` and `PATCH /recipes/:id`:
|
||||
1. Validate ingredients exist in product library
|
||||
2. Call `NutritionCalculatorService.calculateRecipeNutrition()`
|
||||
3. Call `NutritionCalculatorService.generateWarnings()`
|
||||
4. Store computed `totalNutrition`, `perServingNutrition`, `warnings` on document
|
||||
1. Validate ingredients exist in product library (including soft-deleted)
|
||||
2. Run each ingredient through `UnitConversionService.toMetric()` so persisted `unit` is always `RecipeUnit`; preserve the user's original input as `originalQuantity`/`originalUnit` for display
|
||||
3. Call `NutritionCalculatorService.calculateRecipeNutrition()`
|
||||
4. Call `NutritionCalculatorService.generateWarnings()`
|
||||
5. Store computed `totalNutrition`, `perServingNutrition`, `warnings` on document
|
||||
- On product nutrition update (Phase 5 edit), trigger background recalculation:
|
||||
- Find all recipes where `ingredients[].productId == updatedProductId`
|
||||
- Recalculate each recipe's nutrition
|
||||
|
|
@ -186,9 +203,9 @@ class NutritionCalculatorService {
|
|||
- `POST /recipes/import-text`:
|
||||
- Accepts `{ text: string }` (pasted recipe)
|
||||
- Calls `ILlmProvider.parseRecipe(text)`
|
||||
- LLM returns structured: `{ name, servings, ingredients[]: { name, quantity, unit }, steps[] }`
|
||||
- Service attempts to match ingredient names to existing products (fuzzy match by name)
|
||||
- Returns structured recipe for user review — unmatched ingredients flagged for manual product creation
|
||||
- LLM returns structured: `{ name, servings, ingredients[]: { name, quantity, unit }, steps[] }` where `unit` may be imperial
|
||||
- Service runs each ingredient through `UnitConversionService.toMetric()` and matches names to existing products (fuzzy match by name)
|
||||
- Returns the structured recipe for user review — unmatched ingredients and `MISSING_DENSITY` failures are flagged for manual resolution; nothing is persisted yet
|
||||
- `POST /recipes/import-url`:
|
||||
- Calls `ILlmProvider.parseRecipeFromUrl(url)`
|
||||
- Same flow as text import
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue