Arag / app /models /pricing_plan.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
6.19 kB
"""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"<PricingPlan id={self.id} monthly=${self.monthly_price_usd}>"