""" Domain Listing models for "Pounce For Sale" feature. This implements the "Micro-Marktplatz" strategy from analysis_3.md: - Users can create professional landing pages for domains they want to sell - Buyers can contact sellers through Pounce - DNS verification ensures only real owners can list domains DATABASE TABLES TO CREATE: 1. domain_listings - Main listing table 2. listing_inquiries - Contact requests from potential buyers 3. listing_views - Track views for analytics Run migrations: alembic upgrade head """ from datetime import datetime from typing import Optional, List from sqlalchemy import String, DateTime, Float, Integer, Text, ForeignKey, Boolean, Enum as SQLEnum from sqlalchemy.orm import Mapped, mapped_column, relationship import enum from app.database import Base class ListingStatus(str, enum.Enum): """Status of a domain listing.""" DRAFT = "draft" # Not yet published PENDING_VERIFICATION = "pending_verification" # Awaiting DNS verification ACTIVE = "active" # Live and visible SOLD = "sold" # Marked as sold EXPIRED = "expired" # Listing expired SUSPENDED = "suspended" # Suspended by admin class VerificationStatus(str, enum.Enum): """DNS verification status.""" NOT_STARTED = "not_started" PENDING = "pending" VERIFIED = "verified" FAILED = "failed" class DomainListing(Base): """ Domain listing for the Pounce marketplace. Users can list their domains for sale with a professional landing page. URL: pounce.ch/buy/{slug} Features: - DNS verification for ownership proof - Professional landing page with valuation - Contact form for buyers - Analytics (views, inquiries) From analysis_3.md: "Ein User (Trader/Tycoon) kann für seine Domains mit einem Klick eine schicke Verkaufsseite erstellen." """ __tablename__ = "domain_listings" 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), unique=True, nullable=False, index=True) slug: Mapped[str] = mapped_column(String(300), unique=True, nullable=False, index=True) # Listing details title: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) # Custom headline description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Pricing asking_price: Mapped[Optional[float]] = mapped_column(Float, nullable=True) min_offer: Mapped[Optional[float]] = mapped_column(Float, nullable=True) currency: Mapped[str] = mapped_column(String(3), default="USD") price_type: Mapped[str] = mapped_column(String(20), default="fixed") # fixed, negotiable, make_offer # Pounce valuation (calculated) pounce_score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # 0-100 estimated_value: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # Verification (from analysis_3.md - Säule 2: Asset Verification) verification_status: Mapped[str] = mapped_column( String(20), default=VerificationStatus.NOT_STARTED.value ) verification_code: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) verified_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # Status status: Mapped[str] = mapped_column(String(30), default=ListingStatus.DRAFT.value, index=True) # Features show_valuation: Mapped[bool] = mapped_column(Boolean, default=True) allow_offers: Mapped[bool] = mapped_column(Boolean, default=True) featured: Mapped[bool] = mapped_column(Boolean, default=False) # Premium placement # Analytics view_count: Mapped[int] = mapped_column(Integer, default=0) inquiry_count: Mapped[int] = mapped_column(Integer, default=0) # Expiry expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) published_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # Relationships user: Mapped["User"] = relationship("User", back_populates="listings") inquiries: Mapped[List["ListingInquiry"]] = relationship( "ListingInquiry", back_populates="listing", cascade="all, delete-orphan" ) def __repr__(self) -> str: return f"" @property def is_verified(self) -> bool: return self.verification_status == VerificationStatus.VERIFIED.value @property def is_active(self) -> bool: return self.status == ListingStatus.ACTIVE.value @property def public_url(self) -> str: return f"/buy/{self.slug}" class ListingInquiry(Base): """ Contact request from a potential buyer. From analysis_3.md: "Ein einfaches Kontaktformular, das die Anfrage direkt an den User leitet." Security (from analysis_3.md - Säule 3): - Keyword blocking for phishing prevention - Rate limiting per IP/user """ __tablename__ = "listing_inquiries" id: Mapped[int] = mapped_column(primary_key=True, index=True) listing_id: Mapped[int] = mapped_column(ForeignKey("domain_listings.id"), index=True, nullable=False) # Inquirer info name: Mapped[str] = mapped_column(String(100), nullable=False) email: Mapped[str] = mapped_column(String(255), nullable=False) phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) company: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) # Message message: Mapped[str] = mapped_column(Text, nullable=False) offer_amount: Mapped[Optional[float]] = mapped_column(Float, nullable=True) # Status status: Mapped[str] = mapped_column(String(20), default="new") # new, read, replied, spam # Tracking ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) user_agent: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # Timestamps created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) read_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) replied_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) # Relationships listing: Mapped["DomainListing"] = relationship("DomainListing", back_populates="inquiries") def __repr__(self) -> str: return f"" class ListingView(Base): """ Track listing page views for analytics. """ __tablename__ = "listing_views" id: Mapped[int] = mapped_column(primary_key=True, index=True) listing_id: Mapped[int] = mapped_column(ForeignKey("domain_listings.id"), index=True, nullable=False) # Visitor info ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) user_agent: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) referrer: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # User (if logged in) user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), nullable=True) # Timestamp viewed_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) def __repr__(self) -> str: return f""