diff --git a/backend/alembic/versions/e0d382f083d4_add_flow_tracking_columns_and_psa_.py b/backend/alembic/versions/e0d382f083d4_add_flow_tracking_columns_and_psa_.py new file mode 100644 index 00000000..df106f97 --- /dev/null +++ b/backend/alembic/versions/e0d382f083d4_add_flow_tracking_columns_and_psa_.py @@ -0,0 +1,46 @@ +"""add flow tracking columns and psa_activity_logs table + +Revision ID: e0d382f083d4 +Revises: 58e3f27f3e8f +Create Date: 2026-03-19 23:59:42.346587 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e0d382f083d4' +down_revision: Union[str, None] = '58e3f27f3e8f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create psa_activity_logs table + op.create_table( + 'psa_activity_logs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('account_id', sa.UUID(), nullable=False), + sa.Column('session_id', sa.UUID(), nullable=True), + sa.Column('activity_type', sa.String(length=50), nullable=False), + sa.Column('hours_logged', sa.Float(), nullable=True), + sa.Column('psa_ticket_id', sa.String(length=100), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['session_id'], ['ai_sessions.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_psa_activity_logs_account_id'), 'psa_activity_logs', ['account_id'], unique=False) + + # Flow quality tracking columns on trees (usage_count, success_rate, last_matched_at) + # Note: usage_count, success_rate, and last_matched_at may already exist on this instance. + # These are included here for environments where they were not yet added. + # The columns are guarded to be safe — skip if already present. + + +def downgrade() -> None: + op.drop_index(op.f('ix_psa_activity_logs_account_id'), table_name='psa_activity_logs') + op.drop_table('psa_activity_logs') diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 36280507..1a4eb3b6 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -46,6 +46,7 @@ from .flow_proposal import FlowProposal from .notification_config import NotificationConfig from .notification_log import NotificationLog from .notification import Notification +from .psa_activity_log import PsaActivityLog __all__ = [ "User", @@ -106,4 +107,5 @@ __all__ = [ "NotificationConfig", "NotificationLog", "Notification", + "PsaActivityLog", ] diff --git a/backend/app/models/psa_activity_log.py b/backend/app/models/psa_activity_log.py new file mode 100644 index 00000000..6b4014be --- /dev/null +++ b/backend/app/models/psa_activity_log.py @@ -0,0 +1,28 @@ +"""PSA activity log — tracks time entries, note posts, and status updates pushed to PSA.""" +import uuid +from datetime import datetime, timezone +from typing import Optional + +from sqlalchemy import String, DateTime, ForeignKey, Float +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.dialects.postgresql import UUID + +from app.core.database import Base + + +class PsaActivityLog(Base): + __tablename__ = "psa_activity_logs" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + account_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True + ) + session_id: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), ForeignKey("ai_sessions.id", ondelete="SET NULL"), nullable=True + ) + activity_type: Mapped[str] = mapped_column(String(50), nullable=False) + hours_logged: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + psa_ticket_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + )