We are unable to create a creative for our custom native template (ID: 12451018) via the API.
If we include the size field, we get [CommonError.CANNOT_UPDATE @ [0].size].
If we omit the size field, we get [RequiredSizeError.REQUIRED @ [0].size].
The creative is not created in the UI.
However, we can create the creative via the UI with no problem.
Please advise how to create a creative for this template via the API.I have created the Order and Line item but i was unable to create the Creative , could you please help me?
import pytz
import uuid
from fastapi import APIRouter, HTTPException, Body, Depends, BackgroundTasks
from googleads import ad_manager
from typing import Dict, List, Any, Optional
from pydantic import BaseModel, Field
from pymongo import MongoClient
from cmsapi.utils.config import settings
from cmsapi.utils.dependencies import get_mongo_client
import logging
from fastapi.responses import JSONResponse
from bson import ObjectId
from datetime import datetime, timedelta
import os
import requests
import base64
from PIL import Image
from io import BytesIO
router = APIRouter(prefix="/gam", tags=["Google Ad Manager Enhanced"])
GOOGLEADS_YAML = os.path.abspath(
os.path.join(os.path.dirname(__file__), "googleads.yaml")
)
# Enhanced Pydantic models for complete GAM support
class GeoTargeting(BaseModel):
targeted_locations: List[Dict[str, Any]] = []
excluded_locations: List[Dict[str, Any]] = []
class TechnologyTargeting(BaseModel):
device_categories: List[str] = []
operating_systems: List[str] = []
browsers: List[str] = []
mobile_carriers: List[str] = []
class CustomTargeting(BaseModel):
key_values: List[Dict[str, Any]] = []
class Targeting(BaseModel):
geo_targeting: Optional[GeoTargeting] = None
technology_targeting: Optional[TechnologyTargeting] = None
custom_targeting: Optional[CustomTargeting] = None
ad_unit_targeting: List[str] = []
class CreativeSize(BaseModel):
width: str
height: str
is_aspect_ratio: bool = False
class CreativePlaceholder(BaseModel):
size: CreativeSize
expected_creative_count: int = 1
class PrimaryGoal(BaseModel):
units: str
unit_type: str = "IMPRESSIONS" # IMPRESSIONS, CLICKS, VIEWABLE_IMPRESSIONS, COMPLETED_VIEWS
goal_type: str = "LIFETIME" # LIFETIME, DAILY
class Money(BaseModel):
currency_code: str = "INR"
micro_amount: str
class EnhancedLineItemConfig(BaseModel):
name: str
line_item_type: str = "STANDARD"
priority: int = Field(8, ge=1, le=16)
cost_type: str = "CPM"
cost_per_unit: Money
primary_goal: PrimaryGoal
creative_placeholders: List[CreativePlaceholder]
start_date_time_type: str = "IMMEDIATELY"
end_date_time: Optional[datetime] = None
unlimited_end_date_time: bool = False
delivery_rate_type: str = "FRONTLOADED"
creative_rotation_type: str = "OPTIMIZED"
targeting: Optional[Targeting] = None
allow_overbook: bool = True
discount_type: str = "PERCENTAGE"
discount: float = 0.0
contracted_units_bought: Optional[str] = None
class TemplateVariable(BaseModel):
xsi_type: str
unique_name: str
value: str
class EnhancedCreativeConfig(BaseModel):
name: str
creative_type: str = "ImageCreative"
size: Optional[CreativeSize] = None
advertiser_id: int
# For ImageCreative
primary_image_asset_url: Optional[str] = None
# For TemplateCreative
creative_template_id: Optional[str] = None
creative_template_variable_values: List[TemplateVariable] = []
# For VideoCreative
video_source_url: Optional[str] = None
duration: Optional[str] = None
class EnhancedOrderConfig(BaseModel):
name: str
advertiser_id: int
trafficker_id: int
start_date_time: Optional[datetime] = None
end_date_time: Optional[datetime] = None
notes: Optional[str] = None
unlimited_end_date_time: bool = False
class ComprehensiveGAMRequest(BaseModel):
revenue_order_id: str
mode: str = "custom" # "auto" or "custom"
order_config: Optional[EnhancedOrderConfig] = None
line_items: List[EnhancedLineItemConfig] = []
creatives: List[EnhancedCreativeConfig] = []
auto_create_associations: bool = True
class GAMResponse(BaseModel):
success: bool
message: str
data: Dict[str, Any]
errors: List[str] = []
# Native creative templates with enhanced metadata
NATIVE_TEMPLATES = {
"native_content": {
"id": "10004520",
"name": "Native Content Ad",
"required_variables": ["Headline", "Body", "Calltoaction"]
},
"native_app_install": {
"id": "10004400",
"name": "Native App Install",
"required_variables": ["Headline", "Body", "Calltoaction", "AppIcon", "StarRating"]
},
"native_video_content": {
"id": "10007040",
"name": "Native Video Content",
"required_variables": ["Headline", "Body", "Calltoaction", "VideoUrl"]
},
"short_news": {
"id": "12402825",
"name": "Short News Format",
"required_variables": ["Headline", "Body", "Source"]
},
"promoted_story": {
"id": "12451018",
"name": "Promoted Story",
"required_variables": ["Headline", "Body", "Calltoaction", "Image"]
}
}
# Comprehensive ad unit targeting options
AD_UNIT_TARGETS = {
"homepage": {"name": "Homepage", "id": "21675094086"},
"article_pages": {"name": "Article Pages", "id": "21675094087"},
"category_pages": {"name": "Category Pages", "id": "21675094088"},
"mobile_app": {"name": "Mobile App", "id": "21675094089"}
}
# Device targeting options
DEVICE_CATEGORIES = {
"desktop": {"id": "30000", "name": "Desktop"},
"smartphone": {"id": "30001", "name": "Smartphone"},
"tablet": {"id": "30002", "name": "Tablet"},
"connected_tv": {"id": "30004", "name": "Connected TV"}
}
# Geographic targeting for India (enhanced)
INDIA_GEO_TARGETS = {
"india": {"id": "2356", "name": "India"},
"maharashtra": {"id": "21167", "name": "Maharashtra, India"},
"karnataka": {"id": "21166", "name": "Karnataka, India"},
"delhi": {"id": "1007785", "name": "Delhi, India"},
"mumbai": {"id": "1007786", "name": "Mumbai, India"},
"bangalore": {"id": "1007809", "name": "Bangalore, India"},
"hyderabad": {"id": "1007810", "name": "Hyderabad, India"},
"chennai": {"id": "1007835", "name": "Chennai, India"},
"kolkata": {"id": "1007836", "name": "Kolkata, India"}
}
def download_and_encode_image(image_url):
try:
response = requests.get(image_url, timeout=10) # Add timeout to prevent hangs
response.raise_for_status() # Raise error for bad status (e.g., 404, 403)
image_data = response.content
# Validate it's a real image and get dimensions
try:
img = Image.open(BytesIO(image_data))
img.verify() # Verifies if it's a valid image
width, height = img.size
logging.info(f"Downloaded valid image: {width}x{height}")
except Exception as e:
raise ValueError(f"Invalid image format or corrupted: {str(e)}")
# Ensure it's JPEG/PNG/GIF (GAM supported)
if response.headers.get('Content-Type') not in ['image/jpeg', 'image/png', 'image/gif']:
raise ValueError("Unsupported image type. Must be JPEG, PNG, or GIF.")
encoded_data = base64.b64encode(image_data).decode('utf-8')
file_name = image_url.split("/")[-1]
return encoded_data, file_name, (width, height) # Return actual dimensions
except requests.RequestException as e:
raise ValueError(f"Failed to download image from {image_url}: {str(e)}")
def get_ad_manager_client():
"""Initialize and return the Google Ad Manager client."""
if not os.path.exists(GOOGLEADS_YAML):
raise HTTPException(500, f"googleads.yaml not found at {GOOGLEADS_YAML}")
try:
return ad_manager.AdManagerClient.LoadFromStorage(GOOGLEADS_YAML)
except Exception as e:
raise HTTPException(500, f"GAM client init failed: {e}")
def to_gam_datetime(dt_input) -> Dict[str, Any]:
"""Convert datetime input into the dict format GAM expects."""
try:
if isinstance(dt_input, str):
if dt_input.endswith('Z'):
dt_input = dt_input.replace('Z', '+00:00')
dt = datetime.datetime.fromisoformat(dt_input)
else:
dt = dt_input
tz = pytz.timezone("Asia/Kolkata")
dt = dt.astimezone(tz) if dt.tzinfo else tz.localize(dt)
return {
"date": {"year": dt.year, "month": dt.month, "day": dt.day},
"hour": dt.hour,
"minute": dt.minute,
"second": dt.second,
"timeZoneId": "Asia/Kolkata"
}
except Exception as e:
raise HTTPException(400, f"Invalid datetime '{dt_input}': {e}")
async def get_revenue_order_from_db(revenue_order_id: str, db_client: MongoClient) -> Dict[str, Any]:
"""Fetch revenue order from MongoDB."""
db = db_client[settings.DATABASE_NAME]
revenue_collection = db[settings.REVENUE_COLLECTION]
revenue_order = revenue_collection.find_one({"order_id": int(revenue_order_id)})
if not revenue_order:
raise HTTPException(404, f"Revenue order {revenue_order_id} not found")
revenue_order["_id"] = str(revenue_order["_id"])
return revenue_order
def find_or_create_advertiser(client, advertiser_name: str) -> int:
"""Find an existing advertiser by name or create a new one."""
svc = client.GetService("CompanyService", version="v202411")
# Search for existing advertiser
stmt = (
ad_manager.StatementBuilder(version="v202411")
.Where("name = :name AND type = :type")
.WithBindVariable("name", advertiser_name)
.WithBindVariable("type", "ADVERTISER")
.Limit(1)
)
resp = svc.getCompaniesByStatement(stmt.ToStatement())
companies = getattr(resp, "results", [])
if companies:
return companies[0].id
# Create new advertiser
new_adv = {"name": advertiser_name, "type": "ADVERTISER"}
created = svc.createCompanies([new_adv])[0]
return created.id
def get_current_user_id(client) -> int:
"""Get the current user's ID to use as trafficker."""
try:
user_service = client.GetService("UserService", version="v202411")
response = user_service.getCurrentUser()
return response.id
except Exception as e:
raise HTTPException(500, f"Failed to get current user: {e}")
def create_enhanced_gam_order(client, order_config: EnhancedOrderConfig) -> Dict[str, Any]:
"""Create GAM order with comprehensive configuration."""
svc = client.GetService("OrderService", version="v202411")
payload = {
"name": order_config.name,
"advertiserId": order_config.advertiser_id,
"traffickerId": order_config.trafficker_id,
"startDateTime": to_gam_datetime(order_config.start_date_time),
"endDateTime": to_gam_datetime(order_config.end_date_time),
"notes": order_config.notes,
"unlimitedEndDateTime": order_config.unlimited_end_date_time
}
created_order = svc.createOrders([payload])[0]
return {
"id": created_order.id,
"name": created_order.name,
"advertiserId": created_order.advertiserId,
"traffickerId": created_order.traffickerId,
"startDateTime": payload["startDateTime"],
"endDateTime": payload["endDateTime"],
"notes": created_order.notes
}
def build_targeting_object(targeting: Targeting) -> Dict[str, Any]:
"""Build comprehensive targeting object for GAM."""
targeting_obj = {}
if targeting.geo_targeting:
geo_targeting = {}
if targeting.geo_targeting.targeted_locations:
geo_targeting["targetedLocations"] = targeting.geo_targeting.targeted_locations
if targeting.geo_targeting.excluded_locations:
geo_targeting["excludedLocations"] = targeting.geo_targeting.excluded_locations
targeting_obj["geoTargeting"] = geo_targeting
if targeting.technology_targeting:
tech_targeting = {}
if targeting.technology_targeting.device_categories:
tech_targeting["deviceCategoryTargeting"] = {
"targetedDeviceCategories": [
{"id": cat_id} for cat_id in targeting.technology_targeting.device_categories
],
"isTargeted": True
}
if targeting.technology_targeting.operating_systems:
tech_targeting["operatingSystemTargeting"] = {
"targetedOperatingSystems": [
{"id": os_id} for os_id in targeting.technology_targeting.operating_systems
],
"isTargeted": True
}
if targeting.technology_targeting.browsers:
tech_targeting["browserTargeting"] = {
"targetedBrowsers": [
{"id": browser_id} for browser_id in targeting.technology_targeting.browsers
],
"isTargeted": True
}
targeting_obj["technologyTargeting"] = tech_targeting
if targeting.custom_targeting and targeting.custom_targeting.key_values:
custom_criteria = []
for kv in targeting.custom_targeting.key_values:
custom_criteria.append({
"xsi_type": "CustomCriteria",
"keyId": kv.get("key_id"),
"valueIds": kv.get("value_ids", []),
"operator": kv.get("operator", "IS")
})
targeting_obj["customTargeting"] = {
"children": custom_criteria,
"logicalOperator": "AND"
}
if targeting.ad_unit_targeting:
targeting_obj["inventoryTargeting"] = {
"targetedAdUnits": [
{"adUnitId": ad_unit_id, "includeDescendants": True}
for ad_unit_id in targeting.ad_unit_targeting
]
}
return targeting_obj
def create_enhanced_line_items(client, order_id: int, line_items: List[EnhancedLineItemConfig]) -> List[Dict[str, Any]]:
"""Create line items with comprehensive configuration."""
svc = client.GetService("LineItemService", version="v202411")
created_line_items = []
for li_config in line_items:
try:
payload = {
"name": li_config.name,
"orderId": order_id,
"lineItemType": li_config.line_item_type,
"priority": li_config.priority,
"costType": li_config.cost_type,
"costPerUnit": {
"currencyCode": li_config.cost_per_unit.currency_code,
"microAmount": li_config.cost_per_unit.micro_amount
},
"primaryGoal": {
"units": li_config.primary_goal.units,
"unitType": li_config.primary_goal.unit_type,
"goalType": li_config.primary_goal.goal_type
},
"creativePlaceholders": [
{
"size": {
"width": cp.size.width,
"height": cp.size.height,
"isAspectRatio": cp.size.is_aspect_ratio
},
"expectedCreativeCount": cp.expected_creative_count
}
for cp in li_config.creative_placeholders
],
"startDateTimeType": li_config.start_date_time_type,
"deliveryRateType": li_config.delivery_rate_type,
"creativeRotationType": li_config.creative_rotation_type,
"allowOverbook": li_config.allow_overbook,
"discountType": li_config.discount_type,
"discount": li_config.discount
}
# Add end date time if specified
if li_config.end_date_time and not li_config.unlimited_end_date_time:
payload["endDateTime"] = to_gam_datetime(li_config.end_date_time)
if li_config.unlimited_end_date_time:
payload["unlimitedEndDateTime"] = True
# Add contracted units if specified
if li_config.contracted_units_bought:
payload["contractedUnitsBought"] = li_config.contracted_units_bought
# Add targeting if specified
if li_config.targeting:
payload["targeting"] = build_targeting_object(li_config.targeting)
created_li = svc.createLineItems([payload])[0]
created_line_items.append({
"id": created_li.id,
"name": created_li.name,
"orderId": created_li.orderId,
"priority": created_li.priority,
"lineItemType": created_li.lineItemType,
"status": created_li.status,
"costType": created_li.costType,
"costPerUnit": li_config.cost_per_unit.micro_amount,
"goalUnits": li_config.primary_goal.units
})
except Exception as e:
logging.error(f"Failed to create line item {li_config.name}: {e}")
continue
return created_line_items
def create_enhanced_creatives(client, creatives):
"""
Create creatives with comprehensive configuration for Native (TemplateCreative).
"""
svc = client.GetService("CreativeService", version="v202411")
created_creatives = []
for cr_config in creatives:
try:
if cr_config.creative_type == "TemplateCreative":
# DO NOT include 'size' field for TemplateCreative
base_payload = {
"name": cr_config.name,
"advertiserId": cr_config.advertiser_id,
"xsi_type": "TemplateCreative",
"destinationUrl": cr_config.destination_url,
"creativeTemplateId": cr_config.creative_template_id,
"creativeTemplateVariableValues": [
{
"xsi_type": var.xsi_type,
"uniqueName": var.unique_name,
"value": var.value
}
for var in cr_config.creative_template_variable_values
]
}
logging.info(f"Creating native creative with payload: {base_payload}")
created_cr = svc.createCreatives([base_payload])[0]
created_creatives.append({
"id": created_cr.id,
"name": created_cr.name,
"advertiserId": created_cr.advertiserId,
"size": "native",
"type": cr_config.creative_type
})
elif cr_config.creative_type == "ImageCreative":
# For ImageCreative, size IS required
base_payload = {
"name": cr_config.name,
"advertiserId": cr_config.advertiser_id,
"xsi_type": "ImageCreative",
"destinationUrl": cr_config.destination_url,
"size": {
"width": int(cr_config.size.width),
"height": int(cr_config.size.height),
"isAspectRatio": cr_config.size.is_aspect_ratio
}
}
if cr_config.primary_image_asset_url:
try:
encoded_data, file_name, dimensions = download_and_encode_image(
cr_config.primary_image_asset_url
)
base_payload["primaryImageAsset"] = {
"assetByteArray": encoded_data,
"fileName": file_name
}
except Exception as e:
logging.error(f"Failed to process image: {e}")
continue
logging.info(f"Creating image creative with payload keys: {list(base_payload.keys())}")
created_cr = svc.createCreatives([base_payload])[0]
created_creatives.append({
"id": created_cr.id,
"name": created_cr.name,
"advertiserId": created_cr.advertiserId,
"size": f"{cr_config.size.width}x{cr_config.size.height}",
"type": cr_config.creative_type
})
else:
raise ValueError(f"Unsupported creative type: {cr_config.creative_type}")
except Exception as e:
logging.error(f"Failed to create creative {cr_config.name}: {e}")
continue
return created_creatives
def create_line_item_creative_associations(client, line_items: List[Dict[str, Any]], creatives: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Create associations between line items and creatives."""
svc = client.GetService("LineItemCreativeAssociationService", version="v202411")
associations = []
created_associations = []
# Create associations for compatible sizes
for line_item in line_items:
for creative in creatives:
# Match creatives to line items based on size compatibility or other logic
associations.append({
"lineItemId": line_item["id"],
"creativeId": creative["id"],
"startDateTimeType": "IMMEDIATELY"
})
if associations:
try:
created_licas = svc.createLineItemCreativeAssociations(associations)
for lica in created_licas:
created_associations.append({
"lineItemId": lica.lineItemId,
"creativeId": lica.creativeId,
"status": "READY"
})
except Exception as e:
logging.error(f"Failed to create associations: {e}")
return created_associations
@router.post("/create-comprehensive-campaign")
async def create_comprehensive_gam_campaign(
request: ComprehensiveGAMRequest,
background_tasks: BackgroundTasks,
db_client: MongoClient = Depends(get_mongo_client)
) -> GAMResponse:
errors: List[str] = []
try:
# Fetch the revenue order
revenue_data = await get_revenue_order_from_db(request.revenue_order_id, db_client)
client = get_ad_manager_client()
# ── STEP 1: Create GAM Order ────────────────────────────────────────────────
gam_order = None
order_id = None
# Always create order with provided config or default
if not request.order_config:
# Create default order config
advertiser_id = find_or_create_advertiser(client, revenue_data["Advertiser"])
trafficker_id = get_current_user_id(client)
start_date = datetime.now(pytz.timezone("Asia/Kolkata")) + timedelta(days=1)
end_date = start_date + timedelta(days=30)
request.order_config = EnhancedOrderConfig(
name=f"Campaign-{revenue_data['Advertiser']}-{revenue_data['order_id']}",
advertiser_id=advertiser_id,
trafficker_id=trafficker_id,
start_date_time=start_date,
end_date_time=end_date,
notes=f"Created from Revenue Order #{revenue_data['order_id']}"
)
# Create GAM order
gam_order = create_enhanced_gam_order(client, request.order_config)
order_id = gam_order["id"]
# ── STEP 2: Create Line Items ───────────────────────────────────────────────
created_line_items: List[Dict[str, Any]] = []
# FIXED: Always create line items when provided, or create default for custom mode
if request.line_items and len(request.line_items) > 0:
print(f"Creating {len(request.line_items)} line items...")
created_line_items = create_enhanced_line_items(client, order_id, request.line_items)
elif request.mode == "custom":
# Create default line item for custom mode if none provided
print("No line items provided, creating default line item...")
default_line_item = EnhancedLineItemConfig(
name=f"LineItem-{revenue_data['order_id']}",
line_item_type="STANDARD",
priority=8,
cost_type="CPM",
cost_per_unit=Money(currency_code="INR", micro_amount="1000"), # ₹0.001 CPM
primary_goal=PrimaryGoal(units="1000", unit_type="IMPRESSIONS", goal_type="LIFETIME"),
creative_placeholders=[
CreativePlaceholder(
size=CreativeSize(width="300", height="250", is_aspect_ratio=False),
expected_creative_count=1
)
],
delivery_rate_type="FRONTLOADED",
creative_rotation_type="OPTIMIZED"
)
created_line_items = create_enhanced_line_items(client, order_id, [default_line_item])
# ── STEP 3: Create Creatives ────────────────────────────────────────────────
created_creatives: List[Dict[str, Any]] = []
# FIXED: Always create creatives when provided, or create default for custom mode
if request.creatives and len(request.creatives) > 0:
print(f"Creating {len(request.creatives)} creatives...")
# Ensure all creatives have advertiser_id set
for cr in request.creatives:
if not cr.advertiser_id:
cr.advertiser_id = request.order_config.advertiser_id
created_creatives = create_enhanced_creatives(client, request.creatives)
elif request.mode == "custom":
# Create default creative for custom mode if none provided
print("No creatives provided, creating default creative...")
advertiser_id = request.order_config.advertiser_id
default_creative = EnhancedCreativeConfig(
name=f"Creative-{revenue_data['order_id']}",
creative_type="ImageCreative",
size=CreativeSize(width="300", height="250", is_aspect_ratio=False),
advertiser_id=advertiser_id,
)
created_creatives = create_enhanced_creatives(client, [default_creative])
# ── STEP 4: Associate Line-Items ↔ Creatives ─────────────────────
created_associations: List[Dict[str, Any]] = []
if request.auto_create_associations and created_line_items and created_creatives:
print(f"Creating associations between {len(created_line_items)} line items and {len(created_creatives)} creatives...")
created_associations = create_line_item_creative_associations(
client,
created_line_items,
created_creatives
)
print(f"Campaign creation summary: Order={order_id}, LineItems={len(created_line_items)}, Creatives={len(created_creatives)}, Associations={len(created_associations)}")
# ── STEP 5: Update Revenue Order in Database ───────────────────────────────────
gam_data = {
"gam_order_id": order_id,
"gam_line_item_ids": [li["id"] for li in created_line_items],
"gam_creative_ids": [cr["id"] for cr in created_creatives],
"creation_mode": request.mode,
"gam_created_date": datetime.now(),
"gam_campaign_status": "CREATED"
}
background_tasks.add_task(
update_revenue_order_with_gam_data,
request.revenue_order_id,
gam_data,
db_client
)
return GAMResponse(
success=True,
message="Comprehensive GAM campaign created successfully",
data={
"revenue_order_id": request.revenue_order_id,
"gam_order": gam_order,
"gam_order_id": order_id,
"line_items": created_line_items,
"creatives": created_creatives,
"associations": created_associations,
"summary": {
"order_id": order_id,
"total_line_items": len(created_line_items),
"total_creatives": len(created_creatives),
"total_associations": len(created_associations)
}
},
errors=errors
)
except Exception as e:
logging.error(f"create-comprehensive-campaign failed: {e}")
import traceback
traceback.print_exc()
return GAMResponse(
success=False,
message=f"Failed to create GAM campaign: {str(e)}",
data={},
errors=[str(e)]
)
async def update_revenue_order_with_gam_data(order_id: str, gam_data: Dict[str, Any], db_client: MongoClient):
"""Update revenue order with GAM campaign data."""
try:
db = db_client[settings.DATABASE_NAME]
revenue_collection = db[settings.REVENUE_COLLECTION]
revenue_collection.update_one(
{"order_id": int(order_id)},
{"$set": gam_data}
)
except Exception as e:
logging.error(f"Failed to update revenue order {order_id} with GAM data: {e}")
# Missing endpoints that frontend expects
@router.get("/revenue-order/{revenue_order_id}/gam-status")
async def check_gam_status(
revenue_order_id: str,
db_client: MongoClient = Depends(get_mongo_client)
):
"""Check if GAM campaign already exists for revenue order."""
try:
revenue_data = await get_revenue_order_from_db(revenue_order_id, db_client)
gam_exists = bool(revenue_data.get("gam_order_id"))
return {
"success": True,
"revenue_order_id": revenue_order_id,
"gam_campaign_exists": gam_exists,
"gam_order_id": revenue_data.get("gam_order_id"),
"ad_type": revenue_data.get("ad_type"),
"created_date": revenue_data.get("gam_created_date"),
"campaign_status": revenue_data.get("gam_campaign_status", "NOT_CREATED"),
"line_item_count": len(revenue_data.get("gam_line_item_ids", [])),
"creative_count": len(revenue_data.get("gam_creative_ids", []))
}
except Exception as e:
return {
"success": False,
"error": str(e),
"gam_campaign_exists": False
}
@router.get("/revenue-order/{revenue_order_id}/campaign-details")
async def get_campaign_details(
revenue_order_id: str,
db_client: MongoClient = Depends(get_mongo_client)
):
"""Get detailed campaign information for editing."""
try:
revenue_data = await get_revenue_order_from_db(revenue_order_id, db_client)
if not revenue_data.get("gam_order_id"):
return {
"success": False,
"message": "No GAM campaign found for this revenue order"
}
# Initialize GAM client and fetch detailed campaign data
client = get_ad_manager_client()
# Get order details
order_service = client.GetService("OrderService", version="v202411")
order = order_service.getOrdersByStatement(
ad_manager.StatementBuilder(version="v202411")
.Where("id = :orderId")
.WithBindVariable("orderId", revenue_data["gam_order_id"])
.ToStatement()
).results[0]
# Get line items
line_item_service = client.GetService("LineItemService", version="v202411")
line_items = line_item_service.getLineItemsByStatement(
ad_manager.StatementBuilder(version="v202411")
.Where("orderId = :orderId")
.WithBindVariable("orderId", revenue_data["gam_order_id"])
.ToStatement()
).results
# Get creatives
creative_service = client.GetService("CreativeService", version="v202411")
creatives = creative_service.getCreativesByStatement(
ad_manager.StatementBuilder(version="v202411")
.Where("advertiserId = :advertiserId")
.WithBindVariable("advertiserId", order.advertiserId)
.ToStatement()
).results
return {
"success": True,
"data": {
"order": {
"id": order.id,
"name": order.name,
"advertiserId": order.advertiserId,
"startDateTime": order.startDateTime,
"endDateTime": order.endDateTime,
"notes": order.notes
},
"lineItems": [
{
"id": li.id,
"name": li.name,
"priority": li.priority,
"lineItemType": li.lineItemType,
"costType": li.costType,
"status": li.status,
"budget": li.costPerUnit.microAmount if li.costPerUnit else 0,
"goalUnits": li.primaryGoal.units if li.primaryGoal else 0
}
for li in line_items
],
"creatives": [
{
"id": cr.id,
"name": cr.name,
"size": f"{cr.size.width}x{cr.size.height}" if cr.size else "N/A"
}
for cr in creatives
]
}
}
except Exception as e:
return {
"success": False,
"error": str(e)
}
# Add the endpoint with the name frontend expects
@router.post("/auto-create-campaign")
async def auto_create_campaign_alias(
request: ComprehensiveGAMRequest,
background_tasks: BackgroundTasks,
db_client: MongoClient = Depends(get_mongo_client)
) -> GAMResponse:
"""Alias endpoint for frontend compatibility."""
return await create_comprehensive_gam_campaign(request, background_tasks, db_client)
@router.post("/orders/{order_id}/line-items")
async def create_line_items_for_order(
order_id: int,
items: List[EnhancedLineItemConfig],
db_client: MongoClient = Depends(get_mongo_client)
):
client = get_ad_manager_client()
created = create_enhanced_line_items(client, order_id, items)
return {"success": True, "created_line_items": created}
@router.post("/orders/{order_id}/creatives")
async def create_creatives_for_order(
order_id: int,
creatives: List[EnhancedCreativeConfig],
db_client: MongoClient = Depends(get_mongo_client)
):
client = get_ad_manager_client()
# ensure each creative.advertiser_id is set to order's advertiser
created = create_enhanced_creatives(client, creatives)
return {"success": True, "created_creatives": created}
@router.post("/orders/{order_id}/associations")
async def associate_line_items_and_creatives(
order_id: int,
payload: Dict[str, List[int]], # { lineItemIds: [...], creativeIds: [...] }
db_client: MongoClient = Depends(get_mongo_client)
):
client = get_ad_manager_client()
line_items = payload.get("lineItemIds", [])
creatives = payload.get("creativeIds", [])
assoc = create_line_item_creative_associations(
client,
[{"id": id} for id in line_items],
[{"id": id} for id in creatives]
)
return {"success": True, "created_associations": assoc}
# Utility endpoints for configuration data
@router.get("/config/native-templates")
async def get_native_templates():
"""Get available native creative templates with metadata."""
return {
"success": True,
"templates": NATIVE_TEMPLATES
}
@router.get("/config/device-categories")
async def get_device_categories():
"""Get available device categories for targeting."""
return {
"success": True,
"device_categories": DEVICE_CATEGORIES
}
@router.get("/config/geo-targets")
async def get_geo_targets():
"""Get available geographic targets for India."""
return {
"success": True,
"geo_targets": INDIA_GEO_TARGETS
}
@router.get("/config/ad-units")
async def get_ad_units():
"""Get available ad unit targets."""
return {
"success": True,
"ad_units": AD_UNIT_TARGETS
}
@router.get("/orders")
def list_orders(limit: int = 10):
"""List Orders with enhanced details"""
try:
client = get_ad_manager_client()
order_service = client.GetService("OrderService", version="v202411")
statement = ad_manager.StatementBuilder(version="v202411").Limit(limit)
response = order_service.getOrdersByStatement(statement.ToStatement())
orders = getattr(response, "results", [])
return {"success": True, "orders": [o.__dict__ for o in orders]}
except Exception as e:
return {"success": False, "error": str(e)}
@router.get("/line-items")
def list_line_items(limit: int = 10):
"""List Line Items with enhanced details"""
try:
client = get_ad_manager_client()
line_item_service = client.GetService("LineItemService", version="v202411")
statement = ad_manager.StatementBuilder(version="v202411").Limit(limit)
response = line_item_service.getLineItemsByStatement(statement.ToStatement())
items = getattr(response, "results", [])
return {"success": True, "line_items": [i.__dict__ for i in items]}
except Exception as e:
return {"success": False, "error": str(e)}
@router.get("/creatives")
def list_creatives(limit: int = 10):
"""List Creatives with enhanced details"""
try:
client = get_ad_manager_client()
creative_service = client.GetService("CreativeService", version="v202411")
statement = ad_manager.StatementBuilder(version="v202411").Limit(limit)
response = creative_service.getCreativesByStatement(statement.ToStatement())
creatives = getattr(response, "results", [])
return {"success": True, "creatives": [c.__dict__ for c in creatives]}
except Exception as e:
return {"success": False, "error": str(e)}
@router.get("/users")
def list_users(limit: int = 50):
"""List GAM Users for trafficker selection"""
try:
client = get_ad_manager_client()
user_service = client.GetService("UserService", version="v202411")
statement = ad_manager.StatementBuilder(version="v202411").Limit(limit)
response = user_service.getUsersByStatement(statement.ToStatement())
users = getattr(response, "results", [])
return {"success": True, "users": [u.__dict__ for u in users]}
except Exception as e:
return {"success": False, "error": str(e)}
@router.get("/advertisers")
def list_advertisers(limit: int = 50, offset: int = 0):
"""
List all advertisers in Google Ad Manager
"""
try:
client = get_ad_manager_client()
company_service = client.GetService("CompanyService", version="v202411")
# Build statement to fetch advertisers
statement = (
ad_manager.StatementBuilder(version="v202411")
.Where("type = :type")
.WithBindVariable("type", "ADVERTISER")
.Limit(limit)
.Offset(offset)
)
# Fetch advertisers
response = company_service.getCompaniesByStatement(statement.ToStatement())
advertisers = getattr(response, "results", [])
return {
"success": True,
"advertisers": [
{
"id": adv.id,
"name": adv.name,
"type": adv.type,
"creditStatus": adv.creditStatus
}
for adv in advertisers
],
"totalCount": response.totalResultSetSize
}
except Exception as e:
logging.error(f"Failed to fetch advertisers: {e}")
return {"success": False, "error": str(e)}
@router.post("/line-items")
def create_line_item(
line_item: EnhancedLineItemConfig,
db_client: MongoClient = Depends(get_mongo_client)
):
try:
db = db_client[settings.DATABASE_NAME]
line_item_data = line_item.dict()
line_item_data['created_at'] = datetime.utcnow()
line_item_data['source'] = 'API'
line_item_data['id'] = str(ObjectId())
db['line_items'].insert_one(line_item_data)
return JSONResponse({
'success': True,
'local_id': line_item_data['id'],
'message': 'Line item created locally'
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating line item: {str(e)}")