""" SLA Tasks - ServiceManagerWeb Workers Tareas para monitoreo y gestión de SLAs """ from celery import current_task from datetime import datetime, timedelta from typing import List, Dict, Any, Optional import structlog import asyncio from sqlalchemy import select, and_, or_, func from sqlalchemy.ext.asyncio import AsyncSession from app.celery import celery_app from app.core.config import get_settings from app.core.logging import get_logger from app.core.database import get_async_session_context from app.tasks.email_tasks import send_templated_email_task # Import models import sys import os # En Docker, backend está montado en /backend backend_path = '/backend' if os.path.exists('/backend') else '../../backend' if backend_path not in sys.path: sys.path.insert(0, backend_path) from app.models.ticket import Ticket, TicketStatus from app.models.user import User from app.models.category import Category from app.models.tenant import Tenant settings = get_settings() logger = get_logger(__name__) @celery_app.task( bind=True, time_limit=300, soft_time_limit=240 ) def check_sla_violations(self) -> Dict[str, Any]: """ Check for SLA violations and send alerts. This task runs every 5 minutes to check for: - Response SLA violations - Resolution SLA violations - SLA warnings (approaching deadline) Returns: Dict with check results """ task_logger = logger.bind( task_id=self.request.id, task_name=self.name ) task_logger.info("Starting SLA violations check") if not settings.SLA_CHECK_ENABLED: task_logger.info("SLA check disabled, skipping") return {"status": "disabled"} async def check_violations_async(): """Inner async function for database operations""" async with get_async_session_context() as db: current_time = datetime.utcnow() results = { "checked_at": current_time.isoformat(), "response_violations": [], "resolution_violations": [], "warnings": [], "notifications_sent": 0 } try: # Query para Response SLA violations # Tickets sin primera respuesta y con SLA vencido response_violations_query = select(Ticket).where( and_( Ticket.first_response_at == None, Ticket.sla_response_due != None, Ticket.sla_response_due < current_time, Ticket.status.notin_([TicketStatus.RESOLVED, TicketStatus.CLOSED]) ) ) response_result = await db.execute(response_violations_query) response_tickets = response_result.scalars().all() task_logger.info(f"Found {len(response_tickets)} response SLA violations") # Procesar violaciones de respuesta for ticket in response_tickets: # Cargar relaciones await db.refresh(ticket, ['created_by', 'assigned_to', 'category']) violation = { "ticket_id": str(ticket.id), "ticket_number": ticket.ticket_number, "subject": ticket.subject, "priority": ticket.priority.value, "sla_type": "response", "due_at": ticket.sla_response_due.isoformat(), "assigned_to_email": ticket.assigned_to.email if ticket.assigned_to else None, "created_by_email": ticket.created_by.email, "tenant_id": str(ticket.tenant_id) } results["response_violations"].append(violation) task_logger.info( "Processing response SLA violation", ticket_id=violation["ticket_id"], ticket_number=violation["ticket_number"] ) # Enviar notificación al agente asignado if violation["assigned_to_email"]: send_templated_email_task.apply_async(kwargs={ "to_email": violation["assigned_to_email"], "template_name": "sla_response_violation", "context": { "ticket_number": violation["ticket_number"], "subject": violation["subject"], "priority": violation["priority"], "due_at": violation["due_at"], "ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}" }, "tenant_id": violation["tenant_id"], "correlation_id": self.request.id }) results["notifications_sent"] += 1 # Query para Resolution SLA violations # Tickets no resueltos con SLA de resolución vencido resolution_violations_query = select(Ticket).where( and_( Ticket.resolved_at == None, Ticket.sla_resolution_due != None, Ticket.sla_resolution_due < current_time, Ticket.status.notin_([TicketStatus.RESOLVED, TicketStatus.CLOSED]) ) ) resolution_result = await db.execute(resolution_violations_query) resolution_tickets = resolution_result.scalars().all() task_logger.info(f"Found {len(resolution_tickets)} resolution SLA violations") # Procesar violaciones de resolución for ticket in resolution_tickets: await db.refresh(ticket, ['created_by', 'assigned_to', 'category']) violation = { "ticket_id": str(ticket.id), "ticket_number": ticket.ticket_number, "subject": ticket.subject, "priority": ticket.priority.value, "sla_type": "resolution", "due_at": ticket.sla_resolution_due.isoformat(), "assigned_to_email": ticket.assigned_to.email if ticket.assigned_to else None, "created_by_email": ticket.created_by.email, "tenant_id": str(ticket.tenant_id) } results["resolution_violations"].append(violation) task_logger.info( "Processing resolution SLA violation", ticket_id=violation["ticket_id"], ticket_number=violation["ticket_number"] ) # Enviar escalación al manager # TODO: Obtener email del manager desde configuración del tenant send_templated_email_task.apply_async(kwargs={ "to_email": violation["assigned_to_email"] or "manager@example.com", "template_name": "sla_resolution_violation", "context": { "ticket_number": violation["ticket_number"], "subject": violation["subject"], "priority": violation["priority"], "assigned_to": violation["assigned_to_email"] or "Sin asignar", "ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}" }, "tenant_id": violation["tenant_id"], "correlation_id": self.request.id }) results["notifications_sent"] += 1 task_logger.info( "SLA violations check completed", response_violations=len(results["response_violations"]), resolution_violations=len(results["resolution_violations"]), warnings=len(results["warnings"]), notifications_sent=results["notifications_sent"] ) return results except Exception as exc: task_logger.error( "Error during SLA violations check", error=str(exc), exc_info=True ) raise try: # Ejecutar la función async loop = asyncio.get_event_loop() if loop.is_running(): # Si ya hay un loop corriendo, crear uno nuevo loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop.run_until_complete(check_violations_async()) except Exception as exc: task_logger.error( "SLA violations check failed", error=str(exc), exc_info=True ) raise @celery_app.task( bind=True, time_limit=600, soft_time_limit=540 ) def calculate_sla_metrics(self, tenant_id: str, date_from: str, date_to: str) -> Dict[str, Any]: """ Calculate SLA metrics for a tenant and date range. Args: tenant_id: Tenant ID date_from: Start date (ISO format) date_to: End date (ISO format) Returns: Dict with SLA metrics """ task_logger = logger.bind( task_id=self.request.id, task_name=self.name, tenant_id=tenant_id ) task_logger.info( "Starting SLA metrics calculation", date_from=date_from, date_to=date_to ) async def calculate_metrics_async(): """Inner async function for database operations""" async with get_async_session_context() as db: try: from uuid import UUID tenant_uuid = UUID(tenant_id) date_from_dt = datetime.fromisoformat(date_from) date_to_dt = datetime.fromisoformat(date_to) # Query base para tickets del período base_query = select(Ticket).where( and_( Ticket.tenant_id == tenant_uuid, Ticket.created_at >= date_from_dt, Ticket.created_at <= date_to_dt ) ) result = await db.execute(base_query) tickets = result.scalars().all() total_count = len(tickets) # Calcular métricas de Response SLA response_met = 0 response_total = 0 response_times = [] for ticket in tickets: if ticket.sla_response_due: response_total += 1 if ticket.first_response_at: if ticket.first_response_at <= ticket.sla_response_due: response_met += 1 response_time = (ticket.first_response_at - ticket.created_at).total_seconds() / 3600 response_times.append(response_time) avg_response_time = sum(response_times) / len(response_times) if response_times else 0 response_percentage = (response_met / response_total * 100) if response_total > 0 else 0 # Calcular métricas de Resolution SLA resolution_met = 0 resolution_total = 0 resolution_times = [] for ticket in tickets: if ticket.sla_resolution_due: resolution_total += 1 if ticket.resolved_at: if ticket.resolved_at <= ticket.sla_resolution_due: resolution_met += 1 resolution_time = (ticket.resolved_at - ticket.created_at).total_seconds() / 3600 resolution_times.append(resolution_time) avg_resolution_time = sum(resolution_times) / len(resolution_times) if resolution_times else 0 resolution_percentage = (resolution_met / resolution_total * 100) if resolution_total > 0 else 0 # Métricas por prioridad by_priority = {} for priority in ["LOW", "MEDIUM", "HIGH", "URGENT"]: priority_tickets = [t for t in tickets if t.priority.value == priority] p_response_met = sum(1 for t in priority_tickets if t.first_response_at and t.sla_response_due and t.first_response_at <= t.sla_response_due) p_response_total = sum(1 for t in priority_tickets if t.sla_response_due) p_response_pct = (p_response_met / p_response_total * 100) if p_response_total > 0 else 0 p_resolution_met = sum(1 for t in priority_tickets if t.resolved_at and t.sla_resolution_due and t.resolved_at <= t.sla_resolution_due) p_resolution_total = sum(1 for t in priority_tickets if t.sla_resolution_due) p_resolution_pct = (p_resolution_met / p_resolution_total * 100) if p_resolution_total > 0 else 0 by_priority[priority] = { "response_sla_percentage": round(p_response_pct, 1), "resolution_sla_percentage": round(p_resolution_pct, 1) } metrics = { "tenant_id": tenant_id, "date_from": date_from, "date_to": date_to, "calculated_at": datetime.utcnow().isoformat(), "response_sla": { "target_hours": 2, # Promedio estimado "met_count": response_met, "total_count": response_total, "percentage": round(response_percentage, 1), "avg_response_time_hours": round(avg_response_time, 1) }, "resolution_sla": { "target_hours": 24, # Promedio estimado "met_count": resolution_met, "total_count": resolution_total, "percentage": round(resolution_percentage, 1), "avg_resolution_time_hours": round(avg_resolution_time, 1) }, "by_priority": by_priority, "trends": { "response_sla_trend": "Calculating...", "resolution_sla_trend": "Calculating..." } } task_logger.info( "SLA metrics calculation completed", response_sla_percentage=metrics["response_sla"]["percentage"], resolution_sla_percentage=metrics["resolution_sla"]["percentage"], total_tickets=total_count ) return metrics except Exception as exc: task_logger.error( "Error during SLA metrics calculation", error=str(exc), exc_info=True ) raise try: # Ejecutar la función async loop = asyncio.get_event_loop() if loop.is_running(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop.run_until_complete(calculate_metrics_async()) except Exception as exc: task_logger.error( "SLA metrics calculation failed", error=str(exc), exc_info=True ) raise @celery_app.task( bind=True, time_limit=300, soft_time_limit=240 ) def send_sla_warnings(self, tenant_id: Optional[str] = None) -> Dict[str, Any]: """ Send SLA warning notifications for tickets approaching deadline. Args: tenant_id: Optional tenant ID to filter by Returns: Dict with warning results """ task_logger = logger.bind( task_id=self.request.id, task_name=self.name, tenant_id=tenant_id ) task_logger.info("Starting SLA warnings check") try: current_time = datetime.utcnow() warning_threshold = settings.SLA_WARNING_THRESHOLD # 80% of SLA time # TODO: Query database for tickets approaching SLA deadlines # Mock warnings warnings = [ { "ticket_id": "mock-ticket-2", "ticket_number": "TKT-2024-000002", "subject": "Consulta técnica", "priority": "MEDIUM", "sla_type": "response", "due_at": (current_time + timedelta(minutes=30)).isoformat(), "time_remaining_percent": 15.0, "assigned_to_email": "agent@example.com", "tenant_id": "mock-tenant-1" } ] notifications_sent = 0 for warning in warnings: if warning["time_remaining_percent"] <= (1 - warning_threshold) * 100: task_logger.info( "Sending SLA warning", ticket_id=warning["ticket_id"], time_remaining_percent=warning["time_remaining_percent"] ) send_templated_email_task.apply_async(kwargs={ "to_email": warning["assigned_to_email"], "template_name": "sla_warning", "context": { "ticket_number": warning["ticket_number"], "subject": warning["subject"], "priority": warning["priority"], "sla_type": warning["sla_type"], "due_at": warning["due_at"], "time_remaining_percent": warning["time_remaining_percent"], "ticket_url": f"https://admin.servicemanager.local/tickets/{warning['ticket_id']}" }, "tenant_id": warning["tenant_id"], "correlation_id": self.request.id }) notifications_sent += 1 task_logger.info( "SLA warnings check completed", warnings_found=len(warnings), notifications_sent=notifications_sent ) return { "warnings_found": len(warnings), "notifications_sent": notifications_sent, "warnings": warnings } except Exception as exc: task_logger.error( "SLA warnings check failed", error=str(exc), exc_info=True ) raise