# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/marketing.py """ Marketing & Ad Placement Public API Endpoints. Provides: - GET /placements/{zone} — Get active ads for a specific placement zone """ from datetime import datetime, timezone from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from pydantic import BaseModel from app.db.session import get_db from app.models.marketing import ( Placement, CampaignPlacement, Campaign, Creative, CampaignStatus, ) router = APIRouter() class AdResponse(BaseModel): """Egy hirdetés válaszmodellje a frontend számára.""" id: int campaign_id: int campaign_name: str creative_id: int creative_type: str content_url: Optional[str] = None html_snippet: Optional[str] = None alt_text: Optional[str] = None target_url: Optional[str] = None priority: int = 0 class PlacementResponse(BaseModel): """Egy placement zóna összes aktív hirdetésének válasza.""" zone: str ads: List[AdResponse] @router.get("/placements/{zone}", response_model=PlacementResponse) async def get_placement_ads( zone: str, db: AsyncSession = Depends(get_db), ): """ Aktív hirdetések lekérése egy adott placement zónához. **Logika:** 1. Megkeresi a Placement rekordot a `name` alapján (case-insensitive). 2. Lekéri az összes aktív kampányhoz tartozó CampaignPlacement kapcsolatot. 3. Visszaadja a kapcsolódó Creative-okat prioritás szerint rendezve. 4. Ha nincs aktív kampány, üres listával tér vissza (nem dob hibát). """ # 1. Find the placement by name (case-insensitive) placement_stmt = select(Placement).where( Placement.name.ilike(zone), Placement.is_active == True, ) result = await db.execute(placement_stmt) placement = result.scalar_one_or_none() if not placement: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Placement zone '{zone}' not found", ) # 2. Find active campaign-placement links with eager-loaded relations campaign_placements_stmt = ( select(CampaignPlacement) .options( selectinload(CampaignPlacement.campaign), selectinload(CampaignPlacement.creative), ) .where( and_( CampaignPlacement.placement_id == placement.id, CampaignPlacement.is_active == True, ) ) .order_by(CampaignPlacement.priority.asc()) ) result = await db.execute(campaign_placements_stmt) campaign_placements = result.scalars().all() # 3. Filter: only include CampaignPlacements where the campaign is ACTIVE now_utc = datetime.now(timezone.utc) active_ads: List[AdResponse] = [] for cp in campaign_placements: campaign = cp.campaign creative = cp.creative if not campaign or not creative: continue # Check campaign status if campaign.status != CampaignStatus.ACTIVE.value: continue # Check campaign date range if campaign.start_date and campaign.start_date > now_utc: continue if campaign.end_date and campaign.end_date < now_utc: continue active_ads.append( AdResponse( id=cp.id, campaign_id=campaign.id, campaign_name=campaign.name, creative_id=creative.id, creative_type=creative.creative_type, content_url=creative.content_url, html_snippet=creative.html_snippet, alt_text=creative.alt_text, target_url=creative.target_url, priority=cp.priority, ) ) return PlacementResponse( zone=zone, ads=active_ads, )