pounce/backend/app/models/subscription.py
yves.gugger 88eca582e5 feat: Remove ALL mock data - real scraped data only
MOCK DATA REMOVED:
- Removed ALL hardcoded auction data from auctions.py
- Now uses real-time scraping from ExpiredDomains.net
- Database stores scraped auctions (domain_auctions table)
- Scraping runs hourly via scheduler (:30 each hour)

AUCTION SCRAPER SERVICE:
- Web scraping from ExpiredDomains.net (aggregator)
- Rate limiting per platform (10 req/min)
- Database caching to minimize requests
- Cleanup of ended auctions (auto-deactivate)
- Scrape logging for monitoring

STRIPE INTEGRATION:
- Full payment flow: Checkout → Webhook → Subscription update
- Customer Portal for managing subscriptions
- Price IDs configurable via env vars
- Handles: checkout.completed, subscription.updated/deleted, payment.failed

EMAIL SERVICE (SMTP):
- Beautiful HTML email templates with pounce branding
- Domain available alerts
- Price change notifications
- Subscription confirmations
- Weekly digest emails
- Configurable via SMTP_* env vars

NEW SUBSCRIPTION TIERS:
- Scout (Free): 5 domains, daily checks
- Trader (€19/mo): 50 domains, hourly, portfolio, valuation
- Tycoon (€49/mo): 500+ domains, realtime, API, bulk tools

DATABASE CHANGES:
- domain_auctions table for scraped data
- auction_scrape_logs for monitoring
- stripe_customer_id on users
- stripe_subscription_id on subscriptions
- portfolio_domain relationships fixed

ENV VARS ADDED:
- STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
- STRIPE_PRICE_TRADER, STRIPE_PRICE_TYCOON
- SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD
- SMTP_FROM_EMAIL, SMTP_FROM_NAME
2025-12-08 14:08:52 +01:00

188 lines
6.0 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": "EUR",
"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": 19,
"currency": "EUR",
"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": 49,
"currency": "EUR",
"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)
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."""
return self.max_domains or self.config["domain_limit"]
@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}>"