""" Yield Domain Routing API. This handles incoming HTTP requests to yield domains: 1. Detect the domain from the Host header 2. Look up the yield configuration 3. Track the click 4. Redirect to the appropriate affiliate landing page In production, this runs on a separate subdomain or IP (yield.pounce.io) that yield domains CNAME to. """ import logging from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, Request, Response, HTTPException from fastapi.responses import RedirectResponse, HTMLResponse from sqlalchemy.orm import Session from app.api.deps import get_db from app.config import get_settings from app.models.yield_domain import YieldDomain, YieldTransaction, AffiliatePartner from app.services.intent_detector import detect_domain_intent logger = logging.getLogger(__name__) settings = get_settings() router = APIRouter(prefix="/r", tags=["yield-routing"]) # Revenue split USER_REVENUE_SHARE = 0.70 def hash_ip(ip: str) -> str: """Hash IP for privacy-compliant storage.""" import hashlib return hashlib.sha256(ip.encode()).hexdigest()[:32] def generate_tracking_url( partner: AffiliatePartner, yield_domain: YieldDomain, click_id: int, ) -> str: """ Generate the tracking URL for a partner. Most affiliate networks expect parameters like: - clickid / subid: Our click tracking ID - ref: Domain name or user reference """ # If partner has a tracking URL template, use it if partner.tracking_url_template: return partner.tracking_url_template.format( click_id=click_id, domain=yield_domain.domain, domain_id=yield_domain.id, partner=partner.slug, ) # Default fallbacks by network network_urls = { "comparis_dental": f"https://www.comparis.ch/zahnarzt?subid={click_id}&ref={yield_domain.domain}", "comparis_health": f"https://www.comparis.ch/krankenkassen?subid={click_id}&ref={yield_domain.domain}", "comparis_insurance": f"https://www.comparis.ch/versicherungen?subid={click_id}&ref={yield_domain.domain}", "comparis_hypo": f"https://www.comparis.ch/hypotheken?subid={click_id}&ref={yield_domain.domain}", "comparis_auto": f"https://www.comparis.ch/autoversicherung?subid={click_id}&ref={yield_domain.domain}", "comparis_immo": f"https://www.comparis.ch/immobilien?subid={click_id}&ref={yield_domain.domain}", "homegate": f"https://www.homegate.ch/?ref=pounce&clickid={click_id}", "immoscout": f"https://www.immoscout24.ch/?ref=pounce&clickid={click_id}", "autoscout": f"https://www.autoscout24.ch/?ref=pounce&clickid={click_id}", "jobs_ch": f"https://www.jobs.ch/?ref=pounce&clickid={click_id}", "booking_com": f"https://www.booking.com/?aid=pounce&clickid={click_id}", "hostpoint": f"https://www.hostpoint.ch/?ref=pounce&clickid={click_id}", "infomaniak": f"https://www.infomaniak.com/?ref=pounce&clickid={click_id}", "galaxus": f"https://www.galaxus.ch/?ref=pounce&clickid={click_id}", "zalando": f"https://www.zalando.ch/?ref=pounce&clickid={click_id}", } if partner.slug in network_urls: return network_urls[partner.slug] # Generic fallback - show Pounce marketplace return f"{settings.site_url}/buy?ref={yield_domain.domain}&clickid={click_id}" def generate_landing_page( yield_domain: YieldDomain, partner: Optional[AffiliatePartner], click_id: int, ) -> str: """ Generate an interstitial landing page. Shows for a moment before redirecting, to: 1. Improve user experience 2. Allow for A/B testing 3. Comply with affiliate disclosure requirements """ intent = detect_domain_intent(yield_domain.domain) # Partner info partner_name = partner.name if partner else "Partner" partner_desc = partner.description if partner else "Find the best offers" # Generate redirect URL redirect_url = ( generate_tracking_url(partner, yield_domain, click_id) if partner else f"{settings.site_url}/buy" ) return f""" {yield_domain.domain} - Redirecting
{yield_domain.domain}
{intent.category.replace('_', ' ')}
Redirecting to {partner_name}...
{partner_desc}

Click here if not redirected

""" @router.get("/{domain}") async def route_yield_domain( domain: str, request: Request, db: Session = Depends(get_db), direct: bool = False, # Skip landing page if true ): """ Route traffic for a yield domain. This is the main entry point for yield domain traffic. Query params: - direct: If true, redirect immediately without landing page """ domain = domain.lower().strip() # Find yield domain yield_domain = db.query(YieldDomain).filter( YieldDomain.domain == domain, YieldDomain.status == "active", ).first() if not yield_domain: # Domain not found or not active - show error page logger.warning(f"Route request for unknown/inactive domain: {domain}") return HTMLResponse( content=f""" Domain Not Active

Domain Not Active

The domain {domain} is not currently active for yield routing.

Visit Pounce

""", status_code=404 ) # Get partner partner = None if yield_domain.partner_id: partner = db.query(AffiliatePartner).filter( AffiliatePartner.id == yield_domain.partner_id, AffiliatePartner.is_active == True, ).first() # If no partner assigned, try to find one based on intent if not partner and yield_domain.detected_intent: partner = db.query(AffiliatePartner).filter( AffiliatePartner.intent_categories.contains(yield_domain.detected_intent.split('_')[0]), AffiliatePartner.is_active == True, ).order_by(AffiliatePartner.priority.desc()).first() # Create click transaction client_ip = request.client.host if request.client else None user_agent = request.headers.get("user-agent") referrer = request.headers.get("referer") transaction = YieldTransaction( yield_domain_id=yield_domain.id, event_type="click", partner_slug=partner.slug if partner else "unknown", gross_amount=0, net_amount=0, currency="CHF", referrer=referrer, user_agent=user_agent[:500] if user_agent else None, ip_hash=hash_ip(client_ip) if client_ip else None, status="confirmed", confirmed_at=datetime.utcnow(), ) db.add(transaction) # Update domain stats yield_domain.total_clicks += 1 yield_domain.last_click_at = datetime.utcnow() db.commit() db.refresh(transaction) # Generate redirect URL redirect_url = ( generate_tracking_url(partner, yield_domain, transaction.id) if partner else f"{settings.site_url}/buy?ref={domain}" ) # Direct redirect or show landing page if direct: return RedirectResponse(url=redirect_url, status_code=302) # Show interstitial landing page html = generate_landing_page(yield_domain, partner, transaction.id) return HTMLResponse(content=html) @router.get("/") async def yield_routing_info(): """Info endpoint for yield routing service.""" return { "service": "Pounce Yield Routing", "version": "1.0.0", "docs": f"{settings.site_url}/docs#/yield-routing", "status": "active", } # ============================================================================ # Host-based routing (for production deployment) # ============================================================================ @router.api_route("/catch-all", methods=["GET", "HEAD"]) async def catch_all_route( request: Request, db: Session = Depends(get_db), ): """ Catch-all route for host-based routing. In production, this endpoint handles requests where the Host header is the yield domain itself (e.g., zahnarzt-zuerich.ch). This requires: 1. Yield domains to CNAME to yield.pounce.io 2. Nginx/Caddy to route all hosts to this backend 3. This endpoint to parse the Host header """ host = request.headers.get("host", "").lower() # Remove port if present if ":" in host: host = host.split(":")[0] # Skip our own domains our_domains = ["pounce.ch", "pounce.io", "localhost", "127.0.0.1"] if any(host.endswith(d) for d in our_domains): return {"status": "not a yield domain", "host": host} # Look up yield domain yield_domain = db.query(YieldDomain).filter( YieldDomain.domain == host, YieldDomain.status == "active", ).first() if not yield_domain: return HTMLResponse( content=f"

Domain {host} not configured

", status_code=404 ) # Redirect to routing endpoint return RedirectResponse( url=f"/api/v1/r/{host}", status_code=302 )