""" Capa de servicio para lógica de negocio de tenants """ from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException from typing import List, Optional import json import logging from .models import Tenant, TenantType from .dto import TenantCreateDTO, TenantUpdateDTO, TenantResponseDTO logger = logging.getLogger(__name__) class TenantService: """Servicio para gestión de tenants""" def __init__(self, db: Session): self.db = db def create_tenant(self, tenant_data: TenantCreateDTO) -> TenantResponseDTO: """ Crea un nuevo tenant en el sistema Args: tenant_data: Datos del tenant a crear Returns: TenantResponseDTO con información del tenant creado Raises: HTTPException: Si el slug o realm ya existen """ try: # Verificar que no exista el slug existing = self.db.query(Tenant).filter(Tenant.slug == tenant_data.slug).first() if existing: raise HTTPException(status_code=400, detail=f"Tenant with slug '{tenant_data.slug}' already exists") # Crear tenant db_tenant = Tenant( name=tenant_data.name, slug=tenant_data.slug, keycloak_realm=tenant_data.keycloak_realm, type=TenantType(tenant_data.type.value), contact_name=tenant_data.contact_name, contact_email=tenant_data.contact_email, contact_phone=tenant_data.contact_phone, is_active=True ) self.db.add(db_tenant) self.db.commit() self.db.refresh(db_tenant) logger.info(f"Tenant created: {db_tenant.id} - {db_tenant.name}") return TenantResponseDTO.model_validate(db_tenant) except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating tenant: {str(e)}") raise HTTPException(status_code=400, detail="Tenant with this slug or realm already exists") except HTTPException: raise except Exception as e: self.db.rollback() logger.error(f"Error creating tenant: {str(e)}") raise HTTPException(status_code=500, detail="Error creating tenant") def get_tenant(self, tenant_id: int) -> Optional[TenantResponseDTO]: """ Obtiene un tenant por ID Args: tenant_id: ID del tenant Returns: TenantResponseDTO o None si no existe """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return None return TenantResponseDTO.model_validate(tenant) def get_tenant_by_slug(self, slug: str) -> Optional[TenantResponseDTO]: """Obtiene un tenant por slug""" tenant = self.db.query(Tenant).filter(Tenant.slug == slug).first() if not tenant: return None return TenantResponseDTO.model_validate(tenant) def list_tenants(self, skip: int = 0, limit: int = 100, active_only: bool = False) -> List[TenantResponseDTO]: """ Lista todos los tenants Args: skip: Número de registros a omitir limit: Número máximo de registros a retornar active_only: Si True, solo retorna tenants activos Returns: Lista de TenantResponseDTO """ query = self.db.query(Tenant) if active_only: query = query.filter(Tenant.is_active == True) tenants = query.offset(skip).limit(limit).all() return [TenantResponseDTO.model_validate(t) for t in tenants] def update_tenant(self, tenant_id: int, tenant_data: TenantUpdateDTO) -> Optional[TenantResponseDTO]: """ Actualiza un tenant Args: tenant_id: ID del tenant a actualizar tenant_data: Datos a actualizar Returns: TenantResponseDTO actualizado o None si no existe """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return None # Actualizar solo campos proporcionados update_data = tenant_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(tenant, field, value) try: self.db.commit() self.db.refresh(tenant) logger.info(f"Tenant updated: {tenant_id}") return TenantResponseDTO.model_validate(tenant) except Exception as e: self.db.rollback() logger.error(f"Error updating tenant {tenant_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating tenant") def delete_tenant(self, tenant_id: int) -> bool: """ Elimina (desactiva) un tenant Args: tenant_id: ID del tenant a eliminar Returns: True si se eliminó, False si no existe """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return False # Soft delete: marcar como inactivo tenant.is_active = False try: self.db.commit() logger.info(f"Tenant deleted (soft): {tenant_id}") return True except Exception as e: self.db.rollback() logger.error(f"Error deleting tenant {tenant_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting tenant") def upgrade_to_dedicated(self, tenant_id: int, db_config: dict) -> Optional[TenantResponseDTO]: """ Actualiza un tenant de BD compartida a BD dedicada Args: tenant_id: ID del tenant db_config: Configuración de BD dedicada Returns: TenantResponseDTO actualizado """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return None tenant.type = TenantType.DEDICATED tenant.db_config = json.dumps(db_config) try: self.db.commit() self.db.refresh(tenant) logger.info(f"Tenant upgraded to dedicated DB: {tenant_id}") return TenantResponseDTO.model_validate(tenant) except Exception as e: self.db.rollback() logger.error(f"Error upgrading tenant {tenant_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error upgrading tenant")