113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
"""Grace periods, cancellation, and a record of what changed.
|
|||
|
|
|
||
|
|
Before this, a subscription had two states: working and locked out. A customer
|
||
|
|
whose card failed went straight from one to the other, with nothing in between
|
||
|
|
and no record of when or why it happened.
|
||
|
|
|
||
|
|
Adds:
|
||
|
|
|
||
|
|
- `subscription_plans.grace_period_days` — how long after expiry a workspace stays
|
||
|
|
read-only rather than locked out. Per plan, because it is a commercial decision
|
||
|
|
rather than a constant. Defaults to 0, which is exactly the old behaviour.
|
||
|
|
- `tenants.cancelled_at` — cancellation is a decision with a date, not the absence
|
||
|
|
of an active flag.
|
||
|
|
- `tenant_subscription_history` — what changed, when, and who did it. Answering
|
||
|
|
"why does this workspace have that plan" currently requires guessing.
|
||
|
|
|
||
|
|
Revision ID: d4f6a8b0c902
|
||
|
|
Revises: c3d5e7f9a801
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import Sequence, Union
|
||
|
|
|
||
|
|
import sqlalchemy as sa
|
||
|
|
from alembic import op
|
||
|
|
from sqlalchemy.dialects.postgresql import UUID
|
||
|
|
|
||
|
|
revision: str = "d4f6a8b0c902"
|
||
|
|
down_revision: Union[str, Sequence[str], None] = "c3d5e7f9a801"
|
||
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
||
|
|
depends_on: Union[str, Sequence[str], None] = None
|
||
|
|
|
||
|
|
|
||
|
|
def upgrade() -> None:
|
||
|
|
op.add_column(
|
||
|
|
"subscription_plans",
|
||
|
|
sa.Column("grace_period_days", sa.Integer(), nullable=False, server_default="0"),
|
||
|
|
)
|
||
|
|
op.add_column(
|
||
|
|
"tenants", sa.Column("cancelled_at", sa.DateTime(timezone=True), nullable=True)
|
||
|
|
)
|
||
|
|
|
||
|
|
op.create_table(
|
||
|
|
"tenant_subscription_history",
|
||
|
|
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(
|
||
|
|
"from_plan_id",
|
||
|
|
UUID(as_uuid=True),
|
||
|
|
sa.ForeignKey("subscription_plans.id", ondelete="SET NULL"),
|
||
|
|
nullable=True,
|
||
|
|
),
|
||
|
|
sa.Column(
|
||
|
|
"to_plan_id",
|
||
|
|
UUID(as_uuid=True),
|
||
|
|
sa.ForeignKey("subscription_plans.id", ondelete="SET NULL"),
|
||
|
|
nullable=True,
|
||
|
|
),
|
||
|
|
sa.Column("change_type", sa.String(30), nullable=False),
|
||
|
|
sa.Column("from_end_date", sa.Date(), nullable=True),
|
||
|
|
sa.Column("to_end_date", sa.Date(), nullable=True),
|
||
|
|
sa.Column("from_status", sa.String(30), nullable=True),
|
||
|
|
sa.Column("to_status", sa.String(30), nullable=True),
|
||
|
|
sa.Column(
|
||
|
|
"changed_by_id",
|
||
|
|
UUID(as_uuid=True),
|
||
|
|
sa.ForeignKey("users.id", ondelete="SET NULL"),
|
||
|
|
nullable=True,
|
||
|
|
),
|
||
|
|
sa.Column("changed_by_email", sa.String(), nullable=True),
|
||
|
|
sa.Column("notes", sa.Text(), nullable=True),
|
||
|
|
sa.Column(
|
||
|
|
"created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
|
||
|
|
),
|
||
|
|
)
|
||
|
|
op.create_index(
|
||
|
|
"ix_tenant_subscription_history_tenant",
|
||
|
|
"tenant_subscription_history",
|
||
|
|
["tenant_id", sa.text("created_at DESC")],
|
||
|
|
)
|
||
|
|
|
||
|
|
op.execute("ALTER TABLE tenant_subscription_history ENABLE ROW LEVEL SECURITY")
|
||
|
|
op.execute("ALTER TABLE tenant_subscription_history FORCE ROW LEVEL SECURITY")
|
||
|
|
op.execute(
|
||
|
|
"""
|
||
|
|
CREATE POLICY tenant_isolation ON tenant_subscription_history
|
||
|
|
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 tenant_subscription_history")
|
||
|
|
op.drop_index(
|
||
|
|
"ix_tenant_subscription_history_tenant", table_name="tenant_subscription_history"
|
||
|
|
)
|
||
|
|
op.drop_table("tenant_subscription_history")
|
||
|
|
op.drop_column("tenants", "cancelled_at")
|
||
|
|
op.drop_column("subscription_plans", "grace_period_days")
|