- Implemented SvelteKit frontend with authentication callback handling. - Created demo routes and paraglide localization functionality. - Added health check and entrypoint scripts for backend services. - Established PostgreSQL and Keycloak initialization scripts with health checks. - Introduced models for database schema using SQLAlchemy. - Configured Vite and SvelteKit for development and testing environments. - Added health check script to verify service statuses and resource usage. - Created Docker entrypoint scripts for seamless service startup.
244 lines
8.6 KiB
Python
244 lines
8.6 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)
|
|
)
|