# services/recipes/app/schemas/recipes.py """ Pydantic schemas for recipe-related API requests and responses """ from pydantic import BaseModel, Field from typing import List, Optional, Dict, Any from uuid import UUID from datetime import datetime from enum import Enum from ..models.recipes import RecipeStatus, MeasurementUnit # Quality Template Association Schemas class QualityStageConfiguration(BaseModel): """Schema for quality checks configuration per production stage""" template_ids: List[UUID] = Field(default_factory=list, description="Quality template IDs for this stage") required_checks: List[str] = Field(default_factory=list, description="Required quality check types") optional_checks: List[str] = Field(default_factory=list, description="Optional quality check types") blocking_on_failure: bool = Field(default=True, description="Block stage progression on critical failures") min_quality_score: Optional[float] = Field(None, ge=0, le=10, description="Minimum quality score to pass stage") class RecipeQualityConfiguration(BaseModel): """Schema for recipe quality configuration across all stages""" stages: Dict[str, QualityStageConfiguration] = Field(default_factory=dict, description="Quality configuration per stage") overall_quality_threshold: float = Field(default=7.0, ge=0, le=10, description="Overall quality threshold for batch") critical_stage_blocking: bool = Field(default=True, description="Block progression if critical checks fail") auto_create_quality_checks: bool = Field(default=True, description="Automatically create quality checks for batches") quality_manager_approval_required: bool = Field(default=False, description="Require quality manager approval") class RecipeQualityConfigurationUpdate(BaseModel): """Schema for updating recipe quality configuration""" stages: Optional[Dict[str, QualityStageConfiguration]] = None overall_quality_threshold: Optional[float] = Field(None, ge=0, le=10) critical_stage_blocking: Optional[bool] = None auto_create_quality_checks: Optional[bool] = None quality_manager_approval_required: Optional[bool] = None class RecipeIngredientCreate(BaseModel): """Schema for creating recipe ingredients""" ingredient_id: UUID quantity: float = Field(..., gt=0) unit: MeasurementUnit alternative_quantity: Optional[float] = None alternative_unit: Optional[MeasurementUnit] = None preparation_method: Optional[str] = None ingredient_notes: Optional[str] = None is_optional: bool = False ingredient_order: int = Field(..., ge=1) ingredient_group: Optional[str] = None substitution_options: Optional[Dict[str, Any]] = None substitution_ratio: Optional[float] = None class RecipeIngredientUpdate(BaseModel): """Schema for updating recipe ingredients""" ingredient_id: Optional[UUID] = None quantity: Optional[float] = Field(None, gt=0) unit: Optional[MeasurementUnit] = None alternative_quantity: Optional[float] = None alternative_unit: Optional[MeasurementUnit] = None preparation_method: Optional[str] = None ingredient_notes: Optional[str] = None is_optional: Optional[bool] = None ingredient_order: Optional[int] = Field(None, ge=1) ingredient_group: Optional[str] = None substitution_options: Optional[Dict[str, Any]] = None substitution_ratio: Optional[float] = None class RecipeIngredientResponse(BaseModel): """Schema for recipe ingredient responses""" id: UUID tenant_id: UUID recipe_id: UUID ingredient_id: UUID quantity: float unit: str quantity_in_base_unit: Optional[float] = None alternative_quantity: Optional[float] = None alternative_unit: Optional[str] = None preparation_method: Optional[str] = None ingredient_notes: Optional[str] = None is_optional: bool ingredient_order: int ingredient_group: Optional[str] = None substitution_options: Optional[Dict[str, Any]] = None substitution_ratio: Optional[float] = None unit_cost: Optional[float] = None total_cost: Optional[float] = None cost_updated_at: Optional[datetime] = None class Config: from_attributes = True class RecipeCreate(BaseModel): """Schema for creating recipes""" name: str = Field(..., min_length=1, max_length=255) recipe_code: Optional[str] = Field(None, max_length=100) version: str = Field(default="1.0", max_length=20) finished_product_id: UUID description: Optional[str] = None category: Optional[str] = Field(None, max_length=100) cuisine_type: Optional[str] = Field(None, max_length=100) difficulty_level: int = Field(default=1, ge=1, le=5) yield_quantity: float = Field(..., gt=0) yield_unit: MeasurementUnit prep_time_minutes: Optional[int] = Field(None, ge=0) cook_time_minutes: Optional[int] = Field(None, ge=0) total_time_minutes: Optional[int] = Field(None, ge=0) rest_time_minutes: Optional[int] = Field(None, ge=0) instructions: Optional[Dict[str, Any]] = None preparation_notes: Optional[str] = None storage_instructions: Optional[str] = None quality_check_configuration: Optional[RecipeQualityConfiguration] = None serves_count: Optional[int] = Field(None, ge=1) nutritional_info: Optional[Dict[str, Any]] = None allergen_info: Optional[Dict[str, Any]] = None dietary_tags: Optional[Dict[str, Any]] = None batch_size_multiplier: float = Field(default=1.0, gt=0) minimum_batch_size: Optional[float] = Field(None, gt=0) maximum_batch_size: Optional[float] = Field(None, gt=0) optimal_production_temperature: Optional[float] = None optimal_humidity: Optional[float] = Field(None, ge=0, le=100) is_seasonal: bool = False season_start_month: Optional[int] = Field(None, ge=1, le=12) season_end_month: Optional[int] = Field(None, ge=1, le=12) is_signature_item: bool = False target_margin_percentage: Optional[float] = Field(None, ge=0) ingredients: List[RecipeIngredientCreate] = Field(..., min_items=1) class RecipeUpdate(BaseModel): """Schema for updating recipes""" name: Optional[str] = Field(None, min_length=1, max_length=255) recipe_code: Optional[str] = Field(None, max_length=100) version: Optional[str] = Field(None, max_length=20) description: Optional[str] = None category: Optional[str] = Field(None, max_length=100) cuisine_type: Optional[str] = Field(None, max_length=100) difficulty_level: Optional[int] = Field(None, ge=1, le=5) yield_quantity: Optional[float] = Field(None, gt=0) yield_unit: Optional[MeasurementUnit] = None prep_time_minutes: Optional[int] = Field(None, ge=0) cook_time_minutes: Optional[int] = Field(None, ge=0) total_time_minutes: Optional[int] = Field(None, ge=0) rest_time_minutes: Optional[int] = Field(None, ge=0) instructions: Optional[Dict[str, Any]] = None preparation_notes: Optional[str] = None storage_instructions: Optional[str] = None quality_check_configuration: Optional[RecipeQualityConfigurationUpdate] = None serves_count: Optional[int] = Field(None, ge=1) nutritional_info: Optional[Dict[str, Any]] = None allergen_info: Optional[Dict[str, Any]] = None dietary_tags: Optional[Dict[str, Any]] = None batch_size_multiplier: Optional[float] = Field(None, gt=0) minimum_batch_size: Optional[float] = Field(None, gt=0) maximum_batch_size: Optional[float] = Field(None, gt=0) optimal_production_temperature: Optional[float] = None optimal_humidity: Optional[float] = Field(None, ge=0, le=100) status: Optional[RecipeStatus] = None is_seasonal: Optional[bool] = None season_start_month: Optional[int] = Field(None, ge=1, le=12) season_end_month: Optional[int] = Field(None, ge=1, le=12) is_signature_item: Optional[bool] = None target_margin_percentage: Optional[float] = Field(None, ge=0) ingredients: Optional[List[RecipeIngredientCreate]] = None class RecipeResponse(BaseModel): """Schema for recipe responses""" id: UUID tenant_id: UUID name: str recipe_code: Optional[str] = None version: str finished_product_id: UUID description: Optional[str] = None category: Optional[str] = None cuisine_type: Optional[str] = None difficulty_level: int yield_quantity: float yield_unit: str prep_time_minutes: Optional[int] = None cook_time_minutes: Optional[int] = None total_time_minutes: Optional[int] = None rest_time_minutes: Optional[int] = None estimated_cost_per_unit: Optional[float] = None last_calculated_cost: Optional[float] = None cost_calculation_date: Optional[datetime] = None target_margin_percentage: Optional[float] = None suggested_selling_price: Optional[float] = None instructions: Optional[Dict[str, Any]] = None preparation_notes: Optional[str] = None storage_instructions: Optional[str] = None quality_check_configuration: Optional[RecipeQualityConfiguration] = None serves_count: Optional[int] = None nutritional_info: Optional[Dict[str, Any]] = None allergen_info: Optional[Dict[str, Any]] = None dietary_tags: Optional[Dict[str, Any]] = None batch_size_multiplier: float minimum_batch_size: Optional[float] = None maximum_batch_size: Optional[float] = None optimal_production_temperature: Optional[float] = None optimal_humidity: Optional[float] = None status: str is_seasonal: bool season_start_month: Optional[int] = None season_end_month: Optional[int] = None is_signature_item: bool created_at: datetime updated_at: datetime created_by: Optional[UUID] = None updated_by: Optional[UUID] = None ingredients: Optional[List[RecipeIngredientResponse]] = None class Config: from_attributes = True class RecipeDeletionSummary(BaseModel): """Summary of what will be deleted when hard-deleting a recipe""" recipe_id: UUID recipe_name: str recipe_code: str production_batches_count: int recipe_ingredients_count: int dependent_recipes_count: int # Recipes that use this as ingredient/sub-recipe affected_orders_count: int # Orders that include this recipe last_used_date: Optional[datetime] = None can_delete: bool warnings: List[str] = [] class RecipeSearchRequest(BaseModel): """Schema for recipe search requests""" search_term: Optional[str] = None status: Optional[RecipeStatus] = None category: Optional[str] = None is_seasonal: Optional[bool] = None is_signature: Optional[bool] = None difficulty_level: Optional[int] = Field(None, ge=1, le=5) limit: int = Field(default=100, ge=1, le=1000) offset: int = Field(default=0, ge=0) class RecipeDuplicateRequest(BaseModel): """Schema for recipe duplication requests""" new_name: str = Field(..., min_length=1, max_length=255) class RecipeFeasibilityResponse(BaseModel): """Schema for recipe feasibility check responses""" recipe_id: UUID recipe_name: str batch_multiplier: float feasible: bool missing_ingredients: List[Dict[str, Any]] = [] insufficient_ingredients: List[Dict[str, Any]] = [] class RecipeStatisticsResponse(BaseModel): """Schema for recipe statistics responses""" total_recipes: int active_recipes: int signature_recipes: int seasonal_recipes: int category_breakdown: List[Dict[str, Any]]