# shared/clients/tenant_client.py """ Tenant Service Client for Inter-Service Communication This client provides a high-level API for interacting with the Tenant Service, which manages tenant metadata, settings, hierarchical relationships (parent-child), and multi-location support for enterprise bakery networks. Key Capabilities: - Tenant Management: Get, create, update tenant records - Settings Management: Category-specific settings (procurement, inventory, production, etc.) - Enterprise Hierarchy: Parent-child tenant relationships for multi-location networks - Tenant Locations: Physical location management (central_production, retail_outlet) - Subscription Management: Subscription tier and quota validation - Multi-Tenancy: Tenant isolation and access control Enterprise Hierarchy Features: - get_child_tenants(): Fetch all child outlets for a parent (central bakery) - get_parent_tenant(): Get parent tenant from child outlet - get_tenant_hierarchy(): Get complete hierarchy path and metadata - get_tenant_locations(): Get all physical locations for a tenant - Supports 3 tenant types: standalone, parent, child Usage Example: ```python from shared.clients import create_tenant_client from shared.config.base import get_settings config = get_settings() client = create_tenant_client(config) # Get parent tenant and all children parent = await client.get_tenant(parent_tenant_id) children = await client.get_child_tenants(parent_tenant_id) # Get hierarchy information hierarchy = await client.get_tenant_hierarchy(tenant_id) # Returns: {tenant_type: 'parent', hierarchy_path: 'parent_id', child_count: 3} # Get physical locations locations = await client.get_tenant_locations(parent_tenant_id) # Returns: [{location_type: 'central_production', ...}, ...] # Get category settings procurement_settings = await client.get_procurement_settings(tenant_id) ``` Settings Categories: - procurement: Min/max order quantities, lead times, reorder points - inventory: FIFO settings, expiry thresholds, temperature monitoring - production: Batch sizes, quality control, equipment settings - supplier: Payment terms, delivery preferences - pos: POS integration settings - order: Order fulfillment rules - notification: Alert preferences Service Architecture: - Base URL: Configured via TENANT_SERVICE_URL environment variable - Authentication: Uses BaseServiceClient with tenant_id header validation - Error Handling: Returns None on errors, logs detailed error context - Async: All methods are async and use httpx for HTTP communication Related Services: - Distribution Service: Uses tenant locations for delivery route planning - Forecasting Service: Uses hierarchy for network demand aggregation - Procurement Service: Validates parent-child for internal transfers - Orchestrator Service: Enterprise dashboard queries hierarchy data For more details, see services/tenant/README.md """ 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 TenantServiceClient(BaseServiceClient): """Client for communicating with the Tenant Service""" def __init__(self, config: BaseServiceSettings): super().__init__("tenant", config) def get_service_base_path(self) -> str: return "/api/v1" # ================================================================ # TENANT SETTINGS ENDPOINTS # ================================================================ async def get_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """ Get all settings for a tenant Args: tenant_id: Tenant ID (UUID as string) Returns: Dictionary with all settings categories """ try: result = await self.get("settings", tenant_id=tenant_id) if result: logger.info("Retrieved all settings from tenant service", tenant_id=tenant_id) return result except Exception as e: logger.error("Error getting all settings", error=str(e), tenant_id=tenant_id) return None async def get_category_settings(self, tenant_id: str, category: str) -> Optional[Dict[str, Any]]: """ Get settings for a specific category Args: tenant_id: Tenant ID (UUID as string) category: Category name (procurement, inventory, production, supplier, pos, order) Returns: Dictionary with category settings """ try: result = await self.get(f"settings/{category}", tenant_id=tenant_id) if result: logger.info("Retrieved category settings from tenant service", tenant_id=tenant_id, category=category) return result except Exception as e: logger.error("Error getting category settings", error=str(e), tenant_id=tenant_id, category=category) return None async def get_procurement_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get procurement settings for a tenant""" result = await self.get_category_settings(tenant_id, "procurement") return result.get('settings', {}) if result else {} async def get_inventory_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get inventory settings for a tenant""" result = await self.get_category_settings(tenant_id, "inventory") return result.get('settings', {}) if result else {} async def get_production_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get production settings for a tenant""" result = await self.get_category_settings(tenant_id, "production") return result.get('settings', {}) if result else {} async def get_supplier_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get supplier settings for a tenant""" result = await self.get_category_settings(tenant_id, "supplier") return result.get('settings', {}) if result else {} async def get_pos_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get POS settings for a tenant""" result = await self.get_category_settings(tenant_id, "pos") return result.get('settings', {}) if result else {} async def get_order_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get order settings for a tenant""" result = await self.get_category_settings(tenant_id, "order") return result.get('settings', {}) if result else {} async def get_notification_settings(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get notification settings for a tenant""" result = await self.get_category_settings(tenant_id, "notification") return result.get('settings', {}) if result else {} async def update_settings(self, tenant_id: str, settings_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: """ Update settings for a tenant Args: tenant_id: Tenant ID (UUID as string) settings_data: Settings data to update Returns: Updated settings dictionary """ try: result = await self.put("settings", data=settings_data, tenant_id=tenant_id) if result: logger.info("Updated tenant settings", tenant_id=tenant_id) return result except Exception as e: logger.error("Error updating tenant settings", error=str(e), tenant_id=tenant_id) return None async def update_category_settings( self, tenant_id: str, category: str, settings_data: Dict[str, Any] ) -> Optional[Dict[str, Any]]: """ Update settings for a specific category Args: tenant_id: Tenant ID (UUID as string) category: Category name settings_data: Settings data to update Returns: Updated settings dictionary """ try: result = await self.put(f"settings/{category}", data=settings_data, tenant_id=tenant_id) if result: logger.info("Updated category settings", tenant_id=tenant_id, category=category) return result except Exception as e: logger.error("Error updating category settings", error=str(e), tenant_id=tenant_id, category=category) return None async def reset_category_settings(self, tenant_id: str, category: str) -> Optional[Dict[str, Any]]: """ Reset category settings to default values Args: tenant_id: Tenant ID (UUID as string) category: Category name Returns: Reset settings dictionary """ try: result = await self.post(f"settings/{category}/reset", data={}, tenant_id=tenant_id) if result: logger.info("Reset category settings to defaults", tenant_id=tenant_id, category=category) return result except Exception as e: logger.error("Error resetting category settings", error=str(e), tenant_id=tenant_id, category=category) return None # ================================================================ # TENANT MANAGEMENT # ================================================================ async def get_tenant(self, tenant_id: str) -> Optional[Dict[str, Any]]: """ Get tenant details Args: tenant_id: Tenant ID (UUID as string) Returns: Tenant data dictionary """ try: # The tenant endpoint is not tenant-scoped, it's a direct path result = await self._make_request("GET", f"tenants/{tenant_id}") if result: logger.info("Retrieved tenant details", tenant_id=tenant_id) return result except Exception as e: logger.error("Error getting tenant details", error=str(e), tenant_id=tenant_id) return None async def get_active_tenants(self, skip: int = 0, limit: int = 100) -> Optional[list]: """ Get all active tenants Args: skip: Number of records to skip (pagination) limit: Maximum number of records to return Returns: List of active tenant dictionaries """ try: # Call tenants endpoint (not tenant-scoped) result = await self._make_request( "GET", f"tenants?skip={skip}&limit={limit}" ) if result: logger.info("Retrieved active tenants from tenant service", count=len(result) if isinstance(result, list) else 0) return result if result else [] except Exception as e: logger.error("Error getting active tenants", error=str(e)) return [] # ================================================================ # ENTERPRISE TIER METHODS # ================================================================ async def get_child_tenants(self, parent_tenant_id: str) -> Optional[List[Dict[str, Any]]]: """ Get all child tenants for a parent tenant Args: parent_tenant_id: Parent tenant ID Returns: List of child tenant dictionaries """ try: result = await self.get("children", tenant_id=parent_tenant_id) if result: logger.info("Retrieved child tenants", parent_tenant_id=parent_tenant_id, child_count=len(result) if isinstance(result, list) else 0) return result except Exception as e: logger.error("Error getting child tenants", error=str(e), parent_tenant_id=parent_tenant_id) return None async def get_tenant_children_count(self, tenant_id: str) -> int: """ Get count of child tenants for a parent tenant Args: tenant_id: Tenant ID to check Returns: Number of child tenants (0 if not a parent) """ try: children = await self.get_child_tenants(tenant_id) return len(children) if children else 0 except Exception as e: logger.error("Error getting child tenant count", error=str(e), tenant_id=tenant_id) return 0 async def get_parent_tenant(self, child_tenant_id: str) -> Optional[Dict[str, Any]]: """ Get parent tenant for a child tenant Args: child_tenant_id: Child tenant ID Returns: Parent tenant dictionary """ try: result = await self.get(f"tenants/{child_tenant_id}/parent", tenant_id=child_tenant_id) if result: logger.info("Retrieved parent tenant", child_tenant_id=child_tenant_id, parent_tenant_id=result.get('id')) return result except Exception as e: logger.error("Error getting parent tenant", error=str(e), child_tenant_id=child_tenant_id) return None async def get_tenant_hierarchy(self, tenant_id: str) -> Optional[Dict[str, Any]]: """ Get complete tenant hierarchy information Args: tenant_id: Tenant ID to get hierarchy for Returns: Hierarchy information dictionary """ try: result = await self.get("hierarchy", tenant_id=tenant_id) if result: logger.info("Retrieved tenant hierarchy", tenant_id=tenant_id, hierarchy_type=result.get('tenant_type')) return result except Exception as e: logger.error("Error getting tenant hierarchy", error=str(e), tenant_id=tenant_id) return None async def get_tenant_locations(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]: """ Get all locations for a tenant Args: tenant_id: Tenant ID Returns: List of tenant location dictionaries """ try: result = await self.get("locations", tenant_id=tenant_id) if result: logger.info("Retrieved tenant locations", tenant_id=tenant_id, location_count=len(result) if isinstance(result, list) else 0) return result except Exception as e: logger.error("Error getting tenant locations", error=str(e), tenant_id=tenant_id) return None # ================================================================ # UTILITY METHODS # ================================================================ async def health_check(self) -> bool: """Check if tenant 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("Tenant service health check failed", error=str(e)) return False # Factory function for dependency injection def create_tenant_client(config: BaseServiceSettings) -> TenantServiceClient: """Create tenant service client instance""" return TenantServiceClient(config)