pounce/backend/app/models/portfolio.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

128 lines
5.0 KiB
Python

"""Portfolio model for tracking owned domains."""
from datetime import datetime
from typing import Optional
from sqlalchemy import String, DateTime, Float, Integer, Text, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class PortfolioDomain(Base):
"""
Portfolio Domain model for tracking domains that users own.
Allows users to track their domain investments, values, and ROI.
"""
__tablename__ = "portfolio_domains"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True, nullable=False)
# Domain info
domain: Mapped[str] = mapped_column(String(255), nullable=False)
# Purchase info
purchase_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
purchase_price: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
purchase_registrar: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
# Current status
registrar: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
renewal_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
renewal_cost: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
auto_renew: Mapped[bool] = mapped_column(Boolean, default=True)
# Valuation
estimated_value: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
value_updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
# Sale info (if sold)
is_sold: Mapped[bool] = mapped_column(Boolean, default=False)
sale_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
sale_price: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
# Status
status: Mapped[str] = mapped_column(String(50), default="active") # active, expired, sold, parked
# Notes
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
tags: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # Comma-separated
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user: Mapped["User"] = relationship("User", back_populates="portfolio_domains")
valuations: Mapped[list["DomainValuation"]] = relationship(
"DomainValuation", back_populates="portfolio_domain", cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<PortfolioDomain {self.domain} (user={self.user_id})>"
@property
def roi(self) -> Optional[float]:
"""Calculate ROI percentage."""
if not self.purchase_price or self.purchase_price == 0:
return None
if self.is_sold and self.sale_price:
return ((self.sale_price - self.purchase_price) / self.purchase_price) * 100
elif self.estimated_value:
return ((self.estimated_value - self.purchase_price) / self.purchase_price) * 100
return None
@property
def total_cost(self) -> float:
"""Calculate total cost including renewals."""
cost = self.purchase_price or 0
# Add renewal costs if we had them tracked
return cost
class DomainValuation(Base):
"""
Domain valuation history.
Stores historical valuations for domains to track value changes over time.
"""
__tablename__ = "domain_valuations"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
domain: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
portfolio_domain_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("portfolio_domains.id"), nullable=True
)
# Valuation breakdown
estimated_value: Mapped[float] = mapped_column(Float, nullable=False)
# Factors
length_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
tld_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
keyword_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
brandability_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100
# SEO metrics
moz_da: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
moz_pa: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
backlinks: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
# Source
source: Mapped[str] = mapped_column(String(50), default="internal") # internal, estibot, godaddy
# Timestamp
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationship
portfolio_domain: Mapped[Optional["PortfolioDomain"]] = relationship(
"PortfolioDomain", back_populates="valuations"
)
def __repr__(self) -> str:
return f"<DomainValuation {self.domain}: ${self.estimated_value}>"