""" 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"" 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""