Create new services: inventory, recipes, suppliers

This commit is contained in:
Urtzi Alfaro
2025-08-13 17:39:35 +02:00
parent fbe7470ad9
commit 16b8a9d50c
151 changed files with 35799 additions and 857 deletions

View File

@@ -0,0 +1 @@
# services/recipes/app/api/__init__.py

View File

@@ -0,0 +1,117 @@
# services/recipes/app/api/ingredients.py
"""
API endpoints for ingredient-related operations (bridge to inventory service)
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from typing import List, Optional
from uuid import UUID
import logging
from ..services.inventory_client import InventoryClient
logger = logging.getLogger(__name__)
router = APIRouter()
def get_tenant_id(x_tenant_id: str = Header(...)) -> UUID:
"""Extract tenant ID from header"""
try:
return UUID(x_tenant_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid tenant ID format")
@router.get("/search")
async def search_ingredients(
tenant_id: UUID = Depends(get_tenant_id),
search_term: Optional[str] = Query(None),
product_type: Optional[str] = Query(None),
category: Optional[str] = Query(None),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0)
):
"""Search ingredients from inventory service"""
try:
inventory_client = InventoryClient()
# This would call the inventory service search endpoint
# For now, return a placeholder response
return {
"ingredients": [],
"total": 0,
"message": "Integration with inventory service needed"
}
except Exception as e:
logger.error(f"Error searching ingredients: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{ingredient_id}")
async def get_ingredient(
ingredient_id: UUID,
tenant_id: UUID = Depends(get_tenant_id)
):
"""Get ingredient details from inventory service"""
try:
inventory_client = InventoryClient()
ingredient = await inventory_client.get_ingredient_by_id(tenant_id, ingredient_id)
if not ingredient:
raise HTTPException(status_code=404, detail="Ingredient not found")
return ingredient
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting ingredient {ingredient_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{ingredient_id}/stock")
async def get_ingredient_stock(
ingredient_id: UUID,
tenant_id: UUID = Depends(get_tenant_id)
):
"""Get ingredient stock level from inventory service"""
try:
inventory_client = InventoryClient()
stock = await inventory_client.get_ingredient_stock_level(tenant_id, ingredient_id)
if not stock:
raise HTTPException(status_code=404, detail="Stock information not found")
return stock
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting stock for ingredient {ingredient_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/check-availability")
async def check_ingredients_availability(
required_ingredients: List[dict],
tenant_id: UUID = Depends(get_tenant_id)
):
"""Check if required ingredients are available for production"""
try:
inventory_client = InventoryClient()
result = await inventory_client.check_ingredient_availability(
tenant_id,
required_ingredients
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return result["data"]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error checking ingredient availability: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,427 @@
# services/recipes/app/api/production.py
"""
API endpoints for production management
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from uuid import UUID
from datetime import date, datetime
import logging
from ..core.database import get_db
from ..services.production_service import ProductionService
from ..schemas.production import (
ProductionBatchCreate,
ProductionBatchUpdate,
ProductionBatchResponse,
ProductionBatchSearchRequest,
ProductionScheduleCreate,
ProductionScheduleUpdate,
ProductionScheduleResponse,
ProductionStatisticsResponse,
StartProductionRequest,
CompleteProductionRequest
)
logger = logging.getLogger(__name__)
router = APIRouter()
def get_tenant_id(x_tenant_id: str = Header(...)) -> UUID:
"""Extract tenant ID from header"""
try:
return UUID(x_tenant_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid tenant ID format")
def get_user_id(x_user_id: str = Header(...)) -> UUID:
"""Extract user ID from header"""
try:
return UUID(x_user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user ID format")
# Production Batch Endpoints
@router.post("/batches", response_model=ProductionBatchResponse)
async def create_production_batch(
batch_data: ProductionBatchCreate,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Create a new production batch"""
try:
production_service = ProductionService(db)
batch_dict = batch_data.dict()
batch_dict["tenant_id"] = tenant_id
result = await production_service.create_production_batch(batch_dict, user_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return ProductionBatchResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating production batch: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/batches/{batch_id}", response_model=ProductionBatchResponse)
async def get_production_batch(
batch_id: UUID,
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get production batch by ID with consumptions"""
try:
production_service = ProductionService(db)
batch = production_service.get_production_batch_with_consumptions(batch_id)
if not batch:
raise HTTPException(status_code=404, detail="Production batch not found")
# Verify tenant ownership
if batch["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
return ProductionBatchResponse(**batch)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting production batch {batch_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.put("/batches/{batch_id}", response_model=ProductionBatchResponse)
async def update_production_batch(
batch_id: UUID,
batch_data: ProductionBatchUpdate,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Update an existing production batch"""
try:
production_service = ProductionService(db)
# Check if batch exists and belongs to tenant
existing_batch = production_service.get_production_batch_with_consumptions(batch_id)
if not existing_batch:
raise HTTPException(status_code=404, detail="Production batch not found")
if existing_batch["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
batch_dict = batch_data.dict(exclude_unset=True)
result = await production_service.update_production_batch(batch_id, batch_dict, user_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return ProductionBatchResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating production batch {batch_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/batches/{batch_id}")
async def delete_production_batch(
batch_id: UUID,
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Delete a production batch"""
try:
production_service = ProductionService(db)
# Check if batch exists and belongs to tenant
existing_batch = production_service.get_production_batch_with_consumptions(batch_id)
if not existing_batch:
raise HTTPException(status_code=404, detail="Production batch not found")
if existing_batch["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
success = production_service.production_repo.delete(batch_id)
if not success:
raise HTTPException(status_code=404, detail="Production batch not found")
return {"message": "Production batch deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting production batch {batch_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/batches", response_model=List[ProductionBatchResponse])
async def search_production_batches(
tenant_id: UUID = Depends(get_tenant_id),
search_term: Optional[str] = Query(None),
status: Optional[str] = Query(None),
priority: Optional[str] = Query(None),
start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None),
recipe_id: Optional[UUID] = Query(None),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db)
):
"""Search production batches with filters"""
try:
production_service = ProductionService(db)
batches = production_service.search_production_batches(
tenant_id=tenant_id,
search_term=search_term,
status=status,
priority=priority,
start_date=start_date,
end_date=end_date,
recipe_id=recipe_id,
limit=limit,
offset=offset
)
return [ProductionBatchResponse(**batch) for batch in batches]
except Exception as e:
logger.error(f"Error searching production batches: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/batches/{batch_id}/start", response_model=ProductionBatchResponse)
async def start_production_batch(
batch_id: UUID,
start_data: StartProductionRequest,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Start production batch and record ingredient consumptions"""
try:
production_service = ProductionService(db)
# Check if batch exists and belongs to tenant
existing_batch = production_service.get_production_batch_with_consumptions(batch_id)
if not existing_batch:
raise HTTPException(status_code=404, detail="Production batch not found")
if existing_batch["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
consumptions_list = [cons.dict() for cons in start_data.ingredient_consumptions]
result = await production_service.start_production_batch(
batch_id,
consumptions_list,
start_data.staff_member or user_id,
start_data.production_notes
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return ProductionBatchResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error starting production batch {batch_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/batches/{batch_id}/complete", response_model=ProductionBatchResponse)
async def complete_production_batch(
batch_id: UUID,
complete_data: CompleteProductionRequest,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Complete production batch and add finished products to inventory"""
try:
production_service = ProductionService(db)
# Check if batch exists and belongs to tenant
existing_batch = production_service.get_production_batch_with_consumptions(batch_id)
if not existing_batch:
raise HTTPException(status_code=404, detail="Production batch not found")
if existing_batch["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
completion_data = complete_data.dict()
result = await production_service.complete_production_batch(
batch_id,
completion_data,
complete_data.staff_member or user_id
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return ProductionBatchResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error completing production batch {batch_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/batches/active/list", response_model=List[ProductionBatchResponse])
async def get_active_production_batches(
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get all active production batches"""
try:
production_service = ProductionService(db)
batches = production_service.get_active_production_batches(tenant_id)
return [ProductionBatchResponse(**batch) for batch in batches]
except Exception as e:
logger.error(f"Error getting active production batches: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/statistics/dashboard", response_model=ProductionStatisticsResponse)
async def get_production_statistics(
tenant_id: UUID = Depends(get_tenant_id),
start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None),
db: Session = Depends(get_db)
):
"""Get production statistics for dashboard"""
try:
production_service = ProductionService(db)
stats = production_service.get_production_statistics(tenant_id, start_date, end_date)
return ProductionStatisticsResponse(**stats)
except Exception as e:
logger.error(f"Error getting production statistics: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
# Production Schedule Endpoints
@router.post("/schedules", response_model=ProductionScheduleResponse)
async def create_production_schedule(
schedule_data: ProductionScheduleCreate,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Create a new production schedule"""
try:
production_service = ProductionService(db)
schedule_dict = schedule_data.dict()
schedule_dict["tenant_id"] = tenant_id
schedule_dict["created_by"] = user_id
result = production_service.create_production_schedule(schedule_dict)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return ProductionScheduleResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating production schedule: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/schedules/{schedule_id}", response_model=ProductionScheduleResponse)
async def get_production_schedule(
schedule_id: UUID,
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get production schedule by ID"""
try:
production_service = ProductionService(db)
schedule = production_service.get_production_schedule(schedule_id)
if not schedule:
raise HTTPException(status_code=404, detail="Production schedule not found")
# Verify tenant ownership
if schedule["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
return ProductionScheduleResponse(**schedule)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting production schedule {schedule_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/schedules/date/{schedule_date}", response_model=ProductionScheduleResponse)
async def get_production_schedule_by_date(
schedule_date: date,
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get production schedule for specific date"""
try:
production_service = ProductionService(db)
schedule = production_service.get_production_schedule_by_date(tenant_id, schedule_date)
if not schedule:
raise HTTPException(status_code=404, detail="Production schedule not found for this date")
return ProductionScheduleResponse(**schedule)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting production schedule for {schedule_date}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/schedules", response_model=List[ProductionScheduleResponse])
async def get_production_schedules(
tenant_id: UUID = Depends(get_tenant_id),
start_date: Optional[date] = Query(None),
end_date: Optional[date] = Query(None),
published_only: bool = Query(False),
db: Session = Depends(get_db)
):
"""Get production schedules within date range"""
try:
production_service = ProductionService(db)
if published_only:
schedules = production_service.get_published_schedules(tenant_id, start_date, end_date)
else:
schedules = production_service.get_production_schedules_range(tenant_id, start_date, end_date)
return [ProductionScheduleResponse(**schedule) for schedule in schedules]
except Exception as e:
logger.error(f"Error getting production schedules: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

View File

@@ -0,0 +1,359 @@
# services/recipes/app/api/recipes.py
"""
API endpoints for recipe management
"""
from fastapi import APIRouter, Depends, HTTPException, Header, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from uuid import UUID
import logging
from ..core.database import get_db
from ..services.recipe_service import RecipeService
from ..schemas.recipes import (
RecipeCreate,
RecipeUpdate,
RecipeResponse,
RecipeSearchRequest,
RecipeDuplicateRequest,
RecipeFeasibilityResponse,
RecipeStatisticsResponse
)
logger = logging.getLogger(__name__)
router = APIRouter()
def get_tenant_id(x_tenant_id: str = Header(...)) -> UUID:
"""Extract tenant ID from header"""
try:
return UUID(x_tenant_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid tenant ID format")
def get_user_id(x_user_id: str = Header(...)) -> UUID:
"""Extract user ID from header"""
try:
return UUID(x_user_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid user ID format")
@router.post("/", response_model=RecipeResponse)
async def create_recipe(
recipe_data: RecipeCreate,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Create a new recipe"""
try:
recipe_service = RecipeService(db)
# Convert Pydantic model to dict
recipe_dict = recipe_data.dict(exclude={"ingredients"})
recipe_dict["tenant_id"] = tenant_id
ingredients_list = [ing.dict() for ing in recipe_data.ingredients]
result = await recipe_service.create_recipe(
recipe_dict,
ingredients_list,
user_id
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating recipe: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{recipe_id}", response_model=RecipeResponse)
async def get_recipe(
recipe_id: UUID,
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get recipe by ID with ingredients"""
try:
recipe_service = RecipeService(db)
recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
# Verify tenant ownership
if recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
return RecipeResponse(**recipe)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.put("/{recipe_id}", response_model=RecipeResponse)
async def update_recipe(
recipe_id: UUID,
recipe_data: RecipeUpdate,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Update an existing recipe"""
try:
recipe_service = RecipeService(db)
# Check if recipe exists and belongs to tenant
existing_recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
# Convert Pydantic model to dict
recipe_dict = recipe_data.dict(exclude={"ingredients"}, exclude_unset=True)
ingredients_list = None
if recipe_data.ingredients is not None:
ingredients_list = [ing.dict() for ing in recipe_data.ingredients]
result = await recipe_service.update_recipe(
recipe_id,
recipe_dict,
ingredients_list,
user_id
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/{recipe_id}")
async def delete_recipe(
recipe_id: UUID,
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Delete a recipe"""
try:
recipe_service = RecipeService(db)
# Check if recipe exists and belongs to tenant
existing_recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
# Use repository to delete
success = recipe_service.recipe_repo.delete(recipe_id)
if not success:
raise HTTPException(status_code=404, detail="Recipe not found")
return {"message": "Recipe deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/", response_model=List[RecipeResponse])
async def search_recipes(
tenant_id: UUID = Depends(get_tenant_id),
search_term: Optional[str] = Query(None),
status: Optional[str] = Query(None),
category: Optional[str] = Query(None),
is_seasonal: Optional[bool] = Query(None),
is_signature: Optional[bool] = Query(None),
difficulty_level: Optional[int] = Query(None, ge=1, le=5),
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db)
):
"""Search recipes with filters"""
try:
recipe_service = RecipeService(db)
recipes = recipe_service.search_recipes(
tenant_id=tenant_id,
search_term=search_term,
status=status,
category=category,
is_seasonal=is_seasonal,
is_signature=is_signature,
difficulty_level=difficulty_level,
limit=limit,
offset=offset
)
return [RecipeResponse(**recipe) for recipe in recipes]
except Exception as e:
logger.error(f"Error searching recipes: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/{recipe_id}/duplicate", response_model=RecipeResponse)
async def duplicate_recipe(
recipe_id: UUID,
duplicate_data: RecipeDuplicateRequest,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Create a duplicate of an existing recipe"""
try:
recipe_service = RecipeService(db)
# Check if original recipe exists and belongs to tenant
existing_recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
result = await recipe_service.duplicate_recipe(
recipe_id,
duplicate_data.new_name,
user_id
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error duplicating recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/{recipe_id}/activate", response_model=RecipeResponse)
async def activate_recipe(
recipe_id: UUID,
tenant_id: UUID = Depends(get_tenant_id),
user_id: UUID = Depends(get_user_id),
db: Session = Depends(get_db)
):
"""Activate a recipe for production"""
try:
recipe_service = RecipeService(db)
# Check if recipe exists and belongs to tenant
existing_recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
result = await recipe_service.activate_recipe(recipe_id, user_id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error activating recipe {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{recipe_id}/feasibility", response_model=RecipeFeasibilityResponse)
async def check_recipe_feasibility(
recipe_id: UUID,
batch_multiplier: float = Query(1.0, gt=0),
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Check if recipe can be produced with current inventory"""
try:
recipe_service = RecipeService(db)
# Check if recipe exists and belongs to tenant
existing_recipe = recipe_service.get_recipe_with_ingredients(recipe_id)
if not existing_recipe:
raise HTTPException(status_code=404, detail="Recipe not found")
if existing_recipe["tenant_id"] != str(tenant_id):
raise HTTPException(status_code=403, detail="Access denied")
result = await recipe_service.check_recipe_feasibility(recipe_id, batch_multiplier)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["error"])
return RecipeFeasibilityResponse(**result["data"])
except HTTPException:
raise
except Exception as e:
logger.error(f"Error checking recipe feasibility {recipe_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/statistics/dashboard", response_model=RecipeStatisticsResponse)
async def get_recipe_statistics(
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get recipe statistics for dashboard"""
try:
recipe_service = RecipeService(db)
stats = recipe_service.get_recipe_statistics(tenant_id)
return RecipeStatisticsResponse(**stats)
except Exception as e:
logger.error(f"Error getting recipe statistics: {e}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/categories/list")
async def get_recipe_categories(
tenant_id: UUID = Depends(get_tenant_id),
db: Session = Depends(get_db)
):
"""Get list of recipe categories used by tenant"""
try:
recipe_service = RecipeService(db)
# Get categories from existing recipes
recipes = recipe_service.search_recipes(tenant_id, limit=1000)
categories = list(set(recipe["category"] for recipe in recipes if recipe["category"]))
categories.sort()
return {"categories": categories}
except Exception as e:
logger.error(f"Error getting recipe categories: {e}")
raise HTTPException(status_code=500, detail="Internal server error")