301 lines
12 KiB
Python
301 lines
12 KiB
Python
# /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())
|