735 lines
30 KiB
Python
735 lines
30 KiB
Python
"""
|
|
Tenant Operations API - BUSINESS operations
|
|
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.
|
|
"""
|
|
|
|
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
|
|
from app.models import AuditLog
|
|
from shared.auth.decorators import (
|
|
get_current_user_dep,
|
|
require_admin_role_dep
|
|
)
|
|
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
|
|
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
|
|
|
|
logger = structlog.get_logger()
|
|
router = APIRouter()
|
|
route_builder = RouteBuilder("tenants")
|
|
|
|
# Initialize audit logger
|
|
audit_logger = create_audit_logger("tenant-service", AuditLog)
|
|
|
|
|
|
# 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")
|
|
|
|
|
|
# ============================================================================
|
|
# 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),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service),
|
|
payment_service: PaymentService = Depends(get_payment_service),
|
|
db: AsyncSession = Depends(get_db)
|
|
):
|
|
"""Register a new bakery/tenant with enhanced validation and features"""
|
|
|
|
try:
|
|
coupon_validation = None
|
|
success = None
|
|
discount = None
|
|
error = None
|
|
|
|
result = await tenant_service.create_bakery(
|
|
bakery_data,
|
|
current_user["user_id"]
|
|
)
|
|
|
|
tenant_id = result.id
|
|
|
|
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"])
|
|
|
|
try:
|
|
from app.services.subscription_service import SubscriptionService
|
|
|
|
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"]
|
|
)
|
|
|
|
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:
|
|
from app.services.coupon_service import CouponService
|
|
|
|
coupon_service = CouponService(db)
|
|
coupon_validation = await coupon_service.validate_coupon_code(
|
|
bakery_data.coupon_code,
|
|
tenant_id
|
|
)
|
|
|
|
if not coupon_validation["valid"]:
|
|
logger.warning(
|
|
"Invalid coupon code provided during registration",
|
|
coupon_code=bakery_data.coupon_code,
|
|
error=coupon_validation["error_message"]
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=coupon_validation["error_message"]
|
|
)
|
|
|
|
success, discount, error = await coupon_service.redeem_coupon(
|
|
bakery_data.coupon_code,
|
|
tenant_id,
|
|
base_trial_days=0
|
|
)
|
|
|
|
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:
|
|
trial_end_date = datetime.now(timezone.utc)
|
|
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(
|
|
"Default subscription created for new tenant",
|
|
tenant_id=str(result.id),
|
|
plan="starter",
|
|
trial_days=0
|
|
)
|
|
except Exception as subscription_error:
|
|
logger.error(
|
|
"Failed to create default subscription for tenant",
|
|
tenant_id=str(result.id),
|
|
error=str(subscription_error)
|
|
)
|
|
|
|
if coupon_validation and coupon_validation["valid"]:
|
|
from app.core.config import settings
|
|
from app.services.coupon_service import CouponService
|
|
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
|
|
|
async with database_manager.get_session() as session:
|
|
coupon_service = CouponService(session)
|
|
success, discount, error = await coupon_service.redeem_coupon(
|
|
bakery_data.coupon_code,
|
|
result.id,
|
|
base_trial_days=0
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
logger.info("Bakery registered successfully",
|
|
name=bakery_data.name,
|
|
owner_email=current_user.get('email'),
|
|
tenant_id=result.id,
|
|
coupon_applied=bakery_data.coupon_code is not None)
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
@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"
|
|
)
|
|
|
|
|
|
@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))
|
|
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"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# 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
|
|
|
|
|
|
@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()
|
|
|
|
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':
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Can only access your own tenants"
|
|
)
|
|
|
|
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)
|
|
return tenants
|
|
|
|
|
|
@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
|
|
|
|
|
|
@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
|
|
|
|
|
|
@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"),
|
|
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 - Fixed endpoint for frontend"""
|
|
|
|
is_demo_user = current_user.get("is_demo", False)
|
|
is_service_account = current_user.get("type") == "service"
|
|
user_role = current_user.get('role', '').lower()
|
|
|
|
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"
|
|
)
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
@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"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""Get all tenant memberships for a user (for authentication service)"""
|
|
|
|
is_demo_user = current_user.get("is_demo", False)
|
|
is_service_account = current_user.get("type") == "service"
|
|
user_role = current_user.get('role', '').lower()
|
|
|
|
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"
|
|
)
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# 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"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# 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
|
|
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))
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
@router.post(route_builder.build_base_route("{tenant_id}/activate", include_tenant_prefix=False))
|
|
@track_endpoint_metrics("tenant_activate")
|
|
@owner_role_required
|
|
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))
|
|
|
|
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"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# 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"
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# USER-TENANT RELATIONSHIP OPERATIONS
|
|
# ============================================================================
|
|
|
|
@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)
|
|
):
|
|
"""
|
|
Get the primary tenant for a user
|
|
|
|
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).
|
|
|
|
Args:
|
|
user_id: The user ID to look up
|
|
|
|
Returns:
|
|
Dictionary with user's primary tenant information, or None if no tenant found
|
|
|
|
Example Response:
|
|
{
|
|
"user_id": "user-uuid",
|
|
"tenant_id": "tenant-uuid",
|
|
"tenant_name": "Bakery Name",
|
|
"tenant_type": "standalone",
|
|
"is_owner": true
|
|
}
|
|
"""
|
|
try:
|
|
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
|
|
|
|
except Exception as e:
|
|
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)}")
|