8.4 KiB
8.4 KiB
Phase 6 — Recipe Management
Goal: Enter, import, and manage recipes. Auto-calculate nutrition from product library ingredients. Highlight nutritional warnings. This phase ties into the product library (Phase 5) and will be consumed by meal planning (Phase 8).
Depends on: Phase 0, Phase 5
Deliverables
RecipeMongoDB schema and full CRUD API- Automatic nutrition calculation from ingredient list
- Nutritional warning generation
- Recipe scaling (adjust servings)
- Recipe import via LLM (plain text → structured)
- Recipe editor web UI with live nutrition sidebar
Data Model
Recipe Schema
// packages/shared/src/types/recipe.ts
export interface Recipe {
id: string;
householdId: string;
name: string;
description?: string;
servings: number;
prepTime?: number; // minutes
cookTime?: number; // minutes
totalTime?: number; // minutes (auto-calculated or manual)
ingredients: RecipeIngredient[];
steps: RecipeStep[];
tags: string[]; // e.g., 'vegetarian', 'quick', 'meal-prep'
cuisine?: string; // e.g., 'Italian', 'Japanese'
imageUrl?: string;
source?: RecipeSource;
totalNutrition: NutritionInfo; // Denormalized, computed on save
perServingNutrition: NutritionInfo; // Denormalized, computed on save
warnings: NutritionWarning[]; // Computed on save
isFavorite: boolean;
createdBy: string;
createdAt: Date;
updatedAt: Date;
}
export interface RecipeIngredient {
productId: string; // Reference to Product
productName: string; // Denormalized for display
quantity: number;
unit: ServingUnit;
preparation?: string; // e.g., 'diced', 'minced', 'melted'
isOptional: boolean;
nutritionContribution: NutritionInfo; // Per-ingredient computed nutrition
}
export interface RecipeStep {
order: number;
instruction: string;
duration?: number; // minutes
tip?: string;
}
export interface RecipeSource {
type: 'manual' | 'url' | 'llm_import' | 'text_import';
url?: string;
importedAt?: Date;
}
export enum NutritionWarning {
HIGH_CALORIES = 'high_calories', // > 800 kcal/serving
HIGH_SODIUM = 'high_sodium', // > 1500mg/serving
HIGH_SUGAR = 'high_sugar', // > 25g/serving
HIGH_SATURATED_FAT = 'high_saturated_fat', // > 13g/serving
LOW_PROTEIN = 'low_protein', // < 10g/serving
LOW_FIBER = 'low_fiber', // < 3g/serving
HIGH_CHOLESTEROL = 'high_cholesterol', // > 200mg/serving
}
MongoDB Indexes
{ householdId: 1, name: 'text', tags: 'text', cuisine: 'text' }
{ householdId: 1, 'ingredients.productId': 1 } // Find recipes using a product
{ householdId: 1, tags: 1 }
{ householdId: 1, isFavorite: 1 }
API Endpoints
RecipesModule
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /recipes |
List/search recipes (paginated) | member |
| GET | /recipes/:id |
Get single recipe | member |
| POST | /recipes |
Create recipe | member |
| PATCH | /recipes/:id |
Update recipe | member |
| DELETE | /recipes/:id |
Soft-delete recipe | admin |
| POST | /recipes/:id/scale |
Get scaled version (preview, not saved) | member |
| POST | /recipes/import-text |
Import from plain text via LLM | member |
| POST | /recipes/import-url |
Import from URL via LLM | member |
| GET | /recipes/by-product/:productId |
Find recipes using a specific product | member |
Query Parameters for GET /recipes
?q=pasta # Full-text search
&tags=vegetarian,quick # Filter by tags
&cuisine=Italian # Filter by cuisine
&maxCalories=600 # Filter by per-serving calories
&isFavorite=true # Favorites only
&cursor=abc123
&limit=20
Tasks
6.1 — Shared Types & Validation
- Add recipe types to
packages/shared/src/types/recipe.ts - Zod schemas:
CreateRecipeSchema— ingredients must reference valid productIdsUpdateRecipeSchema— partialScaleRecipeSchema—{ targetServings: number }ImportRecipeTextSchema—{ text: string }ImportRecipeUrlSchema—{ url: string }
6.2 — Nutrition Calculation Service
NutritionCalculatorService:
class NutritionCalculatorService {
/**
* For each ingredient:
* 1. Lookup the product by productId
* 2. Convert ingredient quantity/unit to product's servingUnit
* 3. Calculate nutrition proportionally: (ingredient_qty / serving_size) * nutrition_per_serving
* 4. Sum across all ingredients → totalNutrition
* 5. Divide by servings → perServingNutrition
*/
calculateRecipeNutrition(
ingredients: RecipeIngredient[],
servings: number,
): RecipeNutritionResult;
/**
* Check per-serving nutrition against warning thresholds
*/
generateWarnings(perServingNutrition: NutritionInfo): NutritionWarning[];
}
- 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
6.3 — Recipe CRUD with Auto-Calculation
- On
POST /recipesandPATCH /recipes/:id:- Validate ingredients exist in product library
- Call
NutritionCalculatorService.calculateRecipeNutrition() - Call
NutritionCalculatorService.generateWarnings() - Store computed
totalNutrition,perServingNutrition,warningson document
- On product nutrition update (Phase 5 edit), trigger background recalculation:
- Find all recipes where
ingredients[].productId == updatedProductId - Recalculate each recipe's nutrition
- Find all recipes where
6.4 — Recipe Scaling
POST /recipes/:id/scalewith{ targetServings: number }:- Returns a scaled preview (not persisted) with adjusted ingredient quantities and recalculated nutrition
scaledQuantity = originalQuantity * (targetServings / originalServings)
6.5 — Recipe Import (LLM)
-
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
- Accepts
-
POST /recipes/import-url:- Calls
ILlmProvider.parseRecipeFromUrl(url) - Same flow as text import
- Calls
-
With
NoOpLlmProvider: returns{ available: false }
6.6 — Web UI: Recipe Management
/recipespage:- Search bar, tag and cuisine filters
- Recipe card grid: image, name, time, calories/serving, warning badges
- Favorites tab
/recipes/newand/recipes/:id/edit:- Recipe metadata form (name, description, servings, times, cuisine, tags)
- Ingredient editor:
- Autocomplete from product library
- Quantity + unit inputs
- "Add ingredient" button, drag-to-reorder
- Per-ingredient nutrition shown inline
- Steps editor: ordered text areas, optional duration per step
- Live nutrition sidebar: updates as ingredients are added/changed
- Shows total and per-serving macros
- Warning badges with explanations
- "Scale" button: adjust servings in sidebar to see scaled amounts
/recipes/:iddetail page:- Full recipe view with ingredients, steps, nutrition panel
- "Import from text" and "Import from URL" buttons in recipe list page
Acceptance Criteria
- Can create a recipe with ingredients linked to products
- Nutrition is automatically calculated and stored on the recipe
- Warnings are generated for recipes exceeding thresholds
- Recipe scaling returns correctly adjusted quantities
- Editing a product's nutrition triggers recipe recalculation
- Text/URL import endpoint delegates to LLM provider interface
- Web UI shows live nutrition as ingredients are added
- Full-text search finds recipes by name, tags, cuisine
Estimated Effort
Medium. Nutrition calculation logic and unit conversion require careful implementation. UI is moderately complex with the live sidebar.