diff --git a/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java b/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java index ea15def357c..389fb88b147 100644 --- a/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java +++ b/src/main/java/org/prebid/server/bidder/seedtag/SeedtagBidder.java @@ -7,6 +7,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderCall; @@ -37,6 +38,7 @@ public class SeedtagBidder implements Bidder { new TypeReference<>() { }; private static final String BIDDER_CURRENCY = "USD"; + private static final String INTEGRATION_TYPE_RON_ID = "ronId"; private final String endpointUrl; private final JacksonMapper mapper; @@ -58,8 +60,8 @@ public Result>> makeHttpRequests(BidRequest request for (Imp imp : request.getImp()) { try { + validateImpExt(imp); final Price bidFloorPrice = resolveBidFloor(imp, request); - modifiedImps.add(modifyImp(imp, bidFloorPrice)); } catch (PreBidException e) { errors.add(BidderError.badInput(e.getMessage())); @@ -79,6 +81,29 @@ public Result>> makeHttpRequests(BidRequest request errors); } + private void validateImpExt(Imp imp) { + final ExtImpSeedtag ext; + try { + ext = mapper.mapper().convertValue(imp.getExt(), SEEDTAG_EXT_TYPE_REFERENCE).getBidder(); + } catch (Exception e) { + throw new PreBidException("Invalid imp.ext.bidder for imp id: %s".formatted(imp.getId())); + } + + if (INTEGRATION_TYPE_RON_ID.equals(ext.getIntegrationType())) { + if (StringUtils.isBlank(ext.getPublisherId())) { + throw new PreBidException( + "imp id %s: publisherId is required when integrationType is '%s'" + .formatted(imp.getId(), INTEGRATION_TYPE_RON_ID)); + } + } else { + if (StringUtils.isBlank(ext.getAdUnitId())) { + throw new PreBidException( + "imp id %s: adUnitId is required when integrationType is not '%s'" + .formatted(imp.getId(), INTEGRATION_TYPE_RON_ID)); + } + } + } + private static Imp modifyImp(Imp imp, Price bidFloorPrice) { return imp.toBuilder() .bidfloorcur(bidFloorPrice.getCurrency()) diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java index 364b63fb7dc..57e44c16ff5 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/seedtag/ExtImpSeedtag.java @@ -9,4 +9,10 @@ public class ExtImpSeedtag { @JsonProperty("adUnitId") String adUnitId; + @JsonProperty("publisherId") + String publisherId; + + @JsonProperty("integrationType") + String integrationType; + } diff --git a/src/main/resources/static/bidder-params/seedtag.json b/src/main/resources/static/bidder-params/seedtag.json index 8d84b059fd0..4e09dd7eb69 100644 --- a/src/main/resources/static/bidder-params/seedtag.json +++ b/src/main/resources/static/bidder-params/seedtag.json @@ -8,9 +8,20 @@ "type": "string", "description": "Ad Unit ID", "minLength": 1 + }, + "publisherId": { + "type": "string", + "description": "Publisher ID (editorial group ID)", + "minLength": 1 + }, + "integrationType": { + "type": "string", + "description": "Integration type", + "enum": ["ronId"] } }, - "required": [ - "adUnitId" + "oneOf": [ + { "required": ["adUnitId"] }, + { "required": ["publisherId", "integrationType"] } ] } diff --git a/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java b/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java index a0e0fb5880b..e16d40f46b4 100644 --- a/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/seedtag/SeedtagBidderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.seedtag; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; @@ -20,6 +21,7 @@ import org.prebid.server.bidder.model.Result; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.seedtag.ExtImpSeedtag; import java.math.BigDecimal; import java.util.List; @@ -127,6 +129,115 @@ public void makeHttpRequestsShouldSkipImpsWithCurrencyThatCanNotBeConverted() { .hasSize(1); } + @Test + public void makeHttpRequestsShouldSucceedWithPublisherIdAndRonIdIntegrationType() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(singletonList( + givenImp(ExtImpSeedtag.of(null, "somePubId", "ronId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + assertThat(result.getValue().get(0).getPayload().getImp()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldSucceedWithAdUnitIdAndPublisherIdWhenIntegrationTypeIsRonId() { + // given: adUnitId is irrelevant when integrationType is ronId — publisherId is what matters + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(singletonList( + givenImp(ExtImpSeedtag.of("someAdUnitId", "somePubId", "ronId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldSucceedWithAdUnitIdWhenIntegrationTypeIsAbsent() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(singletonList( + givenImp(ExtImpSeedtag.of("someAdUnitId", null, null))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1); + } + + @Test + public void makeHttpRequestsShouldSkipImpWithRonIdIntegrationTypeButMissingPublisherId() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(singletonList( + givenImp(ExtImpSeedtag.of("someAdUnitId", null, "ronId"))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).contains("publisherId is required when integrationType is 'ronId'"); + }); + } + + @Test + public void makeHttpRequestsShouldSkipImpWithNoAdUnitIdAndNoRonIdIntegrationType() { + // given: no adUnitId and integrationType is not ronId → adUnitId is required + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(singletonList( + givenImp(ExtImpSeedtag.of(null, "somePubId", null))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).contains("adUnitId is required when integrationType is not 'ronId'"); + }); + } + + @Test + public void makeHttpRequestsShouldSkipImpWithNoAdUnitIdAndNoParams() { + // given + final BidRequest bidRequest = givenBidRequest( + identity(), + requestBuilder -> requestBuilder.imp(singletonList( + givenImp(ExtImpSeedtag.of(null, null, null))))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getValue()).isEmpty(); + assertThat(result.getErrors()).hasSize(1) + .allSatisfy(error -> { + assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input); + assertThat(error.getMessage()).contains("adUnitId is required when integrationType is not 'ronId'"); + }); + } + @Test public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProcessingException { // given @@ -231,7 +342,15 @@ private static BidRequest givenBidRequest(UnaryOperator impCusto } private static Imp givenImp(UnaryOperator impCustomizer) { - return impCustomizer.apply(Imp.builder().id("123")).build(); + final ObjectNode bidderExt = mapper.createObjectNode(); + bidderExt.set("bidder", mapper.valueToTree(ExtImpSeedtag.of("someAdUnitId", null, null))); + return impCustomizer.apply(Imp.builder().id("123").ext(bidderExt)).build(); + } + + private static Imp givenImp(ExtImpSeedtag extImpSeedtag) { + final ObjectNode bidderExt = mapper.createObjectNode(); + bidderExt.set("bidder", mapper.valueToTree(extImpSeedtag)); + return Imp.builder().id("123").ext(bidderExt).build(); } private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { diff --git a/src/test/java/org/prebid/server/it/SeedtagTest.java b/src/test/java/org/prebid/server/it/SeedtagTest.java index 318f2bb506d..f4892edbec5 100644 --- a/src/test/java/org/prebid/server/it/SeedtagTest.java +++ b/src/test/java/org/prebid/server/it/SeedtagTest.java @@ -30,4 +30,5 @@ public void openrtb2AuctionShouldRespondWithBidsFromSeedtag() throws IOException assertJsonEquals("openrtb2/seedtag/test-auction-seedtag-response.json", response, singletonList("seedtag")); } + }