"""referrals: user.referral_code + user.referred_by_user_id + referrals table Phase D.1 of the multi-user billing work. Adds: - `users.referral_code` — unique 8-char URL-safe code per user, generated lazily on first visit to /settings (or signup). - `users.referred_by_user_id` — FK to the user who referred this account, set at signup if `?ref=` was supplied. Null otherwise. - `referrals` — audit trail. One row per (referrer, referred) pair when the link is captured. `converted_at` / `credited_at` filled in D.3 by the Paddle webhook when the referred user makes their first paid subscription. The Credit table that holds actual discount records is deferred to D.3 — no point creating it until Paddle is wired and we know what to write. Revision ID: 0013 Revises: 0012 Create Date: 2026-05-18 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op revision: str = "0013" down_revision: Union[str, None] = "0012" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # batch_alter_table wraps ADD CONSTRAINT in a copy-and-rename for # SQLite (no native ALTER constraints support); on MariaDB/Postgres # it falls through to plain ALTER statements. with op.batch_alter_table("users") as bop: bop.add_column(sa.Column("referral_code", sa.String(16), nullable=True)) bop.create_unique_constraint( "uq_users_referral_code", ["referral_code"], ) bop.add_column(sa.Column("referred_by_user_id", sa.Integer, nullable=True)) bop.create_foreign_key( "fk_users_referred_by", "users", ["referred_by_user_id"], ["id"], ondelete="SET NULL", ) op.create_table( "referrals", sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True), sa.Column("referrer_user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), # UNIQUE — a single user can only be referred once, ever. sa.Column("referred_user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), # converted_at = referred user made their first paid sub. credited_at = # we successfully applied the discount via Paddle. Both filled in D.3. sa.Column("converted_at", sa.DateTime(timezone=True)), sa.Column("credited_at", sa.DateTime(timezone=True)), sa.UniqueConstraint("referred_user_id", name="uq_referrals_referred"), ) op.create_index( "ix_referrals_referrer", "referrals", ["referrer_user_id"], ) def downgrade() -> None: op.drop_index("ix_referrals_referrer", table_name="referrals") op.drop_table("referrals") with op.batch_alter_table("users") as bop: bop.drop_constraint("fk_users_referred_by", type_="foreignkey") bop.drop_column("referred_by_user_id") bop.drop_constraint("uq_users_referral_code", type_="unique") bop.drop_column("referral_code")