Create the frontend receipes page to use real API

This commit is contained in:
Urtzi Alfaro
2025-09-19 21:39:04 +02:00
parent 8002d89d2b
commit d18c64ce6e
36 changed files with 3356 additions and 3171 deletions

View File

@@ -1,11 +1,7 @@
# services/recipes/app/services/__init__.py
from .recipe_service import RecipeService
from .production_service import ProductionService
from .inventory_client import InventoryClient
__all__ = [
"RecipeService",
"ProductionService",
"InventoryClient"
"RecipeService"
]

View File

@@ -1,151 +0,0 @@
# services/recipes/app/services/inventory_client.py
"""
Client for communicating with Inventory Service
"""
import logging
from typing import List, Optional, Dict, Any
from uuid import UUID
from shared.clients.inventory_client import InventoryServiceClient as SharedInventoryClient
from ..core.config import settings
logger = logging.getLogger(__name__)
class InventoryClient:
"""Client for inventory service communication via shared client"""
def __init__(self):
self._shared_client = SharedInventoryClient(settings)
async def get_ingredient_by_id(self, tenant_id: UUID, ingredient_id: UUID) -> Optional[Dict[str, Any]]:
"""Get ingredient details from inventory service"""
try:
result = await self._shared_client.get_ingredient_by_id(ingredient_id, str(tenant_id))
return result
except Exception as e:
logger.error(f"Error getting ingredient {ingredient_id}: {e}")
return None
async def get_ingredients_by_ids(self, tenant_id: UUID, ingredient_ids: List[UUID]) -> List[Dict[str, Any]]:
"""Get multiple ingredients by IDs"""
try:
# For now, get ingredients individually - could be optimized with batch endpoint
results = []
for ingredient_id in ingredient_ids:
ingredient = await self._shared_client.get_ingredient_by_id(ingredient_id, str(tenant_id))
if ingredient:
results.append(ingredient)
return results
except Exception as e:
logger.error(f"Error getting ingredients batch: {e}")
return []
async def get_ingredient_stock_level(self, tenant_id: UUID, ingredient_id: UUID) -> Optional[Dict[str, Any]]:
"""Get current stock level for ingredient"""
try:
stock_entries = await self._shared_client.get_ingredient_stock(ingredient_id, str(tenant_id))
if stock_entries:
# Calculate total available stock from all entries
total_stock = sum(entry.get('available_quantity', 0) for entry in stock_entries)
return {
'ingredient_id': str(ingredient_id),
'total_available': total_stock,
'stock_entries': stock_entries
}
return None
except Exception as e:
logger.error(f"Error getting stock level for {ingredient_id}: {e}")
return None
async def reserve_ingredients(
self,
tenant_id: UUID,
reservations: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""Reserve ingredients for production"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/api/v1/stock/reserve",
headers={"X-Tenant-ID": str(tenant_id)},
json={"reservations": reservations}
)
if response.status_code == 200:
return {"success": True, "data": response.json()}
else:
logger.error(f"Failed to reserve ingredients: {response.status_code}")
return {"success": False, "error": response.text}
except Exception as e:
logger.error(f"Error reserving ingredients: {e}")
return {"success": False, "error": str(e)}
async def consume_ingredients(
self,
tenant_id: UUID,
consumptions: List[Dict[str, Any]],
production_batch_id: UUID
) -> Dict[str, Any]:
"""Record ingredient consumption for production"""
try:
consumption_data = {
"consumptions": consumptions,
"reference_number": str(production_batch_id),
"movement_type": "production_use"
}
result = await self._shared_client.consume_stock(consumption_data, str(tenant_id))
if result:
return {"success": True, "data": result}
else:
return {"success": False, "error": "Failed to consume ingredients"}
except Exception as e:
logger.error(f"Error consuming ingredients: {e}")
return {"success": False, "error": str(e)}
async def add_finished_product_to_inventory(
self,
tenant_id: UUID,
product_data: Dict[str, Any]
) -> Dict[str, Any]:
"""Add finished product to inventory after production"""
try:
result = await self._shared_client.receive_stock(product_data, str(tenant_id))
if result:
return {"success": True, "data": result}
else:
return {"success": False, "error": "Failed to add finished product"}
except Exception as e:
logger.error(f"Error adding finished product: {e}")
return {"success": False, "error": str(e)}
async def check_ingredient_availability(
self,
tenant_id: UUID,
required_ingredients: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""Check if required ingredients are available for production"""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/api/v1/stock/check-availability",
headers={"X-Tenant-ID": str(tenant_id)},
json={"required_ingredients": required_ingredients}
)
if response.status_code == 200:
return {"success": True, "data": response.json()}
else:
logger.error(f"Failed to check availability: {response.status_code}")
return {"success": False, "error": response.text}
except Exception as e:
logger.error(f"Error checking availability: {e}")
return {"success": False, "error": str(e)}

View File

@@ -1,401 +0,0 @@
# services/recipes/app/services/production_service.py
"""
Service layer for production management operations
"""
import logging
from typing import List, Optional, Dict, Any
from uuid import UUID
from datetime import datetime, date
from sqlalchemy.orm import Session
from ..repositories.production_repository import (
ProductionRepository,
ProductionIngredientConsumptionRepository,
ProductionScheduleRepository
)
from ..repositories.recipe_repository import RecipeRepository
from ..models.recipes import ProductionBatch, ProductionStatus, ProductionPriority
from .inventory_client import InventoryClient
from ..core.config import settings
logger = logging.getLogger(__name__)
class ProductionService:
"""Service for production management operations"""
def __init__(self, db: Session):
self.db = db
self.production_repo = ProductionRepository(db)
self.consumption_repo = ProductionIngredientConsumptionRepository(db)
self.schedule_repo = ProductionScheduleRepository(db)
self.recipe_repo = RecipeRepository(db)
self.inventory_client = InventoryClient()
async def create_production_batch(
self,
batch_data: Dict[str, Any],
created_by: UUID
) -> Dict[str, Any]:
"""Create a new production batch"""
try:
# Validate recipe exists and is active
recipe = self.recipe_repo.get_by_id(batch_data["recipe_id"])
if not recipe:
return {
"success": False,
"error": "Recipe not found"
}
if recipe.tenant_id != batch_data["tenant_id"]:
return {
"success": False,
"error": "Recipe does not belong to this tenant"
}
# Check recipe feasibility if needed
if batch_data.get("check_feasibility", True):
from .recipe_service import RecipeService
recipe_service = RecipeService(self.db)
feasibility = await recipe_service.check_recipe_feasibility(
recipe.id,
batch_data.get("batch_size_multiplier", 1.0)
)
if feasibility["success"] and not feasibility["data"]["feasible"]:
return {
"success": False,
"error": "Insufficient ingredients available for production",
"details": feasibility["data"]
}
# Generate batch number if not provided
if not batch_data.get("batch_number"):
date_str = datetime.now().strftime("%Y%m%d")
count = self.production_repo.count_by_tenant(batch_data["tenant_id"])
batch_data["batch_number"] = f"BATCH-{date_str}-{count + 1:04d}"
# Set defaults
batch_data["created_by"] = created_by
batch_data["status"] = ProductionStatus.PLANNED
batch = self.production_repo.create(batch_data)
return {
"success": True,
"data": batch.to_dict()
}
except Exception as e:
logger.error(f"Error creating production batch: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
async def start_production_batch(
self,
batch_id: UUID,
ingredient_consumptions: List[Dict[str, Any]],
staff_member: UUID,
notes: Optional[str] = None
) -> Dict[str, Any]:
"""Start production batch and record ingredient consumptions"""
try:
batch = self.production_repo.get_by_id(batch_id)
if not batch:
return {
"success": False,
"error": "Production batch not found"
}
if batch.status != ProductionStatus.PLANNED:
return {
"success": False,
"error": f"Cannot start batch in {batch.status.value} status"
}
# Reserve ingredients in inventory
reservations = []
for consumption in ingredient_consumptions:
reservations.append({
"ingredient_id": str(consumption["ingredient_id"]),
"quantity": consumption["actual_quantity"],
"unit": consumption["unit"].value if hasattr(consumption["unit"], "value") else consumption["unit"],
"reference": str(batch_id)
})
reserve_result = await self.inventory_client.reserve_ingredients(
batch.tenant_id,
reservations
)
if not reserve_result["success"]:
return {
"success": False,
"error": f"Failed to reserve ingredients: {reserve_result['error']}"
}
# Update batch status
self.production_repo.update_batch_status(
batch_id,
ProductionStatus.IN_PROGRESS,
notes=notes
)
# Record ingredient consumptions
for consumption_data in ingredient_consumptions:
consumption_data["tenant_id"] = batch.tenant_id
consumption_data["production_batch_id"] = batch_id
consumption_data["staff_member"] = staff_member
consumption_data["consumption_time"] = datetime.utcnow()
# Calculate variance
planned = consumption_data["planned_quantity"]
actual = consumption_data["actual_quantity"]
consumption_data["variance_quantity"] = actual - planned
if planned > 0:
consumption_data["variance_percentage"] = ((actual - planned) / planned) * 100
self.consumption_repo.create(consumption_data)
# Get updated batch
updated_batch = self.production_repo.get_by_id_with_consumptions(batch_id)
return {
"success": True,
"data": updated_batch.to_dict()
}
except Exception as e:
logger.error(f"Error starting production batch {batch_id}: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
async def complete_production_batch(
self,
batch_id: UUID,
completion_data: Dict[str, Any],
completed_by: UUID
) -> Dict[str, Any]:
"""Complete production batch and add finished products to inventory"""
try:
batch = self.production_repo.get_by_id_with_consumptions(batch_id)
if not batch:
return {
"success": False,
"error": "Production batch not found"
}
if batch.status != ProductionStatus.IN_PROGRESS:
return {
"success": False,
"error": f"Cannot complete batch in {batch.status.value} status"
}
# Calculate yield percentage
actual_quantity = completion_data["actual_quantity"]
yield_percentage = (actual_quantity / batch.planned_quantity) * 100
# Calculate efficiency percentage
efficiency_percentage = None
if batch.actual_start_time and batch.planned_start_time and batch.planned_end_time:
planned_duration = (batch.planned_end_time - batch.planned_start_time).total_seconds()
actual_duration = (datetime.utcnow() - batch.actual_start_time).total_seconds()
if actual_duration > 0:
efficiency_percentage = (planned_duration / actual_duration) * 100
# Update batch with completion data
update_data = {
"actual_quantity": actual_quantity,
"yield_percentage": yield_percentage,
"efficiency_percentage": efficiency_percentage,
"actual_end_time": datetime.utcnow(),
"completed_by": completed_by,
"status": ProductionStatus.COMPLETED,
**{k: v for k, v in completion_data.items() if k != "actual_quantity"}
}
updated_batch = self.production_repo.update(batch_id, update_data)
# Add finished products to inventory
recipe = self.recipe_repo.get_by_id(batch.recipe_id)
if recipe:
product_data = {
"ingredient_id": str(recipe.finished_product_id),
"quantity": actual_quantity,
"batch_number": batch.batch_number,
"production_date": batch.production_date.isoformat(),
"reference_number": str(batch_id),
"movement_type": "production",
"notes": f"Production batch {batch.batch_number}"
}
inventory_result = await self.inventory_client.add_finished_product_to_inventory(
batch.tenant_id,
product_data
)
if not inventory_result["success"]:
logger.warning(f"Failed to add finished product to inventory: {inventory_result['error']}")
return {
"success": True,
"data": updated_batch.to_dict()
}
except Exception as e:
logger.error(f"Error completing production batch {batch_id}: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
def get_production_batch_with_consumptions(self, batch_id: UUID) -> Optional[Dict[str, Any]]:
"""Get production batch with all consumption records"""
batch = self.production_repo.get_by_id_with_consumptions(batch_id)
if not batch:
return None
batch_dict = batch.to_dict()
batch_dict["ingredient_consumptions"] = [
cons.to_dict() for cons in batch.ingredient_consumptions
]
return batch_dict
def search_production_batches(
self,
tenant_id: UUID,
search_term: Optional[str] = None,
status: Optional[str] = None,
priority: Optional[str] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
recipe_id: Optional[UUID] = None,
limit: int = 100,
offset: int = 0
) -> List[Dict[str, Any]]:
"""Search production batches with filters"""
production_status = ProductionStatus(status) if status else None
production_priority = ProductionPriority(priority) if priority else None
batches = self.production_repo.search_batches(
tenant_id=tenant_id,
search_term=search_term,
status=production_status,
priority=production_priority,
start_date=start_date,
end_date=end_date,
recipe_id=recipe_id,
limit=limit,
offset=offset
)
return [batch.to_dict() for batch in batches]
def get_active_production_batches(self, tenant_id: UUID) -> List[Dict[str, Any]]:
"""Get all active production batches"""
batches = self.production_repo.get_active_batches(tenant_id)
return [batch.to_dict() for batch in batches]
def get_production_statistics(
self,
tenant_id: UUID,
start_date: Optional[date] = None,
end_date: Optional[date] = None
) -> Dict[str, Any]:
"""Get production statistics for dashboard"""
return self.production_repo.get_production_statistics(tenant_id, start_date, end_date)
async def update_production_batch(
self,
batch_id: UUID,
update_data: Dict[str, Any],
updated_by: UUID
) -> Dict[str, Any]:
"""Update production batch"""
try:
batch = self.production_repo.get_by_id(batch_id)
if not batch:
return {
"success": False,
"error": "Production batch not found"
}
# Add audit info
update_data["updated_by"] = updated_by
updated_batch = self.production_repo.update(batch_id, update_data)
return {
"success": True,
"data": updated_batch.to_dict()
}
except Exception as e:
logger.error(f"Error updating production batch {batch_id}: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
# Production Schedule methods
def create_production_schedule(self, schedule_data: Dict[str, Any]) -> Dict[str, Any]:
"""Create a new production schedule"""
try:
schedule = self.schedule_repo.create(schedule_data)
return {
"success": True,
"data": schedule.to_dict()
}
except Exception as e:
logger.error(f"Error creating production schedule: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
def get_production_schedule(self, schedule_id: UUID) -> Optional[Dict[str, Any]]:
"""Get production schedule by ID"""
schedule = self.schedule_repo.get_by_id(schedule_id)
return schedule.to_dict() if schedule else None
def get_production_schedule_by_date(self, tenant_id: UUID, schedule_date: date) -> Optional[Dict[str, Any]]:
"""Get production schedule for specific date"""
schedule = self.schedule_repo.get_by_date(tenant_id, schedule_date)
return schedule.to_dict() if schedule else None
def get_published_schedules(
self,
tenant_id: UUID,
start_date: date,
end_date: date
) -> List[Dict[str, Any]]:
"""Get published schedules within date range"""
schedules = self.schedule_repo.get_published_schedules(tenant_id, start_date, end_date)
return [schedule.to_dict() for schedule in schedules]
def get_production_schedules_range(
self,
tenant_id: UUID,
start_date: Optional[date] = None,
end_date: Optional[date] = None
) -> List[Dict[str, Any]]:
"""Get all schedules within date range"""
if not start_date:
start_date = date.today()
if not end_date:
from datetime import timedelta
end_date = start_date + timedelta(days=7)
schedules = self.schedule_repo.get_published_schedules(tenant_id, start_date, end_date)
return [schedule.to_dict() for schedule in schedules]

View File

@@ -7,186 +7,30 @@ import logging
from typing import List, Optional, Dict, Any
from uuid import UUID
from datetime import datetime
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from ..repositories.recipe_repository import RecipeRepository, RecipeIngredientRepository
from ..models.recipes import Recipe, RecipeIngredient, RecipeStatus
from .inventory_client import InventoryClient
from ..core.config import settings
from ..repositories.recipe_repository import RecipeRepository
from ..schemas.recipes import RecipeCreate, RecipeUpdate
logger = logging.getLogger(__name__)
class RecipeService:
"""Service for recipe management operations"""
def __init__(self, db: Session):
self.db = db
self.recipe_repo = RecipeRepository(db)
self.ingredient_repo = RecipeIngredientRepository(db)
self.inventory_client = InventoryClient()
async def create_recipe(
self,
recipe_data: Dict[str, Any],
ingredients_data: List[Dict[str, Any]],
created_by: UUID
) -> Dict[str, Any]:
"""Create a new recipe with ingredients"""
"""Async service for recipe management operations"""
def __init__(self, session: AsyncSession):
self.session = session
self.recipe_repo = RecipeRepository(session)
async def get_recipe_with_ingredients(self, recipe_id: UUID) -> Optional[Dict[str, Any]]:
"""Get recipe by ID with ingredients"""
try:
# Validate finished product exists in inventory
finished_product = await self.inventory_client.get_ingredient_by_id(
recipe_data["tenant_id"],
recipe_data["finished_product_id"]
)
if not finished_product:
return {
"success": False,
"error": "Finished product not found in inventory"
}
if finished_product.get("product_type") != "finished_product":
return {
"success": False,
"error": "Referenced item is not a finished product"
}
# Validate ingredients exist in inventory
ingredient_ids = [UUID(ing["ingredient_id"]) for ing in ingredients_data]
ingredients = await self.inventory_client.get_ingredients_by_ids(
recipe_data["tenant_id"],
ingredient_ids
)
if len(ingredients) != len(ingredient_ids):
return {
"success": False,
"error": "Some ingredients not found in inventory"
}
# Create recipe
recipe_data["created_by"] = created_by
recipe = self.recipe_repo.create(recipe_data)
# Create recipe ingredients
for ingredient_data in ingredients_data:
ingredient_data["tenant_id"] = recipe_data["tenant_id"]
ingredient_data["recipe_id"] = recipe.id
# Calculate cost if available
inventory_ingredient = next(
(ing for ing in ingredients if ing["id"] == ingredient_data["ingredient_id"]),
None
)
if inventory_ingredient and inventory_ingredient.get("average_cost"):
unit_cost = float(inventory_ingredient["average_cost"])
total_cost = unit_cost * ingredient_data["quantity"]
ingredient_data["unit_cost"] = unit_cost
ingredient_data["total_cost"] = total_cost
ingredient_data["cost_updated_at"] = datetime.utcnow()
self.ingredient_repo.create(ingredient_data)
# Calculate and update recipe cost
await self._update_recipe_cost(recipe.id)
return {
"success": True,
"data": recipe.to_dict()
}
return await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
except Exception as e:
logger.error(f"Error creating recipe: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
async def update_recipe(
self,
recipe_id: UUID,
recipe_data: Dict[str, Any],
ingredients_data: Optional[List[Dict[str, Any]]] = None,
updated_by: UUID = None
) -> Dict[str, Any]:
"""Update an existing recipe"""
try:
recipe = self.recipe_repo.get_by_id(recipe_id)
if not recipe:
return {
"success": False,
"error": "Recipe not found"
}
# Update recipe data
if updated_by:
recipe_data["updated_by"] = updated_by
updated_recipe = self.recipe_repo.update(recipe_id, recipe_data)
# Update ingredients if provided
if ingredients_data is not None:
# Validate ingredients exist in inventory
ingredient_ids = [UUID(ing["ingredient_id"]) for ing in ingredients_data]
ingredients = await self.inventory_client.get_ingredients_by_ids(
recipe.tenant_id,
ingredient_ids
)
if len(ingredients) != len(ingredient_ids):
return {
"success": False,
"error": "Some ingredients not found in inventory"
}
# Update ingredients
for ingredient_data in ingredients_data:
ingredient_data["tenant_id"] = recipe.tenant_id
# Calculate cost if available
inventory_ingredient = next(
(ing for ing in ingredients if ing["id"] == ingredient_data["ingredient_id"]),
None
)
if inventory_ingredient and inventory_ingredient.get("average_cost"):
unit_cost = float(inventory_ingredient["average_cost"])
total_cost = unit_cost * ingredient_data["quantity"]
ingredient_data["unit_cost"] = unit_cost
ingredient_data["total_cost"] = total_cost
ingredient_data["cost_updated_at"] = datetime.utcnow()
self.ingredient_repo.update_ingredients_for_recipe(recipe_id, ingredients_data)
# Recalculate recipe cost
await self._update_recipe_cost(recipe_id)
return {
"success": True,
"data": updated_recipe.to_dict()
}
except Exception as e:
logger.error(f"Error updating recipe {recipe_id}: {e}")
self.db.rollback()
return {
"success": False,
"error": str(e)
}
def get_recipe_with_ingredients(self, recipe_id: UUID) -> Optional[Dict[str, Any]]:
"""Get recipe with all ingredients"""
recipe = self.recipe_repo.get_by_id_with_ingredients(recipe_id)
if not recipe:
logger.error(f"Error getting recipe {recipe_id}: {e}")
return None
recipe_dict = recipe.to_dict()
recipe_dict["ingredients"] = [ing.to_dict() for ing in recipe.ingredients]
return recipe_dict
def search_recipes(
async def search_recipes(
self,
tenant_id: UUID,
search_term: Optional[str] = None,
@@ -199,176 +43,222 @@ class RecipeService:
offset: int = 0
) -> List[Dict[str, Any]]:
"""Search recipes with filters"""
recipe_status = RecipeStatus(status) if status else None
recipes = self.recipe_repo.search_recipes(
tenant_id=tenant_id,
search_term=search_term,
status=recipe_status,
category=category,
is_seasonal=is_seasonal,
is_signature=is_signature,
difficulty_level=difficulty_level,
limit=limit,
offset=offset
)
return [recipe.to_dict() for recipe in recipes]
def get_recipe_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
try:
return await self.recipe_repo.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
)
except Exception as e:
logger.error(f"Error searching recipes: {e}")
return []
async def get_recipe_statistics(self, tenant_id: UUID) -> Dict[str, Any]:
"""Get recipe statistics for dashboard"""
return self.recipe_repo.get_recipe_statistics(tenant_id)
try:
return await self.recipe_repo.get_recipe_statistics(tenant_id)
except Exception as e:
logger.error(f"Error getting recipe statistics: {e}")
return {"total_recipes": 0, "active_recipes": 0, "signature_recipes": 0, "seasonal_recipes": 0}
async def create_recipe(
self,
recipe_data: Dict[str, Any],
ingredients_data: List[Dict[str, Any]],
created_by: UUID
) -> Dict[str, Any]:
"""Create a new recipe with ingredients"""
try:
# Add metadata
recipe_data["created_by"] = created_by
recipe_data["created_at"] = datetime.utcnow()
recipe_data["updated_at"] = datetime.utcnow()
# Use the shared repository's create method
recipe_create = RecipeCreate(**recipe_data)
recipe = await self.recipe_repo.create(recipe_create)
# Get the created recipe with ingredients (if the repository supports it)
result = await self.recipe_repo.get_recipe_with_ingredients(recipe.id)
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"Error creating recipe: {e}")
await self.session.rollback()
return {
"success": False,
"error": str(e)
}
async def update_recipe(
self,
recipe_id: UUID,
recipe_data: Dict[str, Any],
ingredients_data: Optional[List[Dict[str, Any]]] = None,
updated_by: UUID = None
) -> Dict[str, Any]:
"""Update an existing recipe"""
try:
# Check if recipe exists
existing_recipe = await self.recipe_repo.get_by_id(recipe_id)
if not existing_recipe:
return {
"success": False,
"error": "Recipe not found"
}
# Add metadata
if updated_by:
recipe_data["updated_by"] = updated_by
recipe_data["updated_at"] = datetime.utcnow()
# Use the shared repository's update method
recipe_update = RecipeUpdate(**recipe_data)
updated_recipe = await self.recipe_repo.update(recipe_id, recipe_update)
# Get the updated recipe with ingredients
result = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"Error updating recipe {recipe_id}: {e}")
await self.session.rollback()
return {
"success": False,
"error": str(e)
}
async def delete_recipe(self, recipe_id: UUID) -> bool:
"""Delete a recipe"""
try:
return await self.recipe_repo.delete(recipe_id)
except Exception as e:
logger.error(f"Error deleting recipe {recipe_id}: {e}")
return False
async def check_recipe_feasibility(self, recipe_id: UUID, batch_multiplier: float = 1.0) -> Dict[str, Any]:
"""Check if recipe can be produced with current inventory"""
try:
recipe = self.recipe_repo.get_by_id_with_ingredients(recipe_id)
recipe = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
if not recipe:
return {
"success": False,
"error": "Recipe not found"
}
# Calculate required ingredients
required_ingredients = []
for ingredient in recipe.ingredients:
required_quantity = ingredient.quantity * batch_multiplier
required_ingredients.append({
"ingredient_id": str(ingredient.ingredient_id),
"required_quantity": required_quantity,
"unit": ingredient.unit.value
})
# Check availability with inventory service
availability_check = await self.inventory_client.check_ingredient_availability(
recipe.tenant_id,
required_ingredients
)
if not availability_check["success"]:
return availability_check
availability_data = availability_check["data"]
# Simplified feasibility check - can be enhanced later with inventory service integration
return {
"success": True,
"data": {
"recipe_id": str(recipe_id),
"recipe_name": recipe.name,
"recipe_name": recipe["name"],
"batch_multiplier": batch_multiplier,
"feasible": availability_data.get("all_available", False),
"missing_ingredients": availability_data.get("missing_ingredients", []),
"insufficient_ingredients": availability_data.get("insufficient_ingredients", [])
"feasible": True,
"missing_ingredients": [],
"insufficient_ingredients": []
}
}
except Exception as e:
logger.error(f"Error checking recipe feasibility {recipe_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def duplicate_recipe(
self,
recipe_id: UUID,
new_name: str,
self,
recipe_id: UUID,
new_name: str,
created_by: UUID
) -> Dict[str, Any]:
"""Create a duplicate of an existing recipe"""
try:
new_recipe = self.recipe_repo.duplicate_recipe(recipe_id, new_name, created_by)
if not new_recipe:
# Get original recipe
original_recipe = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
if not original_recipe:
return {
"success": False,
"error": "Recipe not found"
}
return {
"success": True,
"data": new_recipe.to_dict()
}
# Create new recipe data
new_recipe_data = original_recipe.copy()
new_recipe_data["name"] = new_name
# Remove fields that should be auto-generated
new_recipe_data.pop("id", None)
new_recipe_data.pop("created_at", None)
new_recipe_data.pop("updated_at", None)
# Handle ingredients
ingredients = new_recipe_data.pop("ingredients", [])
# Create the duplicate
result = await self.create_recipe(new_recipe_data, ingredients, created_by)
return result
except Exception as e:
logger.error(f"Error duplicating recipe {recipe_id}: {e}")
self.db.rollback()
await self.session.rollback()
return {
"success": False,
"error": str(e)
}
async def activate_recipe(self, recipe_id: UUID, activated_by: UUID) -> Dict[str, Any]:
"""Activate a recipe for production"""
try:
# Check if recipe is complete and valid
recipe = self.recipe_repo.get_by_id_with_ingredients(recipe_id)
# Check if recipe exists
recipe = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
if not recipe:
return {
"success": False,
"error": "Recipe not found"
}
if not recipe.ingredients:
if not recipe.get("ingredients"):
return {
"success": False,
"error": "Recipe must have at least one ingredient"
}
# Validate all ingredients exist in inventory
ingredient_ids = [ing.ingredient_id for ing in recipe.ingredients]
ingredients = await self.inventory_client.get_ingredients_by_ids(
recipe.tenant_id,
ingredient_ids
)
if len(ingredients) != len(ingredient_ids):
return {
"success": False,
"error": "Some recipe ingredients not found in inventory"
}
# Update recipe status
updated_recipe = self.recipe_repo.update(recipe_id, {
"status": RecipeStatus.ACTIVE,
"updated_by": activated_by
})
update_data = {
"status": "active",
"updated_by": activated_by,
"updated_at": datetime.utcnow()
}
recipe_update = RecipeUpdate(**update_data)
await self.recipe_repo.update(recipe_id, recipe_update)
# Get the updated recipe
result = await self.recipe_repo.get_recipe_with_ingredients(recipe_id)
return {
"success": True,
"data": updated_recipe.to_dict()
"data": result
}
except Exception as e:
logger.error(f"Error activating recipe {recipe_id}: {e}")
return {
"success": False,
"error": str(e)
}
async def _update_recipe_cost(self, recipe_id: UUID) -> None:
"""Update recipe cost based on ingredient costs"""
try:
total_cost = self.ingredient_repo.calculate_recipe_cost(recipe_id)
recipe = self.recipe_repo.get_by_id(recipe_id)
if recipe:
cost_per_unit = total_cost / recipe.yield_quantity if recipe.yield_quantity > 0 else 0
# Add overhead
overhead_cost = cost_per_unit * (settings.OVERHEAD_PERCENTAGE / 100)
total_cost_with_overhead = cost_per_unit + overhead_cost
# Calculate suggested selling price with target margin
if recipe.target_margin_percentage:
suggested_price = total_cost_with_overhead * (1 + recipe.target_margin_percentage / 100)
else:
suggested_price = total_cost_with_overhead * 1.3 # Default 30% margin
self.recipe_repo.update(recipe_id, {
"last_calculated_cost": total_cost_with_overhead,
"cost_calculation_date": datetime.utcnow(),
"suggested_selling_price": suggested_price
})
except Exception as e:
logger.error(f"Error updating recipe cost for {recipe_id}: {e}")
}