Files
bakery-ia/services/tenant/app/api/tenant_operations.py

735 lines
30 KiB
Python
Raw Normal View History

2025-10-06 15:27:01 +02:00
"""
Tenant Operations API - BUSINESS operations
2026-01-16 15:19:34 +01:00
Handles complex tenant operations, registration, search, and analytics
NOTE: All subscription-related endpoints have been moved to subscription.py
as part of the architecture redesign for better separation of concerns.
2025-10-06 15:27:01 +02:00
"""
import structlog
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
from typing import List, Dict, Any, Optional
from uuid import UUID
from app.schemas.tenants import (
BakeryRegistration, TenantResponse, TenantAccessResponse,
TenantSearchRequest
)
from app.services.tenant_service import EnhancedTenantService
from app.services.payment_service import PaymentService
2025-10-29 06:58:05 +01:00
from app.models import AuditLog
2025-10-06 15:27:01 +02:00
from shared.auth.decorators import (
get_current_user_dep,
require_admin_role_dep
)
2026-01-13 22:22:38 +01:00
from app.core.database import get_db
from sqlalchemy.ext.asyncio import AsyncSession
from shared.auth.access_control import owner_role_required, admin_role_required
2025-10-06 15:27:01 +02:00
from shared.routing.route_builder import RouteBuilder
from shared.database.base import create_database_manager
from shared.monitoring.metrics import track_endpoint_metrics
from shared.security import create_audit_logger, AuditSeverity, AuditAction
from shared.config.base import is_internal_service
2025-10-06 15:27:01 +02:00
logger = structlog.get_logger()
router = APIRouter()
route_builder = RouteBuilder("tenants")
# Initialize audit logger
2025-10-29 06:58:05 +01:00
audit_logger = create_audit_logger("tenant-service", AuditLog)
2025-10-06 15:27:01 +02:00
# 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")
def get_payment_service():
try:
return PaymentService()
except Exception as e:
logger.error("Failed to create payment service", error=str(e))
raise HTTPException(status_code=500, detail="Payment service initialization failed")
2026-01-16 15:19:34 +01:00
# ============================================================================
2025-10-06 15:27:01 +02:00
# TENANT REGISTRATION & ACCESS OPERATIONS
# ============================================================================
@router.post(route_builder.build_base_route("register", include_tenant_prefix=False), response_model=TenantResponse)
async def register_bakery(
bakery_data: BakeryRegistration,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-10-17 18:14:28 +02:00
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service),
2026-01-13 22:22:38 +01:00
payment_service: PaymentService = Depends(get_payment_service),
db: AsyncSession = Depends(get_db)
2025-10-06 15:27:01 +02:00
):
"""Register a new bakery/tenant with enhanced validation and features"""
try:
2025-10-17 18:14:28 +02:00
coupon_validation = None
2026-01-13 22:22:38 +01:00
success = None
discount = None
error = None
2025-10-17 18:14:28 +02:00
2025-10-06 15:27:01 +02:00
result = await tenant_service.create_bakery(
bakery_data,
current_user["user_id"]
)
2026-01-13 22:22:38 +01:00
tenant_id = result.id
2025-12-18 13:26:32 +01:00
2026-01-13 22:22:38 +01:00
if bakery_data.link_existing_subscription and bakery_data.subscription_id:
logger.info("Linking existing subscription to new tenant",
tenant_id=tenant_id,
subscription_id=bakery_data.subscription_id,
user_id=current_user["user_id"])
2025-12-18 13:26:32 +01:00
2026-01-13 22:22:38 +01:00
try:
from app.services.subscription_service import SubscriptionService
2025-12-18 13:26:32 +01:00
2026-01-13 22:22:38 +01:00
subscription_service = SubscriptionService(db)
linking_result = await subscription_service.link_subscription_to_tenant(
subscription_id=bakery_data.subscription_id,
tenant_id=tenant_id,
user_id=current_user["user_id"]
2025-12-18 13:26:32 +01:00
)
2026-01-13 22:22:38 +01:00
logger.info("Subscription linked successfully during tenant registration",
tenant_id=tenant_id,
subscription_id=bakery_data.subscription_id)
except Exception as linking_error:
logger.error("Error linking subscription during tenant registration",
tenant_id=tenant_id,
subscription_id=bakery_data.subscription_id,
error=str(linking_error))
elif bakery_data.coupon_code:
2026-01-16 20:25:45 +01:00
from app.services.coupon_service import CouponService
coupon_service = CouponService(db)
coupon_validation = await coupon_service.validate_coupon_code(
2026-01-13 22:22:38 +01:00
bakery_data.coupon_code,
2026-01-16 20:25:45 +01:00
tenant_id
2026-01-13 22:22:38 +01:00
)
if not coupon_validation["valid"]:
logger.warning(
"Invalid coupon code provided during registration",
coupon_code=bakery_data.coupon_code,
error=coupon_validation["error_message"]
2025-12-18 13:26:32 +01:00
)
2026-01-13 22:22:38 +01:00
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=coupon_validation["error_message"]
)
2026-01-16 20:25:45 +01:00
success, discount, error = await coupon_service.redeem_coupon(
2026-01-13 22:22:38 +01:00
bakery_data.coupon_code,
tenant_id,
2026-01-16 20:25:45 +01:00
base_trial_days=0
2025-12-18 13:26:32 +01:00
)
2026-01-13 22:22:38 +01:00
if success:
logger.info("Coupon redeemed during registration",
coupon_code=bakery_data.coupon_code,
tenant_id=tenant_id)
else:
logger.warning("Failed to redeem coupon during registration",
coupon_code=bakery_data.coupon_code,
error=error)
else:
try:
from app.repositories.subscription_repository import SubscriptionRepository
from app.models.tenants import Subscription
from datetime import datetime, timedelta, timezone
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
async with database_manager.get_session() as session:
subscription_repo = SubscriptionRepository(Subscription, session)
existing_subscription = await subscription_repo.get_by_tenant_id(str(result.id))
if existing_subscription:
logger.info(
"Tenant already has an active subscription, skipping default subscription creation",
tenant_id=str(result.id),
existing_plan=existing_subscription.plan,
subscription_id=str(existing_subscription.id)
)
else:
2026-01-16 09:55:54 +01:00
trial_end_date = datetime.now(timezone.utc)
2026-01-13 22:22:38 +01:00
next_billing_date = trial_end_date
await subscription_repo.create_subscription({
"tenant_id": str(result.id),
"plan": "starter",
"status": "trial",
"billing_cycle": "monthly",
"next_billing_date": next_billing_date,
"trial_ends_at": trial_end_date
})
await session.commit()
logger.info(
2026-01-16 09:55:54 +01:00
"Default subscription created for new tenant",
2026-01-13 22:22:38 +01:00
tenant_id=str(result.id),
plan="starter",
2026-01-16 09:55:54 +01:00
trial_days=0
2026-01-13 22:22:38 +01:00
)
except Exception as subscription_error:
logger.error(
"Failed to create default subscription for tenant",
tenant_id=str(result.id),
error=str(subscription_error)
)
2025-12-18 13:26:32 +01:00
2025-10-17 18:14:28 +02:00
if coupon_validation and coupon_validation["valid"]:
from app.core.config import settings
2026-01-16 20:25:45 +01:00
from app.services.coupon_service import CouponService
2025-10-17 18:14:28 +02:00
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
async with database_manager.get_session() as session:
2026-01-16 20:25:45 +01:00
coupon_service = CouponService(session)
success, discount, error = await coupon_service.redeem_coupon(
2025-10-17 18:14:28 +02:00
bakery_data.coupon_code,
result.id,
2026-01-16 20:25:45 +01:00
base_trial_days=0
2025-10-17 18:14:28 +02:00
)
if success:
logger.info(
"Coupon redeemed successfully",
tenant_id=result.id,
coupon_code=bakery_data.coupon_code,
discount=discount
)
else:
logger.warning(
"Failed to redeem coupon after registration",
tenant_id=result.id,
coupon_code=bakery_data.coupon_code,
error=error
)
2025-10-06 15:27:01 +02:00
logger.info("Bakery registered successfully",
name=bakery_data.name,
owner_email=current_user.get('email'),
2025-10-17 18:14:28 +02:00
tenant_id=result.id,
coupon_applied=bakery_data.coupon_code is not None)
2025-10-06 15:27:01 +02:00
return result
except HTTPException:
raise
except Exception as e:
logger.error("Bakery registration failed",
name=bakery_data.name,
owner_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Bakery registration failed"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("{tenant_id}/my-access", include_tenant_prefix=False), response_model=TenantAccessResponse)
async def get_current_user_tenant_access(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""Get current user's access to tenant with role and permissions"""
try:
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
tenant_service = EnhancedTenantService(database_manager)
access_info = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
return access_info
except Exception as e:
logger.error("Current user access verification failed",
user_id=current_user["user_id"],
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Access verification failed"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("{tenant_id}/access/{user_id}", include_tenant_prefix=False), response_model=TenantAccessResponse)
async def verify_tenant_access(
tenant_id: UUID = Path(..., description="Tenant ID"),
user_id: str = Path(..., description="User ID")
):
"""Verify if user has access to tenant - Enhanced version with detailed permissions"""
if is_internal_service(user_id):
logger.info("Service access granted", service=user_id, tenant_id=str(tenant_id))
2025-10-06 15:27:01 +02:00
return TenantAccessResponse(
has_access=True,
role="service",
permissions=["read", "write"]
)
try:
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
tenant_service = EnhancedTenantService(database_manager)
access_info = await tenant_service.verify_user_access(user_id, str(tenant_id))
return access_info
except Exception as e:
logger.error("Access verification failed",
user_id=user_id,
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Access verification failed"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
# ============================================================================
# TENANT SEARCH & DISCOVERY OPERATIONS
# ============================================================================
@router.get(route_builder.build_base_route("subdomain/{subdomain}", include_tenant_prefix=False), response_model=TenantResponse)
@track_endpoint_metrics("tenant_get_by_subdomain")
async def get_tenant_by_subdomain(
subdomain: str = Path(..., description="Tenant subdomain"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get tenant by subdomain with enhanced validation"""
tenant = await tenant_service.get_tenant_by_subdomain(subdomain)
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
access = await tenant_service.verify_user_access(current_user["user_id"], tenant.id)
if not access.has_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant"
)
return tenant
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("user/{user_id}/owned", include_tenant_prefix=False), response_model=List[TenantResponse])
async def get_user_owned_tenants(
user_id: str = Path(..., description="User ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get all tenants owned by a user with enhanced data"""
user_role = current_user.get('role', '').lower()
2025-11-30 09:12:40 +01:00
is_demo_user = current_user.get("is_demo", False) and user_id == "demo-user"
if user_id != current_user["user_id"] and not is_demo_user and user_role != 'admin':
2025-10-06 15:27:01 +02:00
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only access your own tenants"
)
2025-11-30 09:12:40 +01:00
if current_user.get("is_demo", False):
demo_session_id = current_user.get("demo_session_id")
demo_account_type = current_user.get("demo_account_type", "")
if demo_session_id:
logger.info("Fetching virtual tenants for demo session",
demo_session_id=demo_session_id,
demo_account_type=demo_account_type)
virtual_tenants = await tenant_service.get_virtual_tenants_for_session(demo_session_id, demo_account_type)
return virtual_tenants
else:
virtual_tenants = await tenant_service.get_demo_tenants_by_session_type(
demo_account_type,
str(current_user["user_id"])
)
return virtual_tenants
actual_user_id = current_user["user_id"] if is_demo_user else user_id
tenants = await tenant_service.get_user_tenants(actual_user_id)
2025-10-06 15:27:01 +02:00
return tenants
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("search", include_tenant_prefix=False), response_model=List[TenantResponse])
@track_endpoint_metrics("tenant_search")
async def search_tenants(
search_term: str = Query(..., description="Search term"),
business_type: Optional[str] = Query(None, description="Business type filter"),
city: Optional[str] = Query(None, description="City filter"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Search tenants with advanced filters and pagination"""
tenants = await tenant_service.search_tenants(
search_term=search_term,
business_type=business_type,
city=city,
skip=skip,
limit=limit
)
return tenants
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("nearby", include_tenant_prefix=False), response_model=List[TenantResponse])
@track_endpoint_metrics("tenant_get_nearby")
async def get_nearby_tenants(
latitude: float = Query(..., description="Latitude coordinate"),
longitude: float = Query(..., description="Longitude coordinate"),
radius_km: float = Query(10.0, ge=0.1, le=100.0, description="Search radius in kilometers"),
limit: int = Query(50, ge=1, le=100, description="Maximum number of results"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get tenants near a geographic location with enhanced geospatial search"""
tenants = await tenant_service.get_tenants_near_location(
latitude=latitude,
longitude=longitude,
radius_km=radius_km,
limit=limit
)
return tenants
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("users/{user_id}", include_tenant_prefix=False), response_model=List[TenantResponse])
@track_endpoint_metrics("tenant_get_user_tenants")
async def get_user_tenants(
user_id: str = Path(..., description="User ID"),
2025-12-17 16:28:58 +01:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-10-06 15:27:01 +02:00
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get all tenants owned by a user - Fixed endpoint for frontend"""
2025-12-17 16:28:58 +01:00
is_demo_user = current_user.get("is_demo", False)
is_service_account = current_user.get("type") == "service"
user_role = current_user.get('role', '').lower()
2026-01-16 15:19:34 +01:00
2025-12-17 16:28:58 +01:00
if user_id != current_user["user_id"] and not is_service_account and not (is_demo_user and user_id == "demo-user") and user_role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only access your own tenants"
)
2025-10-06 15:27:01 +02:00
try:
tenants = await tenant_service.get_user_tenants(user_id)
logger.info("Retrieved user tenants", user_id=user_id, tenant_count=len(tenants))
return tenants
except Exception as e:
logger.error("Get user tenants failed", user_id=user_id, error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user tenants"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.get(route_builder.build_base_route("members/user/{user_id}", include_tenant_prefix=False))
@track_endpoint_metrics("tenant_get_user_memberships")
async def get_user_memberships(
user_id: str = Path(..., description="User ID"),
2025-12-17 16:28:58 +01:00
current_user: Dict[str, Any] = Depends(get_current_user_dep),
2025-10-06 15:27:01 +02:00
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get all tenant memberships for a user (for authentication service)"""
2025-12-17 16:28:58 +01:00
is_demo_user = current_user.get("is_demo", False)
is_service_account = current_user.get("type") == "service"
user_role = current_user.get('role', '').lower()
2026-01-16 15:19:34 +01:00
2025-12-17 16:28:58 +01:00
if user_id != current_user["user_id"] and not is_service_account and not (is_demo_user and user_id == "demo-user") and user_role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only access your own memberships"
)
2025-10-06 15:27:01 +02:00
try:
memberships = await tenant_service.get_user_memberships(user_id)
logger.info("Retrieved user memberships", user_id=user_id, membership_count=len(memberships))
return memberships
except Exception as e:
logger.error("Get user memberships failed", user_id=user_id, error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user memberships"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
# ============================================================================
# TENANT MODEL STATUS OPERATIONS
# ============================================================================
@router.put(route_builder.build_base_route("{tenant_id}/model-status", include_tenant_prefix=False))
@track_endpoint_metrics("tenant_update_model_status")
async def update_tenant_model_status(
tenant_id: UUID = Path(..., description="Tenant ID"),
ml_model_trained: bool = Query(..., description="Whether model is trained"),
last_training_date: Optional[datetime] = Query(None, description="Last training date"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Update tenant model training status with enhanced tracking"""
try:
result = await tenant_service.update_model_status(
str(tenant_id),
ml_model_trained,
current_user["user_id"],
last_training_date
)
return result
except HTTPException:
raise
except Exception as e:
logger.error("Model status update failed",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update model status"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
# ============================================================================
# TENANT ACTIVATION/DEACTIVATION OPERATIONS
# ============================================================================
@router.post(route_builder.build_base_route("{tenant_id}/deactivate", include_tenant_prefix=False))
@track_endpoint_metrics("tenant_deactivate")
@owner_role_required
2025-10-06 15:27:01 +02:00
async def deactivate_tenant(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Deactivate a tenant (owner only) with enhanced validation"""
try:
success = await tenant_service.deactivate_tenant(
str(tenant_id),
current_user["user_id"]
)
if success:
try:
from app.core.database import get_db_session
async with get_db_session() as db:
await audit_logger.log_event(
db_session=db,
tenant_id=str(tenant_id),
user_id=current_user["user_id"],
action=AuditAction.DEACTIVATE.value,
resource_type="tenant",
resource_id=str(tenant_id),
severity=AuditSeverity.CRITICAL.value,
description=f"Owner {current_user.get('email', current_user['user_id'])} deactivated tenant",
endpoint="/{tenant_id}/deactivate",
method="POST"
)
except Exception as audit_error:
logger.warning("Failed to log audit event", error=str(audit_error))
2025-10-06 15:27:01 +02:00
return {"success": True, "message": "Tenant deactivated successfully"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to deactivate tenant"
)
except HTTPException:
raise
except Exception as e:
logger.error("Tenant deactivation failed",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to deactivate tenant"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
@router.post(route_builder.build_base_route("{tenant_id}/activate", include_tenant_prefix=False))
@track_endpoint_metrics("tenant_activate")
@owner_role_required
2025-10-06 15:27:01 +02:00
async def activate_tenant(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Activate a previously deactivated tenant (owner only) with enhanced validation"""
try:
success = await tenant_service.activate_tenant(
str(tenant_id),
current_user["user_id"]
)
if success:
try:
from app.core.database import get_db_session
async with get_db_session() as db:
await audit_logger.log_event(
db_session=db,
tenant_id=str(tenant_id),
user_id=current_user["user_id"],
action=AuditAction.ACTIVATE.value,
resource_type="tenant",
resource_id=str(tenant_id),
severity=AuditSeverity.HIGH.value,
description=f"Owner {current_user.get('email', current_user['user_id'])} activated tenant",
endpoint="/{tenant_id}/activate",
method="POST"
)
except Exception as audit_error:
logger.warning("Failed to log audit event", error=str(audit_error))
2025-10-06 15:27:01 +02:00
return {"success": True, "message": "Tenant activated successfully"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to activate tenant"
)
except HTTPException:
raise
except Exception as e:
logger.error("Tenant activation failed",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to activate tenant"
)
2026-01-16 15:19:34 +01:00
2025-10-06 15:27:01 +02:00
# ============================================================================
# TENANT STATISTICS & ANALYTICS
# ============================================================================
@router.get(route_builder.build_base_route("statistics", include_tenant_prefix=False), dependencies=[Depends(require_admin_role_dep)])
@track_endpoint_metrics("tenant_get_statistics")
async def get_tenant_statistics(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get comprehensive tenant statistics (admin only) with enhanced analytics"""
try:
stats = await tenant_service.get_tenant_statistics()
return stats
except Exception as e:
logger.error("Get tenant statistics failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get tenant statistics"
)
2026-01-15 20:45:49 +01:00
2025-10-06 15:27:01 +02:00
# ============================================================================
2026-01-16 15:19:34 +01:00
# USER-TENANT RELATIONSHIP OPERATIONS
2025-10-06 15:27:01 +02:00
# ============================================================================
2026-01-13 22:22:38 +01:00
2026-01-16 15:19:34 +01:00
@router.get(route_builder.build_base_route("users/{user_id}/primary-tenant", include_tenant_prefix=False))
async def get_user_primary_tenant(
user_id: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep)
2026-01-13 22:22:38 +01:00
):
"""
2026-01-16 15:19:34 +01:00
Get the primary tenant for a user
2026-01-13 22:22:38 +01:00
2026-01-16 15:19:34 +01:00
This endpoint is used by the auth service to validate user subscriptions
during login. It returns the user's primary tenant (the one they own or
have primary access to).
2026-01-13 22:22:38 +01:00
Args:
2026-01-16 15:19:34 +01:00
user_id: The user ID to look up
2026-01-13 22:22:38 +01:00
Returns:
2026-01-16 15:19:34 +01:00
Dictionary with user's primary tenant information, or None if no tenant found
2026-01-13 22:22:38 +01:00
2026-01-16 15:19:34 +01:00
Example Response:
{
"user_id": "user-uuid",
"tenant_id": "tenant-uuid",
"tenant_name": "Bakery Name",
"tenant_type": "standalone",
"is_owner": true
2026-01-13 22:22:38 +01:00
}
2026-01-10 21:45:37 +01:00
"""
try:
2026-01-16 15:19:34 +01:00
from app.core.database import database_manager
from app.repositories.tenant_repository import TenantRepository
from app.models.tenants import Tenant
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
# Get user's primary tenant (the one they own)
primary_tenant = await tenant_repo.get_user_primary_tenant(user_id)
if primary_tenant:
logger.info("Found primary tenant for user",
user_id=user_id,
tenant_id=str(primary_tenant.id),
tenant_name=primary_tenant.name)
return {
'user_id': user_id,
'tenant_id': str(primary_tenant.id),
'tenant_name': primary_tenant.name,
'tenant_type': primary_tenant.tenant_type,
'is_owner': True
}
else:
# If no primary tenant found, check if user has access to any tenant
any_tenant = await tenant_repo.get_any_user_tenant(user_id)
if any_tenant:
logger.info("Found accessible tenant for user",
user_id=user_id,
tenant_id=str(any_tenant.id),
tenant_name=any_tenant.name)
return {
'user_id': user_id,
'tenant_id': str(any_tenant.id),
'tenant_name': any_tenant.name,
'tenant_type': any_tenant.tenant_type,
'is_owner': False
}
else:
logger.info("No tenant found for user", user_id=user_id)
return None
2026-01-10 21:45:37 +01:00
except Exception as e:
2026-01-16 15:19:34 +01:00
logger.error(f"Failed to get primary tenant for user {user_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get primary tenant: {str(e)}")