|
|
|
|
@@ -11,7 +11,7 @@ import uuid
|
|
|
|
|
from app.core.database import get_db
|
|
|
|
|
from app.api.deps import get_current_user, get_current_tenant
|
|
|
|
|
from app.models.ticket import Ticket, TicketStatus, TicketPriority
|
|
|
|
|
from app.models.user import User
|
|
|
|
|
from app.models.user import User, UserRole
|
|
|
|
|
from app.models.tenant import Tenant
|
|
|
|
|
from app.models.category import Category
|
|
|
|
|
from app.models.system import System
|
|
|
|
|
@@ -28,98 +28,27 @@ from app.api.v1.helpers import (
|
|
|
|
|
safe_audit_log, generate_next_ticket_number, calculate_sla_deadlines, ticket_to_dict
|
|
|
|
|
)
|
|
|
|
|
from app.services.audit_service import AuditService
|
|
|
|
|
from app.services.ticket_service import TicketService, get_ticket_service
|
|
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
@router.post("/", response_model=TicketResponse, status_code=status.HTTP_201_CREATED)
|
|
|
|
|
async def create_ticket(ticket: TicketCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
|
|
|
async def create_ticket(
|
|
|
|
|
ticket: TicketCreate,
|
|
|
|
|
current_user: User = Depends(get_current_user),
|
|
|
|
|
ticket_service: TicketService = Depends(get_ticket_service),
|
|
|
|
|
):
|
|
|
|
|
"""Crear un nuevo ticket"""
|
|
|
|
|
max_retries = 3
|
|
|
|
|
last_error = None
|
|
|
|
|
|
|
|
|
|
for attempt in range(max_retries):
|
|
|
|
|
try:
|
|
|
|
|
ticket_number = await generate_next_ticket_number(db, current_user.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 db.get(Category, category_uuid)
|
|
|
|
|
if not category:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"La categoría con ID {ticket.category_id} no existe.")
|
|
|
|
|
# ✅ SECURITY: Validate category belongs to current tenant (prevents cross-tenant category injection)
|
|
|
|
|
if category.tenant_id != current_user.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 db.get(System, system_uuid)
|
|
|
|
|
if not system:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"El sistema con ID {ticket.affected_system_id} no existe.")
|
|
|
|
|
# ✅ SECURITY: Validate system belongs to current tenant (prevents cross-tenant system injection)
|
|
|
|
|
if system.tenant_id != current_user.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=current_user.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=current_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()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
db.add(db_ticket)
|
|
|
|
|
await db.commit()
|
|
|
|
|
await db.refresh(db_ticket)
|
|
|
|
|
|
|
|
|
|
await safe_audit_log(db=db, tenant_id=current_user.tenant_id, user_id=current_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 db.rollback()
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid UUID format: {str(e)}")
|
|
|
|
|
except HTTPException:
|
|
|
|
|
await db.rollback()
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
await 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)}")
|
|
|
|
|
return await ticket_service.create_ticket(ticket, current_user.tenant_id, current_user.id)
|
|
|
|
|
|
|
|
|
|
@router.get("/", response_model=List[TicketResponse])
|
|
|
|
|
async def get_tickets(skip: int = 0, limit: int = 100, status: Optional[str] = None, priority: Optional[str] = None,
|
|
|
|
|
db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
|
|
|
"""Obtener tickets con filtros opcionales"""
|
|
|
|
|
query = select(Ticket).where(Ticket.tenant_id == current_user.tenant_id)
|
|
|
|
|
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
|
|
|
|
# Solo CLIENT_USER ve únicamente sus propios tickets.
|
|
|
|
|
# CLIENT_ADMIN ve todos los del tenant.
|
|
|
|
|
if current_user.role == UserRole.CLIENT_USER:
|
|
|
|
|
query = query.where(Ticket.created_by == current_user.id)
|
|
|
|
|
|
|
|
|
|
query = apply_enum_filter(query, Ticket.status, status, TicketStatus, "status")
|
|
|
|
|
@@ -142,14 +71,14 @@ async def get_all_tickets_admin(skip: int = 0, limit: int = 100, status_filter:
|
|
|
|
|
assigned_to_filter: Optional[str] = None, search: Optional[str] = None, date_from: Optional[str] = None,
|
|
|
|
|
date_to: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user)):
|
|
|
|
|
"""Obtener todos los tickets del tenant del administrador (ADMIN/SUPPORT_MANAGER)."""
|
|
|
|
|
if current_user.role not in ["ADMIN", "SUPPORT_MANAGER"]:
|
|
|
|
|
if current_user.role not in (UserRole.ADMIN, UserRole.SUPPORT_MANAGER):
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="No tienes permisos para acceder a esta función")
|
|
|
|
|
|
|
|
|
|
query = select(Ticket, Tenant, User).join(Tenant, Ticket.tenant_id == Tenant.id).join(User, Ticket.created_by == User.id)
|
|
|
|
|
|
|
|
|
|
# SUPPORT_MANAGER solo ve su propio tenant.
|
|
|
|
|
# ADMIN ve todos los tenants (es el administrador de la plataforma).
|
|
|
|
|
if current_user.role == "SUPPORT_MANAGER":
|
|
|
|
|
if current_user.role == UserRole.SUPPORT_MANAGER:
|
|
|
|
|
query = query.where(Ticket.tenant_id == current_user.tenant_id)
|
|
|
|
|
|
|
|
|
|
query = apply_enum_filter(query, Ticket.status, status_filter, TicketStatus, "status")
|
|
|
|
|
@@ -207,7 +136,7 @@ async def get_ticket(ticket_id: str, db: AsyncSession = Depends(get_db), current
|
|
|
|
|
"""Obtener un ticket por ID"""
|
|
|
|
|
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
|
|
|
|
|
query = select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id)
|
|
|
|
|
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
|
|
|
|
if current_user.role.is_client:
|
|
|
|
|
query = query.where(Ticket.created_by == current_user.id)
|
|
|
|
|
|
|
|
|
|
query = query.options(selectinload(Ticket.category), selectinload(Ticket.affected_system), selectinload(Ticket.assigned_to_user))
|
|
|
|
|
@@ -224,7 +153,7 @@ async def update_ticket(ticket_id: str, ticket: TicketUpdate, db: AsyncSession =
|
|
|
|
|
"""Actualizar un ticket"""
|
|
|
|
|
ticket_uuid = validate_uuid_param(ticket_id, "ticket ID")
|
|
|
|
|
query = select(Ticket).where(Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id)
|
|
|
|
|
if current_user.role in ["CLIENT_USER", "CLIENT_ADMIN"]:
|
|
|
|
|
if current_user.role.is_client:
|
|
|
|
|
query = query.where(Ticket.created_by == current_user.id)
|
|
|
|
|
|
|
|
|
|
result = await db.execute(query)
|
|
|
|
|
|