RABAC felépítése megtörtént ill hirdetési portál alapok lerakva

This commit is contained in:
Roo
2026-06-19 06:06:34 +00:00
parent fe3c32597d
commit 9ba2d9180d
30 changed files with 4336 additions and 76 deletions

View File

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

View File

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

View 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())