Release version 1.3.1: Updated backend models and endpoints
This commit is contained in:
@@ -14,6 +14,7 @@ from app.models.ticket import Ticket, TicketStatus, TicketPriority
|
||||
from app.models.user import User
|
||||
from app.models.category import Category # ✅ CORREGIDO: Era TicketCategory
|
||||
from app.models.system import System
|
||||
from app.models.comment import TicketComment
|
||||
import uuid
|
||||
|
||||
router = APIRouter()
|
||||
@@ -40,6 +41,7 @@ class TicketResponse(BaseModel):
|
||||
id: str
|
||||
ticket_number: str
|
||||
subject: str
|
||||
title: str
|
||||
description: str
|
||||
status: str
|
||||
priority: str
|
||||
@@ -124,6 +126,7 @@ async def create_ticket(
|
||||
"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,
|
||||
@@ -186,6 +189,7 @@ async def get_tickets(
|
||||
"id": str(t.id),
|
||||
"ticket_number": t.ticket_number,
|
||||
"subject": t.subject,
|
||||
"title": t.subject,
|
||||
"description": t.description,
|
||||
"status": t.status.value,
|
||||
"priority": t.priority.value,
|
||||
@@ -237,6 +241,7 @@ async def get_ticket(
|
||||
"id": str(ticket.id),
|
||||
"ticket_number": ticket.ticket_number,
|
||||
"subject": ticket.subject,
|
||||
"title": ticket.subject,
|
||||
"description": ticket.description,
|
||||
"status": ticket.status.value,
|
||||
"priority": ticket.priority.value,
|
||||
@@ -305,6 +310,7 @@ async def update_ticket(
|
||||
"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,
|
||||
@@ -369,6 +375,7 @@ async def close_ticket(
|
||||
"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,
|
||||
@@ -388,11 +395,29 @@ async def close_ticket(
|
||||
)
|
||||
|
||||
|
||||
# ===================================
|
||||
# COMMENT ENDPOINTS (placeholder)
|
||||
## ===================================
|
||||
# COMMENT ENDPOINTS
|
||||
# ===================================
|
||||
|
||||
@router.get("/{ticket_id}/comments")
|
||||
class CommentCreate(BaseModel):
|
||||
content: str
|
||||
is_internal: bool = False
|
||||
|
||||
class CommentResponse(BaseModel):
|
||||
id: str
|
||||
ticket_id: str
|
||||
author_id: str
|
||||
author_name: str
|
||||
content: str
|
||||
is_internal: bool
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/{ticket_id}/comments", response_model=List[CommentResponse])
|
||||
async def get_ticket_comments(
|
||||
ticket_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -401,23 +426,151 @@ async def get_ticket_comments(
|
||||
"""
|
||||
Obtener comentarios de un ticket
|
||||
"""
|
||||
return []
|
||||
try:
|
||||
ticket_uuid = uuid.UUID(ticket_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid ticket ID format"
|
||||
)
|
||||
|
||||
# Verificar que el ticket existe y el usuario tiene acceso
|
||||
ticket_query = select(Ticket).where(
|
||||
Ticket.id == ticket_uuid,
|
||||
Ticket.tenant_id == current_user.tenant_id
|
||||
)
|
||||
ticket_result = await db.execute(ticket_query)
|
||||
ticket = ticket_result.scalars().first()
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Ticket {ticket_id} not found"
|
||||
)
|
||||
|
||||
# Obtener comentarios
|
||||
comments_query = select(TicketComment, User).join(
|
||||
User, TicketComment.author_id == User.id
|
||||
).where(
|
||||
TicketComment.ticket_id == ticket_uuid
|
||||
).order_by(TicketComment.created_at.asc())
|
||||
|
||||
result = await db.execute(comments_query)
|
||||
comments_with_users = result.all()
|
||||
|
||||
# Formatear respuesta
|
||||
return [
|
||||
{
|
||||
"id": str(comment.id),
|
||||
"ticket_id": str(comment.ticket_id),
|
||||
"author_id": str(comment.author_id),
|
||||
"author_name": f"{user.first_name} {user.last_name}",
|
||||
"content": comment.content,
|
||||
"is_internal": comment.is_internal,
|
||||
"created_at": comment.created_at,
|
||||
"updated_at": comment.updated_at
|
||||
}
|
||||
for comment, user in comments_with_users
|
||||
]
|
||||
|
||||
|
||||
@router.post("/{ticket_id}/comments", status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/{ticket_id}/comments", response_model=CommentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_comment(
|
||||
ticket_id: str,
|
||||
comment: CommentCreate,
|
||||
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"
|
||||
try:
|
||||
ticket_uuid = uuid.UUID(ticket_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid ticket ID format"
|
||||
)
|
||||
|
||||
# Verificar que el ticket existe y el usuario tiene acceso
|
||||
ticket_query = select(Ticket).where(
|
||||
Ticket.id == ticket_uuid,
|
||||
Ticket.tenant_id == current_user.tenant_id
|
||||
)
|
||||
|
||||
ticket_result = await db.execute(ticket_query)
|
||||
ticket_obj = ticket_result.scalars().first()
|
||||
|
||||
if not ticket_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Ticket {ticket_id} not found"
|
||||
)
|
||||
|
||||
# Crear comentario
|
||||
new_comment = TicketComment(
|
||||
id=uuid.uuid4(),
|
||||
ticket_id=ticket_uuid,
|
||||
author_id=current_user.id,
|
||||
content=comment.content,
|
||||
is_internal=comment.is_internal,
|
||||
created_at=datetime.utcnow(),
|
||||
updated_at=datetime.utcnow()
|
||||
)
|
||||
|
||||
db.add(new_comment)
|
||||
|
||||
# Actualizar el ticket updated_at
|
||||
ticket_obj.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(new_comment)
|
||||
|
||||
# Retornar con el nombre del autor
|
||||
return {
|
||||
"id": str(new_comment.id),
|
||||
"ticket_id": str(new_comment.ticket_id),
|
||||
"author_id": str(new_comment.author_id),
|
||||
"author_name": f"{current_user.first_name} {current_user.last_name}",
|
||||
"content": new_comment.content,
|
||||
"is_internal": new_comment.is_internal,
|
||||
"created_at": new_comment.created_at,
|
||||
"updated_at": new_comment.updated_at
|
||||
}
|
||||
@router.delete("/{ticket_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_ticket(
|
||||
ticket_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Eliminar un ticket (solo admin/manager)
|
||||
"""
|
||||
try:
|
||||
ticket_uuid = uuid.UUID(ticket_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid ticket ID format"
|
||||
)
|
||||
|
||||
query = select(Ticket).where(
|
||||
Ticket.id == ticket_uuid,
|
||||
Ticket.tenant_id == current_user.tenant_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"
|
||||
)
|
||||
|
||||
await db.delete(db_ticket)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "Ticket deleted successfully"}
|
||||
|
||||
# ===================================
|
||||
# ATTACHMENT ENDPOINTS (placeholder)
|
||||
|
||||
451
backend/app/api/v1/endpoints/tickets.py.backup
Normal file
451
backend/app/api/v1/endpoints/tickets.py.backup
Normal file
@@ -0,0 +1,451 @@
|
||||
"""
|
||||
Tickets endpoints - ServiceManagerWeb
|
||||
"""
|
||||
|
||||
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 # ✅ CORREGIDO: Era TicketCategory
|
||||
from app.models.system import System
|
||||
import uuid
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# ===================================
|
||||
# SCHEMAS
|
||||
# ===================================
|
||||
|
||||
class TicketCreate(BaseModel):
|
||||
subject: str
|
||||
description: str
|
||||
category_id: Optional[str] = None
|
||||
affected_system_id: Optional[str] = None # ✅ CORREGIDO: Era system_id
|
||||
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 # ✅ CORREGIDO: Era system_id
|
||||
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 # ✅ CORREGIDO
|
||||
|
||||
# ✅ CORREGIDO: Validar en la tabla correcta con el nombre correcto del modelo
|
||||
if category_uuid:
|
||||
category = await db.get(Category, category_uuid) # ✅ Category, no TicketCategory
|
||||
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 affected_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."
|
||||
)
|
||||
|
||||
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, # ✅ CORREGIDO: Nombre correcto del campo
|
||||
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)
|
||||
|
||||
# ✅ CORREGIDO: Usar affected_system_id en respuesta
|
||||
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,
|
||||
"affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO
|
||||
"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()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid UUID format: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
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
|
||||
"""
|
||||
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()
|
||||
|
||||
# ✅ CORREGIDO: 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,
|
||||
"affected_system_id": str(t.affected_system_id) if t.affected_system_id else None, # ✅ CORREGIDO
|
||||
"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
|
||||
]
|
||||
|
||||
|
||||
@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"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
# ✅ CORREGIDO: 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,
|
||||
"affected_system_id": str(ticket.affected_system_id) if ticket.affected_system_id else None, # ✅ CORREGIDO
|
||||
"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
|
||||
}
|
||||
|
||||
|
||||
@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"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
# ✅ CORREGIDO: 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,
|
||||
"affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO
|
||||
"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 Exception as e:
|
||||
await db.rollback()
|
||||
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"
|
||||
)
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
try:
|
||||
db_ticket.status = TicketStatus.CLOSED
|
||||
db_ticket.updated_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(db_ticket)
|
||||
|
||||
# ✅ CORREGIDO: 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,
|
||||
"affected_system_id": str(db_ticket.affected_system_id) if db_ticket.affected_system_id else None, # ✅ CORREGIDO
|
||||
"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 Exception as e:
|
||||
await db.rollback()
|
||||
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"
|
||||
)
|
||||
@@ -21,6 +21,7 @@ from app.models.system import System
|
||||
from app.models.category import Category
|
||||
from app.models.user import User
|
||||
from app.models.ticket import Ticket
|
||||
from app.models.comment import TicketComment
|
||||
|
||||
from app.core.logging import setup_logging
|
||||
from app.api.v1.router import api_router
|
||||
@@ -198,4 +199,4 @@ if __name__ == "__main__":
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.ENVIRONMENT == "development"
|
||||
)
|
||||
)
|
||||
|
||||
69
backend/app/models/comment.py
Normal file
69
backend/app/models/comment.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Comment Model - ServiceManagerWeb
|
||||
|
||||
Modelo para comentarios en tickets
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, Text, Boolean, ForeignKey, DateTime
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class TicketComment(Base):
|
||||
"""Comentarios en tickets."""
|
||||
|
||||
__tablename__ = "ticket_comments"
|
||||
|
||||
# Columnas
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=uuid.uuid4
|
||||
)
|
||||
|
||||
ticket_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("tickets.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
author_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id"),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
is_internal: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
default=False,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
default=datetime.utcnow,
|
||||
onupdate=datetime.utcnow,
|
||||
nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="comments")
|
||||
author: Mapped["User"] = relationship("User")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TicketComment {self.id} by {self.author_id}>"
|
||||
@@ -119,6 +119,12 @@ class Ticket(Base):
|
||||
foreign_keys=[assigned_to],
|
||||
back_populates="assigned_tickets"
|
||||
)
|
||||
|
||||
comments: Mapped[list["TicketComment"]] = relationship(
|
||||
"TicketComment",
|
||||
back_populates="ticket",
|
||||
cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# ✅ AÑADIDOS: Constraints según schema.sql
|
||||
__table_args__ = (
|
||||
|
||||
@@ -48,7 +48,7 @@ class User(Base):
|
||||
|
||||
# Autenticación
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[UserRole] = mapped_column(ENUM(UserRole), nullable=False)
|
||||
role: Mapped[UserRole] = mapped_column(ENUM(UserRole, name="user_role_enum"), nullable=False)
|
||||
|
||||
# 2FA (opcional para staff interno)
|
||||
totp_secret: Mapped[Optional[str]] = mapped_column(String(32))
|
||||
@@ -132,4 +132,4 @@ class User(Base):
|
||||
def requires_2fa(self) -> bool:
|
||||
"""Check if 2FA is required for this user."""
|
||||
# 2FA opcional para staff interno, no requerido para clientes
|
||||
return self.is_staff
|
||||
return self.is_staff
|
||||
|
||||
Reference in New Issue
Block a user