# 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""" def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"): super().__init__(calling_service_name, config) 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: result = await self.get(f"recipes/recipes/{recipe_id}", tenant_id=tenant_id) 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)} result = await self.get("recipes/recipes/by-products", tenant_id=tenant_id, params=params) 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) result = await self.get("recipes/requirements", tenant_id=tenant_id, params=params) 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) result = await self.get("recipes/ingredient-requirements", tenant_id=tenant_id, params=params) 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 } result = await self.post("recipes/operations/calculate-ingredients", data=data, tenant_id=tenant_id) 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} result = await self.post("recipes/operations/calculate-batch-ingredients", data=data, tenant_id=tenant_id) 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: result = await self.get(f"recipes/recipes/{recipe_id}/production-instructions", tenant_id=tenant_id) 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: result = await self.get(f"recipes/recipes/{recipe_id}/yield", tenant_id=tenant_id) 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 } result = await self.post("recipes/operations/validate-feasibility", data=data, tenant_id=tenant_id) 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: result = await self.get(f"recipes/recipes/{recipe_id}/cost-analysis", tenant_id=tenant_id) 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} result = await self.post("recipes/operations/optimize-batch", data=data, tenant_id=tenant_id) 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: result = await self.get("recipes/dashboard/summary", tenant_id=tenant_id) 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} result = await self.get("recipes/analytics/popular-recipes", tenant_id=tenant_id, params=params) 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 [] # ================================================================ # 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 # ================================================================ # UTILITY METHODS # ================================================================ 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)