Files
saas_backend/alembic/versions/a1d4f7c3e95b_org_unit_seats.py
T
2026-08-31 20:39:41 -04:00

82 lines
3.3 KiB
Python

"""Seats allocated to a branch, not just to a workspace.
A workspace buys fifty seats. Nothing stops the Lahore branch using forty-eight
of them, and nobody finds out until Karachi cannot add anybody. The workspace
limit is real and enforced; it is simply the wrong grain for an organisation with
branches that have their own budgets.
## What an allocation is, and is not
It is a **cap on a unit**, not a reservation. Nine seats allocated to a branch
with two people in it does not stop the other forty-one being used elsewhere —
it stops that branch exceeding nine.
A unit with **no allocation row is unconstrained**, and that is the default. Most
workspaces will never want this, and the ones that do will want it on two or
three units rather than on all of them. Requiring a row per unit would mean
inventing a number for every branch nobody has an opinion about.
## The rule that makes the numbers add up
The sum of every allocation may not exceed what the workspace has bought. Without
it, allocations become a set of promises that cannot all be kept, and the branch
that discovers this is whichever one hires last.
Revision ID: a1d4f7c3e95b
Revises: f8c3a6e1b72d
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import UUID
revision: str = "a1d4f7c3e95b"
down_revision: Union[str, Sequence[str], None] = "f8c3a6e1b72d"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"org_unit_seat_allocations",
sa.Column("id", UUID(as_uuid=True), primary_key=True,
server_default=sa.text("gen_random_uuid()")),
sa.Column("tenant_id", UUID(as_uuid=True),
sa.ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False),
sa.Column("org_unit_id", UUID(as_uuid=True),
sa.ForeignKey("org_units.id", ondelete="CASCADE"), nullable=False),
sa.Column("seat_limit", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now(), onupdate=sa.func.now()),
sa.UniqueConstraint("org_unit_id", name="uq_seat_allocation_unit"),
)
op.create_index("ix_seat_allocations_tenant", "org_unit_seat_allocations",
["tenant_id"])
op.execute("ALTER TABLE org_unit_seat_allocations ENABLE ROW LEVEL SECURITY")
op.execute("ALTER TABLE org_unit_seat_allocations FORCE ROW LEVEL SECURITY")
op.execute(
"""
CREATE POLICY tenant_isolation ON org_unit_seat_allocations
USING (
current_setting('app.bypass_rls', true) = 'on'
OR tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid
)
WITH CHECK (
current_setting('app.bypass_rls', true) = 'on'
OR tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::uuid
)
"""
)
def downgrade() -> None:
op.execute("DROP POLICY IF EXISTS tenant_isolation ON org_unit_seat_allocations")
op.drop_index("ix_seat_allocations_tenant",
table_name="org_unit_seat_allocations")
op.drop_table("org_unit_seat_allocations")