From 974b188c1ee0df0250fa77ce56a3ed9c6fd6c7f7 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Wed, 6 May 2026 03:30:22 -0400 Subject: [PATCH] feat(billing): add plan_billing sibling table for Stripe + catalog metadata Co-Authored-By: Claude Opus 4.7 --- .../versions/f236a91224d0_add_plan_billing.py | 41 +++++++++++++++++++ backend/app/models/__init__.py | 2 + backend/app/models/plan_billing.py | 31 ++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 backend/alembic/versions/f236a91224d0_add_plan_billing.py create mode 100644 backend/app/models/plan_billing.py diff --git a/backend/alembic/versions/f236a91224d0_add_plan_billing.py b/backend/alembic/versions/f236a91224d0_add_plan_billing.py new file mode 100644 index 00000000..0cef8aa9 --- /dev/null +++ b/backend/alembic/versions/f236a91224d0_add_plan_billing.py @@ -0,0 +1,41 @@ +"""add plan_billing + +Revision ID: f236a91224d0 +Revises: 2aa73d3231c2 +Create Date: 2026-05-06 07:30:06.807887 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f236a91224d0' +down_revision: Union[str, None] = '2aa73d3231c2' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "plan_billing", + sa.Column("plan", sa.String(50), sa.ForeignKey("plan_limits.plan"), primary_key=True), + sa.Column("display_name", sa.String(255), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("monthly_price_cents", sa.Integer(), nullable=True), + sa.Column("annual_price_cents", sa.Integer(), nullable=True), + sa.Column("stripe_product_id", sa.String(255), nullable=True), + sa.Column("stripe_monthly_price_id", sa.String(255), nullable=True), + sa.Column("stripe_annual_price_id", sa.String(255), nullable=True), + sa.Column("is_public", sa.Boolean(), nullable=False, server_default=sa.text("true")), + sa.Column("is_archived", sa.Boolean(), nullable=False, server_default=sa.text("false")), + sa.Column("sort_order", sa.Integer(), nullable=False, server_default=sa.text("0")), + 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()), + ) + + +def downgrade() -> None: + op.drop_table("plan_billing") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e130224b..47e3ca3a 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -63,6 +63,7 @@ from .session_suggested_fix import SessionSuggestedFix from .draft_template import DraftTemplate from .account_settings import AccountSettings from .oauth_identity import OAuthIdentity # noqa: F401 +from .plan_billing import PlanBilling # noqa: F401 __all__ = [ "User", @@ -140,4 +141,5 @@ __all__ = [ "DraftTemplate", "AccountSettings", "OAuthIdentity", + "PlanBilling", ] diff --git a/backend/app/models/plan_billing.py b/backend/app/models/plan_billing.py new file mode 100644 index 00000000..64ca7575 --- /dev/null +++ b/backend/app/models/plan_billing.py @@ -0,0 +1,31 @@ +from datetime import datetime, timezone +from typing import Optional +from sqlalchemy import String, Integer, Boolean, DateTime, ForeignKey, Text +from sqlalchemy.orm import Mapped, mapped_column +from app.core.database import Base + + +class PlanBilling(Base): + __tablename__ = "plan_billing" + + plan: Mapped[str] = mapped_column( + String(50), ForeignKey("plan_limits.plan"), primary_key=True + ) + display_name: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + monthly_price_cents: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + annual_price_cents: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + stripe_product_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + stripe_monthly_price_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + stripe_annual_price_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + is_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + is_archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + )