# /opt/docker/dev/service_finder/backend/app/models/marketing.py """ 📱 Ad Engine — Marketing Schema Models Extensible Native Ad Engine for Service Finder. Supports: - Campaign lifecycle (draft → scheduled → active → paused → completed → cancelled) - Internal (IMAGE) and external (HTML_SNIPPET) creatives - Priority-based + weighted random ad selection with fallback - Impression and click tracking for analytics Schema: marketing """ from __future__ import annotations import enum from datetime import datetime from typing import Optional, List from sqlalchemy import ( String, Integer, Boolean, DateTime, Float, Text, ForeignKey, UniqueConstraint, Index, CheckConstraint, text ) from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from app.database import Base # ── Enums (Python-side only; stored as String in DB for portability) ─────────── class CampaignStatus(str, enum.Enum): """KampĂĄny Ă©letciklus ĂĄllapotai.""" DRAFT = "draft" SCHEDULED = "scheduled" ACTIVE = "active" PAUSED = "paused" COMPLETED = "completed" CANCELLED = "cancelled" class CreativeType(str, enum.Enum): """KreatĂ­v tĂ­pusok (bƑvĂ­thetƑ).""" IMAGE = "image" # BelsƑ feltöltĂ©sƱ kĂ©p HTML_SNIPPET = "html_snippet" # KĂŒlsƑ HTML kĂłd (pl. Google Ads) # JövƑbeli: VIDEO, CAROUSEL, TEXT class PlacementType(str, enum.Enum): """HirdetĂ©si helyek a felĂŒleten.""" SIDEBAR = "sidebar" BANNER_TOP = "banner_top" BANNER_BOTTOM = "banner_bottom" MODAL = "modal" INLINE = "inline" DASHBOARD_WIDGET = "dashboard_widget" # ── Models ───────────────────────────────────────────────────────────────────── class Campaign(Base): """ HirdetĂ©si kampĂĄny. Egy kampĂĄny több kreatĂ­vot Ă©s több elhelyezĂ©st (placement) is tartalmazhat a many-to-many kapcsolaton keresztĂŒl. """ __tablename__ = "campaigns" __table_args__ = ( CheckConstraint( "status IN ('draft', 'scheduled', 'active', 'paused', 'completed', 'cancelled')", name="ck_campaign_status" ), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(200), nullable=False, index=True) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # KampĂĄny Ă©letciklus (String instead of native PG enum for portability) status: Mapped[str] = mapped_column( String(20), default=CampaignStatus.DRAFT.value, nullable=False, index=True, ) # CĂ©lzĂĄsi beĂĄllĂ­tĂĄsok (JSONB a rugalmassĂĄgĂ©rt) # Pl. {"target_tier": ["free", "premium"], "target_country": ["HU", "UK"]} targeting: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True, server_default=text("'{}'::jsonb")) # PrioritĂĄs (kisebb szĂĄm = magasabb prioritĂĄs) priority: Mapped[int] = mapped_column(Integer, default=100, nullable=False) # IdƑbeli korlĂĄtok start_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) end_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) # Napi költĂ©si limit (impressziĂłban vagy pĂ©nzben) daily_impression_limit: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) total_impression_limit: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # MetrikĂĄk (denormalizĂĄlt gyors lekĂ©rdezĂ©shez) current_impressions: Mapped[int] = mapped_column(Integer, default=0) current_clicks: Mapped[int] = mapped_column(Integer, default=0) # Metaadatok created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), onupdate=func.now()) created_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Admin user ID # Kapcsolatok creatives: Mapped[List["CampaignCreative"]] = relationship( "CampaignCreative", back_populates="campaign", cascade="all, delete-orphan" ) placements: Mapped[List["CampaignPlacement"]] = relationship( "CampaignPlacement", back_populates="campaign", cascade="all, delete-orphan" ) class Creative(Base): """ KreatĂ­v (hirdetĂ©s anyag). KĂ©t fƑ tĂ­pus: - IMAGE: belsƑ feltöltĂ©sƱ kĂ©p (image_url, alt_text) - HTML_SNIPPET: kĂŒlsƑ HTML kĂłd (pl. Google Ads vagy mĂĄs hirdetĂ©si hĂĄlĂłzat) """ __tablename__ = "creatives" __table_args__ = ( CheckConstraint( "creative_type IN ('image', 'html_snippet')", name="ck_creative_type" ), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(200), nullable=False) # TĂ­pus (String instead of native PG enum for portability) creative_type: Mapped[str] = mapped_column( String(20), nullable=False, ) # IMAGE tĂ­pushoz image_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) alt_text: Mapped[Optional[str]] = mapped_column(String(300), nullable=True) click_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # ÁtkattintĂĄsi link # HTML_SNIPPET tĂ­pushoz html_snippet: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Közös mezƑk width: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) height: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) weight: Mapped[float] = mapped_column(Float, default=1.0) # SĂșly a random kivĂĄlasztĂĄshoz is_active: Mapped[bool] = mapped_column(Boolean, default=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) # Kapcsolatok campaigns: Mapped[List["CampaignCreative"]] = relationship( "CampaignCreative", back_populates="creative", cascade="all, delete-orphan" ) class Placement(Base): """ HirdetĂ©si hely (placement) a felĂŒleten. MeghatĂĄrozza, hogy a hirdetĂ©s hol jelenjen meg a frontenden. """ __tablename__ = "placements" __table_args__ = ( CheckConstraint( "placement_type IN ('sidebar', 'banner_top', 'banner_bottom', 'modal', 'inline', 'dashboard_widget')", name="ck_placement_type" ), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) placement_type: Mapped[str] = mapped_column( String(20), nullable=False, ) description: Mapped[Optional[str]] = mapped_column(String(300), nullable=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True) # Kapcsolatok campaigns: Mapped[List["CampaignPlacement"]] = relationship( "CampaignPlacement", back_populates="placement", cascade="all, delete-orphan" ) class CampaignCreative(Base): """ Many-to-Many kapcsolĂłtĂĄbla: Campaign <-> Creative. LehetƑvĂ© teszi, hogy egy kampĂĄny több kreatĂ­vot hasznĂĄljon, Ă©s egy kreatĂ­v több kampĂĄnyban is szerepelhessen. """ __tablename__ = "campaign_creatives" __table_args__ = ( UniqueConstraint("campaign_id", "creative_id", name="uq_campaign_creative"), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) campaign_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketing.campaigns.id", ondelete="CASCADE"), nullable=False) creative_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketing.creatives.id", ondelete="CASCADE"), nullable=False) # Kapcsolatok campaign: Mapped["Campaign"] = relationship("Campaign", back_populates="creatives") creative: Mapped["Creative"] = relationship("Creative", back_populates="campaigns") class CampaignPlacement(Base): """ Many-to-Many kapcsolĂłtĂĄbla: Campaign <-> Placement. MeghatĂĄrozza, hogy egy kampĂĄny mely hirdetĂ©si helyeken jelenjen meg. """ __tablename__ = "campaign_placements" __table_args__ = ( UniqueConstraint("campaign_id", "placement_id", name="uq_campaign_placement"), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) campaign_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketing.campaigns.id", ondelete="CASCADE"), nullable=False) placement_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketing.placements.id", ondelete="CASCADE"), nullable=False) # Kapcsolatok campaign: Mapped["Campaign"] = relationship("Campaign", back_populates="placements") placement: Mapped["Placement"] = relationship("Placement", back_populates="campaigns") class AdImpression(Base): """ HirdetĂ©s megjelenĂ­tĂ©si naplĂł. Minden egyes alkalommal rögzĂ­ti, amikor egy hirdetĂ©s megjelent a felĂŒleten. Ez az adat szolgĂĄl a kampĂĄnyok analitikĂĄjĂĄhoz. """ __tablename__ = "ad_impressions" __table_args__ = ( Index("idx_impression_campaign", "campaign_id"), Index("idx_impression_created", "created_at"), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) campaign_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketing.campaigns.id", ondelete="CASCADE"), nullable=False) creative_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketing.creatives.id", ondelete="SET NULL"), nullable=True) placement_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketing.placements.id", ondelete="SET NULL"), nullable=True) # Kontextus user_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Bejelentkezett user (ha van) session_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # Anonymous session ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) # IPv4 vagy IPv6 user_agent: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # IdƑbĂ©lyeg created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) class AdClick(Base): """ HirdetĂ©s kattintĂĄsi naplĂł. Minden egyes kattintĂĄst rögzĂ­t, ami egy hirdetĂ©sen törtĂ©nt. """ __tablename__ = "ad_clicks" __table_args__ = ( Index("idx_click_campaign", "campaign_id"), Index("idx_click_created", "created_at"), {"schema": "marketing"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) impression_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("marketing.ad_impressions.id", ondelete="SET NULL"), nullable=True ) campaign_id: Mapped[int] = mapped_column(Integer, ForeignKey("marketing.campaigns.id", ondelete="CASCADE"), nullable=False) creative_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("marketing.creatives.id", ondelete="SET NULL"), nullable=True) user_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) session_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())