Files
saas_backend/alembic/versions/a7c9e3f5b205_subscription_notices.py
T

84 lines
3.0 KiB
Python
Raw Normal View History

2026-08-31 20:04:12 -04:00
"""Tell customers before their subscription lapses, not after.
The lifecycle was enforced silently: the first a customer heard that their
subscription had ended was a save failing, or — once the banner shipped — a
notice on a workspace that had already gone read-only. Nothing warned them while
there was still time to act.
Adds:
- `tenants.billing_email` — who to tell. Without it there is no one specific: a
workspace has users, not an owner or a billing contact, so the alternative is
emailing everybody, which is how notices become noise and stop being read.
- `subscription_notices` — what has already been sent. Keyed on the end date it
was sent *for*, so a renewal starts a fresh cycle and the same warning fires
again next time, while a worker running twice in a day sends nothing twice.
Revision ID: a7c9e3f5b205
Revises: f6b8c2d4e104
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects.postgresql import UUID
revision: str = "a7c9e3f5b205"
down_revision: Union[str, Sequence[str], None] = "f6b8c2d4e104"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column("tenants", sa.Column("billing_email", sa.String(255), nullable=True))
op.create_table(
"subscription_notices",
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("kind", sa.String(30), nullable=False),
sa.Column("for_end_date", sa.Date(), nullable=True),
sa.Column("sent_to", sa.String(255), nullable=True),
sa.Column(
"sent_at", sa.DateTime(timezone=True), nullable=False,
server_default=sa.func.now(),
),
)
op.create_index(
"uq_subscription_notice_once",
"subscription_notices",
["tenant_id", "kind", "for_end_date"],
unique=True,
)
op.execute("ALTER TABLE subscription_notices ENABLE ROW LEVEL SECURITY")
op.execute("ALTER TABLE subscription_notices FORCE ROW LEVEL SECURITY")
op.execute(
"""
CREATE POLICY tenant_isolation ON subscription_notices
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 subscription_notices")
op.drop_index("uq_subscription_notice_once", table_name="subscription_notices")
op.drop_table("subscription_notices")
op.drop_column("tenants", "billing_email")