Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/yieldagent/integrations/linkedin/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def line_item_locale(audience: Audience) -> dict[str, str]:
Derived from the first audience geo if it is a valid ISO 3166-1 alpha-2
code; defaults to en/US otherwise.
"""
country = audience.geos[0].upper() if audience.geos else "US"
country = audience.geos[0].strip().upper() if audience.geos else "US"
if pycountry.countries.get(alpha_2=country) is None:
country = "US"
return {"country": country, "language": "en"}
Expand Down
3 changes: 2 additions & 1 deletion src/yieldagent/integrations/linkedin/targeting.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ async def _resolve_geos(self, audience: Audience) -> tuple[list[str], list[str]]
"""
urns: list[str] = []
unresolved: list[str] = []
for code in audience.geos:
for raw in audience.geos:
code = raw.strip().upper()
name = _country_name(code)
urn = None
if name:
Expand Down
13 changes: 12 additions & 1 deletion tests/integrations/test_linkedin_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

from datetime import date

from yieldagent.domain import CreativeAsset, Flight
from yieldagent.domain import Audience, CreativeAsset, Flight
from yieldagent.integrations.linkedin.mapping import (
campaign_run_schedule,
creative_content_reference,
flight_to_run_schedule,
line_item_locale,
post_article_content,
)

Expand Down Expand Up @@ -74,3 +75,13 @@ def test_post_article_content_defaults_source_when_no_landing_url() -> None:

def test_creative_content_reference_wraps_post_urn() -> None:
assert creative_content_reference("urn:li:share:123") == {"reference": "urn:li:share:123"}


def test_line_item_locale_strips_and_uppercases_geo() -> None:
# A padded/lowercase code must still produce a valid locale, not fall back to US.
locale = line_item_locale(Audience(description="x", geos=["pt "]))
assert locale == {"country": "PT", "language": "en"}


def test_line_item_locale_defaults_to_us_for_unknown_code() -> None:
assert line_item_locale(Audience(description="x", geos=["ZZ"]))["country"] == "US"
15 changes: 15 additions & 0 deletions tests/integrations/test_linkedin_targeting.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ async def test_unknown_geo_code_is_unresolved_not_guessed() -> None:
assert resolved.unresolved == {"geos": ["ZZ"]}


async def test_geo_codes_are_normalized_before_lookup() -> None:
resolver = TargetingResolver(_FakeTargetingClient())
resolved = await resolver.resolve(Audience(description="x", geos=[" us ", "Pt"]))
# Whitespace/case variations still resolve to the same URNs.
assert _clause_facets(resolved.criteria) == {FACET_LOCATIONS: [_US_GEO, _PT_GEO]}
assert resolved.unresolved == {}


async def test_unresolved_geo_is_reported_normalized() -> None:
resolver = TargetingResolver(_FakeTargetingClient())
resolved = await resolver.resolve(Audience(description="x", geos=["zz "]))
# Unresolved output is the normalized code, not the raw padded input.
assert resolved.unresolved == {"geos": ["ZZ"]}


async def test_enum_facets_resolve_and_surface_misses() -> None:
resolver = TargetingResolver(_FakeTargetingClient())
resolved = await resolver.resolve(
Expand Down