feat: Version 1.11.0 - Mejoras en auditoría, SLA, frontend y correcciones de sincronización
- Refactorización de endpoints de auditoría y helpers - Mejoras en esquemas de auditoría (audit.py) - Correcciones en endpoint SLA - Actualizaciones en múltiples rutas del frontend interno: layout, tickets, usuarios, tenants, categorías, sistemas, SLA (at-risk, violations), auditoría (main + security), login, perfil - Actualización de tailwind.config.js - Eliminación de docs de versiones anteriores (CAMBIOS_v1.10.0, v1.8.0, OPTIMIZACIONES) - Nuevos scripts de prueba: generate_security_test_data.py, generate_sla_test_data.py - Script de prueba de sincronización crítica (test_critical_sync.ps1) - README actualizado en scripts/
This commit is contained in:
399
backend/scripts/generate_sla_test_data.py
Normal file
399
backend/scripts/generate_sla_test_data.py
Normal file
@@ -0,0 +1,399 @@
|
||||
"""
|
||||
Script para generar datos de prueba de SLA Management.
|
||||
Crea tickets con diferentes estados de SLA para probar el dashboard.
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import uuid
|
||||
import random
|
||||
|
||||
# Agregar el directorio raíz al path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.core.database import AsyncSessionLocal
|
||||
from app.models.ticket import Ticket, TicketStatus, TicketPriority
|
||||
from app.models.category import Category
|
||||
from app.models.user import User
|
||||
from app.models.tenant import Tenant
|
||||
from app.models.system import System
|
||||
|
||||
|
||||
async def generate_sla_test_data():
|
||||
"""Genera tickets de prueba con diferentes estados de SLA."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
# Obtener tenant y usuarios
|
||||
tenant_result = await db.execute(select(Tenant).limit(1))
|
||||
tenant = tenant_result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
print("❌ No se encontró ningún tenant. Ejecuta las migraciones primero.")
|
||||
return
|
||||
|
||||
# Obtener usuarios
|
||||
users_result = await db.execute(
|
||||
select(User).where(User.tenant_id == tenant.id).limit(5)
|
||||
)
|
||||
users = list(users_result.scalars().all())
|
||||
|
||||
if not users:
|
||||
print("❌ No se encontraron usuarios. Crea usuarios primero.")
|
||||
return
|
||||
|
||||
creator = users[0]
|
||||
agents = users if len(users) > 1 else [creator]
|
||||
|
||||
# Obtener o crear categorías
|
||||
categories_result = await db.execute(
|
||||
select(Category).where(Category.tenant_id == tenant.id)
|
||||
)
|
||||
categories = list(categories_result.scalars().all())
|
||||
|
||||
if not categories:
|
||||
print("📁 Creando categorías de prueba...")
|
||||
category_data = [
|
||||
{"name": "Soporte Técnico", "sla_response_hours": 2, "sla_resolution_hours": 24, "color": "#3B82F6"},
|
||||
{"name": "Facturación", "sla_response_hours": 4, "sla_resolution_hours": 48, "color": "#10B981"},
|
||||
{"name": "Incidente Crítico", "sla_response_hours": 1, "sla_resolution_hours": 8, "color": "#EF4444"},
|
||||
{"name": "Consulta General", "sla_response_hours": 8, "sla_resolution_hours": 72, "color": "#6B7280"},
|
||||
]
|
||||
|
||||
for cat_data in category_data:
|
||||
category = Category(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
name=cat_data["name"],
|
||||
description=f"Categoría de {cat_data['name']}",
|
||||
color=cat_data["color"],
|
||||
sla_response_hours=cat_data["sla_response_hours"],
|
||||
sla_resolution_hours=cat_data["sla_resolution_hours"],
|
||||
is_active=True
|
||||
)
|
||||
db.add(category)
|
||||
categories.append(category)
|
||||
|
||||
await db.commit()
|
||||
print(f" ✓ Creadas {len(categories)} categorías")
|
||||
|
||||
# Obtener o crear sistemas afectados
|
||||
systems_result = await db.execute(
|
||||
select(System).where(System.tenant_id == tenant.id)
|
||||
)
|
||||
systems = list(systems_result.scalars().all())
|
||||
|
||||
if not systems:
|
||||
print("🖥️ Creando sistemas de prueba...")
|
||||
system_names = ["Portal Web", "API REST", "Base de Datos", "Sistema de Pagos"]
|
||||
for sys_name in system_names:
|
||||
system = System(
|
||||
id=uuid.uuid4(),
|
||||
tenant_id=tenant.id,
|
||||
name=sys_name,
|
||||
description=f"Sistema {sys_name}",
|
||||
is_active=True
|
||||
)
|
||||
db.add(system)
|
||||
systems.append(system)
|
||||
|
||||
await db.commit()
|
||||
print(f" ✓ Creados {len(systems)} sistemas")
|
||||
|
||||
print(f"✅ Usando tenant: {tenant.name}")
|
||||
print(f"✅ Usuarios disponibles: {len(users)}")
|
||||
print(f"✅ Categorías disponibles: {len(categories)}")
|
||||
print()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
tickets_created = 0
|
||||
|
||||
# Función auxiliar para crear ticket
|
||||
def create_ticket(
|
||||
subject: str,
|
||||
description: str,
|
||||
priority: TicketPriority,
|
||||
status: TicketStatus,
|
||||
category: Category,
|
||||
created_hours_ago: int,
|
||||
first_response_hours_after: int = None,
|
||||
resolved_hours_after: int = None,
|
||||
assigned: bool = True
|
||||
):
|
||||
nonlocal tickets_created
|
||||
|
||||
ticket_id = uuid.uuid4()
|
||||
created_at = now - timedelta(hours=created_hours_ago)
|
||||
|
||||
# Calcular SLA deadlines basados en la categoría (sin timezone para la BD)
|
||||
sla_response_due = (created_at + timedelta(hours=category.sla_response_hours)).replace(tzinfo=None)
|
||||
sla_resolution_due = (created_at + timedelta(hours=category.sla_resolution_hours)).replace(tzinfo=None)
|
||||
|
||||
# Primera respuesta (si aplica)
|
||||
first_response_at = None
|
||||
if first_response_hours_after is not None:
|
||||
first_response_at = (created_at + timedelta(hours=first_response_hours_after)).replace(tzinfo=None)
|
||||
|
||||
# Resolución (si aplica)
|
||||
resolved_at = None
|
||||
if resolved_hours_after is not None:
|
||||
resolved_at = (created_at + timedelta(hours=resolved_hours_after)).replace(tzinfo=None)
|
||||
|
||||
ticket = Ticket(
|
||||
id=ticket_id,
|
||||
tenant_id=tenant.id,
|
||||
ticket_number=f"TKT-{1000 + tickets_created}",
|
||||
subject=subject,
|
||||
description=description,
|
||||
status=status,
|
||||
priority=priority,
|
||||
created_by=creator.id,
|
||||
assigned_to=random.choice(agents).id if assigned else None,
|
||||
category_id=category.id,
|
||||
affected_system_id=random.choice(systems).id if systems else None,
|
||||
sla_response_due=sla_response_due,
|
||||
sla_resolution_due=sla_resolution_due,
|
||||
first_response_at=first_response_at,
|
||||
resolved_at=resolved_at,
|
||||
created_at=created_at,
|
||||
updated_at=resolved_at or first_response_at or created_at
|
||||
)
|
||||
|
||||
db.add(ticket)
|
||||
tickets_created += 1
|
||||
return ticket
|
||||
|
||||
# ============================================
|
||||
# 1. TICKETS CUMPLIENDO SLA RESPONSE (Verde)
|
||||
# ============================================
|
||||
print("✅ Generando tickets CUMPLIENDO Response SLA...")
|
||||
|
||||
for i in range(15):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.LOW, TicketPriority.MEDIUM, TicketPriority.HIGH])
|
||||
|
||||
# Creado hace X horas, respondido ANTES del deadline
|
||||
created_hours_ago = random.randint(24, 120)
|
||||
response_time = random.uniform(0.5, category.sla_response_hours * 0.7) # 70% del SLA
|
||||
|
||||
status = random.choice([TicketStatus.IN_PROGRESS, TicketStatus.WAITING_CUSTOMER])
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket con respuesta a tiempo #{i+1}",
|
||||
description=f"Este ticket fue respondido dentro del SLA de {category.name}",
|
||||
priority=priority,
|
||||
status=status,
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 15 tickets cumpliendo Response SLA")
|
||||
|
||||
# ============================================
|
||||
# 2. TICKETS VIOLANDO SLA RESPONSE (Rojo)
|
||||
# ============================================
|
||||
print("🔴 Generando tickets VIOLANDO Response SLA...")
|
||||
|
||||
for i in range(8):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.HIGH, TicketPriority.URGENT])
|
||||
|
||||
# Creado hace más tiempo que el SLA, SIN respuesta
|
||||
created_hours_ago = category.sla_response_hours + random.randint(1, 10)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket SIN respuesta - VIOLACIÓN #{i+1}",
|
||||
description=f"Este ticket lleva {created_hours_ago}h sin respuesta (SLA: {category.sla_response_hours}h)",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.NEW, TicketStatus.TRIAGE]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=None, # Sin respuesta!
|
||||
assigned=random.choice([True, False])
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 8 tickets VIOLANDO Response SLA")
|
||||
|
||||
# ============================================
|
||||
# 3. TICKETS EN RIESGO Response (Amarillo)
|
||||
# ============================================
|
||||
print("⚠️ Generando tickets EN RIESGO Response SLA...")
|
||||
|
||||
for i in range(10):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.MEDIUM, TicketPriority.HIGH, TicketPriority.URGENT])
|
||||
|
||||
# Creado hace tiempo, cerca del deadline (80-95% consumido)
|
||||
sla_hours = category.sla_response_hours
|
||||
time_consumed = random.uniform(0.8, 0.95) * sla_hours
|
||||
created_hours_ago = time_consumed
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket cerca de vencer respuesta #{i+1}",
|
||||
description=f"Este ticket está al {int(time_consumed/sla_hours*100)}% del SLA de respuesta",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.TRIAGE, TicketStatus.NEW]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=None, # Aún sin respuesta
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 10 tickets EN RIESGO Response SLA")
|
||||
|
||||
# ============================================
|
||||
# 4. TICKETS CUMPLIENDO SLA RESOLUTION
|
||||
# ============================================
|
||||
print("✅ Generando tickets CUMPLIENDO Resolution SLA...")
|
||||
|
||||
for i in range(20):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.LOW, TicketPriority.MEDIUM, TicketPriority.HIGH])
|
||||
|
||||
# Creado, respondido y resuelto dentro del SLA
|
||||
created_hours_ago = random.randint(72, 240)
|
||||
response_time = random.uniform(1, category.sla_response_hours * 0.5)
|
||||
resolution_time = random.uniform(
|
||||
response_time + 1,
|
||||
category.sla_resolution_hours * 0.8
|
||||
)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket resuelto a tiempo #{i+1}",
|
||||
description=f"Este ticket fue resuelto dentro del SLA de {category.name}",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.RESOLVED, TicketStatus.CLOSED]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
resolved_hours_after=resolution_time,
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 20 tickets cumpliendo Resolution SLA")
|
||||
|
||||
# ============================================
|
||||
# 5. TICKETS VIOLANDO SLA RESOLUTION
|
||||
# ============================================
|
||||
print("🔴 Generando tickets VIOLANDO Resolution SLA...")
|
||||
|
||||
for i in range(6):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.HIGH, TicketPriority.URGENT])
|
||||
|
||||
# Creado hace más del SLA de resolución, con respuesta pero sin resolver
|
||||
created_hours_ago = category.sla_resolution_hours + random.randint(5, 48)
|
||||
response_time = random.uniform(1, category.sla_response_hours * 0.5)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket sin resolver - VIOLACIÓN #{i+1}",
|
||||
description=f"Ticket lleva {created_hours_ago}h sin resolver (SLA: {category.sla_resolution_hours}h)",
|
||||
priority=priority,
|
||||
status=random.choice([TicketStatus.IN_PROGRESS, TicketStatus.WAITING_CUSTOMER]),
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
resolved_hours_after=None, # Sin resolver!
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 6 tickets VIOLANDO Resolution SLA")
|
||||
|
||||
# ============================================
|
||||
# 6. TICKETS EN RIESGO Resolution
|
||||
# ============================================
|
||||
print("⚠️ Generando tickets EN RIESGO Resolution SLA...")
|
||||
|
||||
for i in range(12):
|
||||
category = random.choice(categories)
|
||||
priority = random.choice([TicketPriority.MEDIUM, TicketPriority.HIGH])
|
||||
|
||||
# Con respuesta, cerca del deadline de resolución
|
||||
sla_hours = category.sla_resolution_hours
|
||||
time_consumed = random.uniform(0.75, 0.95) * sla_hours
|
||||
created_hours_ago = time_consumed
|
||||
response_time = random.uniform(0.5, category.sla_response_hours * 0.5)
|
||||
|
||||
create_ticket(
|
||||
subject=f"Ticket cerca de vencer resolución #{i+1}",
|
||||
description=f"Este ticket está al {int(time_consumed/sla_hours*100)}% del SLA de resolución",
|
||||
priority=priority,
|
||||
status=TicketStatus.IN_PROGRESS,
|
||||
category=category,
|
||||
created_hours_ago=created_hours_ago,
|
||||
first_response_hours_after=response_time,
|
||||
resolved_hours_after=None,
|
||||
assigned=True
|
||||
)
|
||||
|
||||
print(f" ✓ Creados 12 tickets EN RIESGO Resolution SLA")
|
||||
|
||||
# Guardar todos los tickets
|
||||
await db.commit()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("✅ GENERACIÓN DE DATOS SLA COMPLETADA")
|
||||
print(f" Total de tickets creados: {tickets_created}")
|
||||
print()
|
||||
print("📊 Distribución esperada:")
|
||||
print(" ✅ Response cumplidos: 15 tickets")
|
||||
print(" 🔴 Response violados: 8 tickets")
|
||||
print(" ⚠️ Response en riesgo: 10 tickets")
|
||||
print(" ✅ Resolution cumplidos: 20 tickets")
|
||||
print(" 🔴 Resolution violados: 6 tickets")
|
||||
print(" ⚠️ Resolution en riesgo: 12 tickets")
|
||||
print()
|
||||
print("🌐 Ve los resultados en:")
|
||||
print(" Dashboard SLA: http://localhost:3001/sla")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
async def cleanup_sla_test_data():
|
||||
"""Elimina tickets de prueba."""
|
||||
async with AsyncSessionLocal() as db:
|
||||
tenant_result = await db.execute(select(Tenant).limit(1))
|
||||
tenant = tenant_result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
print("❌ No se encontró ningún tenant.")
|
||||
return
|
||||
|
||||
# Eliminar tickets que empiezan con TKT-
|
||||
result = await db.execute(
|
||||
select(Ticket).where(
|
||||
Ticket.tenant_id == tenant.id,
|
||||
Ticket.ticket_number.like('TKT-%')
|
||||
)
|
||||
)
|
||||
tickets = result.scalars().all()
|
||||
|
||||
if not tickets:
|
||||
print("ℹ️ No hay tickets de prueba para eliminar.")
|
||||
return
|
||||
|
||||
for ticket in tickets:
|
||||
await db.delete(ticket)
|
||||
|
||||
await db.commit()
|
||||
|
||||
print(f"✅ Eliminados {len(tickets)} tickets de prueba")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "cleanup":
|
||||
print("🧹 Limpiando datos de prueba de SLA...")
|
||||
print()
|
||||
asyncio.run(cleanup_sla_test_data())
|
||||
else:
|
||||
print("🚀 Generando datos de prueba para SLA Management...")
|
||||
print()
|
||||
asyncio.run(generate_sla_test_data())
|
||||
print()
|
||||
print("💡 Para limpiar estos datos de prueba, ejecuta:")
|
||||
print(" python scripts/generate_sla_test_data.py cleanup")
|
||||
Reference in New Issue
Block a user