Spaces:
Running
Running
| """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 | |
| ) | |
| def features(self) -> list[str]: | |
| """Parse the features JSON list for display.""" | |
| try: | |
| return json.loads(self.features_json) | |
| except (json.JSONDecodeError, TypeError): | |
| return [] | |
| 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"<PricingPlan id={self.id} monthly=${self.monthly_price_usd}>" | |