273 lines
11 KiB
Python
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]] |