""" Servizio tenant – gestione organizzazioni (solo super_admin). """ import uuid from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.exceptions import ConflictError, NotFoundError from app.core.security import hash_password from app.models.mailbox import Mailbox from app.models.tenant import Tenant from app.models.user import User from app.schemas.tenant import TenantCreateRequest, TenantResponse, TenantUpdateRequest class TenantService: def __init__(self, db: AsyncSession) -> None: self.db = db async def create_tenant(self, data: TenantCreateRequest) -> tuple[Tenant, User]: """Crea un nuovo tenant con il suo utente admin iniziale.""" # Verifica slug univoco existing = await self.db.execute( select(Tenant).where(Tenant.slug == data.slug) ) if existing.scalar_one_or_none(): raise ConflictError(f"Slug '{data.slug}' già in uso") tenant = Tenant( slug=data.slug, name=data.name, plan=data.plan, max_mailboxes=data.max_mailboxes, max_users=data.max_users, ) self.db.add(tenant) await self.db.flush() # ottieni tenant.id # Crea utente admin iniziale admin = User( tenant_id=tenant.id, email=data.admin_email.lower(), password_hash=hash_password(data.admin_password), full_name=data.admin_full_name, role="admin", ) self.db.add(admin) await self.db.flush() return tenant, admin async def get_tenant(self, tenant_id: uuid.UUID) -> Tenant: tenant = await self.db.get(Tenant, tenant_id) if not tenant: raise NotFoundError("tenant") return tenant async def get_tenant_with_stats(self, tenant_id: uuid.UUID) -> TenantResponse: """Restituisce il tenant con conteggi utenti e caselle.""" tenant = await self.get_tenant(tenant_id) user_count = ( await self.db.execute( select(func.count(User.id)).where( User.tenant_id == tenant_id, User.is_active == True, # noqa: E712 ) ) ).scalar_one() mailbox_count = ( await self.db.execute( select(func.count(Mailbox.id)).where( Mailbox.tenant_id == tenant_id, Mailbox.status != "deleted", ) ) ).scalar_one() resp = TenantResponse.model_validate(tenant) resp.user_count = user_count resp.mailbox_count = mailbox_count return resp async def list_tenants(self) -> list[Tenant]: result = await self.db.execute( select(Tenant).order_by(Tenant.created_at.desc()) ) return list(result.scalars().all()) async def list_tenants_with_stats(self) -> list[TenantResponse]: """ Restituisce tutti i tenant con conteggi utenti e caselle in una singola query efficiente (LEFT JOIN con GROUP BY). """ stmt = ( select( Tenant, func.count(User.id.distinct()).label("user_count"), func.count(Mailbox.id.distinct()).label("mailbox_count"), ) .outerjoin( User, (User.tenant_id == Tenant.id) & (User.is_active == True), # noqa: E712 ) .outerjoin( Mailbox, (Mailbox.tenant_id == Tenant.id) & (Mailbox.status != "deleted"), ) .group_by(Tenant.id) .order_by(Tenant.created_at.desc()) ) rows = (await self.db.execute(stmt)).all() result = [] for row in rows: tenant_obj, user_count, mailbox_count = row resp = TenantResponse.model_validate(tenant_obj) resp.user_count = user_count or 0 resp.mailbox_count = mailbox_count or 0 result.append(resp) return result async def update_tenant( self, tenant_id: uuid.UUID, data: TenantUpdateRequest ) -> Tenant: tenant = await self.get_tenant(tenant_id) if data.name is not None: tenant.name = data.name if data.plan is not None: tenant.plan = data.plan if data.is_active is not None: tenant.is_active = data.is_active if data.max_mailboxes is not None: tenant.max_mailboxes = data.max_mailboxes if data.max_users is not None: tenant.max_users = data.max_users return tenant