# 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): super().__init__("suppliers", 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/list", tenant_id=tenant_id, params=params) logger.info("Retrieved all suppliers from suppliers service", suppliers_count=len(result), 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"] = search if category: params["category"] = category result = await self.get("suppliers/list/search", tenant_id=tenant_id, params=params) suppliers = result.get('suppliers', []) 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 [] # ================================================================ # 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 # ================================================================ # PURCHASE ORDER MANAGEMENT # ================================================================ async def create_purchase_order(self, tenant_id: str, order_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Create a new purchase order""" try: result = await self.post("suppliers/purchase-orders", data=order_data, tenant_id=tenant_id) if result: logger.info("Created purchase order", order_id=result.get('id'), supplier_id=order_data.get('supplier_id'), tenant_id=tenant_id) return result except Exception as e: logger.error("Error creating purchase order", error=str(e), tenant_id=tenant_id) return None async def get_purchase_orders(self, tenant_id: str, status: Optional[str] = None, supplier_id: Optional[str] = None) -> Optional[List[Dict[str, Any]]]: """Get purchase orders with optional filtering""" try: params = {} if status: params["status"] = status if supplier_id: params["supplier_id"] = supplier_id result = await self.get("suppliers/purchase-orders", tenant_id=tenant_id, params=params) orders = result.get('orders', []) if result else [] logger.info("Retrieved purchase orders from suppliers service", orders_count=len(orders), tenant_id=tenant_id) return orders except Exception as e: logger.error("Error getting purchase orders", error=str(e), tenant_id=tenant_id) return [] async def update_purchase_order_status(self, tenant_id: str, order_id: str, status: str) -> Optional[Dict[str, Any]]: """Update purchase order status""" try: data = {"status": status} result = await self.put(f"suppliers/purchase-orders/{order_id}/status", data=data, tenant_id=tenant_id) if result: logger.info("Updated purchase order status", order_id=order_id, status=status, tenant_id=tenant_id) return result except Exception as e: logger.error("Error updating purchase order status", error=str(e), order_id=order_id, tenant_id=tenant_id) return None async def approve_purchase_order( self, tenant_id: str, po_id: str, approval_data: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """ Auto-approve a purchase order Args: tenant_id: Tenant ID po_id: Purchase Order ID approval_data: Approval data including: - approved_by: User ID or "system" for auto-approval - approval_notes: Notes about the approval - auto_approved: Boolean flag indicating auto-approval - approval_reasons: List of reasons for auto-approval Returns: Updated purchase order data or None """ try: # Format the approval request payload payload = { "action": "approve", "notes": approval_data.get("approval_notes", "Auto-approved by system") } result = await self.post( f"suppliers/purchase-orders/{po_id}/approve", data=payload, tenant_id=tenant_id ) if result: logger.info("Auto-approved purchase order", po_id=po_id, tenant_id=tenant_id, auto_approved=approval_data.get("auto_approved", True)) return result except Exception as e: logger.error("Error auto-approving purchase order", error=str(e), po_id=po_id, tenant_id=tenant_id) return None async def get_supplier(self, tenant_id: str, supplier_id: str) -> Optional[Dict[str, Any]]: """ Get supplier details with performance metrics Args: tenant_id: Tenant ID supplier_id: Supplier ID Returns: Supplier data including performance metrics or None """ try: # Use the existing get_supplier_by_id method which returns full supplier data result = await self.get_supplier_by_id(tenant_id, supplier_id) if result: logger.info("Retrieved supplier data for auto-approval", supplier_id=supplier_id, tenant_id=tenant_id) return result except Exception as e: logger.error("Error getting supplier data", error=str(e), supplier_id=supplier_id, tenant_id=tenant_id) return None # ================================================================ # DELIVERY MANAGEMENT # ================================================================ async def get_deliveries(self, tenant_id: str, status: Optional[str] = None, date: Optional[str] = None) -> Optional[List[Dict[str, Any]]]: """Get deliveries with optional filtering""" try: params = {} if status: params["status"] = status if date: params["date"] = date result = await self.get("suppliers/deliveries", tenant_id=tenant_id, params=params) deliveries = result.get('deliveries', []) if result else [] logger.info("Retrieved deliveries from suppliers service", deliveries_count=len(deliveries), tenant_id=tenant_id) return deliveries except Exception as e: logger.error("Error getting deliveries", error=str(e), tenant_id=tenant_id) return [] async def update_delivery_status(self, tenant_id: str, delivery_id: str, status: str, notes: Optional[str] = None) -> Optional[Dict[str, Any]]: """Update delivery status""" try: data = {"status": status} if notes: data["notes"] = notes result = await self.put(f"suppliers/deliveries/{delivery_id}/status", data=data, tenant_id=tenant_id) if result: logger.info("Updated delivery status", delivery_id=delivery_id, status=status, tenant_id=tenant_id) return result except Exception as e: logger.error("Error updating delivery status", error=str(e), delivery_id=delivery_id, tenant_id=tenant_id) return None async def get_supplier_order_summaries(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get supplier order summaries for central bakery dashboard""" try: result = await self.get("suppliers/dashboard/order-summaries", tenant_id=tenant_id) if result: logger.info("Retrieved supplier order summaries from suppliers service", tenant_id=tenant_id) return result except Exception as e: logger.error("Error getting supplier order summaries", error=str(e), 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)