"""User model for Mantimon TCG. This module defines the User model for player accounts with OAuth support and premium subscription tracking. Example: user = User( email="player@example.com", display_name="Player1", oauth_provider="google", oauth_id="123456789" ) """ from datetime import datetime from typing import TYPE_CHECKING from sqlalchemy import Boolean, DateTime, Index, String from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base if TYPE_CHECKING: from app.db.models.campaign import CampaignProgress from app.db.models.collection import Collection from app.db.models.deck import Deck from app.db.models.oauth_account import OAuthLinkedAccount class User(Base): """User account model with OAuth and premium support. Attributes: id: Unique identifier (UUID). email: User's email address (unique). display_name: Public display name. avatar_url: URL to user's avatar image. oauth_provider: OAuth provider name ('google', 'discord'). oauth_id: Unique ID from the OAuth provider. is_premium: Whether user has active premium subscription. premium_until: When premium subscription expires. created_at: Account creation timestamp. last_login: Last login timestamp. Relationships: decks: User's deck configurations. collection: User's card collection. campaign_progress: User's campaign state. linked_accounts: Additional linked OAuth providers. """ __tablename__ = "users" # Basic info email: Mapped[str] = mapped_column( String(255), unique=True, nullable=False, index=True, doc="User's email address", ) display_name: Mapped[str] = mapped_column( String(50), nullable=False, doc="Public display name", ) avatar_url: Mapped[str | None] = mapped_column( String(500), nullable=True, doc="URL to avatar image", ) # OAuth oauth_provider: Mapped[str] = mapped_column( String(20), nullable=False, doc="OAuth provider name (google, discord)", ) oauth_id: Mapped[str] = mapped_column( String(255), nullable=False, doc="Unique ID from OAuth provider", ) # Premium subscription is_premium: Mapped[bool] = mapped_column( Boolean, default=False, nullable=False, doc="Whether user has active premium", ) premium_until: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, doc="Premium subscription expiration", ) # Activity tracking last_login: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, doc="Last login timestamp", ) # Relationships decks: Mapped[list["Deck"]] = relationship( "Deck", back_populates="user", cascade="all, delete-orphan", lazy="selectin", ) collection: Mapped[list["Collection"]] = relationship( "Collection", back_populates="user", cascade="all, delete-orphan", lazy="selectin", ) campaign_progress: Mapped["CampaignProgress | None"] = relationship( "CampaignProgress", back_populates="user", cascade="all, delete-orphan", uselist=False, lazy="selectin", ) linked_accounts: Mapped[list["OAuthLinkedAccount"]] = relationship( "OAuthLinkedAccount", back_populates="user", cascade="all, delete-orphan", lazy="selectin", ) # Indexes __table_args__ = (Index("ix_users_oauth", "oauth_provider", "oauth_id", unique=True),) @property def has_active_premium(self) -> bool: """Check if premium subscription is currently active. Returns: True if user has premium and it hasn't expired. """ if not self.is_premium: return False if self.premium_until is None: return False return datetime.now(self.premium_until.tzinfo) < self.premium_until @property def max_decks(self) -> int: """Get maximum number of decks user can have. Returns: 5 for free users, unlimited (999) for premium. """ return 999 if self.has_active_premium else 5 def __repr__(self) -> str: return f""