feat(psa): add PsaPostLog model and migration

Audit trail for notes posted to PSA systems. Tracks session ID,
ticket ID, note type, content, status (success/failed), and any
status changes made alongside the note post.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-14 23:28:21 -04:00
parent dcf8bce2bf
commit 7059969d05
4 changed files with 118 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ from app.models.ai_suggestion import AISuggestion # noqa: F401
from app.models.kb_import import KBImport, KBImportNode # noqa: F401
from app.models.script_template import ScriptCategory, ScriptTemplate, ScriptGeneration # noqa: F401
from app.models.psa_connection import PsaConnection # noqa: F401
from app.models.psa_post_log import PsaPostLog # noqa: F401
from app.core.config import settings
# this is the Alembic Config object

View File

@@ -0,0 +1,57 @@
"""Add psa_post_log table for PSA note posting audit trail.
Revision ID: 060
Revises: 059
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = "060"
down_revision = "059"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"psa_post_log",
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
sa.Column(
"session_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
),
sa.Column(
"psa_connection_id",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("psa_connections.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("ticket_id", sa.String(100), nullable=False),
sa.Column("note_type", sa.String(50), nullable=False),
sa.Column("content_posted", sa.Text(), nullable=False),
sa.Column("external_note_id", sa.String(100), nullable=True),
sa.Column("status", sa.String(20), nullable=False),
sa.Column("error_message", sa.Text(), nullable=True),
sa.Column("status_changed_from", sa.String(100), nullable=True),
sa.Column("status_changed_to", sa.String(100), nullable=True),
sa.Column(
"posted_by",
postgresql.UUID(as_uuid=True),
sa.ForeignKey("users.id"),
nullable=False,
),
sa.Column(
"posted_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
)
def downgrade() -> None:
op.drop_table("psa_post_log")

View File

@@ -37,6 +37,7 @@ from .survey_invite import SurveyInvite
from .kb_import import KBImport, KBImportNode
from .script_template import ScriptCategory, ScriptTemplate, ScriptGeneration
from .psa_connection import PsaConnection
from .psa_post_log import PsaPostLog
__all__ = [
"User",
@@ -88,4 +89,5 @@ __all__ = [
"ScriptTemplate",
"ScriptGeneration",
"PsaConnection",
"PsaPostLog",
]

View File

@@ -0,0 +1,58 @@
"""Audit trail for notes posted to PSA systems."""
import uuid
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import String, DateTime, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from app.core.database import Base
class PsaPostLog(Base):
__tablename__ = "psa_post_log"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
session_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("sessions.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
psa_connection_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("psa_connections.id", ondelete="SET NULL"),
nullable=True,
)
ticket_id: Mapped[str] = mapped_column(String(100), nullable=False)
note_type: Mapped[str] = mapped_column(String(50), nullable=False)
content_posted: Mapped[str] = mapped_column(Text, nullable=False)
external_note_id: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True
)
status: Mapped[str] = mapped_column(
String(20), nullable=False
) # 'success' or 'failed'
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
status_changed_from: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True
)
status_changed_to: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True
)
posted_by: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id"), nullable=False
)
posted_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=lambda: datetime.now(timezone.utc),
)
# Relationships
session = relationship("Session", foreign_keys=[session_id])
psa_connection = relationship("PsaConnection", foreign_keys=[psa_connection_id])
user = relationship("User", foreign_keys=[posted_by])