Arag / app /models /client_access.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
3.13 kB
"""Author RAG Chatbot SaaS β€” Client Access (Subscription) Model.
Records every subscription grant from SuperAdmin to an author.
The token_hash is stored (never the raw token) for security.
Revocation is instant via Redis blacklist + this record.
"""
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Integer, String, Text
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base, TimestampMixin, generate_uuid
class ClientAccess(Base, TimestampMixin):
"""Subscription access record β€” one row per grant from SuperAdmin."""
__tablename__ = "client_access"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid)
author_id: Mapped[str] = mapped_column(
String(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
)
granted_by: Mapped[str | None] = mapped_column(
String(36), ForeignKey("users.id"), nullable=True
) # NULL for Stripe-auto-provisioned subscriptions (no human granter)
plan: Mapped[str] = mapped_column(
String(20),
nullable=False,
) # Legacy plan string β€” also see plan_id FK below
plan_id: Mapped[str | None] = mapped_column(
String(50), nullable=True, index=True
) # FK to PricingPlan.id β€” set for Stripe-provisioned subscriptions
stripe_subscription_id: Mapped[str | None] = mapped_column(
String(255), nullable=True, unique=True, index=True
) # Links this access record to the Stripe subscription for webhook updates
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
token_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
# Token budget
token_budget: Mapped[int] = mapped_column(Integer, nullable=False)
tokens_used: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
bonus_tokens: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
# Revocation
is_revoked: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, index=True)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
revoked_by: Mapped[str | None] = mapped_column(String(36), nullable=True)
revoke_reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
# Auto-renewal
auto_renew: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
# Admin notes
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
# Relationships
author: Mapped["User"] = relationship("User", foreign_keys=[author_id], back_populates="access_records")
@property
def total_token_budget(self) -> int:
"""Total available tokens including any bonus grants."""
return self.token_budget + self.bonus_tokens
def __repr__(self) -> str:
return f"<ClientAccess author={self.author_id} plan={self.plan} expires={self.expires_at}>"