359 lines
14 KiB
Python
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)}") |