BACKEND - New Models:
- DomainListing: For sale landing pages with DNS verification
- ListingInquiry: Contact form submissions from buyers
- ListingView: Analytics tracking
- SniperAlert: Hyper-personalized auction filters
- SniperAlertMatch: Matched auctions for alerts
BACKEND - New APIs:
- /listings: Browse, create, manage domain listings
- /listings/{slug}/inquire: Buyer contact form
- /listings/{id}/verify-dns: DNS ownership verification
- /sniper-alerts: Create, manage, test alert filters
FRONTEND - New Pages:
- /buy: Public marketplace browse page
- /buy/[slug]: Individual listing page with contact form
- /command/listings: Manage your listings
- /command/alerts: Sniper alerts dashboard
FRONTEND - Updated:
- Sidebar: Added For Sale + Sniper Alerts nav items
- Landing page: New features teaser section
DOCS:
- DATABASE_MIGRATIONS.md: Complete SQL for new tables
From analysis_3.md:
- Strategie 2: Micro-Marktplatz (For Sale Pages)
- Strategie 4: Alerts nach Maß (Sniper Alerts)
- Säule 2: DNS Ownership Verification
204 lines
7.6 KiB
Python
204 lines
7.6 KiB
Python
"""
|
|
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"<DomainListing {self.domain} ({self.status})>"
|
|
|
|
@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"<ListingInquiry from {self.email} for listing #{self.listing_id}>"
|
|
|
|
|
|
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"<ListingView #{self.listing_id} at {self.viewed_at}>"
|
|
|