"""Portfolio API routes.""" from datetime import datetime from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, status, Query from pydantic import BaseModel, Field from sqlalchemy import select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.api.auth import get_current_user from app.models.user import User from app.models.portfolio import PortfolioDomain, DomainValuation from app.services.valuation import valuation_service router = APIRouter() # ============== Schemas ============== class PortfolioDomainCreate(BaseModel): """Schema for creating a portfolio domain.""" domain: str = Field(..., min_length=3, max_length=255) purchase_date: Optional[datetime] = None purchase_price: Optional[float] = Field(None, ge=0) purchase_registrar: Optional[str] = None registrar: Optional[str] = None renewal_date: Optional[datetime] = None renewal_cost: Optional[float] = Field(None, ge=0) auto_renew: bool = True notes: Optional[str] = None tags: Optional[str] = None class PortfolioDomainUpdate(BaseModel): """Schema for updating a portfolio domain.""" purchase_date: Optional[datetime] = None purchase_price: Optional[float] = Field(None, ge=0) purchase_registrar: Optional[str] = None registrar: Optional[str] = None renewal_date: Optional[datetime] = None renewal_cost: Optional[float] = Field(None, ge=0) auto_renew: Optional[bool] = None status: Optional[str] = None notes: Optional[str] = None tags: Optional[str] = None class PortfolioDomainSell(BaseModel): """Schema for marking a domain as sold.""" sale_date: datetime sale_price: float = Field(..., ge=0) class PortfolioDomainResponse(BaseModel): """Response schema for portfolio domain.""" id: int domain: str purchase_date: Optional[datetime] purchase_price: Optional[float] purchase_registrar: Optional[str] registrar: Optional[str] renewal_date: Optional[datetime] renewal_cost: Optional[float] auto_renew: bool estimated_value: Optional[float] value_updated_at: Optional[datetime] is_sold: bool sale_date: Optional[datetime] sale_price: Optional[float] status: str notes: Optional[str] tags: Optional[str] roi: Optional[float] created_at: datetime updated_at: datetime class Config: from_attributes = True class PortfolioSummary(BaseModel): """Summary of user's portfolio.""" total_domains: int active_domains: int sold_domains: int total_invested: float total_value: float total_sold_value: float unrealized_profit: float realized_profit: float overall_roi: float class ValuationResponse(BaseModel): """Response schema for domain valuation.""" domain: str estimated_value: float currency: str scores: dict factors: dict confidence: str source: str calculated_at: str # ============== Portfolio Endpoints ============== @router.get("", response_model=List[PortfolioDomainResponse]) async def get_portfolio( status: Optional[str] = Query(None, description="Filter by status"), sort_by: str = Query("created_at", description="Sort field"), sort_order: str = Query("desc", description="Sort order (asc/desc)"), limit: int = Query(100, le=500), offset: int = Query(0, ge=0), current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get user's portfolio domains.""" query = select(PortfolioDomain).where(PortfolioDomain.user_id == current_user.id) # Filter by status if status: query = query.where(PortfolioDomain.status == status) # Sorting sort_column = getattr(PortfolioDomain, sort_by, PortfolioDomain.created_at) if sort_order == "asc": query = query.order_by(sort_column.asc()) else: query = query.order_by(sort_column.desc()) # Pagination query = query.offset(offset).limit(limit) result = await db.execute(query) domains = result.scalars().all() # Calculate ROI for each domain responses = [] for d in domains: response = PortfolioDomainResponse( id=d.id, domain=d.domain, purchase_date=d.purchase_date, purchase_price=d.purchase_price, purchase_registrar=d.purchase_registrar, registrar=d.registrar, renewal_date=d.renewal_date, renewal_cost=d.renewal_cost, auto_renew=d.auto_renew, estimated_value=d.estimated_value, value_updated_at=d.value_updated_at, is_sold=d.is_sold, sale_date=d.sale_date, sale_price=d.sale_price, status=d.status, notes=d.notes, tags=d.tags, roi=d.roi, created_at=d.created_at, updated_at=d.updated_at, ) responses.append(response) return responses @router.get("/summary", response_model=PortfolioSummary) async def get_portfolio_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get portfolio summary statistics.""" result = await db.execute( select(PortfolioDomain).where(PortfolioDomain.user_id == current_user.id) ) domains = result.scalars().all() total_domains = len(domains) active_domains = sum(1 for d in domains if d.status == "active" and not d.is_sold) sold_domains = sum(1 for d in domains if d.is_sold) total_invested = sum(d.purchase_price or 0 for d in domains) total_value = sum(d.estimated_value or 0 for d in domains if not d.is_sold) total_sold_value = sum(d.sale_price or 0 for d in domains if d.is_sold) # Calculate active investment for ROI active_investment = sum(d.purchase_price or 0 for d in domains if not d.is_sold) sold_investment = sum(d.purchase_price or 0 for d in domains if d.is_sold) unrealized_profit = total_value - active_investment realized_profit = total_sold_value - sold_investment overall_roi = 0.0 if total_invested > 0: overall_roi = ((total_value + total_sold_value - total_invested) / total_invested) * 100 return PortfolioSummary( total_domains=total_domains, active_domains=active_domains, sold_domains=sold_domains, total_invested=round(total_invested, 2), total_value=round(total_value, 2), total_sold_value=round(total_sold_value, 2), unrealized_profit=round(unrealized_profit, 2), realized_profit=round(realized_profit, 2), overall_roi=round(overall_roi, 2), ) @router.post("", response_model=PortfolioDomainResponse, status_code=status.HTTP_201_CREATED) async def add_portfolio_domain( data: PortfolioDomainCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Add a domain to portfolio.""" # Check if domain already exists in user's portfolio existing = await db.execute( select(PortfolioDomain).where( and_( PortfolioDomain.user_id == current_user.id, PortfolioDomain.domain == data.domain.lower(), ) ) ) if existing.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Domain already in portfolio", ) # Get initial valuation valuation = await valuation_service.estimate_value(data.domain, db, save_result=True) estimated_value = valuation.get("estimated_value") if "error" not in valuation else None # Create portfolio entry domain = PortfolioDomain( user_id=current_user.id, domain=data.domain.lower(), purchase_date=data.purchase_date, purchase_price=data.purchase_price, purchase_registrar=data.purchase_registrar, registrar=data.registrar or data.purchase_registrar, renewal_date=data.renewal_date, renewal_cost=data.renewal_cost, auto_renew=data.auto_renew, estimated_value=estimated_value, value_updated_at=datetime.utcnow() if estimated_value else None, notes=data.notes, tags=data.tags, ) db.add(domain) await db.commit() await db.refresh(domain) return PortfolioDomainResponse( id=domain.id, domain=domain.domain, purchase_date=domain.purchase_date, purchase_price=domain.purchase_price, purchase_registrar=domain.purchase_registrar, registrar=domain.registrar, renewal_date=domain.renewal_date, renewal_cost=domain.renewal_cost, auto_renew=domain.auto_renew, estimated_value=domain.estimated_value, value_updated_at=domain.value_updated_at, is_sold=domain.is_sold, sale_date=domain.sale_date, sale_price=domain.sale_price, status=domain.status, notes=domain.notes, tags=domain.tags, roi=domain.roi, created_at=domain.created_at, updated_at=domain.updated_at, ) @router.get("/{domain_id}", response_model=PortfolioDomainResponse) async def get_portfolio_domain( domain_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get a specific portfolio domain.""" result = await db.execute( select(PortfolioDomain).where( and_( PortfolioDomain.id == domain_id, PortfolioDomain.user_id == current_user.id, ) ) ) domain = result.scalar_one_or_none() if not domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Domain not found in portfolio", ) return PortfolioDomainResponse( id=domain.id, domain=domain.domain, purchase_date=domain.purchase_date, purchase_price=domain.purchase_price, purchase_registrar=domain.purchase_registrar, registrar=domain.registrar, renewal_date=domain.renewal_date, renewal_cost=domain.renewal_cost, auto_renew=domain.auto_renew, estimated_value=domain.estimated_value, value_updated_at=domain.value_updated_at, is_sold=domain.is_sold, sale_date=domain.sale_date, sale_price=domain.sale_price, status=domain.status, notes=domain.notes, tags=domain.tags, roi=domain.roi, created_at=domain.created_at, updated_at=domain.updated_at, ) @router.put("/{domain_id}", response_model=PortfolioDomainResponse) async def update_portfolio_domain( domain_id: int, data: PortfolioDomainUpdate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Update a portfolio domain.""" result = await db.execute( select(PortfolioDomain).where( and_( PortfolioDomain.id == domain_id, PortfolioDomain.user_id == current_user.id, ) ) ) domain = result.scalar_one_or_none() if not domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Domain not found in portfolio", ) # Update fields update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(domain, field, value) await db.commit() await db.refresh(domain) return PortfolioDomainResponse( id=domain.id, domain=domain.domain, purchase_date=domain.purchase_date, purchase_price=domain.purchase_price, purchase_registrar=domain.purchase_registrar, registrar=domain.registrar, renewal_date=domain.renewal_date, renewal_cost=domain.renewal_cost, auto_renew=domain.auto_renew, estimated_value=domain.estimated_value, value_updated_at=domain.value_updated_at, is_sold=domain.is_sold, sale_date=domain.sale_date, sale_price=domain.sale_price, status=domain.status, notes=domain.notes, tags=domain.tags, roi=domain.roi, created_at=domain.created_at, updated_at=domain.updated_at, ) @router.post("/{domain_id}/sell", response_model=PortfolioDomainResponse) async def mark_domain_sold( domain_id: int, data: PortfolioDomainSell, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Mark a domain as sold.""" result = await db.execute( select(PortfolioDomain).where( and_( PortfolioDomain.id == domain_id, PortfolioDomain.user_id == current_user.id, ) ) ) domain = result.scalar_one_or_none() if not domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Domain not found in portfolio", ) domain.is_sold = True domain.sale_date = data.sale_date domain.sale_price = data.sale_price domain.status = "sold" await db.commit() await db.refresh(domain) return PortfolioDomainResponse( id=domain.id, domain=domain.domain, purchase_date=domain.purchase_date, purchase_price=domain.purchase_price, purchase_registrar=domain.purchase_registrar, registrar=domain.registrar, renewal_date=domain.renewal_date, renewal_cost=domain.renewal_cost, auto_renew=domain.auto_renew, estimated_value=domain.estimated_value, value_updated_at=domain.value_updated_at, is_sold=domain.is_sold, sale_date=domain.sale_date, sale_price=domain.sale_price, status=domain.status, notes=domain.notes, tags=domain.tags, roi=domain.roi, created_at=domain.created_at, updated_at=domain.updated_at, ) @router.delete("/{domain_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_portfolio_domain( domain_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Delete a domain from portfolio.""" result = await db.execute( select(PortfolioDomain).where( and_( PortfolioDomain.id == domain_id, PortfolioDomain.user_id == current_user.id, ) ) ) domain = result.scalar_one_or_none() if not domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Domain not found in portfolio", ) await db.delete(domain) await db.commit() @router.post("/{domain_id}/refresh-value", response_model=PortfolioDomainResponse) async def refresh_domain_value( domain_id: int, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Refresh the estimated value of a portfolio domain.""" result = await db.execute( select(PortfolioDomain).where( and_( PortfolioDomain.id == domain_id, PortfolioDomain.user_id == current_user.id, ) ) ) domain = result.scalar_one_or_none() if not domain: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Domain not found in portfolio", ) # Get new valuation valuation = await valuation_service.estimate_value(domain.domain, db, save_result=True) if "error" not in valuation: domain.estimated_value = valuation["estimated_value"] domain.value_updated_at = datetime.utcnow() await db.commit() await db.refresh(domain) return PortfolioDomainResponse( id=domain.id, domain=domain.domain, purchase_date=domain.purchase_date, purchase_price=domain.purchase_price, purchase_registrar=domain.purchase_registrar, registrar=domain.registrar, renewal_date=domain.renewal_date, renewal_cost=domain.renewal_cost, auto_renew=domain.auto_renew, estimated_value=domain.estimated_value, value_updated_at=domain.value_updated_at, is_sold=domain.is_sold, sale_date=domain.sale_date, sale_price=domain.sale_price, status=domain.status, notes=domain.notes, tags=domain.tags, roi=domain.roi, created_at=domain.created_at, updated_at=domain.updated_at, ) # ============== Valuation Endpoints ============== @router.get("/valuation/{domain}", response_model=ValuationResponse) async def get_domain_valuation( domain: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get estimated value for any domain.""" valuation = await valuation_service.estimate_value(domain, db, save_result=True) if "error" in valuation: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=valuation["error"], ) return ValuationResponse(**valuation)