pounce/backend/app/models/subscription.py
Yves Gugger bb7ce97330
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Deploy: referral rewards antifraud + legal contact updates
2025-12-15 13:56:43 +01:00

192 lines
6.2 KiB
Python

"""Subscription model."""
from datetime import datetime
from enum import Enum
from typing import Optional
from sqlalchemy import String, DateTime, ForeignKey, Integer, Boolean, Enum as SQLEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class SubscriptionTier(str, Enum):
"""
Subscription tiers for pounce.ch
Scout (Free): 5 domains, daily checks, email alerts
Trader (€19/mo): 50 domains, hourly checks, portfolio, valuation
Tycoon (€49/mo): 500+ domains, 10-min checks, API, bulk tools
"""
SCOUT = "scout" # Free tier
TRADER = "trader" # €19/month
TYCOON = "tycoon" # €49/month
class SubscriptionStatus(str, Enum):
"""Subscription status."""
ACTIVE = "active"
CANCELLED = "cancelled"
EXPIRED = "expired"
PENDING = "pending"
PAST_DUE = "past_due"
# Plan configuration - matches frontend pricing page
TIER_CONFIG = {
SubscriptionTier.SCOUT: {
"name": "Scout",
"price": 0,
"currency": "USD",
"domain_limit": 5,
"portfolio_limit": 0,
"check_frequency": "daily",
"history_days": 0,
"features": {
"email_alerts": True,
"sms_alerts": False,
"priority_alerts": False,
"full_whois": False,
"expiration_tracking": False,
"domain_valuation": False,
"market_insights": False,
"api_access": False,
"webhooks": False,
"bulk_tools": False,
"seo_metrics": False,
}
},
SubscriptionTier.TRADER: {
"name": "Trader",
"price": 9,
"currency": "USD",
"domain_limit": 50,
"portfolio_limit": 25,
"check_frequency": "hourly",
"history_days": 90,
"features": {
"email_alerts": True,
"sms_alerts": True,
"priority_alerts": True,
"full_whois": True,
"expiration_tracking": True,
"domain_valuation": True,
"market_insights": True,
"api_access": False,
"webhooks": False,
"bulk_tools": False,
"seo_metrics": False,
}
},
SubscriptionTier.TYCOON: {
"name": "Tycoon",
"price": 29,
"currency": "USD",
"domain_limit": 500,
"portfolio_limit": -1, # Unlimited
"check_frequency": "realtime", # Every 10 minutes
"history_days": -1, # Unlimited
"features": {
"email_alerts": True,
"sms_alerts": True,
"priority_alerts": True,
"full_whois": True,
"expiration_tracking": True,
"domain_valuation": True,
"market_insights": True,
"api_access": True,
"webhooks": True,
"bulk_tools": True,
"seo_metrics": True,
}
},
}
class Subscription(Base):
"""
Subscription model for tracking user plans.
Integrates with Stripe for payment processing.
"""
__tablename__ = "subscriptions"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True, nullable=False)
# Plan details
tier: Mapped[SubscriptionTier] = mapped_column(
SQLEnum(SubscriptionTier), default=SubscriptionTier.SCOUT
)
status: Mapped[SubscriptionStatus] = mapped_column(
SQLEnum(SubscriptionStatus), default=SubscriptionStatus.ACTIVE
)
# Limits (can be overridden)
max_domains: Mapped[int] = mapped_column(Integer, default=5)
# Referral reward bonus (3C.2): additive, computed deterministically from qualified referrals
referral_bonus_domains: Mapped[int] = mapped_column(Integer, default=0)
check_frequency: Mapped[str] = mapped_column(String(50), default="daily")
# Stripe integration
stripe_subscription_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Legacy payment reference (for migration)
payment_reference: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# Dates
started_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
cancelled_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship
user: Mapped["User"] = relationship("User", back_populates="subscription")
@property
def is_active(self) -> bool:
"""Check if subscription is currently active."""
if self.status not in [SubscriptionStatus.ACTIVE, SubscriptionStatus.PAST_DUE]:
return False
if self.expires_at and self.expires_at < datetime.utcnow():
return False
return True
@property
def config(self) -> dict:
"""Get configuration for this subscription tier."""
return TIER_CONFIG.get(self.tier, TIER_CONFIG[SubscriptionTier.SCOUT])
@property
def tier_name(self) -> str:
"""Get human-readable tier name."""
return self.config["name"]
@property
def price(self) -> float:
"""Get price for this tier."""
return self.config["price"]
@property
def domain_limit(self) -> int:
"""Get maximum allowed domains for this subscription."""
base = int(self.max_domains or self.config["domain_limit"] or 0)
bonus = int(self.referral_bonus_domains or 0)
return max(0, base + bonus)
@property
def portfolio_limit(self) -> int:
"""Get maximum portfolio domains. -1 = unlimited."""
return self.config.get("portfolio_limit", 0)
@property
def history_days(self) -> int:
"""Get history retention days. -1 = unlimited."""
return self.config["history_days"]
def has_feature(self, feature: str) -> bool:
"""Check if subscription has a specific feature."""
return self.config.get("features", {}).get(feature, False)
def __repr__(self) -> str:
return f"<Subscription {self.tier.value} for user {self.user_id}>"