Nuevos cambios en el servidor

This commit is contained in:
2026-01-27 13:22:13 -07:00
parent 6215fc40a7
commit 7bb3d1a0ac
33 changed files with 8737 additions and 169 deletions

View File

@@ -85,6 +85,9 @@ async def update_tenant(
raise HTTPException(status_code=404, detail="Tenant not found")
update_data = tenant_in.model_dump(exclude_unset=True)
if "status" in update_data:
tenant.is_active = update_data.pop("status") == TenantStatus.active
for field, value in update_data.items():
setattr(tenant, field, value)
@@ -92,3 +95,18 @@ async def update_tenant(
await db.commit()
await db.refresh(tenant)
return tenant
@router.delete("/{tenant_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_tenant(
tenant_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user = Depends(deps.get_current_active_superuser)
):
"""Eliminar un cliente (tenant) por ID."""
tenant = await db.get(Tenant, tenant_id)
if not tenant:
raise HTTPException(status_code=404, detail="Tenant not found")
await db.delete(tenant)
await db.commit()
return {"message": "Tenant deleted successfully"}

View File

@@ -1,8 +1,9 @@
"""
Tickets endpoints - ServiceManagerWeb
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
@@ -11,10 +12,13 @@ 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 pydantic import BaseModel
from app.models.category import Category
from app.models.system import System
import uuid
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
# ===================================
# SCHEMAS
@@ -24,7 +28,7 @@ class TicketCreate(BaseModel):
subject: str
description: str
category_id: Optional[str] = None
system_id: Optional[str] = None
affected_system_id: Optional[str] = None # Updated field name
priority: str = "MEDIUM"
class TicketUpdate(BaseModel):
@@ -42,7 +46,7 @@ class TicketResponse(BaseModel):
status: str
priority: str
category_id: Optional[str] = None
system_id: Optional[str] = None
affected_system_id: Optional[str] = None # Updated field name
created_by: str
assigned_to: Optional[str] = None
created_at: datetime
@@ -78,8 +82,27 @@ async def create_ticket(
# 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.system_id) if ticket.system_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,
@@ -87,7 +110,7 @@ async def create_ticket(
subject=ticket.subject,
description=ticket.description,
category_id=category_uuid,
system_id=system_uuid,
affected_system_id=system_uuid, # Updated field name
priority=TicketPriority[ticket.priority.upper()],
created_by=current_user.id,
status=TicketStatus.NEW,
@@ -99,7 +122,9 @@ async def create_ticket(
await db.commit()
await db.refresh(db_ticket)
# Convertir a respuesta
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,
@@ -108,7 +133,7 @@ async def create_ticket(
"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.system_id) if db_ticket.system_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,
@@ -117,12 +142,17 @@ async def create_ticket(
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)}"
@@ -140,43 +170,55 @@ async def get_tickets(
"""
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()
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.system_id) if t.system_id else None,
"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
]
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)
@@ -196,35 +238,45 @@ async def get_ticket(
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"
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)}"
)
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.system_id) if ticket.system_id else None,
"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)
@@ -245,22 +297,22 @@ async def update_ticket(
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:
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():
@@ -278,6 +330,9 @@ async def update_ticket(
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,
@@ -286,15 +341,19 @@ async def update_ticket(
"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.system_id) if db_ticket.system_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)}"
@@ -319,28 +378,31 @@ async def close_ticket(
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:
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,
@@ -349,15 +411,19 @@ async def close_ticket(
"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.system_id) if db_ticket.system_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)}"

View File

@@ -5,6 +5,7 @@ Router principal para la API v1
"""
from fastapi import APIRouter
from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, tickets
api_router = APIRouter()
@@ -51,4 +52,5 @@ api_router.include_router(
tickets.router,
prefix="/tickets",
tags=["tickets"]
)
)

View File

@@ -139,4 +139,8 @@ def get_settings() -> Settings:
Using lru_cache to create a singleton pattern for settings.
"""
return Settings()
return Settings()
# Crear una instancia global de Settings
settings = Settings()

View File

@@ -1,24 +1,29 @@
"""
Category Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean, ForeignKey
from sqlalchemy import String, Text, Boolean, ForeignKey, Column, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from typing import List, Optional
import uuid
from app.core.database import Base
from app.models.tenant import Tenant
from app.models.ticket import Ticket
class Category(Base):
__tablename__ = "categories"
__tablename__ = "ticket_categories" # Updated table name
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Optional: Tenant specific categories?
tenant_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True)
# Updated tenant_id to be required
tenant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("tenants.id", ondelete="CASCADE"),
nullable=False
)
# Relationships
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="category")

View File

@@ -1,4 +1,3 @@
"""
System Model - ServiceManagerWeb
"""
@@ -19,7 +18,11 @@ class System(Base):
# Relationships
# If we want tickets to link to systems, we will add relationship in Ticket later or now.
# We will assume Ticket links to System.
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="system")
tickets: Mapped[List["Ticket"]] = relationship(
"Ticket",
back_populates="affected_system",
foreign_keys="Ticket.affected_system_id"
)
def __repr__(self) -> str:
return f"<System(id={self.id}, name='{self.name}')>"

View File

@@ -54,15 +54,12 @@ class Tenant(Base):
String(20),
default=TenantStatus.ACTIVE
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
# Relaciones
users: Mapped[List["User"]] = relationship("User", back_populates="tenant")
tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="tenant")
categories: Mapped[List["Category"]] = relationship("Category", back_populates="tenant")
def __repr__(self) -> str:
return f"<Tenant(id={self.id}, name='{self.name}', slug='{self.slug}')>"
@property
def is_active(self) -> bool:
"""Check if tenant is active."""
return self.status == TenantStatus.ACTIVE
return f"<Tenant(id={self.id}, name='{self.name}', slug='{self.slug}')>"

View File

@@ -4,11 +4,17 @@ Ticket Model - ServiceManagerWeb
from sqlalchemy import String, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import Optional
from typing import Optional, TYPE_CHECKING
import enum
import uuid
from app.core.database import Base
from .tenant import Tenant
from .system import System
from .user import User
if TYPE_CHECKING:
from app.models.category import Category
class TicketStatus(str, enum.Enum):
NEW = "NEW"
@@ -43,7 +49,7 @@ class Ticket(Base):
created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
assigned_to: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
affected_system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
category_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("categories.id"), nullable=True)
# Relationships