diff --git a/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java b/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java index 9dfc6d158e3..81e1c8091f5 100644 --- a/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java +++ b/src/main/java/org/prebid/server/bidder/oms/OmsBidder.java @@ -1,18 +1,27 @@ package org.prebid.server.bidder.oms; +import com.fasterxml.jackson.core.type.TypeReference; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; +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; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.omx.ExtImpOms; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -23,6 +32,8 @@ public class OmsBidder implements Bidder { + private static final TypeReference> EXT_TYPE_REFERENCE = new TypeReference<>() { + }; private final String endpointUrl; private final JacksonMapper mapper; @@ -32,8 +43,31 @@ public OmsBidder(String endpointUrl, JacksonMapper mapper) { } @Override - public final Result>> makeHttpRequests(BidRequest bidRequest) { - return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); + public Result>> makeHttpRequests(BidRequest request) { + try { + final ExtImpOms impExt = parseImpExt(request.getImp().getFirst()); + final String publisherId = resolverPublisherId(impExt.getPid(), impExt.getPublisherId()); + final String encodedPublisherId = HttpUtil.encodeUrl(publisherId); + final String url = "%s?publisherId=%s".formatted(endpointUrl, encodedPublisherId); + return Result.withValue(BidderUtil.defaultRequest(request, url, mapper)); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); + } + } + + private ExtImpOms parseImpExt(Imp imp) throws PreBidException { + try { + return mapper.mapper().convertValue(imp.getExt(), EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("Invalid ext. Imp.Id: " + imp.getId()); + } + } + + private String resolverPublisherId(String pid, Integer publisherId) { + if (StringUtils.isEmpty(pid) && publisherId != null && publisherId > 0) { + return String.valueOf(publisherId); + } + return pid; } @Override @@ -59,7 +93,33 @@ private static List bidsFromResponse(BidResponse bidResponse) { .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> createBidderBid(bid, bidResponse.getCur())) .toList(); } + + private static BidderBid createBidderBid(Bid bid, String currency) { + final BidType bidType = getBidType(bid); + return BidderBid.builder() + .bid(bid) + .type(bidType) + .bidCurrency(currency) + .videoInfo(videoInfo(bidType, bid)) + .build(); + } + + private static BidType getBidType(Bid bid) { + return Objects.equals(bid.getMtype(), 2) ? BidType.video : BidType.banner; + } + + private static ExtBidPrebidVideo videoInfo(BidType bidType, Bid bid) { + if (bidType != BidType.video) { + return null; + } + final List cat = bid.getCat(); + final Integer duration = bid.getDur(); + + return ExtBidPrebidVideo.of( + ObjectUtils.defaultIfNull(duration, 0), + CollectionUtils.isNotEmpty(cat) ? cat.getFirst() : StringUtils.EMPTY); + } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/omx/ExtImpOms.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/omx/ExtImpOms.java new file mode 100644 index 00000000000..f6a96821d44 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/omx/ExtImpOms.java @@ -0,0 +1,13 @@ +package org.prebid.server.proto.openrtb.ext.request.omx; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +@Value(staticConstructor = "of") +public class ExtImpOms { + + String pid; + + @JsonProperty("publisherId") + Integer publisherId; +} diff --git a/src/main/resources/bidder-config/oms.yaml b/src/main/resources/bidder-config/oms.yaml index 0b46d3e01ed..16c4a4cac08 100644 --- a/src/main/resources/bidder-config/oms.yaml +++ b/src/main/resources/bidder-config/oms.yaml @@ -5,7 +5,9 @@ adapters: maintainer-email: prebid@onlinemediasolutions.com app-media-types: - banner + - video site-media-types: - banner + - video supported-vendors: vendor-id: 0 diff --git a/src/main/resources/static/bidder-params/oms.json b/src/main/resources/static/bidder-params/oms.json index 1ab7e25eb7f..fa9dec36f61 100644 --- a/src/main/resources/static/bidder-params/oms.json +++ b/src/main/resources/static/bidder-params/oms.json @@ -6,11 +6,25 @@ "properties": { "pid": { "type": "string", - "description": "An id used to identify OMS publisher.", + "description": "Deprecated: An id used to identify OMS publisher.", "minLength": 5 + }, + "publisherId": { + "type": "integer", + "description": "An ID used to identify OMS publisher.", + "minimum": 10000 } }, - "required": [ - "pid" + "oneOf": [ + { + "required": [ + "pid" + ] + }, + { + "required": [ + "publisherId" + ] + } ] } diff --git a/src/test/java/org/prebid/server/bidder/oms/OmsBidderTest.java b/src/test/java/org/prebid/server/bidder/oms/OmsBidderTest.java index 7d9f4e84e06..81b3186bc71 100644 --- a/src/test/java/org/prebid/server/bidder/oms/OmsBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/oms/OmsBidderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidder.oms; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Banner; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; @@ -15,7 +16,13 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.omx.ExtImpOms; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.function.UnaryOperator; @@ -24,7 +31,7 @@ import static java.util.function.UnaryOperator.identity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.bidder.model.BidderError.badInput; public class OmsBidderTest extends VertxTest { @@ -37,10 +44,40 @@ public void creationShouldFailOnInvalidEndpointUrl() { assertThatIllegalArgumentException().isThrownBy(() -> new OmsBidder("invalid_url", jacksonMapper)); } + @Test + public void makeHttpRequestsShouldReturnErrorWhenRequestHasInvalidImpression() { + // given + final ObjectNode invalidExt = mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode())); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(invalidExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).hasSize(1).first().isEqualTo(badInput("Invalid ext. Imp.Id: 123")); + } + @Test public void makeHttpRequestsShouldCreateExpectedUrl() { // given - final BidRequest bidRequest = givenBidRequest(identity()); + final ExtImpOms impExt = ExtImpOms.of("otherTagId", 12345); + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer.ext(givenImpExt(impExt))); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(1) + .extracting(HttpRequest::getUri) + .containsExactly("https://randomurl.com?publisherId=otherTagId"); + } + + @Test + public void makeHttpRequestsShouldCreateExpectedUrlWithPublisherId() { + // given + final ExtImpOms impExt = ExtImpOms.of(null, 12345); + final BidRequest bidRequest = givenBidRequest(impCustomizer -> impCustomizer.ext(givenImpExt(impExt))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -49,7 +86,45 @@ public void makeHttpRequestsShouldCreateExpectedUrl() { assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()).hasSize(1) .extracting(HttpRequest::getUri) - .containsExactly("https://randomurl.com"); + .containsExactly("https://randomurl.com?publisherId=12345"); + } + + @Test + public void makeHttpRequestsShouldIncludePidInRequestWhenPresent() { + // given + final ObjectNode bidderExt = mapper.createObjectNode().put("pid", "examplePid"); + final ObjectNode impExt = mapper.createObjectNode().set("bidder", bidderExt); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(impExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(impExt); + } + + @Test + public void makeHttpRequestsShouldIncludePublisherIdInRequestWhenPresent() { + // given + final ObjectNode bidderExt = mapper.createObjectNode().put("publisherId", 12345); + final ObjectNode impExt = mapper.createObjectNode().set("bidder", bidderExt); + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.ext(impExt)); + + // when + final Result>> result = target.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getExt) + .containsExactly(impExt); } @Test @@ -101,32 +176,97 @@ public void makeBidsShouldReturnBannerBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build())), - mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123")))); + mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123").mtype(1)))); // when final Result> result = target.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) - .containsExactly(BidderBid.of(givenBid(), banner, null)); + assertThat(result.getValue()).extracting(BidderBid::getType).containsExactly(BidType.banner); } @Test - public void makeBidsShouldReturnBannerBidIfBannerAndVideoAndAudioAndNativeIsAbsentInRequestImp() - throws JsonProcessingException { + public void makeBidsShouldReturnVideoBid() throws JsonProcessingException { // given final BidderCall httpCall = givenHttpCall( - givenBidRequest(identity()), - mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123")))); + givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build())), + mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123").mtype(2)))); // when final Result> result = target.makeBids(httpCall, null); // then assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).extracting(BidderBid::getType).containsExactly(BidType.video); + } + + @Test + public void makeBidsShouldReturnBannerWhenMTypeIsUnsupported() throws JsonProcessingException { + // given + final BidderCall httpCall = givenHttpCall( + givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build())), + mapper.writeValueAsString(givenBidResponse(impBuilder -> impBuilder.impid("123").mtype(99)))); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).extracting(BidderBid::getType).containsExactly(BidType.banner); + } + + @Test + public void makeBidsShouldExtractAllBidsFromMultipleSeatBids() throws JsonProcessingException { + // given + final Bid bid1 = Bid.builder().impid("bid1").mtype(1).build(); + final Bid bid2 = Bid.builder().impid("bid2").mtype(1).build(); + final Bid bid3 = Bid.builder().impid("bid3").mtype(2).build(); + + final SeatBid seatBid1 = SeatBid.builder().bid(Arrays.asList(bid1, bid2)).build(); + final SeatBid seatBid2 = SeatBid.builder().bid(Collections.singletonList(bid3)).build(); + + final BidResponse bidResponse = BidResponse.builder() + .seatbid(Arrays.asList(seatBid1, seatBid2)) + .cur("USD") + .build(); + final String bidResponseJson = mapper.writeValueAsString(bidResponse); + + final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder.banner(Banner.builder().build())); + final BidderCall httpCall = givenHttpCall(bidRequest, bidResponseJson); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()).hasSize(3) + .extracting(BidderBid::getType) + .containsExactly(BidType.banner, BidType.banner, BidType.video); + assertThat(result.getValue()).extracting(BidderBid::getBidCurrency).containsOnly("USD"); + } + + @Test + public void makeBidsShouldReturnVideoBidWithVideoInfo() throws JsonProcessingException { + // given + final Bid videoBid = Bid.builder() + .mtype(2) + .dur(30) + .cat(List.of("IAB1")) + .build(); + final BidResponse bidResponse = BidResponse.builder() + .seatbid(List.of(SeatBid.builder().bid(List.of(videoBid)).build())) + .cur("USD") + .build(); + final BidderCall httpCall = givenHttpCall(null, mapper.writeValueAsString(bidResponse)); + + // when + final Result> result = target.makeBids(httpCall, null); + + // then assertThat(result.getValue()) - .containsExactly(BidderBid.of(givenBid(), banner, null)); + .extracting(BidderBid::getVideoInfo) + .containsExactly(ExtBidPrebidVideo.of(30, "IAB1")); } private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { @@ -148,14 +288,14 @@ private static BidResponse givenBidResponse(Function givenHttpCall(BidRequest bidRequest, String body) { return BidderCall.succeededHttp( HttpRequest.builder().payload(bidRequest).build(), HttpResponse.of(200, null, body), null); } + + private ObjectNode givenImpExt(ExtImpOms impExt) { + return mapper.valueToTree(ExtPrebid.of(null, impExt)); + } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-request.json index 777f07481d7..38808efff2a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-request.json @@ -9,7 +9,7 @@ }, "ext": { "oms": { - "pid": "exampleProperty" + "publisherId": 12345 } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json index 315114b4f75..52f16380f34 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json @@ -9,6 +9,7 @@ "exp": 300, "price": 3.33, "crid": "creativeId", + "mtype": 1, "ext": { "origbidcpm": 3.33, "prebid": { diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-request.json index 1eb99acd606..d0020c7c009 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-request.json @@ -11,7 +11,7 @@ "ext": { "tid": "${json-unit.any-string}", "bidder": { - "pid": "exampleProperty" + "publisherId": 12345 } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-response.json index 04d26e04318..6922c116b46 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-oms-bid-response.json @@ -7,7 +7,8 @@ "id": "bid_id", "impid": "imp_id", "price": 3.33, - "crid": "creativeId" + "crid": "creativeId", + "mtype": 1 } ] }