pounce/backend/app/models/portfolio.py
Yves Gugger 31b02e6790
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
fix: Conservative yield calculator, real TLD data on discover, fix acquire/pricing
2025-12-13 18:04:09 +01:00

136 lines
5.6 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
# DNS Verification (required for Yield and For Sale)
# All fields nullable=True to avoid migration issues on existing databases
is_dns_verified: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, nullable=True)
verification_status: Mapped[Optional[str]] = mapped_column(String(50), default="unverified", nullable=True)
verification_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
verification_started_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
verified_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
# 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}>"