pounce/backend/app/models/portfolio.py
yves.gugger 6e84103a5b feat: Complete business model expansion
MAJOR FEATURES:
- New pricing tiers: Scout (Free), Trader (€19/mo), Tycoon (€49/mo)
- Portfolio management: Track owned domains with purchase price, value, ROI
- Domain valuation engine: Algorithmic estimates based on length, TLD, keywords, brandability
- Dashboard tabs: Watchlist + Portfolio views
- Valuation modal: Score breakdown with confidence level

BACKEND:
- New models: PortfolioDomain, DomainValuation
- New API routes: /portfolio/* with full CRUD
- Valuation service with multi-factor algorithm
- Database migration for portfolio tables

FRONTEND:
- Updated pricing page with comparison table and billing toggle
- Dashboard with Watchlist/Portfolio tabs
- Portfolio summary stats: Total value, invested, unrealized P/L, ROI
- Add portfolio domain modal with all fields
- Domain valuation modal with score visualization
- Updated landing page with new tier pricing
- Hero section with large puma logo

DESIGN:
- Consistent minimalist dark theme
- Responsive on all devices
- Professional animations and transitions
2025-12-08 11:08:18 +01:00

114 lines
4.5 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)
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)
# 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)
def __repr__(self) -> str:
return f"<DomainValuation {self.domain}: ${self.estimated_value}>"