This commit is contained in:
2026-03-03 14:02:18 -07:00
parent f10b15d91b
commit 3b46f48655
13 changed files with 1041 additions and 99 deletions

View File

@@ -0,0 +1,170 @@
"""
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)