""" Ticket Service - ServiceManagerWeb Lógica de negocio para creación y gestión de tickets. Inyectable vía Depends() en los endpoints de FastAPI. """ import uuid from datetime import datetime from fastapi import Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.models.ticket import Ticket, TicketStatus, TicketPriority from app.models.category import Category from app.models.system import System from app.api.schemas.ticket import TicketCreate from app.api.v1.helpers import ( generate_next_ticket_number, calculate_sla_deadlines, safe_audit_log, ) class TicketService: """Servicio de tickets: encapsula lógica de negocio fuera del router.""" def __init__(self, db: AsyncSession = Depends(get_db)): self.db = db async def create_ticket( self, ticket: TicketCreate, tenant_id: uuid.UUID, user_id: uuid.UUID, ) -> dict: """ Crea un ticket con validación multi-tenant, cálculo de SLA y auto-asignación. Args: ticket: Datos del ticket a crear. tenant_id: Tenant del usuario autenticado. user_id: ID del usuario que crea el ticket. Returns: dict compatible con TicketResponse. Raises: HTTPException 400: UUID inválido, categoría/sistema no encontrado o de otro tenant. HTTPException 500: Fallo persistente tras max_retries. """ max_retries = 3 last_error = None for attempt in range(max_retries): try: ticket_number = await generate_next_ticket_number(self.db, tenant_id) category_uuid = uuid.UUID(ticket.category_id) if ticket.category_id else None system_uuid = uuid.UUID(ticket.affected_system_id) if ticket.affected_system_id else None category = None if category_uuid: category = await self.db.get(Category, category_uuid) if not category or category.tenant_id != tenant_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"La categoría con ID {ticket.category_id} no existe.", ) if system_uuid: system = await self.db.get(System, system_uuid) if not system or system.tenant_id != tenant_id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"El sistema con ID {ticket.affected_system_id} no existe.", ) sla_response_due, sla_resolution_due = calculate_sla_deadlines(category) assigned_to_user = category.auto_assign_to if category and category.auto_assign_to else None db_ticket = Ticket( id=uuid.uuid4(), tenant_id=tenant_id, ticket_number=ticket_number, subject=ticket.subject, description=ticket.description, category_id=category_uuid, affected_system_id=system_uuid, priority=TicketPriority[ticket.priority.upper()], created_by=user_id, assigned_to=assigned_to_user, status=TicketStatus.NEW, sla_response_due=sla_response_due, sla_resolution_due=sla_resolution_due, created_at=datetime.utcnow(), updated_at=datetime.utcnow(), ) self.db.add(db_ticket) await self.db.commit() await self.db.refresh(db_ticket) await safe_audit_log( db=self.db, tenant_id=tenant_id, user_id=user_id, action="ticket.create", resource_type="ticket", resource_id=db_ticket.id, new_values={ "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, "priority": db_ticket.priority.value, "status": db_ticket.status.value, }, ) return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": db_ticket.subject, "title": db_ticket.subject, "description": db_ticket.description, "status": db_ticket.status.value, "priority": db_ticket.priority.value, "category_id": str(db_ticket.category_id) if db_ticket.category_id else None, "affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, "system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, "contact_email": ticket.contact_email, "contact_phone": ticket.contact_phone, "created_by": str(db_ticket.created_by), "assigned_to": str(db_ticket.assigned_to) if db_ticket.assigned_to else None, "created_at": db_ticket.created_at, "updated_at": db_ticket.updated_at, "sla_response_due": db_ticket.sla_response_due, "sla_resolution_due": db_ticket.sla_resolution_due, "first_response_at": db_ticket.first_response_at, "resolved_at": db_ticket.resolved_at, } except ValueError as e: await self.db.rollback() raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid UUID format: {str(e)}", ) except HTTPException: await self.db.rollback() raise except Exception as e: await self.db.rollback() last_error = e if "duplicate key" in str(e).lower() and "ticket_number" in str(e).lower(): if attempt < max_retries - 1: continue raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error creating ticket: {str(e)}", ) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"No se pudo crear el ticket después de {max_retries} intentos: {str(last_error)}", ) def get_ticket_service(db: AsyncSession = Depends(get_db)) -> TicketService: """Factory function para inyectar TicketService vía Depends().""" return TicketService(db)