|
112 | 112 | }, |
113 | 113 | "delivery_measurement": {"provider": "internal"}, |
114 | 114 | }, |
| 115 | + # Storyboard test fixtures referenced by @adcp/client compliance YAMLs. |
| 116 | + # The runner's media_buy_seller suite expects these product IDs to be |
| 117 | + # discoverable without an explicit seed_product call. |
| 118 | + { |
| 119 | + "product_id": "outdoor_display_q2", |
| 120 | + "name": "Outdoor Display Q2", |
| 121 | + "description": "Outdoor display inventory for Q2 storyboards", |
| 122 | + "delivery_type": "non_guaranteed", |
| 123 | + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], |
| 124 | + "format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}], |
| 125 | + "pricing_options": [ |
| 126 | + { |
| 127 | + "pricing_option_id": "cpm_standard", |
| 128 | + "pricing_model": "cpm", |
| 129 | + "floor_price": 5.00, |
| 130 | + "currency": "USD", |
| 131 | + } |
| 132 | + ], |
| 133 | + "reporting_capabilities": { |
| 134 | + "available_metrics": ["impressions", "spend", "clicks", "ctr"], |
| 135 | + "available_reporting_frequencies": ["hourly", "daily"], |
| 136 | + "date_range_support": "date_range", |
| 137 | + "supports_webhooks": False, |
| 138 | + "expected_delay_minutes": 60, |
| 139 | + "timezone": "UTC", |
| 140 | + }, |
| 141 | + "delivery_measurement": {"provider": "internal"}, |
| 142 | + }, |
| 143 | + { |
| 144 | + "product_id": "outdoor_video_q2", |
| 145 | + "name": "Outdoor Video Q2", |
| 146 | + "description": "Outdoor video inventory for Q2 storyboards", |
| 147 | + "delivery_type": "non_guaranteed", |
| 148 | + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], |
| 149 | + "format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}], |
| 150 | + "pricing_options": [ |
| 151 | + { |
| 152 | + "pricing_option_id": "cpm_standard", |
| 153 | + "pricing_model": "cpm", |
| 154 | + "floor_price": 8.00, |
| 155 | + "currency": "USD", |
| 156 | + } |
| 157 | + ], |
| 158 | + "reporting_capabilities": { |
| 159 | + "available_metrics": ["impressions", "spend", "clicks", "ctr"], |
| 160 | + "available_reporting_frequencies": ["hourly", "daily"], |
| 161 | + "date_range_support": "date_range", |
| 162 | + "supports_webhooks": False, |
| 163 | + "expected_delay_minutes": 60, |
| 164 | + "timezone": "UTC", |
| 165 | + }, |
| 166 | + "delivery_measurement": {"provider": "internal"}, |
| 167 | + }, |
| 168 | + { |
| 169 | + "product_id": "sports_preroll_q2", |
| 170 | + "name": "Sports Preroll Q2", |
| 171 | + "description": "Sports preroll video inventory for Q2 storyboards", |
| 172 | + "delivery_type": "guaranteed", |
| 173 | + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], |
| 174 | + "format_ids": [{"agent_url": AGENT_URL, "id": "display_970x250"}], |
| 175 | + "pricing_options": [ |
| 176 | + { |
| 177 | + "pricing_option_id": "cpm_guaranteed", |
| 178 | + "pricing_model": "cpm", |
| 179 | + "floor_price": 25.00, |
| 180 | + "currency": "USD", |
| 181 | + } |
| 182 | + ], |
| 183 | + "reporting_capabilities": { |
| 184 | + "available_metrics": ["impressions", "spend", "clicks", "ctr"], |
| 185 | + "available_reporting_frequencies": ["hourly", "daily"], |
| 186 | + "date_range_support": "date_range", |
| 187 | + "supports_webhooks": False, |
| 188 | + "expected_delay_minutes": 60, |
| 189 | + "timezone": "UTC", |
| 190 | + }, |
| 191 | + "delivery_measurement": {"provider": "internal"}, |
| 192 | + }, |
| 193 | + { |
| 194 | + "product_id": "lifestyle_display_q2", |
| 195 | + "name": "Lifestyle Display Q2", |
| 196 | + "description": "Lifestyle display inventory for Q2 storyboards", |
| 197 | + "delivery_type": "non_guaranteed", |
| 198 | + "publisher_properties": [{"publisher_domain": "example.com", "selection_type": "all"}], |
| 199 | + "format_ids": [{"agent_url": AGENT_URL, "id": "display_300x250"}], |
| 200 | + "pricing_options": [ |
| 201 | + { |
| 202 | + "pricing_option_id": "cpm_standard", |
| 203 | + "pricing_model": "cpm", |
| 204 | + "floor_price": 6.00, |
| 205 | + "currency": "USD", |
| 206 | + } |
| 207 | + ], |
| 208 | + "reporting_capabilities": { |
| 209 | + "available_metrics": ["impressions", "spend", "clicks", "ctr"], |
| 210 | + "available_reporting_frequencies": ["hourly", "daily"], |
| 211 | + "date_range_support": "date_range", |
| 212 | + "supports_webhooks": False, |
| 213 | + "expected_delay_minutes": 60, |
| 214 | + "timezone": "UTC", |
| 215 | + }, |
| 216 | + "delivery_measurement": {"provider": "internal"}, |
| 217 | + }, |
115 | 218 | ] |
116 | 219 |
|
117 | 220 |
|
@@ -262,14 +365,42 @@ async def create_media_buy(self, params: dict[str, Any], context: Any = None) -> |
262 | 365 | field="product_id", |
263 | 366 | suggestion="Use get_products to discover available products", |
264 | 367 | ) |
265 | | - packages.append( |
266 | | - { |
267 | | - "package_id": f"pkg-{uuid.uuid4().hex[:8]}", |
268 | | - "product_id": product_id, |
269 | | - "pricing_option_id": pkg.get("pricing_option_id"), |
270 | | - "budget": pkg.get("budget"), |
271 | | - } |
272 | | - ) |
| 368 | + # Reject aggressive measurement_terms. The compliance runner sends |
| 369 | + # max_variance_percent=0 with a c30 window (unworkable) on the |
| 370 | + # rejection path, then retries with c7 + 10% variance. |
| 371 | + terms = pkg.get("measurement_terms") or {} |
| 372 | + billing = terms.get("billing_measurement") or {} |
| 373 | + window = billing.get("measurement_window") |
| 374 | + variance = billing.get("max_variance_percent") |
| 375 | + if (variance is not None and variance < 5) or ( |
| 376 | + window is not None and window not in ("c3", "c7") |
| 377 | + ): |
| 378 | + return adcp_error( |
| 379 | + "TERMS_REJECTED", |
| 380 | + "Measurement terms unworkable: variance must be >=5%, " |
| 381 | + "measurement_window must be c3 or c7.", |
| 382 | + field="measurement_terms", |
| 383 | + recovery="correctable", |
| 384 | + ) |
| 385 | + built_pkg: dict[str, Any] = { |
| 386 | + "package_id": f"pkg-{uuid.uuid4().hex[:8]}", |
| 387 | + "product_id": product_id, |
| 388 | + "pricing_option_id": pkg.get("pricing_option_id"), |
| 389 | + "budget": pkg.get("budget"), |
| 390 | + } |
| 391 | + # Persist caller-supplied package fields the runner expects to |
| 392 | + # round-trip on get_media_buys (targeting_overlay) or to drive |
| 393 | + # status transitions (creative_assignments, creatives, |
| 394 | + # measurement_terms). |
| 395 | + for field in ( |
| 396 | + "targeting_overlay", |
| 397 | + "creative_assignments", |
| 398 | + "creatives", |
| 399 | + "measurement_terms", |
| 400 | + ): |
| 401 | + if pkg.get(field) is not None: |
| 402 | + built_pkg[field] = pkg[field] |
| 403 | + packages.append(built_pkg) |
273 | 404 |
|
274 | 405 | has_creatives = any( |
275 | 406 | pkg.get("creative_assignments") or pkg.get("creatives") for pkg in params["packages"] |
@@ -320,15 +451,30 @@ async def update_media_buy(self, params: dict[str, Any], context: Any = None) -> |
320 | 451 | return adcp_error("CONFLICT", "Revision mismatch - refetch and retry") |
321 | 452 |
|
322 | 453 | if params.get("packages"): |
323 | | - existing_pkg_ids = {p["package_id"] for p in mb.get("packages", [])} |
| 454 | + existing_by_id = {p["package_id"]: p for p in mb.get("packages", [])} |
324 | 455 | for pkg_update in params["packages"]: |
325 | 456 | pkg_id = pkg_update.get("package_id") |
326 | | - if pkg_id and pkg_id not in existing_pkg_ids: |
| 457 | + if pkg_id and pkg_id not in existing_by_id: |
327 | 458 | return adcp_error( |
328 | 459 | "PACKAGE_NOT_FOUND", |
329 | 460 | f"Package '{pkg_id}' not found in media buy {mb_id}", |
330 | 461 | field="package_id", |
331 | 462 | ) |
| 463 | + # Apply incoming targeting/budget/creative deltas to the |
| 464 | + # persisted package so a subsequent get_media_buys reflects |
| 465 | + # the change. Storyboard inventory_list_targeting/update |
| 466 | + # asserts targeting_overlay round-trips through this path. |
| 467 | + if pkg_id and pkg_id in existing_by_id: |
| 468 | + target = existing_by_id[pkg_id] |
| 469 | + for field in ( |
| 470 | + "targeting_overlay", |
| 471 | + "creative_assignments", |
| 472 | + "creatives", |
| 473 | + "measurement_terms", |
| 474 | + "budget", |
| 475 | + ): |
| 476 | + if pkg_update.get(field) is not None: |
| 477 | + target[field] = pkg_update[field] |
332 | 478 |
|
333 | 479 | status = mb["status"] |
334 | 480 | if status == "pending_creatives" and params.get("packages"): |
@@ -699,7 +845,11 @@ async def seed_creative_format( |
699 | 845 | context: Any = None, |
700 | 846 | ) -> dict[str, Any]: |
701 | 847 | data = dict(fixture or {}) |
702 | | - fid = format_id or (data.get("format_id") or {}).get("id") or f"fmt-seeded-{uuid.uuid4().hex[:8]}" |
| 848 | + fid = ( |
| 849 | + format_id |
| 850 | + or (data.get("format_id") or {}).get("id") |
| 851 | + or f"fmt-seeded-{uuid.uuid4().hex[:8]}" |
| 852 | + ) |
703 | 853 | data.setdefault("format_id", {"agent_url": AGENT_URL, "id": fid}) |
704 | 854 | data.setdefault("name", fid) |
705 | 855 | data.setdefault("renders", []) |
|
0 commit comments