Files
bakery-ia/services/recipes/app/schemas/recipes.py
2025-10-27 16:33:26 +01:00

273 lines
11 KiB
Python

# 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]]