diff --git a/backend/alembic/versions/ff6fe5895ea2_extend_flow_proposals_l1.py b/backend/alembic/versions/ff6fe5895ea2_extend_flow_proposals_l1.py new file mode 100644 index 00000000..b585c081 --- /dev/null +++ b/backend/alembic/versions/ff6fe5895ea2_extend_flow_proposals_l1.py @@ -0,0 +1,52 @@ +"""extend_flow_proposals_l1 + +Revision ID: ff6fe5895ea2 +Revises: a8186f22506d +Create Date: 2026-05-28 16:26:06.932886 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ff6fe5895ea2' +down_revision: Union[str, None] = 'a8186f22506d' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('flow_proposals', sa.Column('source', sa.String(30), nullable=True)) + op.add_column('flow_proposals', sa.Column('linked_ticket_id', sa.String(64), nullable=True)) + op.add_column('flow_proposals', sa.Column('linked_ticket_kind', sa.String(10), nullable=True)) + op.add_column( + 'flow_proposals', + sa.Column('validated_by_outcome', sa.Boolean(), nullable=False, server_default='false'), + ) + + # Backfill existing rows then enforce NOT NULL on source + op.execute("UPDATE flow_proposals SET source = 'manual_draft' WHERE source IS NULL") + op.alter_column('flow_proposals', 'source', nullable=False) + + op.create_check_constraint( + 'ck_flow_proposals_source', + 'flow_proposals', + "source IN ('ai_realtime_l1', 'kb_accelerator', 'manual_draft', 'ai_promoted')", + ) + op.create_check_constraint( + 'ck_flow_proposals_linked_ticket_kind', + 'flow_proposals', + "linked_ticket_kind IS NULL OR linked_ticket_kind IN ('psa', 'internal')", + ) + + +def downgrade() -> None: + op.drop_constraint('ck_flow_proposals_linked_ticket_kind', 'flow_proposals', type_='check') + op.drop_constraint('ck_flow_proposals_source', 'flow_proposals', type_='check') + op.drop_column('flow_proposals', 'validated_by_outcome') + op.drop_column('flow_proposals', 'linked_ticket_kind') + op.drop_column('flow_proposals', 'linked_ticket_id') + op.drop_column('flow_proposals', 'source') diff --git a/backend/app/models/flow_proposal.py b/backend/app/models/flow_proposal.py index 5450e249..2d95e452 100644 --- a/backend/app/models/flow_proposal.py +++ b/backend/app/models/flow_proposal.py @@ -7,7 +7,7 @@ import uuid from datetime import datetime, timezone from typing import Optional, Any, TYPE_CHECKING -from sqlalchemy import String, Text, DateTime, ForeignKey, Integer, Float, CheckConstraint +from sqlalchemy import String, Text, DateTime, ForeignKey, Integer, Float, Boolean, CheckConstraint, text as sa_text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID, JSONB @@ -48,6 +48,14 @@ class FlowProposal(Base): "status IN ('pending', 'approved', 'modified', 'rejected', 'dismissed', 'auto_reinforced')", name="ck_flow_proposals_status", ), + CheckConstraint( + "source IN ('ai_realtime_l1', 'kb_accelerator', 'manual_draft', 'ai_promoted')", + name="ck_flow_proposals_source", + ), + CheckConstraint( + "linked_ticket_kind IS NULL OR linked_ticket_kind IN ('psa', 'internal')", + name="ck_flow_proposals_linked_ticket_kind", + ), ) id: Mapped[uuid.UUID] = mapped_column( @@ -135,6 +143,16 @@ class FlowProposal(Base): comment="The flow that was created/updated when this proposal was approved", ) + # ── L1 workspace ── + source: Mapped[str] = mapped_column( + String(30), nullable=False, server_default=sa_text("'manual_draft'"), + ) + linked_ticket_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) + linked_ticket_kind: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + validated_by_outcome: Mapped[bool] = mapped_column( + Boolean(), nullable=False, server_default=sa_text('false'), + ) + # ── Timestamps ── created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)