import uuid from datetime import datetime, timezone from typing import Optional, TYPE_CHECKING from sqlalchemy import String, DateTime, ForeignKey, Boolean, Integer from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base if TYPE_CHECKING: from app.models.account import Account class Subscription(Base): __tablename__ = "subscriptions" id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) account_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), unique=True, nullable=False) stripe_subscription_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) stripe_price_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) plan: Mapped[str] = mapped_column(String(50), nullable=False, default="free") billing_interval: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) status: Mapped[str] = mapped_column(String(50), nullable=False, default="active") seat_limit: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) current_period_start: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) current_period_end: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) cancel_at_period_end: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) # Relationships account: Mapped["Account"] = relationship("Account", back_populates="subscription") @property def is_active(self) -> bool: return self.status in ("active", "trialing", "complimentary") @property def is_paid(self) -> bool: # Excludes complimentary and trialing so MRR/paid-customer metrics aren't inflated. return self.plan in ("pro", "starter", "enterprise") and self.status not in ("complimentary", "trialing") @property def has_pro_entitlement(self) -> bool: """True if the account can access Pro features right now.""" if self.plan in ("pro", "starter", "enterprise"): if self.status in ("active", "complimentary"): return True if self.status == "trialing" and self.current_period_end is not None: from datetime import datetime, timezone return self.current_period_end > datetime.now(timezone.utc) return False