""" Tenant Hierarchy API - Handles parent-child tenant relationships """ from fastapi import APIRouter, Depends, HTTPException, status, Path from typing import List, Dict, Any from uuid import UUID from app.schemas.tenants import TenantResponse from app.services.tenant_service import EnhancedTenantService from app.repositories.tenant_repository import TenantRepository from shared.auth.decorators import get_current_user_dep from shared.routing.route_builder import RouteBuilder from shared.database.base import create_database_manager from shared.monitoring.metrics import track_endpoint_metrics import structlog logger = structlog.get_logger() router = APIRouter() route_builder = RouteBuilder("tenants") # Dependency injection for enhanced tenant service def get_enhanced_tenant_service(): try: from app.core.config import settings database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service") return EnhancedTenantService(database_manager) except Exception as e: logger.error("Failed to create enhanced tenant service", error=str(e)) raise HTTPException(status_code=500, detail="Service initialization failed") @router.get(route_builder.build_base_route("{tenant_id}/children", include_tenant_prefix=False), response_model=List[TenantResponse]) @track_endpoint_metrics("tenant_children_list") async def get_tenant_children( tenant_id: UUID = Path(..., description="Parent Tenant ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service) ): """ Get all child tenants for a parent tenant. This endpoint returns all active child tenants associated with the specified parent tenant. """ try: logger.info( "Get tenant children request received", tenant_id=str(tenant_id), user_id=current_user.get("user_id"), user_type=current_user.get("type", "user"), is_service=current_user.get("type") == "service", role=current_user.get("role"), service_name=current_user.get("service", "none") ) # Skip access check for service-to-service calls is_service_call = current_user.get("type") == "service" if not is_service_call: # Verify user has access to the parent tenant access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id)) if not access_info.has_access: logger.warning( "Access denied to parent tenant", tenant_id=str(tenant_id), user_id=current_user.get("user_id") ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to parent tenant" ) else: logger.debug( "Service-to-service call - bypassing access check", service=current_user.get("service"), tenant_id=str(tenant_id) ) # Get child tenants from repository from app.models.tenants import Tenant async with tenant_service.database_manager.get_session() as session: tenant_repo = TenantRepository(Tenant, session) child_tenants = await tenant_repo.get_child_tenants(str(tenant_id)) logger.debug( "Get tenant children successful", tenant_id=str(tenant_id), child_count=len(child_tenants) ) # Convert to plain dicts while still in session to avoid lazy-load issues child_dicts = [] for child in child_tenants: # Handle subscription_tier safely - avoid lazy load try: # Try to get subscription_tier if subscriptions are already loaded sub_tier = child.__dict__.get('_subscription_tier_cache', 'enterprise') except: sub_tier = 'enterprise' # Default for enterprise children child_dict = { 'id': str(child.id), 'name': child.name, 'subdomain': child.subdomain, 'business_type': child.business_type, 'business_model': child.business_model, 'address': child.address, 'city': child.city, 'postal_code': child.postal_code, 'latitude': child.latitude, 'longitude': child.longitude, 'phone': child.phone, 'email': child.email, 'timezone': child.timezone, 'owner_id': str(child.owner_id), 'parent_tenant_id': str(child.parent_tenant_id) if child.parent_tenant_id else None, 'tenant_type': child.tenant_type, 'hierarchy_path': child.hierarchy_path, 'subscription_tier': sub_tier, # Use the safely retrieved value 'ml_model_trained': child.ml_model_trained, 'last_training_date': child.last_training_date, 'is_active': child.is_active, 'is_demo': child.is_demo, 'demo_session_id': child.demo_session_id, 'created_at': child.created_at, 'updated_at': child.updated_at } child_dicts.append(child_dict) # Convert to Pydantic models outside the session without from_attributes child_responses = [TenantResponse(**child_dict) for child_dict in child_dicts] return child_responses except HTTPException: raise except Exception as e: logger.error("Get tenant children failed", tenant_id=str(tenant_id), user_id=current_user.get("user_id"), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Get tenant children failed" ) @router.get(route_builder.build_base_route("{tenant_id}/children/count", include_tenant_prefix=False)) @track_endpoint_metrics("tenant_children_count") async def get_tenant_children_count( tenant_id: UUID = Path(..., description="Parent Tenant ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service) ): """ Get count of child tenants for a parent tenant. This endpoint returns the number of active child tenants associated with the specified parent tenant. """ try: logger.info( "Get tenant children count request received", tenant_id=str(tenant_id), user_id=current_user.get("user_id") ) # Skip access check for service-to-service calls is_service_call = current_user.get("type") == "service" if not is_service_call: # Verify user has access to the parent tenant access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id)) if not access_info.has_access: logger.warning( "Access denied to parent tenant", tenant_id=str(tenant_id), user_id=current_user.get("user_id") ) raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to parent tenant" ) else: logger.debug( "Service-to-service call - bypassing access check", service=current_user.get("service"), tenant_id=str(tenant_id) ) # Get child count from repository from app.models.tenants import Tenant async with tenant_service.database_manager.get_session() as session: tenant_repo = TenantRepository(Tenant, session) child_count = await tenant_repo.get_child_tenant_count(str(tenant_id)) logger.debug( "Get tenant children count successful", tenant_id=str(tenant_id), child_count=child_count ) return { "parent_tenant_id": str(tenant_id), "child_count": child_count } except HTTPException: raise except Exception as e: logger.error("Get tenant children count failed", tenant_id=str(tenant_id), user_id=current_user.get("user_id"), error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Get tenant children count failed" ) # Register the router in the main app def register_hierarchy_routes(app): """Register hierarchy routes with the main application""" from shared.routing.route_builder import RouteBuilder route_builder = RouteBuilder("tenants") # Include the hierarchy routes with proper tenant prefix app.include_router( router, prefix="/api/v1", tags=["tenant-hierarchy"] )