@prefix : <https://ontology.inferal.com/modules/advertising-entity/> .
@prefix adm: <https://ontology.inferal.com/modules/advertising-measurement/> .
@prefix ci: <https://ontology.inferal.com/modules/company-intelligence/> .
@prefix ex: <https://example.com/advertising-entity/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rng: <https://ontology.inferal.com/modules/range/> .
@prefix time: <http://www.w3.org/2006/time#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

# Worked instances for the Advertising Entity module.
#
# Five scenarios:
#   A. A hierarchy on a platform whose tier names agree with the shared ones,
#      with containment reused from the measurement module rather than restated.
#   B. The same hierarchy on a platform whose tier names disagree, where the
#      provider's own campaign is the targeting tier.
#   C. One creative delivered by several ads.
#   D. An account held by an agency on a client's behalf, and another held in
#      house, so two holders appear.
#   E. Two platforms issuing the same identifier value, which is the collision
#      that makes an unqualified identifier useless.


# --- Instants used by the validity intervals ---------------------------------

ex:t-start a time:Instant ;
    time:inXSDDateTimeStamp "2026-04-01T00:00:00Z"^^xsd:dateTimeStamp .
ex:t-move a time:Instant ;
    time:inXSDDateTimeStamp "2026-06-16T00:00:00Z"^^xsd:dateTimeStamp .

ex:since-start a rng:Range ;
    rdfs:label "from the start of the flight" ;
    rng:lowerBound ex:t-start ;
    rng:lowerInclusive true .

ex:until-move a rng:Range ;
    rdfs:label "from the start of the flight until the ad moved" ;
    rng:lowerBound ex:t-start ;
    rng:upperBound ex:t-move ;
    rng:lowerInclusive true ;
    rng:upperInclusive false .


# --- Scenario A: names that agree, containment reused ------------------------

ex:meta-account
    a :AdAccount ;
    rdfs:label "an advertiser account" ;
    :identifiedBy ex:meta-account-id ;
    :accountChannel adm:metaChannel ;
    :accountCurrency "USD" ;
    :entityName "Northwind Trading" ;
    :heldUnderRole ex:in-house-holding .

ex:meta-account-id
    a :ProviderIdentity ;
    :identifierValue "act_1180022" ;
    :issuingChannel adm:metaChannel .

ex:meta-campaign
    a :Campaign ;
    rdfs:label "a campaign, which this provider also calls a campaign" ;
    :identifiedBy ex:meta-campaign-id ;
    :entityName "Summer sale" ;
    :pursuesObjective :salesObjective .

ex:meta-campaign-id
    a :ProviderIdentity ;
    :identifierValue "23851004" ;
    :issuingChannel adm:metaChannel .

ex:meta-adset
    a :TargetingGroup ;
    rdfs:label "a targeting group, which this provider calls an ad set" ;
    :identifiedBy ex:meta-adset-id ;
    :entityName "Lookalike, 25 to 44" ;
    :carriesTargeting ex:lookalike-audience ;
    :bidAmount ex:meta-bid .

ex:meta-adset-id
    a :ProviderIdentity ;
    :identifierValue "23851009" ;
    :issuingChannel adm:metaChannel .

ex:lookalike-audience
    a rdfs:Resource ;
    rdfs:label "a lookalike audience specification" ;
    rdfs:comment "Left unstructured: this provider ships targeting as an untyped blob, and describing its internals would make that data unrepresentable." .

ex:meta-bid
    a adm:MonetaryAmount ;
    adm:amountValue "2.40"^^xsd:decimal ;
    adm:currencyCode "USD" .

# Containment comes from the measurement module, reified with the interval it
# held over. Restating it here as a parent property is what would let a
# historical metric be read against today's hierarchy.

ex:campaign-in-account
    a adm:HierarchyMembership ;
    rdfs:label "the campaign sits in the account" ;
    adm:containedResource ex:meta-campaign ;
    adm:containedIn ex:meta-account ;
    adm:validityWindow ex:since-start .

ex:adset-in-campaign
    a adm:HierarchyMembership ;
    rdfs:label "the targeting group sits in the campaign" ;
    adm:containedResource ex:meta-adset ;
    adm:containedIn ex:meta-campaign ;
    adm:validityWindow ex:since-start .


# --- Scenario B: names that disagree -----------------------------------------
#
# This provider's campaign group is the budget and objective tier, and its
# campaign carries targeting and bid. Mapping on the word would put the two
# tiers the wrong way round, which is why the tiers are named for their function.

ex:linkedin-campaign-group
    a :Campaign ;
    rdfs:label "a campaign, which this provider calls a campaign group" ;
    rdfs:comment "Named a campaign group on the wire. It carries the objective and the budget scope, so it is the campaign tier." ;
    :identifiedBy ex:linkedin-group-id ;
    :entityName "Q3 demand generation" ;
    :pursuesObjective :leadObjective .

ex:linkedin-group-id
    a :ProviderIdentity ;
    :identifierValue "607441122" ;
    :issuingChannel adm:linkedinChannel .

ex:linkedin-campaign
    a :TargetingGroup ;
    rdfs:label "a targeting group, which this provider calls a campaign" ;
    rdfs:comment "Named a campaign on the wire, and it is not the campaign tier: it carries the audience and the bid, so it sits below the campaign group. This is the mapping a name-based reading gets backwards." ;
    :identifiedBy ex:linkedin-campaign-id ;
    :entityName "Senior operations, North America" ;
    :carriesTargeting ex:seniority-audience ;
    :bidAmount ex:linkedin-bid .

ex:linkedin-campaign-id
    a :ProviderIdentity ;
    :identifierValue "urn:li:sponsoredCampaign:607441987" ;
    :issuingChannel adm:linkedinChannel .

ex:seniority-audience
    a rdfs:Resource ;
    rdfs:label "a seniority and function targeting specification" .

ex:linkedin-bid
    a adm:MonetaryAmount ;
    adm:amountValue "11.75"^^xsd:decimal ;
    adm:currencyCode "USD" .

ex:campaign-in-group
    a adm:HierarchyMembership ;
    rdfs:label "the targeting tier sits in the budget tier, despite the names" ;
    adm:containedResource ex:linkedin-campaign ;
    adm:containedIn ex:linkedin-campaign-group ;
    adm:validityWindow ex:since-start .


# --- Scenario C: one creative, several ads -----------------------------------
#
# The same artwork runs in two targeting groups. Two ads, one creative. The
# folded form counts it twice and reports twice as much creative production as
# actually happened.

ex:summer-hero-creative
    a :Creative ;
    rdfs:label "Summer hero video and copy" ;
    rdfs:comment "Delivered by two ads in different targeting groups. Its impressions are the sum of theirs; it is still one creative." ;
    :identifiedBy ex:summer-hero-id ;
    :entityName "summer-hero-v3" ;
    :creativeFormat :videoFormat .

ex:summer-hero-id
    a :ProviderIdentity ;
    :identifierValue "120214881103450" ;
    :issuingChannel adm:metaChannel .

ex:ad-lookalike
    a :Ad ;
    rdfs:label "the hero creative, to the lookalike audience" ;
    :identifiedBy ex:ad-lookalike-id ;
    :deliversCreative ex:summer-hero-creative .

ex:ad-lookalike-id
    a :ProviderIdentity ;
    :identifierValue "23851101" ;
    :issuingChannel adm:metaChannel .

ex:ad-retargeting
    a :Ad ;
    rdfs:label "the same creative, to a retargeting audience" ;
    :identifiedBy ex:ad-retargeting-id ;
    :deliversCreative ex:summer-hero-creative .

ex:ad-retargeting-id
    a :ProviderIdentity ;
    :identifierValue "23851102" ;
    :issuingChannel adm:metaChannel .

# The ad moved between targeting groups partway through the flight, which is
# why the membership carries an interval rather than a bare parent link.
ex:ad-in-first-group
    a adm:HierarchyMembership ;
    rdfs:label "the ad sat in the first targeting group until it moved" ;
    adm:containedResource ex:ad-lookalike ;
    adm:containedIn ex:meta-adset ;
    adm:validityWindow ex:until-move .


# --- Scenario D: two holders -------------------------------------------------
#
# One account held in house, one held by an agency for the same client. Making
# an account a kind of organisation cannot express either: it would make the
# account the holder and lose the distinction between them.

ex:in-house-holding
    a ci:Role ;
    rdfs:label "held in house by the advertiser" ;
    ci:roleHolder ex:northwind-trading ;
    ci:roleInEntity ex:meta-account ;
    ci:roleTitle "advertiser" .

ex:agency-holding
    a ci:Role ;
    rdfs:label "held by an agency on the client's behalf" ;
    rdfs:comment "Time-bounded, because holders change and past spend belongs to whoever was responsible at the time." ;
    ci:roleHolder ex:brightpath-agency ;
    ci:roleInEntity ex:googleads-account ;
    ci:roleTitle "agency of record" .


# --- Scenario E: the identifier collision ------------------------------------
#
# Two platforms issue the value 23851004. On one it names a campaign; on the
# other it names an account. An identifier carried on a bare shared predicate
# joins them, which is the defect that makes the value-and-channel pair
# necessary rather than tidy.

ex:googleads-account
    a :AdAccount ;
    rdfs:label "an account an agency holds for a client" ;
    :identifiedBy ex:googleads-account-id , ex:colliding-account-id ;
    :accountChannel adm:googleAdsChannel ;
    :accountCurrency "GBP" ;
    :entityName "Northwind Trading, paid search" ;
    :heldUnderRole ex:agency-holding .

ex:googleads-account-id
    a :ProviderIdentity ;
    rdfs:comment "The identifier the platform running the ads issues." ;
    :identifierValue "customers/4419002133" ;
    :issuingChannel adm:googleAdsChannel .

ex:colliding-account-id
    a :ProviderIdentity ;
    rdfs:comment "A measurement platform issues its own identifier for this account, and the value it chose is one another platform already uses for an unrelated campaign. Both are needed to join the two sources, and neither identifies anything without its channel." ;
    :identifierValue "23851004" ;
    :issuingChannel adm:tiktokChannel .
