diff --git a/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java b/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java index 2e88d1becd7..d184aefc297 100644 --- a/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java +++ b/src/main/java/org/prebid/server/bidder/medianet/MedianetBidder.java @@ -3,19 +3,23 @@ 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.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.medianet.model.response.InterestGroupAuctionIntent; +import org.prebid.server.bidder.medianet.model.response.MedianetBidResponse; +import org.prebid.server.bidder.medianet.model.response.MedianetBidResponseExt; 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.CompositeBidderResponse; 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.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig; import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; @@ -24,6 +28,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; public class MedianetBidder implements Bidder { @@ -40,22 +45,36 @@ public Result>> makeHttpRequests(BidRequest bidRequ return Result.withValue(BidderUtil.defaultRequest(bidRequest, endpointUrl, mapper)); } + /** + * @deprecated for this bidder in favor of @link{makeBidderResponse} which supports additional response data + */ @Override - public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final BidResponse bidResponse; + @Deprecated(forRemoval = true) + public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + return Result.withError(BidderError.generic("Deprecated adapter method invoked")); + } + + @Override + public final CompositeBidderResponse makeBidderResponse(BidderCall httpCall, BidRequest bidRequest) { + final MedianetBidResponse bidResponse; try { - bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); + bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), MedianetBidResponse.class); } catch (DecodeException e) { - return Result.withError(BidderError.badServerResponse(e.getMessage())); + return CompositeBidderResponse.withError(BidderError.badServerResponse(e.getMessage())); } final List errors = new ArrayList<>(); final List bids = extractBids(httpCall.getRequest().getPayload(), bidResponse, errors); + final List fledgeAuctionConfigs = extractFledge(bidResponse); - return Result.of(bids, errors); + return CompositeBidderResponse.builder() + .bids(bids) + .fledgeAuctionConfigs(fledgeAuctionConfigs) + .errors(errors) + .build(); } - private static List extractBids(BidRequest bidRequest, BidResponse bidResponse, + private static List extractBids(BidRequest bidRequest, MedianetBidResponse bidResponse, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); @@ -84,9 +103,8 @@ private static BidType resolveBidType(Bid bid, List imps) { case 2 -> BidType.video; case 3 -> BidType.audio; case 4 -> BidType.xNative; - default -> - throw new PreBidException("Unable to fetch mediaType: %s" - .formatted(bid.getImpid())); + default -> throw new PreBidException("Unable to fetch mediaType: %s" + .formatted(bid.getImpid())); }; } @@ -119,4 +137,16 @@ private static BidType resolveBidTypeFromImpId(String impId, List imps) { return BidType.banner; } + + private static List extractFledge(MedianetBidResponse bidResponse) { + return Optional.ofNullable(bidResponse) + .map(MedianetBidResponse::getExt) + .map(MedianetBidResponseExt::getIgi) + .orElse(Collections.emptyList()) + .stream() + .map(InterestGroupAuctionIntent::getIgs) + .flatMap(Collection::stream) + .map(e -> FledgeAuctionConfig.builder().impId(e.getImpId()).config(e.getConfig()).build()) + .toList(); + } } diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionBuyer.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionBuyer.java new file mode 100644 index 00000000000..04340fd8321 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionBuyer.java @@ -0,0 +1,23 @@ +package org.prebid.server.bidder.medianet.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value +public class InterestGroupAuctionBuyer { + + String origin; + + @JsonProperty("maxbid") + Double maxBid; + + @JsonProperty("cur") + String currency; + + @JsonProperty("pbs") + String buyerSignals; + + @JsonProperty("ps") + ObjectNode prioritySignals; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionIntent.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionIntent.java new file mode 100644 index 00000000000..e060c1fb5dd --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionIntent.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.medianet.model.response; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Builder +@Value +public class InterestGroupAuctionIntent { + + List igb; + + List igs; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionSeller.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionSeller.java new file mode 100644 index 00000000000..15ae3ade13b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/InterestGroupAuctionSeller.java @@ -0,0 +1,16 @@ +package org.prebid.server.bidder.medianet.model.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class InterestGroupAuctionSeller { + + @JsonProperty(value = "impid") + String impId; + + ObjectNode config; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponse.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponse.java new file mode 100644 index 00000000000..4677294e6fc --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponse.java @@ -0,0 +1,26 @@ +package org.prebid.server.bidder.medianet.model.response; + +import com.iab.openrtb.response.SeatBid; +import lombok.Builder; +import lombok.Value; + +import java.util.List; + +@Value +@Builder +public class MedianetBidResponse { + + String id; + + List seatbid; + + String bidid; + + String cur; + + String customdata; + + Integer nbr; + + MedianetBidResponseExt ext; +} diff --git a/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponseExt.java b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponseExt.java new file mode 100644 index 00000000000..2ce5775704c --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/medianet/model/response/MedianetBidResponseExt.java @@ -0,0 +1,11 @@ +package org.prebid.server.bidder.medianet.model.response; + +import lombok.Value; + +import java.util.List; + +@Value(staticConstructor = "of") +public class MedianetBidResponseExt { + + List igi; +} diff --git a/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java b/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java index fb541c35d5d..42390ad0f79 100644 --- a/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/medianet/MedianetBidderTest.java @@ -9,18 +9,25 @@ import com.iab.openrtb.response.SeatBid; import org.junit.Test; import org.prebid.server.VertxTest; +import org.prebid.server.bidder.medianet.model.response.InterestGroupAuctionIntent; +import org.prebid.server.bidder.medianet.model.response.InterestGroupAuctionSeller; +import org.prebid.server.bidder.medianet.model.response.MedianetBidResponse; +import org.prebid.server.bidder.medianet.model.response.MedianetBidResponseExt; 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.CompositeBidderResponse; 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.response.FledgeAuctionConfig; import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -63,7 +70,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { final BidderCall httpCall = sampleHttpCall(givenBidRequest(), "invalid response"); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then assertThat(result.getErrors()).hasSize(1) @@ -78,11 +85,11 @@ public void makeBidsShouldReturnEmptyListIfBidResponseIsNull() throws JsonProces httpCall = sampleHttpCall(givenBidRequest(), mapper.writeValueAsString(null)); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then + assertThat(result.getBids()).isEmpty(); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); } @Test @@ -92,11 +99,11 @@ public void makeBidsShouldReturnEmptyListIfBidResponseSeatBidIsNull() throws Jso httpCall = sampleHttpCall(null, mapper.writeValueAsString(BidResponse.builder().build())); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then + assertThat(result.getBids()).isEmpty(); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).isEmpty(); } @Test @@ -107,12 +114,12 @@ public void makeBidsShouldReturnBannerBidIfBannerIsPresent() throws JsonProcessi mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("123")))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then - assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()) + assertThat(result.getBids()) .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), banner, "USD")); + assertThat(result.getErrors()).isEmpty(); } @Test @@ -133,16 +140,16 @@ public void makeBidsShouldReturnCorrespondingMtypesAndAdTypes() throws JsonProce mapper.writeValueAsString(sampleMultiFormatBidResponse(bids))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then + assertThat(result.getBids()).hasSize(4); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(4); final BidderBid bannerBid = BidderBid.of(bid1, banner, "USD"); final BidderBid videoBid = BidderBid.of(bid2, video, "USD"); final BidderBid audioBid = BidderBid.of(bid3, audio, "USD"); final BidderBid xNativeBid = BidderBid.of(bid4, xNative, "USD"); - assertThat(result.getValue()).containsExactlyInAnyOrder(bannerBid, videoBid, audioBid, xNativeBid); + assertThat(result.getBids()).containsExactlyInAnyOrder(bannerBid, videoBid, audioBid, xNativeBid); } @Test @@ -153,13 +160,13 @@ public void makeBidsShouldReturnAdTypeAccordingToImpressionIfMtypeIsAbsent() thr mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("imp_id")))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then + assertThat(result.getBids()).hasSize(1); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); final BidderBid videoBid = BidderBid.of(Bid.builder().impid("imp_id").build(), video, "USD"); - assertThat(result.getValue()).containsExactly(videoBid); + assertThat(result.getBids()).containsExactly(videoBid); } @Test @@ -171,13 +178,13 @@ public void makeBidsShouldReturnBannerAdTypeIfMtypeIsAbsentAndIfNoImpressionIdMa mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("imp_id2")))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then + assertThat(result.getBids()).hasSize(1); assertThat(result.getErrors()).isEmpty(); - assertThat(result.getValue()).hasSize(1); final BidderBid bannerBid = BidderBid.of(Bid.builder().impid("imp_id2").build(), banner, "USD"); - assertThat(result.getValue()).containsExactly(bannerBid); + assertThat(result.getBids()).containsExactly(bannerBid); } @Test @@ -188,18 +195,58 @@ public void makeBidsShouldReturnErrorIfMtypeIsWrong() throws JsonProcessingExcep mapper.writeValueAsString(sampleBidResponse(bidBuilder -> bidBuilder.impid("imp_id").mtype(5)))); // when - final Result> result = target.makeBids(httpCall, null); + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); // then + assertThat(result.getBids()).isEmpty(); assertThat(result.getErrors()).hasSize(1); - assertThat(result.getValue()).isEmpty(); final BidderError error = BidderError.badServerResponse("Unable to fetch mediaType: imp_id"); assertThat(result.getErrors()).containsExactly(error); } - private static BidResponse sampleBidResponse(Function httpCall = sampleHttpCall( + givenBidRequest(), + mapper.writeValueAsString(sampleBidResponseWithFledgeConfig(bidBuilder -> bidBuilder.impid("imp_id")))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1); + assertThat(result.getFledgeAuctionConfigs()) + .containsOnly(FledgeAuctionConfig.builder() + .impId("imp_id") + .config(mapper.createObjectNode().put("someKey", "someValue")) + .build()); + } + + @Test + public void makeBidsShouldReturnFledgeConfigIfBidIsAbsent() throws JsonProcessingException { + // given + final BidderCall httpCall = sampleHttpCall( + givenBidRequest(), + mapper.writeValueAsString(sampleBidResponseWithoutBidAndWithFledgeConfig("imp_id"))); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, null); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).isEmpty(); + assertThat(result.getFledgeAuctionConfigs()) + .containsOnly(FledgeAuctionConfig.builder() + .impId("imp_id") + .config(mapper.createObjectNode().put("someKey", "someValue")) + .build()); + } + + private static MedianetBidResponse sampleBidResponse(Function bidCustomizer) { - return BidResponse.builder() + return MedianetBidResponse.builder() .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) @@ -207,8 +254,38 @@ private static BidResponse sampleBidResponse(Function bids) { - return BidResponse.builder() + private static MedianetBidResponse sampleBidResponseWithFledgeConfig(Function bidCustomizer) { + final Bid bid = bidCustomizer.apply(Bid.builder()).build(); + return MedianetBidResponse.builder() + .cur("USD") + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bid)) + .build())) + .ext(MedianetBidResponseExt.of(List.of(InterestGroupAuctionIntent.builder() + .igs(List.of(InterestGroupAuctionSeller.builder() + .impId(bid.getImpid()) + .config(mapper.createObjectNode().put("someKey", "someValue")) + .build())) + .build()))) + .build(); + } + + private static MedianetBidResponse sampleBidResponseWithoutBidAndWithFledgeConfig(String impId) { + return MedianetBidResponse.builder() + .cur("USD") + .seatbid(emptyList()) + .ext(MedianetBidResponseExt.of(List.of(InterestGroupAuctionIntent.builder() + .igs(List.of(InterestGroupAuctionSeller.builder() + .impId(impId) + .config(mapper.createObjectNode().put("someKey", "someValue")) + .build())) + .build()))) + .build(); + } + + private static MedianetBidResponse sampleMultiFormatBidResponse(List bids) { + return MedianetBidResponse.builder() .cur("USD") .seatbid(singletonList(SeatBid.builder() .bid(bids)