forked from sagnik/Project_Velocity
feat: Complete code integration of modules (#18)
The complete code integration is done. Co-authored-by: Sagnik <sagnik7896@gmail.com> Reviewed-on: sagnik/Project_Velocity#18
This commit is contained in:
@@ -11,14 +11,23 @@ Routes:
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from backend.services.ad_network_service import (
|
||||
AdInsight,
|
||||
BidStrategyUpdate,
|
||||
BudgetUpdate,
|
||||
Platform,
|
||||
ad_network_service,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
@@ -91,6 +100,7 @@ def _sha256_hash(value: str) -> str:
|
||||
|
||||
class CampaignCreateRequest(BaseModel):
|
||||
name: str = Field(..., description="Campaign display name")
|
||||
platform: Platform = Field(default=Platform.META, description="Target ad network platform")
|
||||
objective: str = Field("OUTCOME_LEADS", description="Meta campaign objective enum")
|
||||
budget_daily: int = Field(..., gt=0, description="Daily budget in cents (AED × 100)")
|
||||
status: str = Field("PAUSED", description="Initial campaign status — start PAUSED for review")
|
||||
@@ -121,9 +131,55 @@ class MetaAuthRequest(BaseModel):
|
||||
short_lived_token: str = Field(..., description="Short-lived user access token from Meta OAuth")
|
||||
|
||||
|
||||
@router.get("/campaigns", summary="List unified campaign summaries for the Catalyst marketing tab")
|
||||
async def list_campaigns(platform: Platform | None = Query(default=None)) -> dict:
|
||||
campaigns = await ad_network_service.list_campaigns(platform=platform)
|
||||
insights = await ad_network_service.get_insights(platform=platform, days=7)
|
||||
rollup: dict[str, dict[str, float]] = {}
|
||||
for insight in insights:
|
||||
insight_campaign_id = insight.campaign_id if isinstance(insight, AdInsight) else insight.get("campaign_id")
|
||||
if not insight_campaign_id:
|
||||
continue
|
||||
spent = insight.spend if isinstance(insight, AdInsight) else float(insight.get("spend", 0))
|
||||
impressions = insight.impressions if isinstance(insight, AdInsight) else int(insight.get("impressions", 0))
|
||||
clicks = insight.clicks if isinstance(insight, AdInsight) else int(insight.get("clicks", 0))
|
||||
conversions = insight.conversions if isinstance(insight, AdInsight) else int(insight.get("conversions", 0))
|
||||
slot = rollup.setdefault(
|
||||
insight_campaign_id,
|
||||
{
|
||||
"spent": 0.0,
|
||||
"impressions": 0.0,
|
||||
"clicks": 0.0,
|
||||
"conversions": 0.0,
|
||||
},
|
||||
)
|
||||
slot["spent"] += spent
|
||||
slot["impressions"] += impressions
|
||||
slot["clicks"] += clicks
|
||||
slot["conversions"] += conversions
|
||||
data = [
|
||||
{
|
||||
"id": campaign.id,
|
||||
"name": campaign.name,
|
||||
"platform": campaign.platform.value,
|
||||
"status": campaign.status.value,
|
||||
"budget": campaign.daily_budget,
|
||||
"spent": round(rollup.get(campaign.id, {}).get("spent", campaign.spent), 2),
|
||||
"impressions": int(rollup.get(campaign.id, {}).get("impressions", 0)),
|
||||
"clicks": int(rollup.get(campaign.id, {}).get("clicks", 0)),
|
||||
"conversions": int(rollup.get(campaign.id, {}).get("conversions", 0)),
|
||||
"objective": campaign.objective,
|
||||
"bid_strategy": campaign.bid_strategy,
|
||||
}
|
||||
for campaign in campaigns
|
||||
]
|
||||
source = "ad_network_service_live" if platform else "ad_network_service_unified"
|
||||
return _ok(data, meta={"count": len(data), "source": source})
|
||||
|
||||
|
||||
# ── 1. POST /campaigns/create ─────────────────────────────────────────────────
|
||||
|
||||
@router.post("/campaigns/create", summary="Bulk-create Meta Marketing campaigns")
|
||||
@router.post("/campaigns/create", summary="Create Meta or Google marketing campaigns")
|
||||
async def create_campaigns(
|
||||
request: Request,
|
||||
payload: CampaignCreateRequest,
|
||||
@@ -134,6 +190,25 @@ async def create_campaigns(
|
||||
|
||||
Requires: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID
|
||||
"""
|
||||
if payload.platform == Platform.GOOGLE:
|
||||
campaign_id = f"google-camp-{uuid.uuid4().hex[:8]}"
|
||||
if hasattr(request.app.state, "broadcast_live_event"):
|
||||
await request.app.state.broadcast_live_event(
|
||||
"create",
|
||||
f"Created Google Ads campaign '{payload.name}'.",
|
||||
payload.name,
|
||||
f"Budget: AED {payload.budget_daily / 100:.0f}/day",
|
||||
)
|
||||
return _ok(
|
||||
CampaignCreateResponse(
|
||||
campaign_id=campaign_id,
|
||||
name=payload.name,
|
||||
status=payload.status,
|
||||
created_at=datetime.utcnow().isoformat(),
|
||||
).model_dump(),
|
||||
meta={"platform": "google", "mode": "simulated_or_provider_managed"},
|
||||
)
|
||||
|
||||
_api, account_id = _get_sdk()
|
||||
|
||||
try:
|
||||
@@ -226,53 +301,55 @@ async def sync_creative(
|
||||
|
||||
# ── 3. GET /insights/realtime ─────────────────────────────────────────────────
|
||||
|
||||
@router.get("/insights/realtime", summary="Poll Meta Ads Insights API")
|
||||
@router.get("/insights/realtime", summary="Poll unified Meta and Google Ads insights")
|
||||
async def get_realtime_insights(
|
||||
date_preset: str = "last_7_days",
|
||||
level: str = "adset",
|
||||
campaign_id: str | None = None,
|
||||
platform: Platform | None = Query(default=None),
|
||||
days: int = Query(default=7, ge=1, le=90),
|
||||
) -> dict:
|
||||
"""
|
||||
Polls `AdAccount.get_insights()` for CTR, CPA, spend, impressions across Ad Sets.
|
||||
Supports `date_preset` (e.g. 'today', 'last_7_days', 'last_30_days') and
|
||||
`level` ('campaign', 'adset', 'ad').
|
||||
|
||||
Requires: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID
|
||||
"""
|
||||
_api, account_id = _get_sdk()
|
||||
|
||||
try:
|
||||
from facebook_business.adobjects.adaccount import AdAccount # type: ignore
|
||||
from facebook_business.adobjects.adsinsights import AdsInsights # type: ignore
|
||||
|
||||
account = AdAccount(account_id)
|
||||
fields = [
|
||||
AdsInsights.Field.campaign_name,
|
||||
AdsInsights.Field.adset_name,
|
||||
AdsInsights.Field.spend,
|
||||
AdsInsights.Field.impressions,
|
||||
AdsInsights.Field.clicks,
|
||||
AdsInsights.Field.ctr,
|
||||
AdsInsights.Field.cpp, # cost per purchase (proxy for CPA)
|
||||
AdsInsights.Field.date_start,
|
||||
AdsInsights.Field.date_stop,
|
||||
]
|
||||
params = {
|
||||
"date_preset": date_preset,
|
||||
"level": level,
|
||||
}
|
||||
insights_cursor = account.get_insights(fields=fields, params=params)
|
||||
results = [dict(row) for row in insights_cursor]
|
||||
|
||||
return _ok(results, meta={
|
||||
"account_id": account_id,
|
||||
"date_preset": date_preset,
|
||||
"level": level,
|
||||
"count": len(results),
|
||||
})
|
||||
insights = await ad_network_service.get_insights(campaign_id=campaign_id, platform=platform, days=days)
|
||||
except Exception as exc:
|
||||
logger.error("Insights fetch failed: %s", exc)
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
||||
|
||||
data = [item.model_dump() if isinstance(item, AdInsight) else item for item in insights]
|
||||
return _ok(data, meta={"count": len(data), "days": days, "platform": platform.value if platform else "all"})
|
||||
|
||||
|
||||
@router.put("/budget", summary="Update Meta or Google Ads budget and campaign status")
|
||||
async def update_campaign_budget(request: Request, payload: BudgetUpdate) -> dict:
|
||||
try:
|
||||
result = await ad_network_service.update_budget(payload)
|
||||
if hasattr(request.app.state, "broadcast_live_event"):
|
||||
await request.app.state.broadcast_live_event(
|
||||
"budget_update",
|
||||
f"Updated {payload.platform.value} budget for {payload.campaign_id}.",
|
||||
payload.campaign_id,
|
||||
f"daily={payload.daily_budget} lifetime={payload.lifetime_budget}",
|
||||
)
|
||||
return _ok(result)
|
||||
except Exception as exc:
|
||||
logger.error("Budget update failed: %s", exc)
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
||||
|
||||
|
||||
@router.put("/bid-strategy", summary="Apply Meta or Google Ads bid strategy changes")
|
||||
async def update_bid_strategy(request: Request, payload: BidStrategyUpdate) -> dict:
|
||||
try:
|
||||
action = await ad_network_service.update_bid_strategy(payload)
|
||||
if hasattr(request.app.state, "broadcast_live_event"):
|
||||
await request.app.state.broadcast_live_event(
|
||||
"bid_strategy_update",
|
||||
f"Updated {payload.platform.value} bid strategy for {payload.campaign_id}.",
|
||||
payload.campaign_id,
|
||||
payload.strategy,
|
||||
)
|
||||
return _ok(action.model_dump())
|
||||
except Exception as exc:
|
||||
logger.error("Bid strategy update failed: %s", exc)
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc))
|
||||
|
||||
|
||||
# ── 4. POST /audiences/lookalike ──────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user