"""SQLAlchemy ORM models for all persistent tables. Only auth, billing, storage metadata, and marketplace data live here. User content (notes, tasks, etc.) is NEVER persisted server-side — it lives in E2E-encrypted blobs in S3, referenced by storage_records. Table inventory: users — account credentials + tier refresh_tokens — hashed refresh token store subscriptions — Stripe subscription records storage_records — S3 blob metadata (no plaintext) backup_metadata — encrypted backup manifests plugins — marketplace plugin catalog plugin_installations — per-user install records plugin_reviews — admin review decisions revenue_events — Stripe Connect 70/30 split ledger """ from __future__ import annotations import uuid from datetime import datetime, timezone from sqlalchemy import ( BigInteger, Boolean, DateTime, Enum, Float, ForeignKey, Integer, String, Text, UniqueConstraint, Uuid, func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db import Base # ── Helpers ────────────────────────────────────────────────────────────── def _uuid() -> str: return str(uuid.uuid4()) def _now() -> datetime: return datetime.now(timezone.utc) # ── Enum types ──────────────────────────────────────────────────────────── TierEnum = Enum("free", "pro", "power", "team", name="billing_tier") PluginStatusEnum = Enum("pending_review", "approved", "rejected", name="plugin_status") ReviewDecisionEnum = Enum("approved", "rejected", name="review_decision") # ── Models ──────────────────────────────────────────────────────────────── class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) password_hash: Mapped[str] = mapped_column(String(255), nullable=False) tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free") stripe_customer_id: Mapped[str | None] = mapped_column(String(255), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) refresh_tokens: Mapped[list[RefreshToken]] = relationship( back_populates="user", cascade="all, delete-orphan" ) subscription: Mapped[Subscription | None] = relationship( back_populates="user", uselist=False, cascade="all, delete-orphan" ) class RefreshToken(Base): __tablename__ = "refresh_tokens" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) user: Mapped[User] = relationship(back_populates="refresh_tokens") class Subscription(Base): __tablename__ = "subscriptions" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True ) stripe_subscription_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) tier: Mapped[str] = mapped_column(TierEnum, nullable=False, default="free") status: Mapped[str] = mapped_column(String(50), nullable=False, default="free") current_period_end: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) user: Mapped[User] = relationship(back_populates="subscription") class StorageRecord(Base): __tablename__ = "storage_records" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) table_name: Mapped[str] = mapped_column(String(100), nullable=False) s3_key: Mapped[str] = mapped_column(String(500), nullable=False) checksum: Mapped[str] = mapped_column(String(64), nullable=False) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) class BackupMetadata(Base): __tablename__ = "backup_metadata" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) s3_key: Mapped[str] = mapped_column(String(500), nullable=False) version: Mapped[int] = mapped_column(Integer, nullable=False) timestamp: Mapped[int] = mapped_column(BigInteger, nullable=False) checksum: Mapped[str] = mapped_column(String(64), nullable=False) size_bytes: Mapped[int] = mapped_column(Integer, nullable=False) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) class Plugin(Base): __tablename__ = "plugins" id: Mapped[str] = mapped_column(String(255), primary_key=True) name: Mapped[str] = mapped_column(String(255), nullable=False) description: Mapped[str] = mapped_column(Text, nullable=False, default="") version: Mapped[str] = mapped_column(String(50), nullable=False, default="1.0.0") # nullable until developer account system is built author_id: Mapped[str | None] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) author_name: Mapped[str] = mapped_column(String(255), nullable=False, default="") category: Mapped[str] = mapped_column(String(100), nullable=False, default="") price_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0) permissions: Mapped[str] = mapped_column(Text, nullable=False, default="[]") # JSON list status: Mapped[str] = mapped_column(PluginStatusEnum, nullable=False, default="pending_review") s3_package_key: Mapped[str | None] = mapped_column(String(500), nullable=True) install_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) avg_rating: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) rejection_reason: Mapped[str | None] = mapped_column(Text, nullable=True) submitted_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) installations: Mapped[list[PluginInstallation]] = relationship( back_populates="plugin", cascade="all, delete-orphan" ) reviews: Mapped[list[PluginReview]] = relationship( back_populates="plugin", cascade="all, delete-orphan" ) revenue_events: Mapped[list[RevenueEvent]] = relationship( back_populates="plugin", cascade="all, delete-orphan" ) class PluginInstallation(Base): __tablename__ = "plugin_installations" __table_args__ = (UniqueConstraint("plugin_id", "user_id", name="uq_plugin_user"),) id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) plugin_id: Mapped[str] = mapped_column( String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) installed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) plugin: Mapped[Plugin] = relationship(back_populates="installations") class PluginReview(Base): __tablename__ = "plugin_reviews" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) plugin_id: Mapped[str] = mapped_column( String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True ) reviewer_id: Mapped[str | None] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) decision: Mapped[str] = mapped_column(ReviewDecisionEnum, nullable=False) notes: Mapped[str | None] = mapped_column(Text, nullable=True) reviewed_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) plugin: Mapped[Plugin] = relationship(back_populates="reviews") class RevenueEvent(Base): __tablename__ = "revenue_events" id: Mapped[str] = mapped_column( Uuid(as_uuid=False), primary_key=True, default=_uuid ) plugin_id: Mapped[str] = mapped_column( String(255), ForeignKey("plugins.id", ondelete="CASCADE"), nullable=False, index=True ) user_id: Mapped[str] = mapped_column( Uuid(as_uuid=False), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) amount_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0) developer_share_cents: Mapped[int] = mapped_column(Integer, nullable=False, default=0) stripe_transfer_id: Mapped[str | None] = mapped_column(String(255), nullable=True) paid_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) plugin: Mapped[Plugin] = relationship(back_populates="revenue_events")