Files
plantillas-proyectos/backend/api/v1/modules/a76/licenses/service.py
acazares 52b8fcd434 feat: Implement multi-tenancy support in middleware and security layers
- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
2025-11-11 14:15:31 -06:00

264 lines
8.5 KiB
Python

"""
Servicio de lógica de negocio para licencias
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from typing import Optional
from datetime import datetime, timezone
import logging
from .models import License, LicenseUsage, LicensePlan, LicenseStatus
from .dto import (
LicenseCreateDTO,
LicenseUpdateDTO,
LicenseResponseDTO,
LicenseValidationResponseDTO,
LicenseUsageResponseDTO,
)
logger = logging.getLogger(__name__)
class LicenseService:
"""Servicio para gestión de licencias"""
def __init__(self, db: Session):
self.db = db
def create_license(self, license_data: LicenseCreateDTO) -> LicenseResponseDTO:
"""
Crea una nueva licencia para un tenant
Args:
license_data: Datos de la licencia
Returns:
LicenseResponseDTO
Raises:
HTTPException: Si el tenant ya tiene licencia o hay error
"""
try:
# Verificar que el tenant no tenga ya una licencia
existing = (
self.db.query(License)
.filter(License.tenant_id == license_data.tenant_id)
.first()
)
if existing:
raise HTTPException(
status_code=400,
detail=f"Tenant {license_data.tenant_id} already has a license",
)
# Crear licencia
db_license = License(
tenant_id=license_data.tenant_id,
plan=LicensePlan(license_data.plan.value),
status=LicenseStatus.ACTIVE,
max_users=license_data.max_users,
max_storage_gb=license_data.max_storage_gb,
max_monthly_operations=license_data.max_monthly_operations,
feature_api_access=license_data.feature_api_access,
feature_advanced_reports=license_data.feature_advanced_reports,
feature_integrations=license_data.feature_integrations,
feature_dedicated_support=license_data.feature_dedicated_support,
starts_at=license_data.starts_at,
expires_at=license_data.expires_at,
)
self.db.add(db_license)
self.db.commit()
self.db.refresh(db_license)
logger.info(f"License created for tenant {license_data.tenant_id}")
return LicenseResponseDTO.model_validate(db_license)
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating license: {str(e)}")
raise HTTPException(status_code=400, detail="Database integrity error")
except HTTPException:
raise
except Exception as e:
self.db.rollback()
logger.error(f"Error creating license: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating license")
def get_license_by_tenant(self, tenant_id: int) -> Optional[LicenseResponseDTO]:
"""
Obtiene la licencia de un tenant
Args:
tenant_id: ID del tenant
Returns:
LicenseResponseDTO o None si no existe
"""
license = self.db.query(License).filter(License.tenant_id == tenant_id).first()
if not license:
return None
return LicenseResponseDTO.model_validate(license)
def update_license(
self, tenant_id: int, license_data: LicenseUpdateDTO
) -> Optional[LicenseResponseDTO]:
"""
Actualiza una licencia
Args:
tenant_id: ID del tenant
license_data: Datos a actualizar
Returns:
LicenseResponseDTO actualizado o None si no existe
"""
license = self.db.query(License).filter(License.tenant_id == tenant_id).first()
if not license:
return None
# Actualizar campos proporcionados
update_data = license_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
if field in ["plan", "status"]:
# Convertir enums
value = LicensePlan(value) if field == "plan" else LicenseStatus(value)
setattr(license, field, value)
try:
self.db.commit()
self.db.refresh(license)
logger.info(f"License updated for tenant {tenant_id}")
return LicenseResponseDTO.model_validate(license)
except Exception as e:
self.db.rollback()
logger.error(f"Error updating license for tenant {tenant_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating license")
def validate_license(self, tenant_id: int) -> dict:
"""
Valida si la licencia de un tenant está activa y vigente
Args:
tenant_id: ID del tenant
Returns:
Dict con información de validación
"""
license = self.db.query(License).filter(License.tenant_id == tenant_id).first()
if not license:
return {
"is_valid": False,
"status": "not_found",
"plan": None,
"expires_at": None,
"reason": "License not found",
}
now = datetime.now(timezone.utc)
# Verificar estado
if license.status != LicenseStatus.ACTIVE:
return {
"is_valid": False,
"status": license.status.value,
"plan": license.plan.value,
"expires_at": license.expires_at,
"reason": f"License status is {license.status.value}",
}
# Verificar vigencia
if license.expires_at < now:
# Auto-actualizar a expirada
license.status = LicenseStatus.EXPIRED
self.db.commit()
return {
"is_valid": False,
"status": "expired",
"plan": license.plan.value,
"expires_at": license.expires_at,
"reason": "License has expired",
}
# Licencia válida
return {
"is_valid": True,
"status": license.status.value,
"plan": license.plan.value,
"expires_at": license.expires_at,
"reason": None,
}
def get_usage(self, tenant_id: int) -> Optional[LicenseUsageResponseDTO]:
"""
Obtiene el uso actual de la licencia de un tenant
Args:
tenant_id: ID del tenant
Returns:
LicenseUsageResponseDTO o None
"""
license = self.db.query(License).filter(License.tenant_id == tenant_id).first()
if not license:
return None
# Obtener último registro de uso
usage = (
self.db.query(LicenseUsage)
.filter(LicenseUsage.tenant_id == tenant_id)
.order_by(LicenseUsage.created_at.desc())
.first()
)
if not usage:
# Crear registro inicial si no existe
usage = LicenseUsage(
tenant_id=tenant_id,
period_start=datetime.now(timezone.utc),
period_end=datetime.now(timezone.utc),
active_users=0,
storage_used_gb=0,
operations_count=0,
api_calls_count=0,
)
# Calcular porcentajes
users_usage = (
(usage.active_users / license.max_users * 100)
if license.max_users > 0
else 0
)
storage_usage = (
(usage.storage_used_gb / license.max_storage_gb * 100)
if license.max_storage_gb > 0
else 0
)
operations_usage = (
(usage.operations_count / license.max_monthly_operations * 100)
if license.max_monthly_operations > 0
else 0
)
return LicenseUsageResponseDTO(
tenant_id=tenant_id,
period_start=usage.period_start,
period_end=usage.period_end,
active_users=usage.active_users,
storage_used_gb=usage.storage_used_gb,
operations_count=usage.operations_count,
api_calls_count=usage.api_calls_count,
max_users=license.max_users,
max_storage_gb=license.max_storage_gb,
max_monthly_operations=license.max_monthly_operations,
users_usage_percent=round(users_usage, 2),
storage_usage_percent=round(storage_usage, 2),
operations_usage_percent=round(operations_usage, 2),
)