""" 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"" @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"" @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"" 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""