diff --git a/backend/alembic/versions/a8186f22506d_add_l1_columns.py b/backend/alembic/versions/a8186f22506d_add_l1_columns.py new file mode 100644 index 00000000..00038d38 --- /dev/null +++ b/backend/alembic/versions/a8186f22506d_add_l1_columns.py @@ -0,0 +1,59 @@ +"""add_l1_columns + +Revision ID: a8186f22506d +Revises: b269a1add160 +Create Date: 2026-05-28 16:15:40.900535 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a8186f22506d' +down_revision: Union[str, None] = 'b269a1add160' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + 'users', + sa.Column('can_cover_l1', sa.Boolean(), nullable=False, server_default='false'), + ) + op.add_column( + 'accounts', + sa.Column('l1_seats_purchased', sa.Integer(), nullable=False, server_default='0'), + ) + op.add_column( + 'subscriptions', + sa.Column('l1_seat_limit', sa.Integer(), nullable=True), + ) + op.add_column( + 'audit_logs', + sa.Column('acting_as', sa.String(30), nullable=True), + ) + + # Rotate account_role CHECK constraint to include 'l1_tech' + op.drop_constraint('ck_users_account_role_enum', 'users', type_='check') + op.create_check_constraint( + 'ck_users_account_role_enum', + 'users', + "account_role IN ('owner', 'admin', 'engineer', 'l1_tech', 'viewer')", + ) + + +def downgrade() -> None: + # Reverse the constraint rotation first + op.drop_constraint('ck_users_account_role_enum', 'users', type_='check') + op.create_check_constraint( + 'ck_users_account_role_enum', + 'users', + "account_role IN ('owner', 'admin', 'engineer', 'viewer')", + ) + op.drop_column('audit_logs', 'acting_as') + op.drop_column('subscriptions', 'l1_seat_limit') + op.drop_column('accounts', 'l1_seats_purchased') + op.drop_column('users', 'can_cover_l1') diff --git a/backend/app/models/account.py b/backend/app/models/account.py index b036d20f..4162e844 100644 --- a/backend/app/models/account.py +++ b/backend/app/models/account.py @@ -57,6 +57,11 @@ class Account(Base): team_size_bucket: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) primary_psa: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + # L1 workspace seats + l1_seats_purchased: Mapped[int] = mapped_column( + Integer, nullable=False, server_default="0" + ) + # SSO / SAML groundwork (Task 11) sso_enabled: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") sso_provider: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # "saml" | "oidc" diff --git a/backend/app/models/audit_log.py b/backend/app/models/audit_log.py index 0e795222..48214dc9 100644 --- a/backend/app/models/audit_log.py +++ b/backend/app/models/audit_log.py @@ -35,6 +35,7 @@ class AuditLog(Base): ) details: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) + acting_as: Mapped[Optional[str]] = mapped_column(String(30), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) diff --git a/backend/app/models/subscription.py b/backend/app/models/subscription.py index b4fa284e..3ec541f9 100644 --- a/backend/app/models/subscription.py +++ b/backend/app/models/subscription.py @@ -21,6 +21,7 @@ class Subscription(Base): billing_interval: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) status: Mapped[str] = mapped_column(String(50), nullable=False, default="active") seat_limit: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + l1_seat_limit: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) current_period_start: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) current_period_end: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) cancel_at_period_end: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index ab25c8f0..54361daf 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime, timezone from typing import Optional, TYPE_CHECKING -from sqlalchemy import String, DateTime, ForeignKey, Boolean, CheckConstraint, Text, Integer +from sqlalchemy import String, DateTime, ForeignKey, Boolean, CheckConstraint, Text, Integer, text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base @@ -22,7 +22,7 @@ class User(Base): name='ck_users_role_enum' ), CheckConstraint( - "account_role IN ('owner', 'admin', 'engineer', 'viewer')", + "account_role IN ('owner', 'admin', 'engineer', 'l1_tech', 'viewer')", name='ck_users_account_role_enum' ), ) @@ -50,6 +50,9 @@ class User(Base): index=True ) account_role: Mapped[str] = mapped_column(String(50), nullable=False, default="engineer") + can_cover_l1: Mapped[bool] = mapped_column( + Boolean(), nullable=False, server_default=text('false') + ) # Legacy team columns (kept for PR A coexistence) team_id: Mapped[Optional[uuid.UUID]] = mapped_column(