"""Author RAG Chatbot SaaS — User Model. Represents both Author (client) and SuperAdmin accounts. Role is enforced at the middleware and service layers. """ from datetime import datetime from sqlalchemy import Boolean, DateTime, Integer, String from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base, TimestampMixin, generate_uuid class User(Base, TimestampMixin): """User account — authors (clients) and super admins.""" __tablename__ = "users" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=generate_uuid) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) role: Mapped[str] = mapped_column( String(20), nullable=False, default="author", ) is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) failed_login_attempts: Mapped[int] = mapped_column(Integer, default=0, nullable=False) locked_until: Mapped[str | None] = mapped_column(String(50), nullable=True) totp_secret: Mapped[str | None] = mapped_column(String(64), nullable=True) # R-134 # Author profile fields full_name: Mapped[str | None] = mapped_column(String(255), nullable=True) website_url: Mapped[str | None] = mapped_column(String(500), nullable=True) bio: Mapped[str | None] = mapped_column(String(2000), nullable=True) logo_path: Mapped[str | None] = mapped_column(String(500), nullable=True) timezone: Mapped[str] = mapped_column(String(100), default="America/New_York", nullable=False) # Chatbot config bot_name: Mapped[str] = mapped_column(String(100), default="BookBot", nullable=False) bot_avatar_path: Mapped[str | None] = mapped_column(String(500), nullable=True) welcome_message: Mapped[str] = mapped_column( String(500), default="Hello!", nullable=False, ) fallback_message: Mapped[str] = mapped_column( String(500), default="I'm not sure about that specific detail — but I can tell you plenty about the books. What would you like to know?", nullable=False, ) response_style: Mapped[str] = mapped_column( String(20), default="balanced", nullable=False, ) out_of_scope_message: Mapped[str] = mapped_column( String(500), default="I can only help with questions about the author's books. Could you ask something related?", nullable=False, ) chatbot_is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) widget_theme: Mapped[str] = mapped_column(String(50), default="indigo", nullable=False) widget_position: Mapped[str] = mapped_column(String(20), default="bottom-right", nullable=False) widget_auto_open_delay: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # Email notification preferences notify_weekly_digest: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) notify_token_alerts: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) notify_new_conversation: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) notify_subscription_expiry: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) # ── Stripe Billing ──────────────────────────────────────────────────────── stripe_customer_id: Mapped[str | None] = mapped_column( String(255), nullable=True, unique=True, index=True ) # Set on first Stripe Checkout session stripe_subscription_id: Mapped[str | None] = mapped_column( String(255), nullable=True, unique=True, index=True ) stripe_plan_id: Mapped[str | None] = mapped_column( String(50), nullable=True ) # FK to PricingPlan.id (string FK — no hard FK constraint for flexibility) payment_grace_until: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) # Set when payment fails — access continues until this datetime # ── White-Label (Phase 3) ───────────────────────────────────────────────── remove_branding: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False ) # Hide "Powered by Author RAG" footer (Pro/Enterprise only) custom_widget_css: Mapped[str | None] = mapped_column( String(5000), nullable=True ) # Author-supplied CSS — sanitized server-side before storage # ── Phase 4 — Agency / Connect / GDPR ───────────────────────────────── stripe_connect_account_id: Mapped[str | None] = mapped_column( String(255), nullable=True ) # Stripe Express account for referral commission payouts notes: Mapped[str | None] = mapped_column( String(500), nullable=True ) # Internal flag; e.g. GDPR_DELETE_REQUESTED: # Relationships books: Mapped[list] = relationship("Book", back_populates="author", cascade="all, delete-orphan") documents: Mapped[list] = relationship("Document", back_populates="author", cascade="all, delete-orphan") access_records: Mapped[list] = relationship( "ClientAccess", back_populates="author", cascade="all, delete-orphan", foreign_keys="ClientAccess.author_id", ) def __repr__(self) -> str: return f""