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
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
250 lines
10 KiB
Python
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})>"
|
|
|