pounce/backend/app/models/yield_domain.py
yves.gugger 76a118ddbf
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
feat: implement Yield/Intent Routing feature (pounce_endgame)
Backend:
- Add YieldDomain, YieldTransaction, YieldPayout, AffiliatePartner models
- Create IntentDetector service for keyword-based intent classification
- Implement /api/v1/yield/* endpoints (dashboard, domains, transactions, partners)
- Support domain activation, DNS verification, and revenue tracking

Frontend:
- Add /terminal/yield page with dashboard and activate wizard
- Add YIELD to sidebar navigation under 'Monetize' section
- Add 4th pillar 'Yield' to landing page 'Beyond Hunting' section
- Extend API client with yield endpoints and types

Features:
- AI-powered intent detection (medical, finance, legal, realestate, etc.)
- Swiss/German geo-targeting with city recognition
- Revenue estimation based on intent category and geo
- DNS verification via nameservers or CNAME
- 70/30 revenue split tracking
2025-12-12 14:39:56 +01:00

250 lines
10 KiB
Python

"""
Yield Domain models for Intent Routing feature.
Domains activated for yield generate passive income by routing
visitor intent to affiliate partners.
"""
from datetime import datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import String, DateTime, Float, Integer, Text, ForeignKey, Boolean, Numeric, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class AffiliatePartner(Base):
"""
Affiliate network/partner configuration.
Partners are matched to domains based on detected intent category.
"""
__tablename__ = "affiliate_partners"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
# Identity
name: Mapped[str] = mapped_column(String(100), nullable=False) # "Comparis Dental"
slug: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) # "comparis_dental"
network: Mapped[str] = mapped_column(String(50), nullable=False) # "awin", "partnerstack", "direct"
# Matching criteria (JSON arrays stored as comma-separated for simplicity)
intent_categories: Mapped[str] = mapped_column(Text, nullable=False) # "medical_dental,medical_general"
geo_countries: Mapped[str] = mapped_column(String(200), default="CH,DE,AT") # ISO codes
# Payout configuration
payout_type: Mapped[str] = mapped_column(String(20), default="cpl") # "cpc", "cpl", "cps"
payout_amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), default=0)
payout_currency: Mapped[str] = mapped_column(String(3), default="CHF")
# Integration
tracking_url_template: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
api_endpoint: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
# Note: API keys should be stored encrypted or in env vars, not here
# Display
logo_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
# Status
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
priority: Mapped[int] = mapped_column(Integer, default=0) # Higher = preferred
# 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
yield_domains: Mapped[list["YieldDomain"]] = relationship("YieldDomain", back_populates="partner")
def __repr__(self) -> str:
return f"<AffiliatePartner {self.slug}>"
@property
def intent_list(self) -> list[str]:
"""Parse intent categories as list."""
return [c.strip() for c in self.intent_categories.split(",") if c.strip()]
@property
def country_list(self) -> list[str]:
"""Parse geo countries as list."""
return [c.strip() for c in self.geo_countries.split(",") if c.strip()]
class YieldDomain(Base):
"""
Domain activated for yield/intent routing.
When a user activates a domain for yield:
1. They point DNS to our nameservers
2. We detect the intent (e.g., "zahnarzt.ch" → medical/dental)
3. We route traffic to affiliate partners
4. User earns commission split
"""
__tablename__ = "yield_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), unique=True, nullable=False, index=True)
# Intent detection
detected_intent: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # "medical_dental"
intent_confidence: Mapped[float] = mapped_column(Float, default=0.0) # 0.0 - 1.0
intent_keywords: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # JSON: ["zahnarzt", "zuerich"]
# Routing
partner_id: Mapped[Optional[int]] = mapped_column(ForeignKey("affiliate_partners.id"), nullable=True)
active_route: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # Partner slug
landing_page_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
# Status
status: Mapped[str] = mapped_column(String(30), default="pending", index=True)
# pending, verifying, active, paused, inactive, error
dns_verified: Mapped[bool] = mapped_column(Boolean, default=False)
dns_verified_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
activated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
paused_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
# Revenue tracking (aggregates, updated periodically)
total_clicks: Mapped[int] = mapped_column(Integer, default=0)
total_conversions: Mapped[int] = mapped_column(Integer, default=0)
total_revenue: Mapped[Decimal] = mapped_column(Numeric(12, 2), default=0)
currency: Mapped[str] = mapped_column(String(3), default="CHF")
# Last activity
last_click_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
last_conversion_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)
# Relationships
user: Mapped["User"] = relationship("User", back_populates="yield_domains")
partner: Mapped[Optional["AffiliatePartner"]] = relationship("AffiliatePartner", back_populates="yield_domains")
transactions: Mapped[list["YieldTransaction"]] = relationship(
"YieldTransaction", back_populates="yield_domain", cascade="all, delete-orphan"
)
# Indexes
__table_args__ = (
Index("ix_yield_domains_user_status", "user_id", "status"),
)
def __repr__(self) -> str:
return f"<YieldDomain {self.domain} ({self.status})>"
@property
def is_earning(self) -> bool:
"""Check if domain is actively earning."""
return self.status == "active" and self.dns_verified
@property
def monthly_revenue(self) -> Decimal:
"""Estimate monthly revenue (placeholder - should compute from transactions)."""
# In production: calculate from last 30 days of transactions
return self.total_revenue
class YieldTransaction(Base):
"""
Revenue events from affiliate partners.
Tracks clicks, leads, and sales for each yield domain.
"""
__tablename__ = "yield_transactions"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
yield_domain_id: Mapped[int] = mapped_column(
ForeignKey("yield_domains.id", ondelete="CASCADE"),
index=True,
nullable=False
)
# Event type
event_type: Mapped[str] = mapped_column(String(20), nullable=False) # "click", "lead", "sale"
# Partner info
partner_slug: Mapped[str] = mapped_column(String(50), nullable=False)
partner_transaction_id: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
# Amount
gross_amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), default=0) # Full commission
net_amount: Mapped[Decimal] = mapped_column(Numeric(10, 2), default=0) # After Pounce cut (70%)
currency: Mapped[str] = mapped_column(String(3), default="CHF")
# Attribution
referrer: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
user_agent: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
geo_country: Mapped[Optional[str]] = mapped_column(String(2), nullable=True)
ip_hash: Mapped[Optional[str]] = mapped_column(String(64), nullable=True) # Hashed for privacy
# Status
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
# pending, confirmed, paid, rejected
confirmed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
paid_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
payout_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # FK to future payouts table
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
# Relationships
yield_domain: Mapped["YieldDomain"] = relationship("YieldDomain", back_populates="transactions")
# Indexes
__table_args__ = (
Index("ix_yield_tx_domain_created", "yield_domain_id", "created_at"),
Index("ix_yield_tx_status_created", "status", "created_at"),
)
def __repr__(self) -> str:
return f"<YieldTransaction {self.event_type} {self.net_amount} {self.currency}>"
class YieldPayout(Base):
"""
Payout records for user earnings.
Aggregates confirmed transactions into periodic payouts.
"""
__tablename__ = "yield_payouts"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True, nullable=False)
# Amount
amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="CHF")
# Period
period_start: Mapped[datetime] = mapped_column(DateTime, nullable=False)
period_end: Mapped[datetime] = mapped_column(DateTime, nullable=False)
# Transaction count
transaction_count: Mapped[int] = mapped_column(Integer, default=0)
# Status
status: Mapped[str] = mapped_column(String(20), default="pending", index=True)
# pending, processing, completed, failed
# Payment details
payment_method: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) # "stripe", "bank"
payment_reference: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
processed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
# Relationship
user: Mapped["User"] = relationship("User", back_populates="yield_payouts")
def __repr__(self) -> str:
return f"<YieldPayout {self.amount} {self.currency} ({self.status})>"