RABAC felépítése megtörtént ill hirdetési portál alapok lerakva
This commit is contained in:
@@ -50,6 +50,13 @@ from .identity.security import PendingAction, ActionStatus
|
||||
from .system.legal import LegalDocument, LegalAcceptance
|
||||
from .marketplace.logistics import Location, LocationType
|
||||
|
||||
# 10. Marketing / Ad Engine models
|
||||
from .marketing import (
|
||||
Campaign, Creative, Placement, CampaignCreative,
|
||||
CampaignPlacement, AdImpression, AdClick,
|
||||
CampaignStatus, CreativeType, PlacementType,
|
||||
)
|
||||
|
||||
|
||||
# Aliasok a Digital Twin kompatibilitáshoz
|
||||
Vehicle = Asset
|
||||
@@ -81,5 +88,9 @@ __all__ = [
|
||||
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "ReferenceLookup",
|
||||
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
|
||||
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost", "ExternalReferenceLibrary", "ExternalReferenceQueue",
|
||||
"GbCatalogDiscovery", "Season", "StagedVehicleData", "OneTimePassword"
|
||||
"GbCatalogDiscovery", "Season", "StagedVehicleData", "OneTimePassword",
|
||||
# Marketing / Ad Engine
|
||||
"Campaign", "Creative", "Placement", "CampaignCreative",
|
||||
"CampaignPlacement", "AdImpression", "AdClick",
|
||||
"CampaignStatus", "CreativeType", "PlacementType",
|
||||
]
|
||||
@@ -23,8 +23,8 @@ class SubscriptionTier(Base):
|
||||
is_custom: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
class OrganizationSubscription(Base):
|
||||
"""
|
||||
Szervezetek aktuális előfizetései és azok érvényessége.
|
||||
"""
|
||||
Szervezetek aktuális előfizetései és azok érvényessége.
|
||||
"""
|
||||
__tablename__ = "org_subscriptions"
|
||||
__table_args__ = {"schema": "finance"}
|
||||
@@ -40,6 +40,11 @@ class OrganizationSubscription(Base):
|
||||
valid_from: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
# ── P0 BOOSTER ARCHITECTURE: extra_allowances JSONB ──
|
||||
# Tárolja a darabáron vett extra kvótákat a csomag keretein felül.
|
||||
# Példa: {"extra_vehicles": 1, "extra_garages": 1, "extra_credits": 100}
|
||||
extra_allowances: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"), default=dict)
|
||||
|
||||
class UserSubscription(Base):
|
||||
"""
|
||||
|
||||
300
backend/app/models/marketing.py
Normal file
300
backend/app/models/marketing.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# /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())
|
||||
Reference in New Issue
Block a user