1717
1818import os
1919import uuid
20+ from datetime import datetime , timezone
2021from typing import Any
2122
2223from adcp .server import (
7980accounts : dict [str , dict [str , Any ]] = {}
8081media_buys : dict [str , dict [str , Any ]] = {}
8182creatives : dict [str , dict [str , Any ]] = {}
83+ open_impairments : dict [tuple [str , str ], dict [str , Any ]] = {}
8284proposals : dict [str , dict [str , Any ]] = {}
8385# Used when no account_id is present; single-tenant demo shortcut.
8486# Real sellers must scope directives and tasks by account_id.
8991# Seeded creative formats keyed by the string format ID the storyboard supplies.
9092# list_creative_formats merges these in so storyboard references resolve.
9193seeded_creative_formats : dict [str , dict [str , Any ]] = {}
94+
95+
96+ def _now_z () -> str :
97+ return datetime .now (timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
98+
99+
100+ def _package_creative_ids (pkg : dict [str , Any ]) -> list [str ]:
101+ ids : list [str ] = []
102+ for assignment in pkg .get ("creative_assignments" ) or []:
103+ if isinstance (assignment , dict ) and assignment .get ("creative_id" ):
104+ ids .append (str (assignment ["creative_id" ]))
105+ for creative in pkg .get ("creatives" ) or []:
106+ if isinstance (creative , dict ) and creative .get ("creative_id" ):
107+ ids .append (str (creative ["creative_id" ]))
108+ return list (dict .fromkeys (ids ))
109+
110+
111+ def _health_fields_for_media_buy (media_buy_id : str | None , mb : dict [str , Any ]) -> dict [str , Any ]:
112+ impaired_packages : dict [str , list [str ]] = {}
113+ for pkg in mb .get ("packages" , []):
114+ package_id = pkg .get ("package_id" )
115+ if not package_id :
116+ continue
117+ creative_ids = _package_creative_ids (pkg )
118+ if not creative_ids :
119+ continue
120+ if any (
121+ creatives .get (creative_id , {}).get ("status" ) in {"approved" , "active" }
122+ for creative_id in creative_ids
123+ ):
124+ continue
125+ for creative_id in creative_ids :
126+ creative_status = creatives .get (creative_id , {}).get ("status" )
127+ if creative_status in {"rejected" }:
128+ package_ids = impaired_packages .setdefault (creative_id , [])
129+ if package_id not in package_ids :
130+ package_ids .append (package_id )
131+ media_buy_key = media_buy_id or "__anonymous__"
132+ active_keys = {(media_buy_key , creative_id ) for creative_id in impaired_packages }
133+ for key in [
134+ key for key in open_impairments if key [0 ] == media_buy_key and key not in active_keys
135+ ]:
136+ del open_impairments [key ]
137+
138+ impairments : list [dict [str , Any ]] = []
139+ for creative_id , package_ids in impaired_packages .items ():
140+ creative = creatives .get (creative_id , {})
141+ key = (media_buy_key , creative_id )
142+ if key not in open_impairments :
143+ open_impairments [key ] = {
144+ "impairment_id" : f"imp-{ uuid .uuid4 ().hex [:8 ]} " ,
145+ "observed_at" : creative .get ("status_changed_at" ) or _now_z (),
146+ }
147+ impairment = open_impairments [key ]
148+ impairments .append (
149+ {
150+ "impairment_id" : impairment ["impairment_id" ],
151+ "resource_type" : "creative" ,
152+ "resource_id" : creative_id ,
153+ "package_ids" : package_ids ,
154+ "transition" : {"from" : "approved" , "to" : "rejected" },
155+ "reason_code" : "content_rejected" ,
156+ "reason" : "Creative is no longer approved for delivery." ,
157+ "observed_at" : impairment ["observed_at" ],
158+ "remediation" : "Assign an approved replacement creative." ,
159+ }
160+ )
161+ if impairments :
162+ return {"health" : "impaired" , "impairments" : impairments }
163+ return {"health" : "ok" , "impairments" : []}
164+
165+
92166# Single-shot directives registered by force_create_media_buy_arm; keyed by account_id.
93167pending_directives : dict [str , dict [str , Any ]] = {}
94168# Tasks registered when create_media_buy consumes a 'submitted' directive; keyed by task_id.
@@ -310,9 +384,7 @@ async def sync_governance(self, params: dict[str, Any], context: Any = None) ->
310384 {
311385 "account" : acct_ref ,
312386 "status" : "synced" ,
313- "governance_agents" : [
314- {"url" : a .get ("url" ), "categories" : a .get ("categories" , [])} for a in agents
315- ],
387+ "governance_agents" : [{"url" : a .get ("url" )} for a in agents ],
316388 }
317389 )
318390 return sync_governance_response (results )
@@ -345,7 +417,7 @@ async def get_products(self, params: dict[str, Any], context: Any = None) -> dic
345417 }
346418 ]
347419 return {
348- ** products_response (PRODUCTS ),
420+ ** products_response (PRODUCTS , cache_scope = "public" ),
349421 "proposals" : [
350422 {
351423 "proposal_id" : proposal_id ,
@@ -355,7 +427,7 @@ async def get_products(self, params: dict[str, Any], context: Any = None) -> dic
355427 }
356428 ],
357429 }
358- return products_response (PRODUCTS )
430+ return products_response (PRODUCTS , cache_scope = "public" )
359431
360432 async def create_media_buy (self , params : dict [str , Any ], context : Any = None ) -> dict [str , Any ]:
361433 account_id = (params .get ("account" ) or {}).get ("account_id" ) or _DEFAULT_ACCOUNT_ID
@@ -478,6 +550,7 @@ async def get_media_buys(self, params: dict[str, Any], context: Any = None) -> d
478550 "currency" : mb .get ("currency" , "USD" ),
479551 "packages" : mb .get ("packages" , []),
480552 "total_budget" : total_budget ,
553+ ** _health_fields_for_media_buy (mb_id , mb ),
481554 }
482555 )
483556 return media_buys_response (results )
@@ -605,12 +678,11 @@ async def sync_creatives(self, params: dict[str, Any], context: Any = None) -> d
605678 results = []
606679 for c in params .get ("creatives" , []):
607680 creative_id = c .get ("creative_id" ) or f"c-{ uuid .uuid4 ().hex [:8 ]} "
608- creatives [creative_id ] = {** c , "status" : "approved" }
681+ creatives [creative_id ] = {** c , "status" : "approved" , "status_changed_at" : _now_z () }
609682 results .append (
610683 {
611684 "creative_id" : creative_id ,
612685 "action" : "created" ,
613- "status" : "approved" ,
614686 }
615687 )
616688 # Transition any media buys waiting on creatives to pending_start
@@ -696,6 +768,7 @@ async def force_creative_status(
696768 current_state = prev ,
697769 )
698770 c ["status" ] = status
771+ c ["status_changed_at" ] = _now_z ()
699772 return {"previous_state" : prev , "current_state" : status }
700773
701774 async def simulate_delivery (
@@ -900,6 +973,7 @@ async def seed_creative(
900973 data = dict (fixture or {})
901974 cid = creative_id or data .get ("creative_id" ) or f"c-seeded-{ uuid .uuid4 ().hex [:8 ]} "
902975 data ["creative_id" ] = cid
976+ data .setdefault ("status_changed_at" , _now_z ())
903977 creatives [cid ] = data
904978 return {"creative_id" : cid }
905979
0 commit comments