""" Ticket Comment Model - Collaboration & Communication Enables team collaboration on tickets with threaded discussions. Supports internal comments (team only) and external comments (visible to client). """ from sqlalchemy import Column, String, ForeignKey, Boolean, TIMESTAMP, Text, CheckConstraint from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY from sqlalchemy.orm import relationship from datetime import datetime import uuid from app.core.database import Base class TicketComment(Base): """ Ticket Comment Model Threaded comments on tickets. Supports internal (team) and external (client-visible) comments with mentions and attachments. Links to: - tickets (required) - which ticket this comment belongs to - users (commented by, edited by) - who created/edited comment - ticket_comments (parent) - for threaded discussions - documents (attachments) - optional file attachments """ __tablename__ = "ticket_comments" # Primary Key id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # Foreign Keys ticket_id = Column( UUID(as_uuid=True), ForeignKey("tickets.id", ondelete="CASCADE"), nullable=False, index=True ) user_id = Column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, # Nullable: user may be deleted index=True ) # Comment Content comment_text = Column(Text, nullable=False) # Comment Type & Visibility is_internal = Column(Boolean, default=True, nullable=False) # TRUE = team only, FALSE = visible to client comment_type = Column(String(50), default='note', nullable=False) # 'note', 'issue', 'resolution', 'question', 'update' # Threading (for replies) parent_comment_id = Column( UUID(as_uuid=True), ForeignKey("ticket_comments.id", ondelete="CASCADE"), nullable=True, index=True ) # Mentions & notifications mentioned_user_ids = Column(ARRAY(UUID(as_uuid=True)), nullable=True) # Array of user IDs mentioned in comment (e.g., @john) # Attachments (optional - links to documents) attachment_document_ids = Column(ARRAY(UUID(as_uuid=True)), nullable=True) # Array of document IDs attached to comment # Edit Tracking is_edited = Column(Boolean, default=False, nullable=False) edited_at = Column(TIMESTAMP(timezone=True), nullable=True) edited_by_user_id = Column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) # Metadata additional_metadata = Column( JSONB, nullable=False, default={}, server_default="{}" ) # Timestamps created_at = Column( TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow, server_default="timezone('utc'::text, now())" ) updated_at = Column( TIMESTAMP(timezone=True), nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow, server_default="timezone('utc'::text, now())" ) deleted_at = Column(TIMESTAMP(timezone=True), nullable=True) # Relationships ticket = relationship("Ticket", back_populates="comments") user = relationship("User", foreign_keys=[user_id]) edited_by_user = relationship("User", foreign_keys=[edited_by_user_id]) parent_comment = relationship("TicketComment", remote_side=[id], backref="replies") # Constraints __table_args__ = ( CheckConstraint("LENGTH(TRIM(comment_text)) > 0", name='chk_comment_not_empty'), ) def __repr__(self): return f"" def to_dict(self): """Convert comment to dictionary""" return { "id": str(self.id), "ticket_id": str(self.ticket_id), "user_id": str(self.user_id) if self.user_id else None, "comment_text": self.comment_text, "is_internal": self.is_internal, "comment_type": self.comment_type, "parent_comment_id": str(self.parent_comment_id) if self.parent_comment_id else None, "mentioned_user_ids": [str(uid) for uid in self.mentioned_user_ids] if self.mentioned_user_ids else [], "attachment_document_ids": [str(did) for did in self.attachment_document_ids] if self.attachment_document_ids else [], "is_edited": self.is_edited, "edited_at": self.edited_at.isoformat() if self.edited_at else None, "edited_by_user_id": str(self.edited_by_user_id) if self.edited_by_user_id else None, "additional_metadata": self.additional_metadata, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, }