Files
bakery-ia/services/tenant/app/api/enterprise_upgrade.py
2025-11-30 09:12:40 +01:00

359 lines
14 KiB
Python

"""
Enterprise Upgrade API
Endpoints for upgrading tenants to enterprise tier and managing child outlets
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from typing import Dict, Any, Optional
import uuid
from datetime import datetime, date
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.tenants import Tenant
from app.models.tenant_location import TenantLocation
from app.services.tenant_service import EnhancedTenantService
from app.core.config import settings
from shared.auth.tenant_access import verify_tenant_permission_dep
from shared.auth.decorators import get_current_user_dep
from shared.clients.subscription_client import SubscriptionServiceClient, get_subscription_service_client
from shared.subscription.plans import SubscriptionTier, QuotaLimits
from shared.database.base import create_database_manager
import structlog
logger = structlog.get_logger()
router = APIRouter()
# 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")
# Pydantic models for request bodies
class EnterpriseUpgradeRequest(BaseModel):
location_name: Optional[str] = Field(default="Central Production Facility")
address: Optional[str] = None
city: Optional[str] = None
postal_code: Optional[str] = None
latitude: Optional[float] = None
longitude: Optional[float] = None
production_capacity_kg: Optional[int] = Field(default=1000)
class ChildOutletRequest(BaseModel):
name: str
subdomain: str
address: str
city: Optional[str] = None
postal_code: str
latitude: Optional[float] = None
longitude: Optional[float] = None
phone: Optional[str] = None
email: Optional[str] = None
delivery_days: Optional[list] = None
@router.post("/tenants/{tenant_id}/upgrade-to-enterprise")
async def upgrade_to_enterprise(
tenant_id: str,
upgrade_data: EnterpriseUpgradeRequest,
subscription_client: SubscriptionServiceClient = Depends(get_subscription_service_client),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""
Upgrade a tenant to enterprise tier with central production facility
"""
try:
from app.core.database import database_manager
from app.repositories.tenant_repository import TenantRepository
# Get the current tenant
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
tenant = await tenant_repo.get_by_id(tenant_id)
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
# Verify current subscription allows upgrade to enterprise
current_subscription = await subscription_client.get_subscription(tenant_id)
if current_subscription['plan'] not in [SubscriptionTier.STARTER.value, SubscriptionTier.PROFESSIONAL.value]:
raise HTTPException(status_code=400, detail="Only starter and professional tier tenants can be upgraded to enterprise")
# Verify user has admin/owner role
# This is handled by current_user check
# Update tenant to parent type
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
updated_tenant = await tenant_repo.update(
tenant_id,
{
'tenant_type': 'parent',
'hierarchy_path': f"{tenant_id}" # Root path
}
)
await session.commit()
# Create central production location
location_data = {
'tenant_id': tenant_id,
'name': upgrade_data.location_name,
'location_type': 'central_production',
'address': upgrade_data.address or tenant.address,
'city': upgrade_data.city or tenant.city,
'postal_code': upgrade_data.postal_code or tenant.postal_code,
'latitude': upgrade_data.latitude or tenant.latitude,
'longitude': upgrade_data.longitude or tenant.longitude,
'capacity': upgrade_data.production_capacity_kg,
'is_active': True
}
from app.repositories.tenant_location_repository import TenantLocationRepository
from app.core.database import database_manager
# Create async session
async with database_manager.get_session() as session:
location_repo = TenantLocationRepository(session)
created_location = await location_repo.create_location(location_data)
await session.commit()
# Update subscription to enterprise tier
await subscription_client.update_subscription_plan(
tenant_id=tenant_id,
new_plan=SubscriptionTier.ENTERPRISE.value
)
return {
'success': True,
'tenant': updated_tenant,
'production_location': created_location,
'message': 'Tenant successfully upgraded to enterprise tier'
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to upgrade tenant: {str(e)}")
@router.post("/tenants/{parent_id}/add-child-outlet")
async def add_child_outlet(
parent_id: str,
child_data: ChildOutletRequest,
subscription_client: SubscriptionServiceClient = Depends(get_subscription_service_client),
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""
Add a new child outlet to a parent tenant
"""
try:
from app.core.database import database_manager
from app.repositories.tenant_repository import TenantRepository
# Get parent tenant and verify it's a parent
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
parent_tenant = await tenant_repo.get_by_id(parent_id)
if not parent_tenant:
raise HTTPException(status_code=400, detail="Parent tenant not found")
parent_dict = {
'id': str(parent_tenant.id),
'name': parent_tenant.name,
'tenant_type': parent_tenant.tenant_type,
'subscription_tier': parent_tenant.subscription_tier,
'business_type': parent_tenant.business_type,
'business_model': parent_tenant.business_model,
'city': parent_tenant.city,
'phone': parent_tenant.phone,
'email': parent_tenant.email,
'owner_id': parent_tenant.owner_id
}
if parent_dict.get('tenant_type') != 'parent':
raise HTTPException(status_code=400, detail="Tenant is not a parent type")
# Validate subscription tier
from shared.clients import get_tenant_client
from shared.subscription.plans import PlanFeatures
tenant_client = get_tenant_client(config=settings, service_name="tenant-service")
subscription = await tenant_client.get_tenant_subscription(parent_id)
if not subscription:
raise HTTPException(
status_code=403,
detail="No active subscription found for parent tenant"
)
tier = subscription.get("plan", "starter")
if not PlanFeatures.validate_tenant_access(tier, "child"):
raise HTTPException(
status_code=403,
detail=f"Creating child outlets requires Enterprise subscription. Current plan: {tier}"
)
# Check if parent has reached child quota
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
current_child_count = await tenant_repo.get_child_tenant_count(parent_id)
# Get max children from subscription plan
max_children = QuotaLimits.get_limit("MAX_CHILD_TENANTS", tier)
if max_children is not None and current_child_count >= max_children:
raise HTTPException(
status_code=403,
detail=f"Child tenant limit reached. Current: {current_child_count}, Maximum: {max_children}"
)
# Create new child tenant
child_id = str(uuid.uuid4())
child_tenant_data = {
'id': child_id,
'name': child_data.name,
'subdomain': child_data.subdomain,
'business_type': parent_dict.get('business_type', 'bakery'),
'business_model': parent_dict.get('business_model', 'retail_bakery'),
'address': child_data.address,
'city': child_data.city or parent_dict.get('city'),
'postal_code': child_data.postal_code,
'latitude': child_data.latitude,
'longitude': child_data.longitude,
'phone': child_data.phone or parent_dict.get('phone'),
'email': child_data.email or parent_dict.get('email'),
'parent_tenant_id': parent_id,
'tenant_type': 'child',
'hierarchy_path': f"{parent_id}.{child_id}",
'owner_id': parent_dict.get('owner_id'), # Same owner as parent
'is_active': True
}
# Use database managed session
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
created_child = await tenant_repo.create(child_tenant_data)
await session.commit()
created_child_dict = {
'id': str(created_child.id),
'name': created_child.name,
'subdomain': created_child.subdomain
}
# Create retail outlet location for the child
location_data = {
'tenant_id': uuid.UUID(child_id),
'name': f"Outlet - {child_data.name}",
'location_type': 'retail_outlet',
'address': child_data.address,
'city': child_data.city or parent_dict.get('city'),
'postal_code': child_data.postal_code,
'latitude': child_data.latitude,
'longitude': child_data.longitude,
'delivery_windows': child_data.delivery_days,
'is_active': True
}
from app.repositories.tenant_location_repository import TenantLocationRepository
# Create async session
async with database_manager.get_session() as session:
location_repo = TenantLocationRepository(session)
created_location = await location_repo.create_location(location_data)
await session.commit()
location_dict = {
'id': str(created_location.id) if created_location else None,
'name': created_location.name if created_location else None
}
# Copy relevant settings from parent (with child-specific overrides)
# This would typically involve copying settings via tenant settings service
# Create child subscription inheriting from parent
await subscription_client.create_child_subscription(
child_tenant_id=child_id,
parent_tenant_id=parent_id
)
return {
'success': True,
'child_tenant': created_child_dict,
'location': location_dict,
'message': 'Child outlet successfully added'
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to add child outlet: {str(e)}")
@router.get("/tenants/{tenant_id}/hierarchy")
async def get_tenant_hierarchy(
tenant_id: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""
Get tenant hierarchy information
"""
try:
from app.core.database import database_manager
from app.repositories.tenant_repository import TenantRepository
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
tenant = await tenant_repo.get_by_id(tenant_id)
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
result = {
'tenant_id': tenant_id,
'name': tenant.name,
'tenant_type': tenant.tenant_type,
'parent_tenant_id': tenant.parent_tenant_id,
'hierarchy_path': tenant.hierarchy_path,
'is_parent': tenant.tenant_type == 'parent',
'is_child': tenant.tenant_type == 'child'
}
# If this is a parent, include child count
if tenant.tenant_type == 'parent':
child_count = await tenant_repo.get_child_tenant_count(tenant_id)
result['child_tenant_count'] = child_count
return result
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get hierarchy: {str(e)}")
@router.get("/users/{user_id}/tenant-hierarchy")
async def get_user_accessible_tenant_hierarchy(
user_id: str,
current_user: Dict[str, Any] = Depends(get_current_user_dep)
):
"""
Get all tenants a user has access to, organized in hierarchy
"""
try:
from app.core.database import database_manager
from app.repositories.tenant_repository import TenantRepository
# Fetch all tenants where user has access, organized hierarchically
async with database_manager.get_session() as session:
tenant_repo = TenantRepository(Tenant, session)
user_tenants = await tenant_repo.get_user_tenants_with_hierarchy(user_id)
return {
'user_id': user_id,
'tenants': user_tenants,
'total_count': len(user_tenants)
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get user hierarchy: {str(e)}")