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
)
}
"
)