Improve the frontend 3
This commit is contained in:
@@ -455,3 +455,174 @@ async def resolve_or_create_products_batch(
|
||||
logger.error("Batch product resolution failed",
|
||||
error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(status_code=500, detail=f"Batch resolution failed: {str(e)}")
|
||||
|
||||
|
||||
# ================================================================
|
||||
# NEW: BATCH API ENDPOINTS FOR ORCHESTRATOR
|
||||
# ================================================================
|
||||
|
||||
class BatchIngredientsRequest(BaseModel):
|
||||
"""Request for batch ingredient fetching"""
|
||||
ingredient_ids: List[UUID] = Field(..., description="List of ingredient IDs to fetch")
|
||||
|
||||
|
||||
class BatchIngredientsResponse(BaseModel):
|
||||
"""Response with ingredient data"""
|
||||
ingredients: List[Dict[str, Any]] = Field(..., description="List of ingredient data")
|
||||
found_count: int = Field(..., description="Number of ingredients found")
|
||||
missing_ids: List[str] = Field(default_factory=list, description="IDs not found")
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("ingredients/batch"),
|
||||
response_model=BatchIngredientsResponse
|
||||
)
|
||||
async def get_ingredients_batch(
|
||||
request: BatchIngredientsRequest,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Fetch multiple ingredients in a single request (for Orchestrator).
|
||||
|
||||
This endpoint reduces N API calls to 1, improving performance when
|
||||
the orchestrator needs ingredient data for production/procurement planning.
|
||||
"""
|
||||
try:
|
||||
if not request.ingredient_ids:
|
||||
return BatchIngredientsResponse(
|
||||
ingredients=[],
|
||||
found_count=0,
|
||||
missing_ids=[]
|
||||
)
|
||||
|
||||
service = InventoryService()
|
||||
ingredients = []
|
||||
found_ids = set()
|
||||
|
||||
for ingredient_id in request.ingredient_ids:
|
||||
try:
|
||||
ingredient = await service.get_ingredient_by_id(ingredient_id, tenant_id, db)
|
||||
if ingredient:
|
||||
ingredients.append({
|
||||
'id': str(ingredient.id),
|
||||
'name': ingredient.name,
|
||||
'type': ingredient.type,
|
||||
'unit': ingredient.unit,
|
||||
'current_stock': float(ingredient.current_stock) if ingredient.current_stock else 0,
|
||||
'reorder_point': float(ingredient.reorder_point) if ingredient.reorder_point else 0,
|
||||
'cost_per_unit': float(ingredient.cost_per_unit) if ingredient.cost_per_unit else 0,
|
||||
'category': ingredient.category,
|
||||
'is_active': ingredient.is_active,
|
||||
'shelf_life_days': ingredient.shelf_life_days
|
||||
})
|
||||
found_ids.add(str(ingredient_id))
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to fetch ingredient in batch",
|
||||
ingredient_id=str(ingredient_id),
|
||||
error=str(e)
|
||||
)
|
||||
continue
|
||||
|
||||
missing_ids = [str(id) for id in request.ingredient_ids if str(id) not in found_ids]
|
||||
|
||||
logger.info(
|
||||
"Batch ingredient fetch complete",
|
||||
requested=len(request.ingredient_ids),
|
||||
found=len(ingredients),
|
||||
missing=len(missing_ids),
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
|
||||
return BatchIngredientsResponse(
|
||||
ingredients=ingredients,
|
||||
found_count=len(ingredients),
|
||||
missing_ids=missing_ids
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Batch ingredient fetch failed",
|
||||
error=str(e),
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Batch ingredient fetch failed: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
class BatchStockLevelsRequest(BaseModel):
|
||||
"""Request for batch stock level fetching"""
|
||||
ingredient_ids: List[UUID] = Field(..., description="List of ingredient IDs")
|
||||
|
||||
|
||||
class BatchStockLevelsResponse(BaseModel):
|
||||
"""Response with stock level data"""
|
||||
stock_levels: Dict[str, float] = Field(..., description="Ingredient ID to stock level mapping")
|
||||
found_count: int = Field(..., description="Number of stock levels found")
|
||||
|
||||
|
||||
@router.post(
|
||||
route_builder.build_operations_route("stock-levels/batch"),
|
||||
response_model=BatchStockLevelsResponse
|
||||
)
|
||||
async def get_stock_levels_batch(
|
||||
request: BatchStockLevelsRequest,
|
||||
tenant_id: UUID = Path(..., description="Tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Fetch stock levels for multiple ingredients in a single request.
|
||||
|
||||
Optimized endpoint for Orchestrator to quickly check inventory levels
|
||||
without making individual API calls per ingredient.
|
||||
"""
|
||||
try:
|
||||
if not request.ingredient_ids:
|
||||
return BatchStockLevelsResponse(
|
||||
stock_levels={},
|
||||
found_count=0
|
||||
)
|
||||
|
||||
service = InventoryService()
|
||||
stock_levels = {}
|
||||
|
||||
for ingredient_id in request.ingredient_ids:
|
||||
try:
|
||||
ingredient = await service.get_ingredient_by_id(ingredient_id, tenant_id, db)
|
||||
if ingredient:
|
||||
stock_levels[str(ingredient_id)] = float(ingredient.current_stock) if ingredient.current_stock else 0.0
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Failed to fetch stock level in batch",
|
||||
ingredient_id=str(ingredient_id),
|
||||
error=str(e)
|
||||
)
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
"Batch stock level fetch complete",
|
||||
requested=len(request.ingredient_ids),
|
||||
found=len(stock_levels),
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
|
||||
return BatchStockLevelsResponse(
|
||||
stock_levels=stock_levels,
|
||||
found_count=len(stock_levels)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Batch stock level fetch failed",
|
||||
error=str(e),
|
||||
tenant_id=str(tenant_id)
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Batch stock level fetch failed: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -137,7 +137,11 @@ class Ingredient(Base):
|
||||
is_perishable = Column(Boolean, default=False)
|
||||
allergen_info = Column(JSONB, nullable=True) # JSON array of allergens
|
||||
nutritional_info = Column(JSONB, nullable=True) # Nutritional information for finished products
|
||||
|
||||
|
||||
# NEW: Local production support (for procurement service integration)
|
||||
produced_locally = Column(Boolean, default=False, nullable=False) # If true, ingredient is produced in-house
|
||||
recipe_id = Column(UUID(as_uuid=True), nullable=True) # Links to recipe for BOM explosion
|
||||
|
||||
# Audit fields
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True),
|
||||
@@ -213,6 +217,9 @@ class Ingredient(Base):
|
||||
'is_perishable': self.is_perishable if self.is_perishable is not None else False,
|
||||
'allergen_info': self.allergen_info,
|
||||
'nutritional_info': self.nutritional_info,
|
||||
# NEW: Local production support
|
||||
'produced_locally': self.produced_locally if self.produced_locally is not None else False,
|
||||
'recipe_id': str(self.recipe_id) if self.recipe_id else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else datetime.now(timezone.utc).isoformat(),
|
||||
'created_by': str(self.created_by) if self.created_by else None,
|
||||
|
||||
@@ -60,7 +60,11 @@ class IngredientCreate(InventoryBaseSchema):
|
||||
# Properties
|
||||
is_perishable: bool = Field(False, description="Is perishable")
|
||||
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
|
||||
|
||||
|
||||
# NEW: Local production support
|
||||
produced_locally: bool = Field(False, description="If true, ingredient is produced in-house")
|
||||
recipe_id: Optional[str] = Field(None, description="Recipe ID for BOM explosion (if produced locally)")
|
||||
|
||||
@validator('reorder_point')
|
||||
def validate_reorder_point(cls, v, values):
|
||||
if 'low_stock_threshold' in values and v <= values['low_stock_threshold']:
|
||||
@@ -99,6 +103,10 @@ class IngredientUpdate(InventoryBaseSchema):
|
||||
is_perishable: Optional[bool] = Field(None, description="Is perishable")
|
||||
allergen_info: Optional[Dict[str, Any]] = Field(None, description="Allergen information")
|
||||
|
||||
# NEW: Local production support
|
||||
produced_locally: Optional[bool] = Field(None, description="If true, ingredient is produced in-house")
|
||||
recipe_id: Optional[str] = Field(None, description="Recipe ID for BOM explosion (if produced locally)")
|
||||
|
||||
|
||||
class IngredientResponse(InventoryBaseSchema):
|
||||
"""Schema for ingredient and finished product API responses"""
|
||||
@@ -125,6 +133,11 @@ class IngredientResponse(InventoryBaseSchema):
|
||||
is_active: bool
|
||||
is_perishable: bool
|
||||
allergen_info: Optional[Dict[str, Any]]
|
||||
|
||||
# NEW: Local production support
|
||||
produced_locally: bool = False
|
||||
recipe_id: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
created_by: Optional[str]
|
||||
|
||||
Reference in New Issue
Block a user