Files
bakery-ia/shared/clients/suppliers_client.py
Urtzi Alfaro c68d82ca7f Fix critical bugs and standardize service integrations
Critical Fixes:
- Orchestrator: Add missing OrchestrationStatus import (fixes HTTP 500 during demo clone)
- Procurement: Migrate from custom cache utils to shared Redis utils
- Suppliers: Use proper Settings for Redis configuration with TLS/auth
- Recipes/Suppliers clients: Fix endpoint paths (remove duplicate path segments)
- Procurement client: Use suppliers service directly for supplier details

Details:
1. services/orchestrator/app/api/internal_demo.py:
   - Added OrchestrationStatus import to fix cloning error
   - This was causing HTTP 500 errors during demo session cloning

2. services/procurement/app/api/purchase_orders.py + service:
   - Replaced app.utils.cache with shared.redis_utils
   - Standardizes caching across all services
   - Removed custom cache utilities (deleted app/utils/cache.py)

3. services/suppliers/app/consumers/alert_event_consumer.py:
   - Use Settings().REDIS_URL instead of os.getenv
   - Ensures proper Redis connection with TLS and authentication

4. shared/clients/recipes_client.py:
   - Fixed endpoint paths: recipes/recipes/{id} → recipes/{id}
   - Applied to all recipe methods (by_id, by_products, instructions, yield)

5. shared/clients/suppliers_client.py:
   - Fixed endpoint path: suppliers/suppliers/{id} → suppliers/{id}

6. shared/clients/procurement_client.py:
   - get_supplier_by_id now uses SuppliersServiceClient directly
   - Removes incorrect call to procurement service for supplier details

Impact:
- Demo session cloning now works without orchestrator errors 
- Consistent Redis usage across all services
- Correct service boundaries (suppliers data from suppliers service)
- Clean client endpoint paths

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 11:33:22 +01:00

297 lines
13 KiB
Python
Executable File

# shared/clients/suppliers_client.py
"""
Suppliers Service Client for Inter-Service Communication
Provides access to supplier data and performance metrics from other services
"""
import structlog
from typing import Dict, Any, Optional, List
from shared.clients.base_service_client import BaseServiceClient
from shared.config.base import BaseServiceSettings
logger = structlog.get_logger()
class SuppliersServiceClient(BaseServiceClient):
"""Client for communicating with the Suppliers 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"
# ================================================================
# SUPPLIER MANAGEMENT
# ================================================================
async def get_supplier_by_id(self, tenant_id: str, supplier_id: str) -> Optional[Dict[str, Any]]:
"""Get supplier details by ID"""
try:
result = await self.get(f"suppliers/{supplier_id}", tenant_id=tenant_id)
if result:
logger.info("Retrieved supplier details from suppliers service",
supplier_id=supplier_id, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier details",
error=str(e), supplier_id=supplier_id, tenant_id=tenant_id)
return None
async def get_all_suppliers(self, tenant_id: str, is_active: Optional[bool] = True) -> Optional[List[Dict[str, Any]]]:
"""Get all suppliers for a tenant"""
try:
params = {}
if is_active is not None:
params["is_active"] = is_active
result = await self.get_paginated("suppliers", tenant_id=tenant_id, params=params)
logger.info("Retrieved all suppliers from suppliers service",
suppliers_count=len(result) if result else 0, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting all suppliers",
error=str(e), tenant_id=tenant_id)
return []
async def search_suppliers(self, tenant_id: str, search: Optional[str] = None, category: Optional[str] = None) -> Optional[List[Dict[str, Any]]]:
"""Search suppliers with filters"""
try:
params = {}
if search:
params["search_term"] = search
if category:
params["supplier_type"] = category
result = await self.get("suppliers", tenant_id=tenant_id, params=params)
suppliers = result if result else []
logger.info("Searched suppliers from suppliers service",
search_term=search, suppliers_count=len(suppliers), tenant_id=tenant_id)
return suppliers
except Exception as e:
logger.error("Error searching suppliers",
error=str(e), tenant_id=tenant_id)
return []
async def get_suppliers_batch(self, tenant_id: str, supplier_ids: List[str]) -> Optional[List[Dict[str, Any]]]:
"""
Get multiple suppliers in a single request for performance optimization.
This method eliminates N+1 query patterns when fetching supplier data
for multiple purchase orders or other entities.
Args:
tenant_id: Tenant ID
supplier_ids: List of supplier IDs to fetch
Returns:
List of supplier dictionaries or empty list if error
"""
try:
if not supplier_ids:
return []
# Join IDs as comma-separated string
ids_param = ",".join(supplier_ids)
params = {"ids": ids_param}
result = await self.get("suppliers/batch", tenant_id=tenant_id, params=params)
suppliers = result if result else []
logger.info("Batch retrieved suppliers from suppliers service",
requested_count=len(supplier_ids),
found_count=len(suppliers),
tenant_id=tenant_id)
return suppliers
except Exception as e:
logger.error("Error batch retrieving suppliers",
error=str(e),
requested_count=len(supplier_ids),
tenant_id=tenant_id)
return []
# ================================================================
# SUPPLIER RECOMMENDATIONS
# ================================================================
async def get_supplier_recommendations(self, tenant_id: str, ingredient_id: str) -> Optional[Dict[str, Any]]:
"""Get supplier recommendations for procurement"""
try:
params = {"ingredient_id": ingredient_id}
result = await self.get("suppliers/recommendations", tenant_id=tenant_id, params=params)
if result:
logger.info("Retrieved supplier recommendations from suppliers service",
ingredient_id=ingredient_id, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier recommendations",
error=str(e), ingredient_id=ingredient_id, tenant_id=tenant_id)
return None
async def get_best_supplier_for_ingredient(self, tenant_id: str, ingredient_id: str, criteria: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
"""Get best supplier for a specific ingredient based on criteria"""
try:
data = {
"ingredient_id": ingredient_id,
"criteria": criteria or {}
}
result = await self.post("suppliers/operations/find-best-supplier", data=data, tenant_id=tenant_id)
if result:
logger.info("Retrieved best supplier from suppliers service",
ingredient_id=ingredient_id, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting best supplier for ingredient",
error=str(e), ingredient_id=ingredient_id, tenant_id=tenant_id)
return None
# ================================================================
# PERFORMANCE TRACKING
# ================================================================
async def get_supplier_performance(self, tenant_id: str, supplier_id: str, period: str = "last_30_days") -> Optional[Dict[str, Any]]:
"""Get supplier performance metrics"""
try:
params = {"period": period}
result = await self.get(f"suppliers/analytics/performance/{supplier_id}", tenant_id=tenant_id, params=params)
if result:
logger.info("Retrieved supplier performance from suppliers service",
supplier_id=supplier_id, period=period, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier performance",
error=str(e), supplier_id=supplier_id, tenant_id=tenant_id)
return None
async def get_performance_alerts(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
"""Get supplier performance alerts"""
try:
result = await self.get("suppliers/alerts/performance", tenant_id=tenant_id)
alerts = result.get('alerts', []) if result else []
logger.info("Retrieved supplier performance alerts",
alerts_count=len(alerts), tenant_id=tenant_id)
return alerts
except Exception as e:
logger.error("Error getting supplier performance alerts",
error=str(e), tenant_id=tenant_id)
return []
async def record_supplier_rating(self, tenant_id: str, supplier_id: str, rating_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Record a rating/review for a supplier"""
try:
result = await self.post(f"suppliers/{supplier_id}/rating", data=rating_data, tenant_id=tenant_id)
if result:
logger.info("Recorded supplier rating",
supplier_id=supplier_id, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error recording supplier rating",
error=str(e), supplier_id=supplier_id, tenant_id=tenant_id)
return None
# ================================================================
# DASHBOARD AND ANALYTICS
# ================================================================
async def get_dashboard_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]:
"""Get suppliers dashboard summary data"""
try:
result = await self.get("suppliers/dashboard/summary", tenant_id=tenant_id)
if result:
logger.info("Retrieved suppliers dashboard summary",
tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting suppliers dashboard summary",
error=str(e), tenant_id=tenant_id)
return None
async def get_cost_analysis(self, tenant_id: str, start_date: str, end_date: str) -> Optional[Dict[str, Any]]:
"""Get cost analysis across suppliers"""
try:
params = {
"start_date": start_date,
"end_date": end_date
}
result = await self.get("suppliers/analytics/cost-analysis", tenant_id=tenant_id, params=params)
if result:
logger.info("Retrieved supplier cost analysis",
start_date=start_date, end_date=end_date, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier cost analysis",
error=str(e), tenant_id=tenant_id)
return None
async def get_supplier_reliability_metrics(self, tenant_id: str) -> Optional[Dict[str, Any]]:
"""Get supplier reliability and quality metrics"""
try:
result = await self.get("suppliers/analytics/reliability-metrics", tenant_id=tenant_id)
if result:
logger.info("Retrieved supplier reliability metrics",
tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error getting supplier reliability metrics",
error=str(e), tenant_id=tenant_id)
return None
# ================================================================
# ALERTS AND NOTIFICATIONS
# ================================================================
async def acknowledge_alert(self, tenant_id: str, alert_id: str) -> Optional[Dict[str, Any]]:
"""Acknowledge a supplier-related alert"""
try:
result = await self.post(f"suppliers/alerts/{alert_id}/acknowledge", data={}, tenant_id=tenant_id)
if result:
logger.info("Acknowledged supplier alert",
alert_id=alert_id, tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error acknowledging supplier alert",
error=str(e), alert_id=alert_id, tenant_id=tenant_id)
return None
# ================================================================
# COUNT AND STATISTICS
# ================================================================
async def count_suppliers(self, tenant_id: str) -> int:
"""
Get the count of suppliers for a tenant
Used for subscription limit tracking
Returns:
int: Number of suppliers for the tenant
"""
try:
result = await self.get("suppliers/count", tenant_id=tenant_id)
count = result.get('count', 0) if result else 0
logger.info("Retrieved supplier count from suppliers service",
count=count, tenant_id=tenant_id)
return count
except Exception as e:
logger.error("Error getting supplier count",
error=str(e), tenant_id=tenant_id)
return 0
# ================================================================
# UTILITY METHODS
# ================================================================
async def health_check(self) -> bool:
"""Check if suppliers 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("Suppliers service health check failed", error=str(e))
return False
# Factory function for dependency injection
def create_suppliers_client(config: BaseServiceSettings) -> SuppliersServiceClient:
"""Create suppliers service client instance"""
return SuppliersServiceClient(config)