194 lines
6.3 KiB
Python
194 lines
6.3 KiB
Python
"""
|
|
Ticket Model - ServiceManagerWeb
|
|
Tickets de soporte - Core del negocio
|
|
"""
|
|
from sqlalchemy import String, ForeignKey, Text, Integer, CheckConstraint, UniqueConstraint, Enum as SAEnum
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship, synonym
|
|
from sqlalchemy.dialects.postgresql import ENUM as PG_ENUM
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
import enum
|
|
import uuid
|
|
|
|
from app.core.database import Base, GUID
|
|
|
|
|
|
def _generate_fallback_ticket_number() -> str:
|
|
# Matches helper format "TK-000001" and stays within VARCHAR(20)
|
|
return f"TK-{(uuid.uuid4().int % 1_000_000):06d}"
|
|
|
|
class TicketStatus(str, enum.Enum):
|
|
"""Estados posibles de un ticket"""
|
|
NEW = "NEW"
|
|
TRIAGE = "TRIAGE"
|
|
IN_PROGRESS = "IN_PROGRESS"
|
|
WAITING_CUSTOMER = "WAITING_CUSTOMER" # ✅ CORREGIDO: nombre según schema.sql
|
|
RESOLVED = "RESOLVED"
|
|
CLOSED = "CLOSED"
|
|
REOPENED = "REOPENED"
|
|
|
|
class TicketPriority(str, enum.Enum):
|
|
"""Prioridades posibles de un ticket"""
|
|
LOW = "LOW"
|
|
MEDIUM = "MEDIUM"
|
|
HIGH = "HIGH"
|
|
URGENT = "URGENT"
|
|
|
|
class Ticket(Base):
|
|
"""Modelo de tickets de soporte"""
|
|
__tablename__ = "tickets"
|
|
|
|
# Multi-tenancy
|
|
tenant_id: Mapped[uuid.UUID] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("tenants.id", ondelete="CASCADE"),
|
|
nullable=False
|
|
)
|
|
|
|
# Campos básicos
|
|
ticket_number: Mapped[str] = mapped_column(
|
|
String(20),
|
|
nullable=False,
|
|
default=_generate_fallback_ticket_number,
|
|
)
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
description: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
# Compatibility aliases (API/UI/tests often use these names)
|
|
title = synonym("subject")
|
|
system_id = synonym("affected_system_id")
|
|
|
|
# Estado y Prioridad
|
|
status: Mapped[TicketStatus] = mapped_column(
|
|
SAEnum(TicketStatus, name="ticket_status_enum", native_enum=False).with_variant(
|
|
PG_ENUM(TicketStatus, name="ticket_status_enum", create_type=True),
|
|
"postgresql",
|
|
),
|
|
default=TicketStatus.NEW,
|
|
nullable=False
|
|
)
|
|
priority: Mapped[TicketPriority] = mapped_column(
|
|
SAEnum(TicketPriority, name="ticket_priority_enum", native_enum=False).with_variant(
|
|
PG_ENUM(TicketPriority, name="ticket_priority_enum", create_type=True),
|
|
"postgresql",
|
|
),
|
|
default=TicketPriority.MEDIUM,
|
|
nullable=False
|
|
)
|
|
|
|
# ✅ CORREGIDO: Foreign Keys apuntan a tablas correctas
|
|
created_by: Mapped[uuid.UUID] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("users.id"),
|
|
nullable=False
|
|
)
|
|
assigned_to: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("users.id"),
|
|
nullable=True
|
|
)
|
|
|
|
# ✅ CORREGIDO: Renombrado de system_id a affected_system_id
|
|
affected_system_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("affected_systems.id"), # ✅ Tabla correcta
|
|
nullable=True
|
|
)
|
|
|
|
# ✅ CORREGIDO: Foreign key a tabla correcta
|
|
category_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("ticket_categories.id"), # ✅ Tabla correcta
|
|
nullable=True
|
|
)
|
|
|
|
# ✅ AÑADIDOS: Campos de SLA según schema.sql
|
|
sla_response_due: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
sla_resolution_due: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
first_response_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
resolved_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
|
|
# ✅ AÑADIDOS: Campos de CSAT (Customer Satisfaction) según schema.sql
|
|
rating: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
|
|
rating_comment: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
rated_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
|
|
|
|
# Relationships
|
|
tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="tickets")
|
|
|
|
# ✅ ACTUALIZADO: Nombre de relación y optional
|
|
affected_system: Mapped[Optional["System"]] = relationship(
|
|
"System",
|
|
back_populates="tickets"
|
|
)
|
|
|
|
category: Mapped[Optional["Category"]] = relationship(
|
|
"Category",
|
|
back_populates="tickets"
|
|
)
|
|
|
|
created_by_user: Mapped["User"] = relationship(
|
|
"User",
|
|
foreign_keys=[created_by],
|
|
back_populates="created_tickets"
|
|
)
|
|
|
|
assigned_to_user: Mapped[Optional["User"]] = relationship(
|
|
"User",
|
|
foreign_keys=[assigned_to],
|
|
back_populates="assigned_tickets"
|
|
)
|
|
|
|
comments: Mapped[list["TicketComment"]] = relationship(
|
|
"TicketComment",
|
|
back_populates="ticket",
|
|
cascade="all, delete-orphan"
|
|
)
|
|
|
|
attachments: Mapped[list["TicketAttachment"]] = relationship(
|
|
"TicketAttachment",
|
|
back_populates="ticket",
|
|
cascade="all, delete-orphan"
|
|
)
|
|
|
|
# ✅ AÑADIDOS: Constraints según schema.sql
|
|
__table_args__ = (
|
|
UniqueConstraint('tenant_id', 'ticket_number', name='uq_tickets_tenant_number'),
|
|
CheckConstraint('rating >= 1 AND rating <= 5', name='check_rating_range'),
|
|
)
|
|
|
|
participants: Mapped[list["TicketParticipant"]] = relationship(
|
|
"TicketParticipant",
|
|
back_populates="ticket",
|
|
cascade="all, delete-orphan"
|
|
)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<Ticket(id={self.id}, number='{self.ticket_number}', status={self.status})>"
|
|
|
|
class TicketParticipant(Base):
|
|
"""Participantes asignados a un ticket"""
|
|
__tablename__ = "ticket_participants"
|
|
|
|
ticket_id: Mapped[uuid.UUID] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("tickets.id", ondelete="CASCADE"),
|
|
nullable=False
|
|
)
|
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
|
GUID(),
|
|
ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False
|
|
)
|
|
role: Mapped[str] = mapped_column(String(50), nullable=False, default="participant")
|
|
|
|
ticket: Mapped["Ticket"] = relationship("Ticket", back_populates="participants")
|
|
user: Mapped["User"] = relationship("User")
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint("ticket_id", "user_id", name="uq_ticket_participant"),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<TicketParticipant(ticket={self.ticket_id}, user={self.user_id})>"
|