171 lines
6.9 KiB
Python
171 lines
6.9 KiB
Python
"""
|
|
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)
|