2025-08-21 20:28:14 +02:00
|
|
|
# shared/clients/recipes_client.py
|
|
|
|
|
"""
|
|
|
|
|
Recipes Service Client for Inter-Service Communication
|
|
|
|
|
Provides access to recipe and ingredient requirements from other services
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import structlog
|
|
|
|
|
from typing import Dict, Any, Optional, List
|
|
|
|
|
from uuid import UUID
|
|
|
|
|
from shared.clients.base_service_client import BaseServiceClient
|
|
|
|
|
from shared.config.base import BaseServiceSettings
|
|
|
|
|
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RecipesServiceClient(BaseServiceClient):
|
|
|
|
|
"""Client for communicating with the Recipes Service"""
|
|
|
|
|
|
2025-11-05 13:34:56 +01:00
|
|
|
def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"):
|
|
|
|
|
super().__init__(calling_service_name, config)
|
2025-08-21 20:28:14 +02:00
|
|
|
|
|
|
|
|
def get_service_base_path(self) -> str:
|
|
|
|
|
return "/api/v1"
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
# RECIPE MANAGEMENT
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
async def get_recipe_by_id(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get recipe details by ID"""
|
|
|
|
|
try:
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get(f"recipes/recipes/{recipe_id}", tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved recipe details from recipes service",
|
|
|
|
|
recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipe details",
|
|
|
|
|
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def get_recipes_by_product_ids(self, tenant_id: str, product_ids: List[str]) -> Optional[List[Dict[str, Any]]]:
|
|
|
|
|
"""Get recipes for multiple products"""
|
|
|
|
|
try:
|
|
|
|
|
params = {"product_ids": ",".join(product_ids)}
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get("recipes/recipes/by-products", tenant_id=tenant_id, params=params)
|
2025-08-21 20:28:14 +02:00
|
|
|
recipes = result.get('recipes', []) if result else []
|
|
|
|
|
logger.info("Retrieved recipes by product IDs from recipes service",
|
|
|
|
|
product_ids_count=len(product_ids),
|
|
|
|
|
recipes_count=len(recipes),
|
|
|
|
|
tenant_id=tenant_id)
|
|
|
|
|
return recipes
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipes by product IDs",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def get_all_recipes(self, tenant_id: str, is_active: Optional[bool] = True) -> Optional[List[Dict[str, Any]]]:
|
|
|
|
|
"""Get all recipes for a tenant"""
|
|
|
|
|
try:
|
|
|
|
|
params = {}
|
|
|
|
|
if is_active is not None:
|
|
|
|
|
params["is_active"] = is_active
|
|
|
|
|
|
|
|
|
|
result = await self.get_paginated("recipes", tenant_id=tenant_id, params=params)
|
|
|
|
|
logger.info("Retrieved all recipes from recipes service",
|
|
|
|
|
recipes_count=len(result), tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting all recipes",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
# INGREDIENT REQUIREMENTS
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
async def get_recipe_requirements(self, tenant_id: str, recipe_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get ingredient requirements for recipes"""
|
|
|
|
|
try:
|
|
|
|
|
params = {}
|
|
|
|
|
if recipe_ids:
|
|
|
|
|
params["recipe_ids"] = ",".join(recipe_ids)
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get("recipes/requirements", tenant_id=tenant_id, params=params)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved recipe requirements from recipes service",
|
|
|
|
|
recipe_ids_count=len(recipe_ids) if recipe_ids else 0,
|
|
|
|
|
tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipe requirements",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def get_ingredient_requirements(self, tenant_id: str, product_ids: Optional[List[str]] = None) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get ingredient requirements for production planning"""
|
|
|
|
|
try:
|
|
|
|
|
params = {}
|
|
|
|
|
if product_ids:
|
|
|
|
|
params["product_ids"] = ",".join(product_ids)
|
|
|
|
|
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get("recipes/ingredient-requirements", tenant_id=tenant_id, params=params)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved ingredient requirements from recipes service",
|
|
|
|
|
product_ids_count=len(product_ids) if product_ids else 0,
|
|
|
|
|
tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting ingredient requirements",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def calculate_ingredients_for_quantity(self, tenant_id: str, recipe_id: str, quantity: float) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Calculate ingredient quantities needed for a specific production quantity"""
|
|
|
|
|
try:
|
|
|
|
|
data = {
|
|
|
|
|
"recipe_id": recipe_id,
|
|
|
|
|
"quantity": quantity
|
|
|
|
|
}
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.post("recipes/operations/calculate-ingredients", data=data, tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Calculated ingredient quantities from recipes service",
|
|
|
|
|
recipe_id=recipe_id, quantity=quantity, tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error calculating ingredient quantities",
|
|
|
|
|
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def calculate_batch_ingredients(self, tenant_id: str, production_requests: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Calculate total ingredient requirements for multiple production batches"""
|
|
|
|
|
try:
|
|
|
|
|
data = {"production_requests": production_requests}
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.post("recipes/operations/calculate-batch-ingredients", data=data, tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Calculated batch ingredient requirements from recipes service",
|
|
|
|
|
batches_count=len(production_requests), tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error calculating batch ingredient requirements",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
# PRODUCTION SUPPORT
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
async def get_production_instructions(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get detailed production instructions for a recipe"""
|
|
|
|
|
try:
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get(f"recipes/recipes/{recipe_id}/production-instructions", tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved production instructions from recipes service",
|
|
|
|
|
recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting production instructions",
|
|
|
|
|
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def get_recipe_yield_info(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get yield information for a recipe"""
|
|
|
|
|
try:
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get(f"recipes/recipes/{recipe_id}/yield", tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved recipe yield info from recipes service",
|
|
|
|
|
recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipe yield info",
|
|
|
|
|
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def validate_recipe_feasibility(self, tenant_id: str, recipe_id: str, quantity: float) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Validate if a recipe can be produced in the requested quantity"""
|
|
|
|
|
try:
|
|
|
|
|
data = {
|
|
|
|
|
"recipe_id": recipe_id,
|
|
|
|
|
"quantity": quantity
|
|
|
|
|
}
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.post("recipes/operations/validate-feasibility", data=data, tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Validated recipe feasibility from recipes service",
|
|
|
|
|
recipe_id=recipe_id, quantity=quantity, tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error validating recipe feasibility",
|
|
|
|
|
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
# ANALYTICS AND OPTIMIZATION
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
async def get_recipe_cost_analysis(self, tenant_id: str, recipe_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get cost analysis for a recipe"""
|
|
|
|
|
try:
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get(f"recipes/recipes/{recipe_id}/cost-analysis", tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved recipe cost analysis from recipes service",
|
|
|
|
|
recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipe cost analysis",
|
|
|
|
|
error=str(e), recipe_id=recipe_id, tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def optimize_production_batch(self, tenant_id: str, requirements: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Optimize production batch to minimize waste and cost"""
|
|
|
|
|
try:
|
|
|
|
|
data = {"requirements": requirements}
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.post("recipes/operations/optimize-batch", data=data, tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Optimized production batch from recipes service",
|
|
|
|
|
requirements_count=len(requirements), tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error optimizing production batch",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# ================================================================
|
|
|
|
|
# DASHBOARD AND ANALYTICS
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
async def get_dashboard_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""Get recipes dashboard summary data"""
|
|
|
|
|
try:
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get("recipes/dashboard/summary", tenant_id=tenant_id)
|
2025-08-21 20:28:14 +02:00
|
|
|
if result:
|
|
|
|
|
logger.info("Retrieved recipes dashboard summary",
|
|
|
|
|
tenant_id=tenant_id)
|
|
|
|
|
return result
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipes dashboard summary",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
async def get_popular_recipes(self, tenant_id: str, period: str = "last_30_days") -> Optional[List[Dict[str, Any]]]:
|
|
|
|
|
"""Get most popular recipes based on production frequency"""
|
|
|
|
|
try:
|
|
|
|
|
params = {"period": period}
|
2025-10-06 15:27:01 +02:00
|
|
|
result = await self.get("recipes/analytics/popular-recipes", tenant_id=tenant_id, params=params)
|
2025-08-21 20:28:14 +02:00
|
|
|
recipes = result.get('recipes', []) if result else []
|
|
|
|
|
logger.info("Retrieved popular recipes from recipes service",
|
|
|
|
|
period=period, recipes_count=len(recipes), tenant_id=tenant_id)
|
|
|
|
|
return recipes
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting popular recipes",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return []
|
|
|
|
|
|
2025-10-23 07:44:54 +02:00
|
|
|
# ================================================================
|
|
|
|
|
# COUNT AND STATISTICS
|
|
|
|
|
# ================================================================
|
|
|
|
|
|
|
|
|
|
async def count_recipes(self, tenant_id: str) -> int:
|
|
|
|
|
"""
|
|
|
|
|
Get the count of recipes for a tenant
|
|
|
|
|
Used for subscription limit tracking
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
int: Number of recipes for the tenant
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
result = await self.get("recipes/count", tenant_id=tenant_id)
|
|
|
|
|
count = result.get('count', 0) if result else 0
|
|
|
|
|
logger.info("Retrieved recipe count from recipes service",
|
|
|
|
|
count=count, tenant_id=tenant_id)
|
|
|
|
|
return count
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Error getting recipe count",
|
|
|
|
|
error=str(e), tenant_id=tenant_id)
|
|
|
|
|
return 0
|
|
|
|
|
|
2025-08-21 20:28:14 +02:00
|
|
|
# ================================================================
|
|
|
|
|
# UTILITY METHODS
|
|
|
|
|
# ================================================================
|
2025-10-23 07:44:54 +02:00
|
|
|
|
2025-08-21 20:28:14 +02:00
|
|
|
async def health_check(self) -> bool:
|
|
|
|
|
"""Check if recipes service is healthy"""
|
|
|
|
|
try:
|
|
|
|
|
result = await self.get("../health") # Health endpoint is not tenant-scoped
|
|
|
|
|
return result is not None
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Recipes service health check failed", error=str(e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Factory function for dependency injection
|
|
|
|
|
def create_recipes_client(config: BaseServiceSettings) -> RecipesServiceClient:
|
|
|
|
|
"""Create recipes service client instance"""
|
|
|
|
|
return RecipesServiceClient(config)
|