""" 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"""
The domain {domain} is not currently active for yield routing.
""", 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"