feat: ConnectWise PSA Integration — Slice 1 (Foundation) #106

Merged
chihlasm merged 29 commits from feat/connectwise-psa-integration into main 2026-03-15 05:45:36 +00:00
3 changed files with 69 additions and 0 deletions
Showing only changes of commit 5bcaf6a9d4 - Show all commits

View File

@@ -0,0 +1,31 @@
"""Add psa_ticket_id and psa_connection_id to sessions.
Revision ID: 059
Revises: 058
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "059"
down_revision = "058"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("sessions", sa.Column("psa_ticket_id", sa.String(100), nullable=True))
op.add_column(
"sessions",
sa.Column(
"psa_connection_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("psa_connections.id", ondelete="SET NULL"),
nullable=True,
),
)
def downgrade() -> None:
op.drop_column("sessions", "psa_connection_id")
op.drop_column("sessions", "psa_ticket_id")

View File

@@ -83,6 +83,15 @@ class Session(Base):
attachments: Mapped[list["Attachment"]] = relationship("Attachment", back_populates="session")
shares: Mapped[list["SessionShare"]] = relationship("SessionShare", back_populates="session", cascade="all, delete-orphan")
# PSA ticket link
psa_ticket_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
psa_connection_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("psa_connections.id", ondelete="SET NULL"),
nullable=True,
)
psa_connection = relationship("PsaConnection", foreign_keys=[psa_connection_id])
# Batch tracking (maintenance flows)
batch_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True), nullable=True, index=True

View File

@@ -94,6 +94,10 @@ class SessionResponse(BaseModel):
batch_id: Optional[UUID] = None
target_label: Optional[str] = None
# PSA ticket link
psa_ticket_id: Optional[str] = None
psa_connection_id: Optional[UUID] = None
class Config:
from_attributes = True
@@ -140,3 +144,28 @@ class SaveAsTreeResponse(BaseModel):
tree_id: UUID
tree_name: str
message: str
# ── PSA ticket link ──────────────────────────────────────────────────
class TicketLinkRequest(BaseModel):
"""Link or unlink a PSA ticket to a session."""
psa_ticket_id: Optional[str] = None # null to unlink
class PSATicketResponse(BaseModel):
"""PSA ticket details returned when linking."""
id: str
summary: str
company_name: Optional[str] = None
board_name: Optional[str] = None
status_name: Optional[str] = None
priority_name: Optional[str] = None
class TicketLinkResponse(BaseModel):
"""Response after linking/unlinking a ticket."""
session_id: str
psa_ticket_id: Optional[str] = None
ticket: Optional[PSATicketResponse] = None