Merge branch 'feature/reportes_facturas' into development

This commit is contained in:
2026-01-21 12:12:23 -06:00
92 changed files with 6924 additions and 1822 deletions

View File

@@ -0,0 +1,28 @@
import os
from celery import Celery
valkey_url = os.getenv("VALKEY_URL", "redis://valkey:6379/0")
celery_app = Celery(
"anexo76_tasks",
broker=valkey_url,
backend=valkey_url,
include=[
"api.v1.modules.a76.reports.importacion.facturas.task",
"api.v1.modules.a76.reports.importacion.consolidados.task"
] # Ruta al módulo donde están las tareas
)
# Configuraciones adicionales
celery_app.conf.update(
task_track_started=True,
task_serializer="json",
accept_content=["json"],
result_serializer="json",
timezone="America/Mexico_City",
enable_utc=True,
)
if __name__ == "__main__":
celery_app.start()

View File

@@ -1,29 +1,15 @@
"""
Middleware personalizado para Anexo76
- Validación de licencias
- Gestión de multi-tenancy
- Logging de requests
"""
import logging
import time
from typing import Callable
from fastapi import HTTPException, Request
from fastapi import HTTPException, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from .config import settings
from .database import CoreSessionLocal
from .security import get_tenant_from_token, verify_token
logger = logging.getLogger(__name__)
class TenantMiddleware(BaseHTTPMiddleware):
"""
Middleware para identificar y validar el tenant en cada request
"""
async def dispatch(self, request: Request, call_next: Callable):
# Rutas públicas que no requieren tenant
# Permitir acceso sin autenticación a rutas de documentación y salud
@@ -37,53 +23,35 @@ class TenantMiddleware(BaseHTTPMiddleware):
]
path = request.url.path
# Permitir cualquier subruta de docs/redoc/openapi.json (por ejemplo, /api/docs, /api/docs/, /api/docs/oauth2-redirect)
if any(
path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes
):
# 3. Bypass para rutas públicas y docs
if any(path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes):
return await call_next(request)
# Permitir rutas públicas exactas o con prefijo
if any(
path == prefix or (prefix != "/" and path.startswith(prefix))
for prefix in public_prefixes
):
if any(path == prefix or (prefix != "/" and path.startswith(prefix)) for prefix in public_prefixes):
return await call_next(request)
# Extraer token y obtener tenant
# 4. Validación estricta de Token (solo para lo que no es público ni OPTIONS)
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=401, detail="Missing or invalid authorization header"
status_code=401,
detail="Missing or invalid authorization header"
)
token = auth_header.split(" ")[1]
try:
user_info = verify_token(token)
tenant_id = get_tenant_from_token(user_info)
# ⚠️ NOTA: tenant_id puede ser None para usuarios SSO que aún no tienen tenant asignado
# En ese caso, el endpoint específico deberá manejarlo
if not tenant_id:
logger.warning(
f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}"
)
# No lanzamos error aquí, dejamos que el endpoint decida qué hacer
# Agregar tenant_id al state del request (puede ser None)
request.state.tenant_id = tenant_id
request.state.user_info = user_info
except HTTPException:
# Re-lanzar HTTPException directamente
raise
except Exception as e:
logger.error(f"❌ Tenant validation error: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid authentication")
response = await call_next(request)
return response
# 5. Continuar con la petición real
return await call_next(request)
class LicenseValidationMiddleware(BaseHTTPMiddleware):