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
|
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)
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
}
|
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)
|
2025-10-01 21:56:38 +02:00
|
|
|
|
|
|
|
|
# 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),
|
2025-10-01 21:56:38 +02:00
|
|
|
"plan": selected_plan,
|
2025-08-08 09:08:41 +02:00
|
|
|
"status": "active"
|
|
|
|
|
}
|
2025-10-01 21:56:38 +02:00
|
|
|
|
2025-08-08 09:08:41 +02:00
|
|
|
subscription = await subscription_repo.create_subscription(subscription_data)
|
2025-10-01 21:56:38 +02:00
|
|
|
|
|
|
|
|
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"""
|
|
|
|
|
|
|
|
|
|
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(
|
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(
|
|
|
|
|
self,
|
|
|
|
|
tenant_id: str,
|
|
|
|
|
user_id: str,
|
|
|
|
|
active_only: bool = True
|
|
|
|
|
) -> List[TenantMemberResponse]:
|
|
|
|
|
"""Get all team members for a tenant"""
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
members = await self.member_repo.get_tenant_members(
|
|
|
|
|
tenant_id, active_only=active_only
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return [TenantMemberResponse.from_orm(member) for member in members]
|
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)"""
|
|
|
|
|
|
|
|
|
|
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
|
2025-09-30 21:58:10 +02:00
|
|
|
TenantService = EnhancedTenantService
|