Files
bakery-ia/services/tenant/app/services/tenant_service.py

1096 lines
43 KiB
Python
Raw Normal View History

2025-07-19 21:16:25 +02:00
"""
2025-08-08 09:08:41 +02:00
Enhanced Tenant Service
Business logic layer using repository pattern for tenant operations
2025-07-19 21:16:25 +02:00
"""
import structlog
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from fastapi import HTTPException, status
2025-08-08 09:08:41 +02:00
from app.repositories import TenantRepository, TenantMemberRepository, SubscriptionRepository
from app.models.tenants import Tenant, TenantMember, Subscription
from app.schemas.tenants import (
BakeryRegistration, TenantResponse, TenantAccessResponse,
TenantUpdate, TenantMemberResponse
)
2025-07-19 21:16:25 +02:00
from app.services.messaging import publish_tenant_created, publish_member_added
2025-08-08 09:08:41 +02:00
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
from shared.database.base import create_database_manager
from shared.database.unit_of_work import UnitOfWork
from shared.clients.nominatim_client import NominatimClient
2025-07-19 21:16:25 +02:00
logger = structlog.get_logger()
2025-08-08 09:08:41 +02:00
class EnhancedTenantService:
"""Enhanced tenant management business logic using repository pattern with dependency injection"""
def __init__(self, database_manager=None):
self.database_manager = database_manager or create_database_manager()
async def _init_repositories(self, session):
"""Initialize repositories with session"""
self.tenant_repo = TenantRepository(Tenant, session)
self.member_repo = TenantMemberRepository(TenantMember, session)
self.subscription_repo = SubscriptionRepository(Subscription, session)
return {
'tenant': self.tenant_repo,
'member': self.member_repo,
'subscription': self.subscription_repo
}
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
async def create_bakery(
self,
bakery_data: BakeryRegistration,
owner_id: str,
session=None
) -> TenantResponse:
"""Create a new bakery/tenant with enhanced validation and features using repository pattern"""
2025-07-19 21:16:25 +02:00
try:
2025-08-08 09:08:41 +02:00
async with self.database_manager.get_session() as db_session:
async with UnitOfWork(db_session) as uow:
# Register repositories
tenant_repo = uow.register_repository("tenants", TenantRepository, Tenant)
member_repo = uow.register_repository("members", TenantMemberRepository, TenantMember)
subscription_repo = uow.register_repository("subscriptions", SubscriptionRepository, Subscription)
# Geocode address using Nominatim
latitude = getattr(bakery_data, 'latitude', None)
longitude = getattr(bakery_data, 'longitude', None)
if not latitude or not longitude:
try:
from app.core.config import settings
nominatim_client = NominatimClient(settings)
location = await nominatim_client.geocode_address(
street=bakery_data.address,
city=bakery_data.city,
postal_code=bakery_data.postal_code,
country="Spain"
)
if location:
latitude = float(location["lat"])
longitude = float(location["lon"])
logger.info(
"Address geocoded successfully",
address=bakery_data.address,
city=bakery_data.city,
latitude=latitude,
longitude=longitude
)
else:
logger.warning(
"Could not geocode address, using default Madrid coordinates",
address=bakery_data.address,
city=bakery_data.city
)
latitude = 40.4168
longitude = -3.7038
except Exception as e:
logger.warning(
"Geocoding failed, using default coordinates",
address=bakery_data.address,
error=str(e)
)
latitude = 40.4168
longitude = -3.7038
2025-08-08 09:08:41 +02:00
# Prepare tenant data
tenant_data = {
"name": bakery_data.name,
"business_type": bakery_data.business_type,
"address": bakery_data.address,
"city": bakery_data.city,
"postal_code": bakery_data.postal_code,
"phone": bakery_data.phone,
"owner_id": owner_id,
"email": getattr(bakery_data, 'email', None),
"latitude": latitude,
"longitude": longitude,
2025-08-08 09:08:41 +02:00
"is_active": True
}
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
# Create tenant using repository
tenant = await tenant_repo.create_tenant(tenant_data)
2025-07-19 21:16:25 +02:00
# Create owner membership
2025-08-08 09:08:41 +02:00
membership_data = {
"tenant_id": str(tenant.id),
"user_id": owner_id,
"role": "owner",
"is_active": True
}
owner_membership = await member_repo.create_membership(membership_data)
# Get subscription plan from user's registration using standardized auth client
selected_plan = "starter" # Default fallback
try:
from shared.clients.auth_client import AuthServiceClient
from app.core.config import settings
auth_client = AuthServiceClient(settings)
selected_plan = await auth_client.get_subscription_plan_from_registration(owner_id)
logger.info("Retrieved subscription plan from registration",
tenant_id=tenant.id,
owner_id=owner_id,
plan=selected_plan)
except Exception as e:
logger.warning("Could not retrieve subscription plan from auth service, using default",
error=str(e),
owner_id=owner_id,
default_plan=selected_plan)
# Create subscription with selected or default plan
2025-08-08 09:08:41 +02:00
subscription_data = {
"tenant_id": str(tenant.id),
"plan": selected_plan,
2025-08-08 09:08:41 +02:00
"status": "active"
}
2025-08-08 09:08:41 +02:00
subscription = await subscription_repo.create_subscription(subscription_data)
logger.info("Subscription created",
tenant_id=tenant.id,
plan=selected_plan)
2025-08-08 09:08:41 +02:00
# Commit the transaction
await uow.commit()
2025-07-19 21:16:25 +02:00
# Publish event
2025-08-08 09:08:41 +02:00
try:
await publish_tenant_created(str(tenant.id), owner_id, bakery_data.name)
except Exception as e:
logger.warning("Failed to publish tenant created event", error=str(e))
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
logger.info("Bakery created successfully",
tenant_id=tenant.id,
name=bakery_data.name,
owner_id=owner_id,
subdomain=tenant.subdomain)
2025-07-19 21:16:25 +02:00
return TenantResponse.from_orm(tenant)
2025-08-08 09:08:41 +02:00
except (ValidationError, DuplicateRecordError) as e:
logger.error("Validation error creating bakery",
name=bakery_data.name,
owner_id=owner_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
2025-07-19 21:16:25 +02:00
except Exception as e:
2025-08-08 09:08:41 +02:00
logger.error("Error creating bakery",
name=bakery_data.name,
owner_id=owner_id,
error=str(e))
2025-07-19 21:16:25 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create bakery"
)
2025-08-08 09:08:41 +02:00
async def verify_user_access(
self,
user_id: str,
tenant_id: str
) -> TenantAccessResponse:
"""Verify if user has access to tenant with enhanced permissions"""
2025-07-19 21:16:25 +02:00
try:
2025-08-08 09:08:41 +02:00
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
access_info = await self.member_repo.verify_user_access(user_id, tenant_id)
2025-07-19 21:16:25 +02:00
return TenantAccessResponse(
2025-08-08 09:08:41 +02:00
has_access=access_info["has_access"],
role=access_info["role"],
permissions=access_info["permissions"],
membership_id=access_info.get("membership_id"),
joined_at=access_info.get("joined_at")
2025-07-19 21:16:25 +02:00
)
except Exception as e:
2025-08-08 09:08:41 +02:00
logger.error("Error verifying user access",
user_id=user_id,
tenant_id=tenant_id,
error=str(e))
2025-07-19 21:16:25 +02:00
return TenantAccessResponse(
has_access=False,
role="none",
permissions=[]
)
2025-08-08 09:08:41 +02:00
async def get_tenant_by_id(self, tenant_id: str) -> Optional[TenantResponse]:
"""Get tenant by ID with enhanced data"""
2025-07-19 21:16:25 +02:00
try:
2025-08-08 09:08:41 +02:00
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenant = await self.tenant_repo.get_by_id(tenant_id)
if tenant:
return TenantResponse.from_orm(tenant)
return None
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
except Exception as e:
logger.error("Error getting tenant",
tenant_id=tenant_id,
error=str(e))
2025-07-19 21:16:25 +02:00
return None
2025-08-08 09:08:41 +02:00
async def get_tenant_by_subdomain(self, subdomain: str) -> Optional[TenantResponse]:
"""Get tenant by subdomain"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenant = await self.tenant_repo.get_by_subdomain(subdomain)
if tenant:
return TenantResponse.from_orm(tenant)
return None
2025-07-19 21:16:25 +02:00
except Exception as e:
2025-08-08 09:08:41 +02:00
logger.error("Error getting tenant by subdomain",
subdomain=subdomain,
error=str(e))
2025-07-19 21:16:25 +02:00
return None
2025-08-08 09:08:41 +02:00
async def get_user_tenants(self, owner_id: str) -> List[TenantResponse]:
"""Get all tenants owned by a user"""
2025-11-05 13:34:56 +01:00
2025-08-08 09:08:41 +02:00
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenants = await self.tenant_repo.get_tenants_by_owner(owner_id)
return [TenantResponse.from_orm(tenant) for tenant in tenants]
2025-11-05 13:34:56 +01:00
2025-08-08 09:08:41 +02:00
except Exception as e:
logger.error("Error getting user tenants",
owner_id=owner_id,
error=str(e))
return []
2025-11-05 13:34:56 +01:00
async def get_active_tenants(self, skip: int = 0, limit: int = 100) -> List[TenantResponse]:
"""Get all active tenants"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenants = await self.tenant_repo.get_active_tenants(skip=skip, limit=limit)
return [TenantResponse.from_orm(tenant) for tenant in tenants]
except Exception as e:
logger.error("Error getting active tenants",
skip=skip,
limit=limit,
error=str(e))
return []
2025-08-08 09:08:41 +02:00
async def search_tenants(
self,
search_term: str,
business_type: str = None,
city: str = None,
skip: int = 0,
limit: int = 50
) -> List[TenantResponse]:
"""Search tenants with filters"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenants = await self.tenant_repo.search_tenants(
search_term, business_type, city, skip, limit
)
return [TenantResponse.from_orm(tenant) for tenant in tenants]
except Exception as e:
logger.error("Error searching tenants",
search_term=search_term,
error=str(e))
return []
async def update_tenant(
2025-09-21 22:56:55 +02:00
self,
tenant_id: str,
update_data: TenantUpdate,
2025-08-08 09:08:41 +02:00
user_id: str,
session: AsyncSession = None
) -> TenantResponse:
"""Update tenant information with permission checks"""
2025-09-21 22:56:55 +02:00
2025-07-19 21:16:25 +02:00
try:
# Verify user has admin access
2025-08-08 09:08:41 +02:00
access = await self.verify_user_access(user_id, tenant_id)
2025-07-19 21:16:25 +02:00
if not access.has_access or access.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to update tenant"
)
2025-09-21 22:56:55 +02:00
# Update tenant using repository with proper session management
2025-07-19 21:16:25 +02:00
update_values = update_data.dict(exclude_unset=True)
if update_values:
2025-09-21 22:56:55 +02:00
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
updated_tenant = await self.tenant_repo.update(tenant_id, update_values)
if not updated_tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
logger.info("Tenant updated successfully",
tenant_id=tenant_id,
updated_by=user_id,
fields=list(update_values.keys()))
return TenantResponse.from_orm(updated_tenant)
# No updates to apply - get current tenant data
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenant = await self.tenant_repo.get_by_id(tenant_id)
return TenantResponse.from_orm(tenant)
2025-07-19 21:16:25 +02:00
except HTTPException:
raise
except Exception as e:
2025-08-08 09:08:41 +02:00
logger.error("Error updating tenant",
tenant_id=tenant_id,
user_id=user_id,
error=str(e))
2025-07-19 21:16:25 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update tenant"
)
2025-08-08 09:08:41 +02:00
async def add_team_member(
self,
tenant_id: str,
user_id: str,
role: str,
invited_by: str,
session: AsyncSession = None
) -> TenantMemberResponse:
"""Add a team member to tenant with enhanced validation"""
2025-07-19 21:16:25 +02:00
try:
# Verify inviter has admin access
2025-08-08 09:08:41 +02:00
access = await self.verify_user_access(invited_by, tenant_id)
2025-07-19 21:16:25 +02:00
if not access.has_access or access.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to add team members"
)
2025-08-08 09:08:41 +02:00
# Create membership using repository
2025-09-12 23:58:26 +02:00
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
membership_data = {
"tenant_id": tenant_id,
"user_id": user_id,
"role": role,
"invited_by": invited_by,
"is_active": True
}
member = await self.member_repo.create_membership(membership_data)
# Publish event
try:
await publish_member_added(tenant_id, user_id, role)
except Exception as e:
logger.warning("Failed to publish member added event", error=str(e))
logger.info("Team member added successfully",
tenant_id=tenant_id,
user_id=user_id,
role=role,
invited_by=invited_by)
return TenantMemberResponse.from_orm(member)
2025-08-08 09:08:41 +02:00
except HTTPException:
raise
except (ValidationError, DuplicateRecordError) as e:
logger.error("Validation error adding team member",
tenant_id=tenant_id,
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error("Error adding team member",
tenant_id=tenant_id,
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add team member"
)
async def get_team_members(
2025-10-24 13:05:04 +02:00
self,
tenant_id: str,
2025-08-08 09:08:41 +02:00
user_id: str,
active_only: bool = True
) -> List[TenantMemberResponse]:
2025-10-24 13:05:04 +02:00
"""Get all team members for a tenant with enriched user information"""
2025-08-08 09:08:41 +02:00
try:
2025-09-12 23:58:26 +02:00
async with self.database_manager.get_session() as session:
# Initialize repositories with session
await self._init_repositories(session)
2025-10-24 13:05:04 +02:00
2025-09-12 23:58:26 +02:00
members = await self.member_repo.get_tenant_members(
2025-10-24 13:05:04 +02:00
tenant_id, active_only=active_only, include_user_info=True
2025-09-12 23:58:26 +02:00
)
2025-10-24 13:05:04 +02:00
2025-09-12 23:58:26 +02:00
return [TenantMemberResponse.from_orm(member) for member in members]
2025-10-24 13:05:04 +02:00
2025-08-08 09:08:41 +02:00
except HTTPException:
raise
except Exception as e:
logger.error("Error getting team members",
tenant_id=tenant_id,
user_id=user_id,
error=str(e))
return []
async def update_member_role(
self,
tenant_id: str,
member_user_id: str,
new_role: str,
updated_by: str,
session: AsyncSession = None
) -> TenantMemberResponse:
"""Update team member role"""
try:
# Verify updater has admin access
access = await self.verify_user_access(updated_by, tenant_id)
if not access.has_access or access.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to update member roles"
)
updated_member = await self.member_repo.update_member_role(
tenant_id, member_user_id, new_role, updated_by
)
if not updated_member:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Member not found"
)
return TenantMemberResponse.from_orm(updated_member)
except HTTPException:
raise
except (ValidationError, DuplicateRecordError) as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
logger.error("Error updating member role",
tenant_id=tenant_id,
member_user_id=member_user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update member role"
)
async def remove_team_member(
self,
tenant_id: str,
member_user_id: str,
removed_by: str,
session: AsyncSession = None
) -> bool:
"""Remove team member from tenant"""
try:
# Verify remover has admin access
access = await self.verify_user_access(removed_by, tenant_id)
if not access.has_access or access.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions to remove team members"
)
removed_member = await self.member_repo.deactivate_membership(
tenant_id, member_user_id, removed_by
)
if not removed_member:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Member not found"
2025-07-19 21:16:25 +02:00
)
2025-08-08 09:08:41 +02:00
return True
except HTTPException:
raise
except ValidationError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
2025-07-19 21:16:25 +02:00
)
2025-08-08 09:08:41 +02:00
except Exception as e:
logger.error("Error removing team member",
tenant_id=tenant_id,
member_user_id=member_user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to remove team member"
)
async def update_model_status(
self,
tenant_id: str,
2025-09-30 21:58:10 +02:00
ml_model_trained: bool,
2025-08-08 09:08:41 +02:00
user_id: str,
last_training_date: datetime = None
) -> TenantResponse:
"""Update tenant model training status"""
try:
# Verify user has access
access = await self.verify_user_access(user_id, tenant_id)
if not access.has_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant"
)
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
updated_tenant = await self.tenant_repo.update_tenant_model_status(
2025-09-30 21:58:10 +02:00
tenant_id, ml_model_trained, last_training_date
2025-08-08 09:08:41 +02:00
)
if not updated_tenant:
2025-07-19 21:16:25 +02:00
raise HTTPException(
2025-08-08 09:08:41 +02:00
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
2025-07-19 21:16:25 +02:00
)
2025-08-08 09:08:41 +02:00
return TenantResponse.from_orm(updated_tenant)
except HTTPException:
raise
except Exception as e:
logger.error("Error updating model status",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update model status"
2025-07-19 21:16:25 +02:00
)
2025-08-08 09:08:41 +02:00
async def get_tenant_statistics(self) -> Dict[str, Any]:
"""Get comprehensive tenant statistics"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
# Get tenant statistics
tenant_stats = await self.tenant_repo.get_tenant_statistics()
# Get subscription statistics
subscription_stats = await self.subscription_repo.get_subscription_statistics()
return {
"tenants": tenant_stats,
"subscriptions": subscription_stats
}
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
except Exception as e:
logger.error("Error getting tenant statistics", error=str(e))
return {
"tenants": {},
"subscriptions": {}
}
async def get_tenants_near_location(
self,
latitude: float,
longitude: float,
radius_km: float = 10.0,
limit: int = 50
) -> List[TenantResponse]:
"""Get tenants near a geographic location"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
tenants = await self.tenant_repo.get_tenants_by_location(
latitude, longitude, radius_km, limit
)
return [TenantResponse.from_orm(tenant) for tenant in tenants]
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
except Exception as e:
logger.error("Error getting tenants by location",
latitude=latitude,
longitude=longitude,
error=str(e))
return []
async def deactivate_tenant(
self,
tenant_id: str,
user_id: str,
session: AsyncSession = None
) -> bool:
"""Deactivate a tenant (admin only)"""
try:
# Verify user is owner
access = await self.verify_user_access(user_id, tenant_id)
if not access.has_access or access.role != "owner":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only tenant owner can deactivate tenant"
)
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
deactivated_tenant = await self.tenant_repo.deactivate_tenant(tenant_id)
2025-07-19 21:16:25 +02:00
2025-08-08 09:08:41 +02:00
if not deactivated_tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
# Also suspend subscription
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
if subscription:
await self.subscription_repo.suspend_subscription(
str(subscription.id),
"Tenant deactivated"
)
logger.info("Tenant deactivated",
tenant_id=tenant_id,
deactivated_by=user_id)
return True
2025-07-19 21:16:25 +02:00
except HTTPException:
raise
except Exception as e:
2025-08-08 09:08:41 +02:00
logger.error("Error deactivating tenant",
tenant_id=tenant_id,
error=str(e))
2025-07-19 21:16:25 +02:00
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
2025-08-08 09:08:41 +02:00
detail="Failed to deactivate tenant"
2025-07-19 21:16:25 +02:00
)
2025-08-08 09:08:41 +02:00
async def activate_tenant(
self,
tenant_id: str,
user_id: str,
session: AsyncSession = None
) -> bool:
"""Activate a previously deactivated tenant (admin only)"""
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
try:
# Verify user is owner
access = await self.verify_user_access(user_id, tenant_id)
if not access.has_access or access.role != "owner":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only tenant owner can activate tenant"
)
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
activated_tenant = await self.tenant_repo.activate_tenant(tenant_id)
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
if not activated_tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
# Also reactivate subscription if exists
subscription = await self.subscription_repo.get_subscription_by_tenant(tenant_id)
if subscription and subscription.status == "suspended":
await self.subscription_repo.reactivate_subscription(str(subscription.id))
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
logger.info("Tenant activated",
tenant_id=tenant_id,
activated_by=user_id)
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
return True
2025-10-31 11:54:19 +01:00
2025-08-08 09:08:41 +02:00
except HTTPException:
raise
except Exception as e:
logger.error("Error activating tenant",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to activate tenant"
)
2025-10-31 11:54:19 +01:00
async def delete_tenant(
self,
tenant_id: str,
requesting_user_id: str = None,
skip_admin_check: bool = False
) -> Dict[str, Any]:
"""
Permanently delete a tenant and all its associated data
Args:
tenant_id: The tenant to delete
requesting_user_id: The user requesting deletion (for permission check)
skip_admin_check: Skip the admin check (for internal service calls)
Returns:
Dict with deletion summary
"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
# Get tenant first to verify it exists
tenant = await self.tenant_repo.get_by_id(tenant_id)
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
# Permission check (unless internal service call)
if not skip_admin_check:
if not requesting_user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User ID required for deletion authorization"
)
access = await self.verify_user_access(requesting_user_id, tenant_id)
if not access.has_access or access.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only tenant owner or admin can delete tenant"
)
# Check if there are other admins (protection against accidental deletion)
admin_members = await self.member_repo.get_tenant_members(
tenant_id,
active_only=True,
role=None # Get all roles, we'll filter
)
admin_count = sum(1 for m in admin_members if m.role in ["owner", "admin"])
# Build deletion summary
deletion_summary = {
"tenant_id": tenant_id,
"tenant_name": tenant.name,
"admin_count": admin_count,
"total_members": len(admin_members),
"deleted_items": {},
"errors": []
}
# Cancel active subscriptions first
try:
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
if subscription:
await self.subscription_repo.cancel_subscription(
str(subscription.id),
reason="Tenant deleted"
)
deletion_summary["deleted_items"]["subscriptions"] = 1
except Exception as e:
logger.warning("Failed to cancel subscription during tenant deletion",
tenant_id=tenant_id,
error=str(e))
deletion_summary["errors"].append(f"Subscription cancellation: {str(e)}")
# Delete all tenant memberships (CASCADE will handle this, but we do it explicitly)
try:
deleted_members = 0
for member in admin_members:
try:
await self.member_repo.delete(str(member.id))
deleted_members += 1
except Exception as e:
logger.warning("Failed to delete membership",
membership_id=member.id,
error=str(e))
deletion_summary["deleted_items"]["memberships"] = deleted_members
except Exception as e:
logger.warning("Failed to delete memberships during tenant deletion",
tenant_id=tenant_id,
error=str(e))
deletion_summary["errors"].append(f"Membership deletion: {str(e)}")
# Finally, delete the tenant itself (CASCADE should handle related records)
try:
await self.tenant_repo.delete(tenant_id)
deletion_summary["deleted_items"]["tenant"] = 1
except Exception as e:
logger.error("Failed to delete tenant record",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete tenant: {str(e)}"
)
# Publish deletion event for other services
try:
from app.services.messaging import publish_tenant_deleted
await publish_tenant_deleted(tenant_id, tenant.name)
except Exception as e:
logger.warning("Failed to publish tenant deletion event",
tenant_id=tenant_id,
error=str(e))
deletion_summary["errors"].append(f"Event publishing: {str(e)}")
logger.info("Tenant deleted successfully",
tenant_id=tenant_id,
tenant_name=tenant.name,
deleted_by=requesting_user_id,
summary=deletion_summary)
return deletion_summary
except HTTPException:
raise
except Exception as e:
logger.error("Error deleting tenant",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete tenant: {str(e)}"
)
async def delete_user_memberships(
self,
user_id: str
) -> Dict[str, Any]:
"""
Delete all tenant memberships for a user
Used when deleting a user from the auth service
Args:
user_id: The user whose memberships should be deleted
Returns:
Dict with deletion summary
"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
# Get all user memberships
memberships = await self.member_repo.get_user_memberships(user_id, active_only=False)
deleted_count = 0
errors = []
for membership in memberships:
try:
# Delete the membership
await self.member_repo.delete(str(membership.id))
deleted_count += 1
except Exception as e:
logger.warning("Failed to delete membership",
membership_id=membership.id,
user_id=user_id,
tenant_id=membership.tenant_id,
error=str(e))
errors.append(f"Membership {membership.id}: {str(e)}")
logger.info("User memberships deleted",
user_id=user_id,
total_memberships=len(memberships),
deleted_count=deleted_count,
errors=len(errors))
return {
"user_id": user_id,
"total_memberships": len(memberships),
"deleted_count": deleted_count,
"errors": errors
}
except Exception as e:
logger.error("Error deleting user memberships",
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete user memberships: {str(e)}"
)
async def transfer_tenant_ownership(
self,
tenant_id: str,
current_owner_id: str,
new_owner_id: str,
requesting_user_id: str = None
) -> TenantResponse:
"""
Transfer tenant ownership to another admin
Args:
tenant_id: The tenant whose ownership to transfer
current_owner_id: Current owner (for verification)
new_owner_id: New owner (must be an existing admin)
requesting_user_id: User requesting the transfer (for permission check)
Returns:
Updated tenant
"""
try:
async with self.database_manager.get_session() as db_session:
async with UnitOfWork(db_session) as uow:
# Register repositories
tenant_repo = uow.register_repository("tenants", TenantRepository, Tenant)
member_repo = uow.register_repository("members", TenantMemberRepository, TenantMember)
# Get tenant
tenant = await tenant_repo.get_by_id(tenant_id)
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
# Verify current ownership
if str(tenant.owner_id) != current_owner_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current owner ID does not match"
)
# Permission check (must be current owner or system)
if requesting_user_id and requesting_user_id != current_owner_id:
access = await self.verify_user_access(requesting_user_id, tenant_id)
if not access.has_access or access.role != "owner":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only current owner can transfer ownership"
)
# Verify new owner is an admin
new_owner_membership = await member_repo.get_membership(tenant_id, new_owner_id)
if not new_owner_membership or not new_owner_membership.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New owner must be an active member of the tenant"
)
if new_owner_membership.role not in ["admin", "owner"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New owner must be an admin"
)
# Update tenant owner
updated_tenant = await tenant_repo.update(tenant_id, {
"owner_id": new_owner_id
})
# Update memberships: current owner -> admin, new owner -> owner
current_owner_membership = await member_repo.get_membership(tenant_id, current_owner_id)
if current_owner_membership:
await member_repo.update_member_role(tenant_id, current_owner_id, "admin")
await member_repo.update_member_role(tenant_id, new_owner_id, "owner")
# Commit transaction
await uow.commit()
logger.info("Tenant ownership transferred",
tenant_id=tenant_id,
from_owner=current_owner_id,
to_owner=new_owner_id,
requested_by=requesting_user_id)
return TenantResponse.from_orm(updated_tenant)
except HTTPException:
raise
except Exception as e:
logger.error("Error transferring tenant ownership",
tenant_id=tenant_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to transfer ownership: {str(e)}"
)
async def get_tenant_admins(
self,
tenant_id: str
) -> List[TenantMemberResponse]:
"""
Get all admins (owner + admins) for a tenant
Used by auth service to check for other admins before tenant deletion
Args:
tenant_id: The tenant to query
Returns:
List of admin members
"""
try:
async with self.database_manager.get_session() as db_session:
await self._init_repositories(db_session)
# Get all active members
all_members = await self.member_repo.get_tenant_members(
tenant_id,
active_only=True,
include_user_info=True
)
# Filter to just admins and owner
admin_members = [m for m in all_members if m.role in ["owner", "admin"]]
return [TenantMemberResponse.from_orm(m) for m in admin_members]
except Exception as e:
logger.error("Error getting tenant admins",
tenant_id=tenant_id,
error=str(e))
return []
2025-08-08 09:08:41 +02:00
# Legacy compatibility alias
2025-09-30 21:58:10 +02:00
TenantService = EnhancedTenantService