Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/ad_seller/interfaces/api/contract_mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,24 @@ def internal_deal_status_to_wire(status: Any) -> DealStatus:

def internal_deal_to_shared_deal(data: dict[str, Any]) -> Deal:
"""Build the shared :class:`Deal` primitive from the internal deal dict."""
product = data.get("product", {})
# Template bookings store flat fields. Preserve nested quote-booked
# sections when present, including deliberately empty sections.
product = data.get("product")
if product is None:
product = {"product_id": data.get("product_id", "")}
pricing = data.get("pricing")
if pricing is None:
pricing = {
"final_cpm": data.get("actual_price_cpm"),
"currency": data.get("currency", "USD"),
}
terms = data.get("terms")
if terms is None:
terms = {
"impressions": data.get("impressions"),
"flight_start": data.get("flight_start"),
"flight_end": data.get("flight_end"),
}
return Deal(
deal_id=data["deal_id"],
deal_type=DealType(data["deal_type"]),
Expand All @@ -361,8 +378,8 @@ def internal_deal_to_shared_deal(data: dict[str, Any]) -> Deal:
name=product.get("name", ""),
inventory_type=product.get("inventory_type"),
),
pricing=_quote_pricing_to_shared(data.get("pricing", {})),
terms=_quote_terms_to_shared(data.get("terms", {})),
pricing=_quote_pricing_to_shared(pricing),
terms=_quote_terms_to_shared(terms),
buyer_tier=AccessTier(data.get("buyer_tier", "public")),
seller_id=data.get("seller_id"),
expires_at=_parse_dt(data.get("expires_at")),
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/test_deal_status_wire_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,42 @@ def test_confirmed_deal_dict_maps_to_shared_deal(self):


class TestGetDealCreatedOutsideQuotePath:
async def test_template_post_and_get_preserve_the_booked_terms(self, client, mock_storage):
app.dependency_overrides[_get_optional_api_key_record] = lambda: SimpleNamespace(
identity=BuyerIdentity()
)
with (
patch("ad_seller.storage.factory.get_storage", return_value=mock_storage),
patch(
"ad_seller.interfaces.api.deps.get_product_catalog",
new=AsyncMock(return_value=_mock_catalog()),
),
):
created = await client.post(
"/api/v1/deals/from-template",
json={
"deal_type": "PD",
"product_id": "ctv-premium-sports",
"impressions": 1_000_000,
"flight_start": "2026-10-06",
"flight_end": "2026-11-05",
},
)
assert created.status_code == 201, created.text
booked = created.json()
response = await client.get(f"/api/v1/deals/{booked['deal_id']}")

assert response.status_code == 200, response.text
deal = response.json()["deal"]
assert deal["product"]["product_id"] == booked["product_id"]
assert deal["pricing"]["final_cpm"] == {
"amount_micros": round(booked["actual_price_cpm"] * 1_000_000),
"currency": "USD",
}
assert deal["terms"]["impressions"] == booked["impressions"]
assert deal["terms"]["flight_start"] == booked["flight_start"]
assert deal["terms"]["flight_end"] == booked["flight_end"]

async def test_from_template_deal_is_readable_as_booked(self, client, mock_storage):
"""Create via the from-template service path (what the REST route
and the MCP tool both call), then GET through the wire mapper."""
Expand Down
83 changes: 83 additions & 0 deletions tests/unit/test_template_deal_readback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Template deal fields survive conversion into the shared wire contract."""

from copy import deepcopy
from datetime import date

import pytest

from ad_seller.interfaces.api import contract_mappers as cm


def _flat_deal():
return {
"deal_id": "DEMO-TEMPLATE",
"deal_type": "PD",
"status": "confirmed",
"product_id": "ctv-sports",
"actual_price_cpm": 28.26,
"currency": "EUR",
"impressions": 1_000_000,
"flight_start": "2026-10-06",
"flight_end": "2026-11-05",
"created_at": "2026-10-05T12:00:00Z",
}


@pytest.mark.parametrize("null_sections", [False, True])
def test_flat_template_fields_are_mapped_without_changing_storage(null_sections):
stored = _flat_deal()
if null_sections:
stored.update(product=None, pricing=None, terms=None)
original = deepcopy(stored)

deal = cm.internal_deal_to_shared_deal(stored)

assert deal.product.product_id == "ctv-sports"
assert deal.product.name == ""
assert deal.pricing.final_cpm.amount_micros == 28_260_000
assert deal.pricing.final_cpm.currency == "EUR"
assert deal.pricing.base_cpm is None
assert deal.terms.impressions == 1_000_000
assert deal.terms.flight_start == date(2026, 10, 6)
assert deal.terms.flight_end == date(2026, 11, 5)
assert stored == original


def test_nested_sections_remain_authoritative_over_conflicting_flat_fields():
stored = _flat_deal()
stored.update(
product={"product_id": "nested-product", "name": "Booked product"},
pricing={"base_cpm": 40, "final_cpm": 32, "currency": "USD"},
terms={"impressions": 2_000_000, "flight_start": "2026-12-01", "guaranteed": True},
)
original = deepcopy(stored)

deal = cm.internal_deal_to_shared_deal(stored)

assert deal.product.product_id == "nested-product"
assert deal.product.name == "Booked product"
assert deal.pricing.base_cpm.amount_micros == 40_000_000
assert deal.pricing.final_cpm.amount_micros == 32_000_000
assert deal.pricing.final_cpm.currency == "USD"
assert deal.terms.impressions == 2_000_000
assert deal.terms.flight_start == date(2026, 12, 1)
assert deal.terms.flight_end is None
assert deal.terms.guaranteed is True
assert stored == original


def test_empty_nested_sections_do_not_pick_up_flat_values():
deal = cm.internal_deal_to_shared_deal(
{**_flat_deal(), "product": {}, "pricing": {}, "terms": {}}
)
assert deal.product.product_id == ""
assert deal.pricing.final_cpm is None
assert deal.terms.impressions is None


def test_zero_price_and_impressions_are_preserved():
deal = cm.internal_deal_to_shared_deal(
{**_flat_deal(), "actual_price_cpm": 0, "impressions": 0}
)
assert deal.pricing.final_cpm.amount_micros == 0
assert deal.terms.impressions == 0
Loading