REFACTOR - Database logic
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Tenant Service Layer
|
||||
Business logic services for tenant operations
|
||||
"""
|
||||
|
||||
from .tenant_service import TenantService, EnhancedTenantService
|
||||
from .messaging import publish_tenant_created, publish_member_added
|
||||
|
||||
__all__ = [
|
||||
"TenantService",
|
||||
"EnhancedTenantService",
|
||||
"publish_tenant_created",
|
||||
"publish_member_added"
|
||||
]
|
||||
@@ -1,269 +1,671 @@
|
||||
# services/tenant/app/services/tenant_service.py
|
||||
"""
|
||||
Tenant service business logic
|
||||
Enhanced Tenant Service
|
||||
Business logic layer using repository pattern for tenant operations
|
||||
"""
|
||||
|
||||
import structlog
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict, Any
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, and_
|
||||
from fastapi import HTTPException, status
|
||||
import uuid
|
||||
import json
|
||||
|
||||
from app.models.tenants import Tenant, TenantMember
|
||||
from app.schemas.tenants import BakeryRegistration, TenantResponse, TenantAccessResponse, TenantUpdate, TenantMemberResponse
|
||||
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
|
||||
)
|
||||
from app.services.messaging import publish_tenant_created, publish_member_added
|
||||
from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.database.unit_of_work import UnitOfWork
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
class TenantService:
|
||||
"""Tenant management business logic"""
|
||||
|
||||
class EnhancedTenantService:
|
||||
"""Enhanced tenant management business logic using repository pattern with dependency injection"""
|
||||
|
||||
@staticmethod
|
||||
async def create_bakery(bakery_data: BakeryRegistration, owner_id: str, db: AsyncSession) -> TenantResponse:
|
||||
"""Create a new bakery/tenant"""
|
||||
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
|
||||
}
|
||||
|
||||
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"""
|
||||
|
||||
try:
|
||||
# Generate subdomain if not provided
|
||||
subdomain = bakery_data.name.lower().replace(' ', '-').replace('á', 'a').replace('é', 'e').replace('í', 'i').replace('ó', 'o').replace('ú', 'u')
|
||||
subdomain = ''.join(c for c in subdomain if c.isalnum() or c == '-')
|
||||
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)
|
||||
|
||||
# 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": getattr(bakery_data, 'latitude', None),
|
||||
"longitude": getattr(bakery_data, 'longitude', None),
|
||||
"is_active": True
|
||||
}
|
||||
|
||||
# Check if subdomain already exists
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.subdomain == subdomain)
|
||||
)
|
||||
if result.scalar_one_or_none():
|
||||
subdomain = f"{subdomain}-{uuid.uuid4().hex[:6]}"
|
||||
|
||||
# Create tenant
|
||||
tenant = Tenant(
|
||||
name=bakery_data.name,
|
||||
subdomain=subdomain,
|
||||
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,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(tenant)
|
||||
await db.commit()
|
||||
await db.refresh(tenant)
|
||||
# Create tenant using repository
|
||||
tenant = await tenant_repo.create_tenant(tenant_data)
|
||||
|
||||
# Create owner membership
|
||||
owner_membership = TenantMember(
|
||||
tenant_id=tenant.id,
|
||||
user_id=owner_id,
|
||||
role="owner",
|
||||
permissions=json.dumps(["read", "write", "admin", "delete"]),
|
||||
is_active=True,
|
||||
joined_at=datetime.now(timezone.utc)
|
||||
)
|
||||
membership_data = {
|
||||
"tenant_id": str(tenant.id),
|
||||
"user_id": owner_id,
|
||||
"role": "owner",
|
||||
"is_active": True
|
||||
}
|
||||
|
||||
db.add(owner_membership)
|
||||
await db.commit()
|
||||
owner_membership = await member_repo.create_membership(membership_data)
|
||||
|
||||
# Create basic subscription
|
||||
subscription_data = {
|
||||
"tenant_id": str(tenant.id),
|
||||
"plan": "basic",
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
subscription = await subscription_repo.create_subscription(subscription_data)
|
||||
|
||||
# Commit the transaction
|
||||
await uow.commit()
|
||||
|
||||
# Publish event
|
||||
await publish_tenant_created(str(tenant.id), owner_id, bakery_data.name)
|
||||
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))
|
||||
|
||||
logger.info(f"Bakery created: {bakery_data.name} (ID: {tenant.id})")
|
||||
logger.info("Bakery created successfully",
|
||||
tenant_id=tenant.id,
|
||||
name=bakery_data.name,
|
||||
owner_id=owner_id,
|
||||
subdomain=tenant.subdomain)
|
||||
|
||||
return TenantResponse.from_orm(tenant)
|
||||
|
||||
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)
|
||||
)
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Error creating bakery: {e}")
|
||||
logger.error("Error creating bakery",
|
||||
name=bakery_data.name,
|
||||
owner_id=owner_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to create bakery"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def verify_user_access(user_id: str, tenant_id: str, db: AsyncSession) -> TenantAccessResponse:
|
||||
"""Verify if user has access to tenant"""
|
||||
async def verify_user_access(
|
||||
self,
|
||||
user_id: str,
|
||||
tenant_id: str
|
||||
) -> TenantAccessResponse:
|
||||
"""Verify if user has access to tenant with enhanced permissions"""
|
||||
|
||||
try:
|
||||
# Check if user is tenant member
|
||||
result = await db.execute(
|
||||
select(TenantMember).where(
|
||||
and_(
|
||||
TenantMember.user_id == user_id,
|
||||
TenantMember.tenant_id == tenant_id,
|
||||
TenantMember.is_active == True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
membership = result.scalar_one_or_none()
|
||||
|
||||
if not membership:
|
||||
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)
|
||||
|
||||
return TenantAccessResponse(
|
||||
has_access=False,
|
||||
role="none",
|
||||
permissions=[]
|
||||
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")
|
||||
)
|
||||
|
||||
# Parse permissions
|
||||
permissions = []
|
||||
if membership.permissions:
|
||||
try:
|
||||
permissions = json.loads(membership.permissions)
|
||||
except:
|
||||
permissions = []
|
||||
|
||||
return TenantAccessResponse(
|
||||
has_access=True,
|
||||
role=membership.role,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying user access: {e}")
|
||||
logger.error("Error verifying user access",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
return TenantAccessResponse(
|
||||
has_access=False,
|
||||
role="none",
|
||||
permissions=[]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def get_tenant_by_id(tenant_id: str, db: AsyncSession) -> Optional[TenantResponse]:
|
||||
"""Get tenant by ID"""
|
||||
async def get_tenant_by_id(self, tenant_id: str) -> Optional[TenantResponse]:
|
||||
"""Get tenant by ID with enhanced data"""
|
||||
|
||||
try:
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
|
||||
tenant = result.scalar_one_or_none()
|
||||
if tenant:
|
||||
return TenantResponse.from_orm(tenant)
|
||||
return None
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting tenant: {e}")
|
||||
logger.error("Error getting tenant",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def update_tenant(tenant_id: str, update_data: TenantUpdate, user_id: str, db: AsyncSession) -> TenantResponse:
|
||||
"""Update tenant information"""
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting tenant by subdomain",
|
||||
subdomain=subdomain,
|
||||
error=str(e))
|
||||
return None
|
||||
|
||||
async def get_user_tenants(self, owner_id: str) -> List[TenantResponse]:
|
||||
"""Get all tenants owned by a user"""
|
||||
|
||||
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]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting user tenants",
|
||||
owner_id=owner_id,
|
||||
error=str(e))
|
||||
return []
|
||||
|
||||
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(
|
||||
self,
|
||||
tenant_id: str,
|
||||
update_data: TenantUpdate,
|
||||
user_id: str,
|
||||
session: AsyncSession = None
|
||||
) -> TenantResponse:
|
||||
"""Update tenant information with permission checks"""
|
||||
|
||||
try:
|
||||
# Verify user has admin access
|
||||
access = await TenantService.verify_user_access(user_id, tenant_id, db)
|
||||
access = await self.verify_user_access(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="Insufficient permissions to update tenant"
|
||||
)
|
||||
|
||||
# Update tenant
|
||||
# Update tenant using repository
|
||||
update_values = update_data.dict(exclude_unset=True)
|
||||
if update_values:
|
||||
update_values["updated_at"] = datetime.now(timezone.utc)
|
||||
updated_tenant = await self.tenant_repo.update(tenant_id, update_values)
|
||||
|
||||
await db.execute(
|
||||
update(Tenant)
|
||||
.where(Tenant.id == tenant_id)
|
||||
.values(**update_values)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# Get updated tenant
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
|
||||
tenant = result.scalar_one()
|
||||
logger.info(f"Tenant updated: {tenant.name} (ID: {tenant_id})")
|
||||
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
|
||||
tenant = await self.tenant_repo.get_by_id(tenant_id)
|
||||
return TenantResponse.from_orm(tenant)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Error updating tenant: {e}")
|
||||
logger.error("Error updating tenant",
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update tenant"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def add_team_member(tenant_id: str, user_id: str, role: str, invited_by: str, db: AsyncSession) -> TenantMemberResponse:
|
||||
"""Add a team member to tenant"""
|
||||
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"""
|
||||
|
||||
try:
|
||||
# Verify inviter has admin access
|
||||
access = await TenantService.verify_user_access(invited_by, tenant_id, db)
|
||||
access = await self.verify_user_access(invited_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 add team members"
|
||||
)
|
||||
|
||||
# Check if user is already a member
|
||||
result = await db.execute(
|
||||
select(TenantMember).where(
|
||||
and_(
|
||||
TenantMember.tenant_id == tenant_id,
|
||||
TenantMember.user_id == user_id
|
||||
)
|
||||
)
|
||||
)
|
||||
# Create membership using repository
|
||||
membership_data = {
|
||||
"tenant_id": tenant_id,
|
||||
"user_id": user_id,
|
||||
"role": role,
|
||||
"invited_by": invited_by,
|
||||
"is_active": True
|
||||
}
|
||||
|
||||
existing_member = result.scalar_one_or_none()
|
||||
if existing_member:
|
||||
if existing_member.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User is already a member of this tenant"
|
||||
)
|
||||
else:
|
||||
# Reactivate existing membership
|
||||
existing_member.is_active = True
|
||||
existing_member.role = role
|
||||
existing_member.joined_at = datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
return TenantMemberResponse.from_orm(existing_member)
|
||||
|
||||
# Create new membership
|
||||
permissions = ["read"]
|
||||
if role in ["admin", "owner"]:
|
||||
permissions.extend(["write", "admin"])
|
||||
if role == "owner":
|
||||
permissions.append("delete")
|
||||
|
||||
member = TenantMember(
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
role=role,
|
||||
permissions=json.dumps(permissions),
|
||||
invited_by=invited_by,
|
||||
is_active=True,
|
||||
joined_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.add(member)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
member = await self.member_repo.create_membership(membership_data)
|
||||
|
||||
# Publish event
|
||||
await publish_member_added(tenant_id, user_id, role)
|
||||
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(f"Team member added: {user_id} to tenant {tenant_id} as {role}")
|
||||
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)
|
||||
|
||||
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:
|
||||
await db.rollback()
|
||||
logger.error(f"Error adding team member: {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(
|
||||
self,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
active_only: bool = True
|
||||
) -> List[TenantMemberResponse]:
|
||||
"""Get all team members for a tenant"""
|
||||
|
||||
try:
|
||||
# Verify user has access to tenant
|
||||
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"
|
||||
)
|
||||
|
||||
members = await self.member_repo.get_tenant_members(
|
||||
tenant_id, active_only=active_only
|
||||
)
|
||||
|
||||
return [TenantMemberResponse.from_orm(member) for member in members]
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValidationError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
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,
|
||||
model_trained: bool,
|
||||
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"
|
||||
)
|
||||
|
||||
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(
|
||||
tenant_id, model_trained, last_training_date
|
||||
)
|
||||
|
||||
if not updated_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
deactivated_tenant = await self.tenant_repo.deactivate_tenant(tenant_id)
|
||||
|
||||
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
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error deactivating tenant",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to deactivate tenant"
|
||||
)
|
||||
|
||||
async def activate_tenant(
|
||||
self,
|
||||
tenant_id: str,
|
||||
user_id: str,
|
||||
session: AsyncSession = None
|
||||
) -> bool:
|
||||
"""Activate a previously deactivated 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 activate tenant"
|
||||
)
|
||||
|
||||
activated_tenant = await self.tenant_repo.activate_tenant(tenant_id)
|
||||
|
||||
if not activated_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# 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))
|
||||
|
||||
logger.info("Tenant activated",
|
||||
tenant_id=tenant_id,
|
||||
activated_by=user_id)
|
||||
|
||||
return True
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
# Legacy compatibility alias
|
||||
TenantService = EnhancedTenantService
|
||||
Reference in New Issue
Block a user