"""Author RAG — Pricing Plan Model. Stores all subscription plan definitions in the database. SuperAdmin manages these via the admin panel — no code deployment needed to change prices, limits, or Stripe Price IDs. Plans are fetched by the public billing API and displayed on the Next.js marketing website's Pricing section dynamically. """ import json from sqlalchemy import Boolean, Float, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base, TimestampMixin class PricingPlan(Base, TimestampMixin): """A subscription plan — prices and limits configurable by SuperAdmin.""" __tablename__ = "pricing_plans" # ── Identity ────────────────────────────────────────────────────────────── id: Mapped[str] = mapped_column( String(50), primary_key=True ) # e.g. "starter" | "agency" | "enterprise" display_name: Mapped[str] = mapped_column( String(100), nullable=False ) # Shown on pricing page # ── Pricing ─────────────────────────────────────────────────────────────── monthly_price_usd: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) annual_price_usd: Mapped[float] = mapped_column( Float, nullable=False, default=0.0 ) # Per-month cost when billed annually # ── Stripe ──────────────────────────────────────────────────────────────── # Paste the Stripe Price IDs from your Stripe Dashboard here via SuperAdmin panel. # Test mode Price IDs start with price_test_*, live with price_*. stripe_price_id_monthly: Mapped[str | None] = mapped_column(String(255), nullable=True) stripe_price_id_annual: Mapped[str | None] = mapped_column(String(255), nullable=True) # ── Usage Limits ────────────────────────────────────────────────────────── # -1 means unlimited max_books: Mapped[int] = mapped_column(Integer, nullable=False, default=5) max_chats_per_month: Mapped[int] = mapped_column(Integer, nullable=False, default=2000) max_token_budget: Mapped[int] = mapped_column(Integer, nullable=False, default=500_000) max_embeds: Mapped[int] = mapped_column(Integer, nullable=False, default=1) # ── Feature Flags ───────────────────────────────────────────────────────── has_analytics: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) has_ab_testing: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) has_white_label: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) has_api_access: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) has_webhooks: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) has_custom_domain: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) has_priority_support: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) # ── Display Config ──────────────────────────────────────────────────────── # JSON list of feature strings shown on the pricing page: # e.g. '["5 books", "2,000 chats/month", "Email support"]' features_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") is_active: Mapped[bool] = mapped_column( Boolean, default=True, nullable=False ) # False = hidden from pricing page is_popular: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False ) # Shows "Most Popular" badge sort_order: Mapped[int] = mapped_column( Integer, default=0, nullable=False ) # Display order on pricing cards # ── Overage Pricing ─────────────────────────────────────────────────────── # Charge per 1K tokens beyond monthly budget (rather than hard cut-off) overage_price_per_1k_tokens: Mapped[float] = mapped_column( Float, nullable=False, default=0.003 ) @property def features(self) -> list[str]: """Parse the features JSON list for display.""" try: return json.loads(self.features_json) except (json.JSONDecodeError, TypeError): return [] @features.setter def features(self, value: list[str]) -> None: self.features_json = json.dumps(value) def to_public_dict(self, billing: str = "monthly") -> dict: """Serialize plan for the public pricing API (consumed by Next.js website).""" price = self.monthly_price_usd if billing == "monthly" else self.annual_price_usd stripe_id = ( self.stripe_price_id_monthly if billing == "monthly" else self.stripe_price_id_annual ) return { "id": self.id, "name": self.display_name, "price": price, "billing": billing, "stripe_price_id": stripe_id, "limits": { "max_books": self.max_books, "max_chats_per_month": self.max_chats_per_month, "max_token_budget": self.max_token_budget, "max_embeds": self.max_embeds, }, "features": self.features, "is_popular": self.is_popular, "overage_price_per_1k_tokens": self.overage_price_per_1k_tokens, } def __repr__(self) -> str: return f""