""" Tickets endpoints - ServiceManagerWeb (VERSION FINAL CORREGIDA) """ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from typing import List, Optional from datetime import datetime from app.core.database import get_db from app.api.deps import get_current_user from app.models.ticket import Ticket, TicketStatus, TicketPriority from app.models.user import User from app.models.category import Category from app.models.system import System import uuid import logging router = APIRouter() logger = logging.getLogger(__name__) # =================================== # SCHEMAS # =================================== class TicketCreate(BaseModel): subject: str description: str category_id: Optional[str] = None affected_system_id: Optional[str] = None # Updated field name priority: str = "MEDIUM" class TicketUpdate(BaseModel): subject: Optional[str] = None description: Optional[str] = None status: Optional[str] = None priority: Optional[str] = None assigned_to: Optional[str] = None class TicketResponse(BaseModel): id: str ticket_number: str subject: str description: str status: str priority: str category_id: Optional[str] = None affected_system_id: Optional[str] = None # Updated field name created_by: str assigned_to: Optional[str] = None created_at: datetime updated_at: datetime class Config: from_attributes = True class TicketCloseRequest(BaseModel): resolution: Optional[str] = None # =================================== # TICKET ENDPOINTS # =================================== @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) ): """ Crear un nuevo ticket """ try: # Generar número de ticket único result = await db.execute( select(func.count(Ticket.id)).where(Ticket.tenant_id == current_user.tenant_id) ) count = result.scalar() or 0 ticket_number = f"TK-{count + 1:06d}" # Convertir IDs de string a UUID si son proporcionados 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 # ✅ CORRECCIÓN: Validar en la tabla correcta 'categories' 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." ) # Validar si el system_id existe en la tabla systems 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." ) # ✅ CORRECCIÓN: Usar 'affected_system_id' (nombre real en la BD) 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, # Updated field name priority=TicketPriority[ticket.priority.upper()], created_by=current_user.id, status=TicketStatus.NEW, created_at=datetime.utcnow(), updated_at=datetime.utcnow() ) db.add(db_ticket) await db.commit() await db.refresh(db_ticket) logger.info(f"Ticket {db_ticket.ticket_number} created by {current_user.email}") # ✅ CORRECCIÓN: Usar affected_system_id return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": 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, "system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORRECTO "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 } except ValueError as e: await db.rollback() logger.error(f"Invalid UUID format: {str(e)}") 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() logger.error(f"Error creating ticket: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error creating ticket: {str(e)}" ) @router.get("/", response_model=List[TicketResponse]) async def get_tickets( skip: int = 0, limit: int = 100, status_filter: Optional[str] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener tickets del usuario actual """ try: query = select(Ticket).where( Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) if status_filter: try: status_enum = TicketStatus[status_filter.upper()] query = query.where(Ticket.status == status_enum) except KeyError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid status: {status_filter}" ) query = query.order_by(Ticket.created_at.desc()).offset(skip).limit(limit) result = await db.execute(query) tickets = result.scalars().all() logger.info(f"Listed {len(tickets)} tickets for user {current_user.email}") # ✅ CORRECCIÓN: Usar affected_system_id return [ { "id": str(t.id), "ticket_number": t.ticket_number, "subject": t.subject, "description": t.description, "status": t.status.value, "priority": t.priority.value, "category_id": str(t.category_id) if t.category_id else None, "system_id": str(t.affected_system_id) if t.affected_system_id else None, # ✅ CORRECTO "created_by": str(t.created_by), "assigned_to": str(t.assigned_to) if t.assigned_to else None, "created_at": t.created_at, "updated_at": t.updated_at } for t in tickets ] except HTTPException: raise except Exception as e: logger.error(f"Error listing tickets: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error listing tickets: {str(e)}" ) @router.get("/{ticket_id}", response_model=TicketResponse) async def get_ticket( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener un ticket específico """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) try: query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) result = await db.execute(query) ticket = result.scalars().first() if not ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) # ✅ CORRECCIÓN: Usar affected_system_id return { "id": str(ticket.id), "ticket_number": ticket.ticket_number, "subject": ticket.subject, "description": ticket.description, "status": ticket.status.value, "priority": ticket.priority.value, "category_id": str(ticket.category_id) if ticket.category_id else None, "system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None, # ✅ CORRECTO "created_by": str(ticket.created_by), "assigned_to": str(ticket.assigned_to) if ticket.assigned_to else None, "created_at": ticket.created_at, "updated_at": ticket.updated_at } except HTTPException: raise except Exception as e: logger.error(f"Error getting ticket: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error getting ticket: {str(e)}" ) @router.patch("/{ticket_id}", response_model=TicketResponse) async def update_ticket( ticket_id: str, ticket_update: TicketUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Actualizar un ticket """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) try: query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) result = await db.execute(query) db_ticket = result.scalars().first() if not db_ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) update_data = ticket_update.dict(exclude_unset=True) for field, value in update_data.items(): if field == "status" and value: setattr(db_ticket, field, TicketStatus[value.upper()]) elif field == "priority" and value: setattr(db_ticket, field, TicketPriority[value.upper()]) elif field == "assigned_to" and value: setattr(db_ticket, field, uuid.UUID(value)) else: setattr(db_ticket, field, value) db_ticket.updated_at = datetime.utcnow() await db.commit() await db.refresh(db_ticket) logger.info(f"Ticket {db_ticket.ticket_number} updated by {current_user.email}") # ✅ CORRECCIÓN: Usar affected_system_id return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": 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, "system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORRECTO "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 } except HTTPException: await db.rollback() raise except Exception as e: await db.rollback() logger.error(f"Error updating ticket: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error updating ticket: {str(e)}" ) @router.patch("/{ticket_id}/close", response_model=TicketResponse) async def close_ticket( ticket_id: str, close_request: TicketCloseRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Cerrar un ticket """ try: ticket_uuid = uuid.UUID(ticket_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid ticket ID format" ) try: query = select(Ticket).where( Ticket.id == ticket_uuid, Ticket.tenant_id == current_user.tenant_id, Ticket.created_by == current_user.id ) result = await db.execute(query) db_ticket = result.scalars().first() if not db_ticket: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Ticket {ticket_id} not found" ) db_ticket.status = TicketStatus.CLOSED db_ticket.updated_at = datetime.utcnow() await db.commit() await db.refresh(db_ticket) logger.info(f"Ticket {db_ticket.ticket_number} closed by {current_user.email}") # ✅ CORRECCIÓN: Usar affected_system_id return { "id": str(db_ticket.id), "ticket_number": db_ticket.ticket_number, "subject": 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, "system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORRECTO "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 } except HTTPException: await db.rollback() raise except Exception as e: await db.rollback() logger.error(f"Error closing ticket: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Error closing ticket: {str(e)}" ) # =================================== # COMMENT ENDPOINTS (placeholder) # =================================== @router.get("/{ticket_id}/comments") async def get_ticket_comments( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener comentarios de un ticket """ return [] @router.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED) async def create_comment( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Agregar un comentario a un ticket """ raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Comments not yet implemented" ) # =================================== # ATTACHMENT ENDPOINTS (placeholder) # =================================== @router.get("/{ticket_id}/attachments") async def get_ticket_attachments( ticket_id: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Obtener adjuntos de un ticket """ return [] @router.post("/{ticket_id}/attachments", status_code=status.HTTP_201_CREATED) async def upload_attachment( ticket_id: str, file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Subir un archivo adjunto a un ticket """ raise HTTPException( status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="File uploads not yet implemented" )