# 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 URL Pattern Architecture (Redesigned): - Registration endpoints: /api/v1/registration/* - Tenant subscription endpoints: /api/v1/tenants/{tenant_id}/subscription/* - Setup intents: /api/v1/setup-intents/* - Payment customers: /api/v1/payment-customers/* 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""" 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""" 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""" 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""" 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""" 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""" try: 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""" try: 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(f"Error getting active tenants: {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""" try: result = await self._make_request("GET", f"tenants/{parent_tenant_id}/children") 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""" 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""" 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""" 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""" 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") return result is not None except Exception as e: logger.error(f"Tenant service health check failed: {str(e)}") return False # ================================================================ # SUBSCRIPTION STATUS ENDPOINTS (NEW URL PATTERNS) # ================================================================ async def get_subscription_status(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get subscription status for a tenant""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/status") if result: logger.info("Retrieved subscription status from tenant service", tenant_id=tenant_id, status=result.get('status')) return result except Exception as e: logger.error("Error getting subscription status", error=str(e), tenant_id=tenant_id) return None async def get_subscription_details(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get detailed subscription information for a tenant""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/details") if result: logger.info("Retrieved subscription details from tenant service", tenant_id=tenant_id, plan=result.get('plan')) return result except Exception as e: logger.error("Error getting subscription details", error=str(e), tenant_id=tenant_id) return None async def get_subscription_tier(self, tenant_id: str) -> Optional[str]: """Get subscription tier for a tenant (cached endpoint)""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/tier") return result.get('tier') if result else None except Exception as e: logger.error("Error getting subscription tier", error=str(e), tenant_id=tenant_id) return None async def get_subscription_limits(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get subscription limits for a tenant""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/limits") return result except Exception as e: logger.error("Error getting subscription limits", error=str(e), tenant_id=tenant_id) return None async def get_usage_summary(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get usage summary vs limits for a tenant""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/usage") return result except Exception as e: logger.error("Error getting usage summary", error=str(e), tenant_id=tenant_id) return None async def has_feature(self, tenant_id: str, feature: str) -> bool: """Check if tenant has access to a specific feature""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/features/{feature}") return result.get('has_feature', False) if result else False except Exception as e: logger.error("Error checking feature access", error=str(e), tenant_id=tenant_id, feature=feature) return False # ================================================================ # QUOTA CHECK ENDPOINTS (NEW URL PATTERNS) # ================================================================ async def can_add_location(self, tenant_id: str) -> Dict[str, Any]: """Check if tenant can add another location""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/limits/locations") return result or {"can_add": False, "reason": "Service unavailable"} except Exception as e: logger.error("Error checking location limits", error=str(e), tenant_id=tenant_id) return {"can_add": False, "reason": str(e)} async def can_add_product(self, tenant_id: str) -> Dict[str, Any]: """Check if tenant can add another product""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/limits/products") return result or {"can_add": False, "reason": "Service unavailable"} except Exception as e: logger.error("Error checking product limits", error=str(e), tenant_id=tenant_id) return {"can_add": False, "reason": str(e)} async def can_add_user(self, tenant_id: str) -> Dict[str, Any]: """Check if tenant can add another user""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/limits/users") return result or {"can_add": False, "reason": "Service unavailable"} except Exception as e: logger.error("Error checking user limits", error=str(e), tenant_id=tenant_id) return {"can_add": False, "reason": str(e)} async def can_add_recipe(self, tenant_id: str) -> Dict[str, Any]: """Check if tenant can add another recipe""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/limits/recipes") return result or {"can_add": False, "reason": "Service unavailable"} except Exception as e: logger.error("Error checking recipe limits", error=str(e), tenant_id=tenant_id) return {"can_add": False, "reason": str(e)} async def can_add_supplier(self, tenant_id: str) -> Dict[str, Any]: """Check if tenant can add another supplier""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/limits/suppliers") return result or {"can_add": False, "reason": "Service unavailable"} except Exception as e: logger.error("Error checking supplier limits", error=str(e), tenant_id=tenant_id) return {"can_add": False, "reason": str(e)} # ================================================================ # SUBSCRIPTION MANAGEMENT ENDPOINTS (NEW URL PATTERNS) # ================================================================ async def cancel_subscription(self, tenant_id: str, reason: str = "") -> Dict[str, Any]: """Cancel a subscription""" try: result = await self._make_request( "POST", f"tenants/{tenant_id}/subscription/cancel", params={"reason": reason} ) return result or {"success": False, "message": "Cancellation failed"} except Exception as e: logger.error("Error cancelling subscription", error=str(e), tenant_id=tenant_id) return {"success": False, "message": str(e)} async def reactivate_subscription(self, tenant_id: str, plan: str = "starter") -> Dict[str, Any]: """Reactivate a subscription""" try: result = await self._make_request( "POST", f"tenants/{tenant_id}/subscription/reactivate", params={"plan": plan} ) return result or {"success": False, "message": "Reactivation failed"} except Exception as e: logger.error("Error reactivating subscription", error=str(e), tenant_id=tenant_id) return {"success": False, "message": str(e)} async def validate_plan_upgrade(self, tenant_id: str, new_plan: str) -> Dict[str, Any]: """Validate plan upgrade eligibility""" try: result = await self._make_request( "GET", f"tenants/{tenant_id}/subscription/validate-upgrade/{new_plan}" ) return result or {"can_upgrade": False, "reason": "Validation failed"} except Exception as e: logger.error("Error validating plan upgrade", error=str(e), tenant_id=tenant_id, new_plan=new_plan) return {"can_upgrade": False, "reason": str(e)} async def upgrade_subscription_plan(self, tenant_id: str, new_plan: str) -> Dict[str, Any]: """Upgrade subscription plan""" try: result = await self._make_request( "POST", f"tenants/{tenant_id}/subscription/upgrade", params={"new_plan": new_plan} ) return result or {"success": False, "message": "Upgrade failed"} except Exception as e: logger.error("Error upgrading subscription plan", error=str(e), tenant_id=tenant_id, new_plan=new_plan) return {"success": False, "message": str(e)} # ================================================================ # PAYMENT MANAGEMENT ENDPOINTS (NEW URL PATTERNS) # ================================================================ async def get_payment_method(self, tenant_id: str) -> Optional[Dict[str, Any]]: """Get payment method for a tenant""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/payment-method") return result except Exception as e: logger.error("Error getting payment method", error=str(e), tenant_id=tenant_id) return None async def update_payment_method(self, tenant_id: str, payment_method_id: str) -> Dict[str, Any]: """Update payment method for a tenant""" try: result = await self._make_request( "POST", f"tenants/{tenant_id}/subscription/payment-method", params={"payment_method_id": payment_method_id} ) return result or {"success": False, "message": "Update failed"} except Exception as e: logger.error("Error updating payment method", error=str(e), tenant_id=tenant_id) return {"success": False, "message": str(e)} async def get_invoices(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]: """Get invoices for a tenant""" try: result = await self._make_request("GET", f"tenants/{tenant_id}/subscription/invoices") return result.get('invoices', []) if result else None except Exception as e: logger.error("Error getting invoices", error=str(e), tenant_id=tenant_id) return None # ================================================================ # REGISTRATION FLOW ENDPOINTS (NEW URL PATTERNS) # ================================================================ async def start_registration_payment_setup(self, user_data: Dict[str, Any]) -> Dict[str, Any]: """Start registration payment setup (SetupIntent-first architecture)""" try: logger.info("Starting registration payment setup via tenant service", email=user_data.get('email'), plan_id=user_data.get('plan_id')) result = await self._make_request( "POST", "registration/payment-setup", data=user_data ) if result and result.get("success"): logger.info("Registration payment setup completed", email=user_data.get('email'), setup_intent_id=result.get('setup_intent_id')) return result else: error_msg = result.get('detail') if result else 'Unknown error' logger.error("Registration payment setup failed", email=user_data.get('email'), error=error_msg) raise Exception(f"Registration payment setup failed: {error_msg}") except Exception as e: logger.error("Failed to start registration payment setup", email=user_data.get('email'), error=str(e)) raise async def complete_registration(self, setup_intent_id: str, user_data: Dict[str, Any]) -> Dict[str, Any]: """Complete registration after 3DS verification""" try: logger.info("Completing registration via tenant service", setup_intent_id=setup_intent_id, email=user_data.get('email')) registration_data = { "setup_intent_id": setup_intent_id, "user_data": user_data } result = await self._make_request( "POST", "registration/complete", data=registration_data ) if result and result.get("success"): logger.info("Registration completed successfully", setup_intent_id=setup_intent_id, subscription_id=result.get('subscription_id')) return result else: error_msg = result.get('detail') if result else 'Unknown error' logger.error("Registration completion failed", setup_intent_id=setup_intent_id, error=error_msg) raise Exception(f"Registration completion failed: {error_msg}") except Exception as e: logger.error("Failed to complete registration", setup_intent_id=setup_intent_id, error=str(e)) raise async def get_registration_state(self, state_id: str) -> Optional[Dict[str, Any]]: """Get registration state by ID""" try: result = await self._make_request("GET", f"registration/state/{state_id}") return result except Exception as e: logger.error("Error getting registration state", error=str(e), state_id=state_id) return None # ================================================================ # SETUP INTENT VERIFICATION (NEW URL PATTERNS) # ================================================================ async def verify_setup_intent(self, setup_intent_id: str) -> Dict[str, Any]: """Verify SetupIntent status""" try: logger.info("Verifying SetupIntent via tenant service", setup_intent_id=setup_intent_id) result = await self._make_request( "GET", f"setup-intents/{setup_intent_id}/verify" ) if result: logger.info("SetupIntent verification result", setup_intent_id=setup_intent_id, status=result.get('status')) return result else: raise Exception("SetupIntent verification failed: No result returned") except Exception as e: logger.error("Failed to verify SetupIntent", setup_intent_id=setup_intent_id, error=str(e)) raise async def verify_setup_intent_for_registration(self, setup_intent_id: str) -> Dict[str, Any]: """Verify SetupIntent status for registration flow (alias for verify_setup_intent)""" return await self.verify_setup_intent(setup_intent_id) # ================================================================ # PAYMENT CUSTOMER MANAGEMENT (NEW URL PATTERNS) # ================================================================ async def create_payment_customer( self, user_data: Dict[str, Any], payment_method_id: Optional[str] = None ) -> Dict[str, Any]: """Create payment customer""" try: logger.info("Creating payment customer via tenant service", email=user_data.get('email'), payment_method_id=payment_method_id) request_data = user_data params = {} if payment_method_id: params["payment_method_id"] = payment_method_id result = await self._make_request( "POST", "payment-customers/create", data=request_data, params=params if params else None ) if result and result.get("success"): logger.info("Payment customer created successfully", email=user_data.get('email'), payment_customer_id=result.get('payment_customer_id')) return result else: error_msg = result.get('detail') if result else 'Unknown error' logger.error("Payment customer creation failed", email=user_data.get('email'), error=error_msg) raise Exception(f"Payment customer creation failed: {error_msg}") except Exception as e: logger.error("Failed to create payment customer", email=user_data.get('email'), error=str(e)) raise # ================================================================ # LEGACY COMPATIBILITY METHODS # ================================================================ async def create_registration_payment_setup(self, user_data: Dict[str, Any]) -> Dict[str, Any]: """Create registration payment setup via tenant service orchestration""" return await self.start_registration_payment_setup(user_data) async def verify_and_complete_registration( self, setup_intent_id: str, user_data: Dict[str, Any] ) -> Dict[str, Any]: """Verify SetupIntent and complete registration""" return await self.complete_registration(setup_intent_id, user_data) async def create_subscription_for_registration( self, user_data: Dict[str, Any], plan_id: str, payment_method_id: str, billing_cycle: str = "monthly", coupon_code: Optional[str] = None ) -> Optional[Dict[str, Any]]: """Create a tenant-independent subscription during user registration""" try: logger.info("Creating tenant-independent subscription for registration", user_id=user_data.get('user_id'), plan_id=plan_id, billing_cycle=billing_cycle) registration_data = { **user_data, "plan_id": plan_id, "payment_method_id": payment_method_id, "billing_cycle": billing_cycle, "coupon_code": coupon_code } setup_result = await self.start_registration_payment_setup(registration_data) if setup_result and setup_result.get("success"): return { "subscription_id": setup_result.get('setup_intent_id'), "customer_id": setup_result.get('customer_id'), "status": "pending_verification", "plan": plan_id, "billing_cycle": billing_cycle, "setup_intent_id": setup_result.get('setup_intent_id'), "client_secret": setup_result.get('client_secret') } return None except Exception as e: logger.error("Failed to create subscription for registration", user_id=user_data.get('user_id'), error=str(e)) return None async def link_subscription_to_tenant( self, tenant_id: str, subscription_id: str, user_id: str ) -> Optional[Dict[str, Any]]: """Link a pending subscription to a tenant""" try: logger.info("Linking subscription to tenant", tenant_id=tenant_id, subscription_id=subscription_id, user_id=user_id) linking_data = { "subscription_id": subscription_id, "user_id": user_id } result = await self._make_request( "POST", f"tenants/{tenant_id}/link-subscription", data=linking_data ) if result and result.get("success"): logger.info("Subscription linked to tenant successfully", tenant_id=tenant_id, subscription_id=subscription_id) return result else: logger.error("Subscription linking failed", tenant_id=tenant_id, subscription_id=subscription_id, error=result.get('detail') if result else 'No detail provided') return None except Exception as e: logger.error("Failed to link subscription to tenant", tenant_id=tenant_id, subscription_id=subscription_id, error=str(e)) return None async def get_user_primary_tenant(self, user_id: str) -> Optional[Dict[str, Any]]: """Get the primary tenant for a user""" try: logger.info("Getting primary tenant for user", user_id=user_id) result = await self._make_request( "GET", f"tenants/users/{user_id}/primary-tenant" ) if result: logger.info("Primary tenant retrieved successfully", user_id=user_id, tenant_id=result.get('tenant_id')) return result else: logger.warning("No primary tenant found for user", user_id=user_id) return None except Exception as e: logger.error("Failed to get primary tenant for user", user_id=user_id, error=str(e)) return None async def get_user_memberships(self, user_id: str) -> Optional[List[Dict[str, Any]]]: """Get all tenant memberships for a user""" try: logger.info("Getting tenant memberships for user", user_id=user_id) result = await self._make_request( "GET", f"tenants/members/user/{user_id}" ) if result: logger.info("User memberships retrieved successfully", user_id=user_id, membership_count=len(result)) return result else: logger.warning("No memberships found for user", user_id=user_id) return [] except Exception as e: logger.error("Failed to get user memberships", user_id=user_id, error=str(e)) return None # Factory function for dependency injection def create_tenant_client(config: BaseServiceSettings) -> TenantServiceClient: """Create tenant service client instance""" return TenantServiceClient(config)