84 lines
3.0 KiB
Python
84 lines
3.0 KiB
Python
"""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")
|