diff --git a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java index 955ae3bb9d6..c8cb6418de9 100644 --- a/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java +++ b/src/main/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidder.java @@ -8,6 +8,7 @@ import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Site; +import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.vertx.core.MultiMap; @@ -21,6 +22,7 @@ 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; @@ -36,6 +38,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; public class NextMillenniumBidder implements Bidder { @@ -57,47 +60,53 @@ public NextMillenniumBidder(String endpointUrl, JacksonMapper mapper, List>> makeHttpRequests(BidRequest bidRequest) { final List errors = new ArrayList<>(); - final List impExts = getImpExts(bidRequest, errors); - - return errors.isEmpty() - ? Result.withValues(makeRequests(bidRequest, impExts)) - : Result.withErrors(errors); - } - - private List getImpExts(BidRequest bidRequest, List errors) { - return bidRequest.getImp().stream() - .map(imp -> convertExt(imp, errors)) - .toList(); - } + final List> httpRequests = new ArrayList<>(); + + for (Imp imp : bidRequest.getImp()) { + final ExtImpNextMillennium extImpNextMillennium; + try { + extImpNextMillennium = convertExt(imp.getExt()); + } catch (PreBidException e) { + errors.add(BidderError.badInput(e.getMessage())); + continue; + } + httpRequests.add(makeHttpRequest(updateBidRequest(bidRequest, extImpNextMillennium))); + } - private List> makeRequests(BidRequest bidRequest, List extImps) { - return extImps.stream() - .map(extImp -> makeHttpRequest(updateBidRequest(bidRequest, extImp))) - .toList(); + return errors.isEmpty() ? Result.withValues(httpRequests) : Result.withErrors(errors); } - private ExtImpNextMillennium convertExt(Imp imp, List errors) { + private ExtImpNextMillennium convertExt(ObjectNode impExt) { try { - return mapper.mapper() - .convertValue(imp.getExt(), NEXTMILLENNIUM_EXT_TYPE_REFERENCE) - .getBidder(); + return mapper.mapper().convertValue(impExt, NEXTMILLENNIUM_EXT_TYPE_REFERENCE).getBidder(); } catch (IllegalArgumentException e) { - errors.add(BidderError.badInput(e.getMessage())); + throw new PreBidException(e.getMessage()); } - return null; } private BidRequest updateBidRequest(BidRequest bidRequest, ExtImpNextMillennium ext) { - final ExtRequestPrebid prebid = ExtRequestPrebid.builder() - .storedrequest(ExtStoredRequest.of(resolveStoredRequestId(bidRequest, ext))) + final ExtStoredRequest storedRequest = ExtStoredRequest.of(resolveStoredRequestId(bidRequest, ext)); + + final ExtRequestPrebid createdExtRequestPrebid = ExtRequestPrebid.builder() + .storedrequest(storedRequest) .build(); - final ExtRequest extRequest = ExtRequest.of(prebid); - final List imps = bidRequest.getImp().stream() - .map(imp -> imp.toBuilder().ext(createImpExt(prebid)).build()) - .toList(); + final ExtRequestPrebid extRequestPrebid = Optional.ofNullable(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(prebid -> prebid.toBuilder().storedrequest(storedRequest).build()) + .orElse(createdExtRequestPrebid); + + return bidRequest.toBuilder() + .imp(updateImps(bidRequest, createdExtRequestPrebid)) + .ext(ExtRequest.of(extRequestPrebid)) + .build(); + } - return bidRequest.toBuilder().imp(imps).ext(extRequest).build(); + private List updateImps(BidRequest bidRequest, ExtRequestPrebid extRequestPrebid) { + return bidRequest.getImp().stream() + .map(imp -> imp.toBuilder().ext(createImpExt(extRequestPrebid)).build()) + .toList(); } private static String resolveStoredRequestId(BidRequest bidRequest, ExtImpNextMillennium extImpNextMillennium) { @@ -164,24 +173,53 @@ private static MultiMap headers() { @Override public final Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { + final List bidderErrors = new ArrayList<>(); try { final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); if (CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Result.empty(); } - return Result.withValues(bidsFromResponse(bidResponse)); + return Result.of(bidsFromResponse(bidResponse, bidderErrors), bidderErrors); } catch (DecodeException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } } - private static List bidsFromResponse(BidResponse bidResponse) { + private static List bidsFromResponse(BidResponse bidResponse, List bidderErrors) { return bidResponse.getSeatbid().stream() .filter(Objects::nonNull) .map(SeatBid::getBid) .filter(Objects::nonNull) .flatMap(Collection::stream) - .map(bid -> BidderBid.of(bid, BidType.banner, bidResponse.getCur())) + .map(bid -> resolveBidderBid(bidResponse, bidderErrors, bid)) + .filter(Objects::nonNull) .toList(); } + + private static BidderBid resolveBidderBid(BidResponse bidResponse, List bidderErrors, Bid bid) { + final BidType bidType = getBidType(bid, bidderErrors); + if (bidType == null) { + return null; + } + + return BidderBid.of(bid, bidType, bidResponse.getCur()); + } + + private static BidType getBidType(Bid bid, List bidderErrors) { + final Integer markupType = bid.getMtype(); + if (markupType == null) { + bidderErrors.add(BidderError.badServerResponse("Missing MType for bid: " + bid.getId())); + return null; + } + + return switch (markupType) { + case 1 -> BidType.banner; + case 2 -> BidType.video; + default -> { + bidderErrors.add(BidderError.badServerResponse( + "Unable to fetch mediaType " + bid.getMtype() + " in multi-format: " + bid.getImpid())); + yield null; + } + }; + } } diff --git a/src/main/resources/bidder-config/nextmillennium.yaml b/src/main/resources/bidder-config/nextmillennium.yaml index 51524341a45..f1039b8e45b 100644 --- a/src/main/resources/bidder-config/nextmillennium.yaml +++ b/src/main/resources/bidder-config/nextmillennium.yaml @@ -5,8 +5,10 @@ adapters: maintainer-email: accountmanagers@nextmillennium.io app-media-types: - banner + - video site-media-types: - banner + - video supported-vendors: vendor-id: 1060 usersync: diff --git a/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java b/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java index 62ac9ae78a5..375ac6e4c73 100644 --- a/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/nextmillennium/NextMillenniumBidderTest.java @@ -28,6 +28,7 @@ import org.prebid.server.proto.openrtb.ext.request.nextmillennium.ExtImpNextMillennium; import org.prebid.server.proto.openrtb.ext.response.BidType; +import java.util.Arrays; import java.util.List; import java.util.function.UnaryOperator; @@ -275,11 +276,11 @@ public void makeHttpRequestsShouldReturnImpExtNextMillenniumWhenNmmFlagsConfigur } @Test - public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingException { + public void makeBidsShouldReturnBannerBidWhenMTypeIsOne() throws JsonProcessingException { // given final BidRequest bidRequest = givenBidRequest(identity()); final BidderCall httpCall = givenHttpCall(bidRequest, - mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.impid("123")))); + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.mtype(1).impid("123")))); // when final Result> result = target.makeBids(httpCall, bidRequest); @@ -287,7 +288,76 @@ public void makeBidsShouldReturnBannerBidByDefault() throws JsonProcessingExcept // then assertThat(result.getErrors()).isEmpty(); assertThat(result.getValue()) - .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), BidType.banner, "USD")); + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("123").build(), + BidType.banner, "USD")); + } + + @Test + public void makeBidsShouldReturnVideoBidWhenMTypeIsTwo() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + final BidderCall httpCall = givenHttpCall(bidRequest, + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.mtype(2).impid("123")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("123").build(), + BidType.video, "USD")); + } + + @Test + public void makeBidsShouldReturnErrorWhenMTypeIsUnknown() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + final BidderCall httpCall = givenHttpCall(bidRequest, + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.mtype(999).impid("123")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()) + .contains(BidderError.badServerResponse("Unable to fetch mediaType 999 in multi-format: 123")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnErrorWhenMTypeIsMissing() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + final BidderCall httpCall = givenHttpCall(bidRequest, + mapper.writeValueAsString(givenBidResponse(bidBuilder -> bidBuilder.id("bidId").impid("123")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()).contains(BidderError.badServerResponse("Missing MType for bid: bidId")); + assertThat(result.getValue()).isEmpty(); + } + + @Test + public void makeBidsShouldReturnBothValidAndInvalidBidderBidAtTheSameTime() throws JsonProcessingException { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + final BidderCall httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(givenBidResponse( + bidBuilder -> bidBuilder.mtype(999).impid("123"), + bidBuilder -> bidBuilder.mtype(1).impid("312")))); + + // when + final Result> result = target.makeBids(httpCall, bidRequest); + + // then + assertThat(result.getErrors()) + .contains(BidderError.badServerResponse("Unable to fetch mediaType 999 in multi-format: 123")); + assertThat(result.getValue()) + .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("312").build(), + BidType.banner, "USD")); } @Test @@ -476,12 +546,15 @@ private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder()).build(); } - private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { + @SafeVarargs + private static BidResponse givenBidResponse(UnaryOperator... bidCustomizers) { return BidResponse.builder() + .cur("USD") .seatbid(singletonList(SeatBid.builder() - .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .bid(Arrays.stream(bidCustomizers) + .map(bidCustomizer -> bidCustomizer.apply(Bid.builder()).build()) + .toList()) .build())) - .cur("USD") .build(); } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json index 5429ae0e438..71068f2cb94 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json @@ -7,6 +7,7 @@ "id": "bid_id", "impid": "imp_id", "price": 0.01, + "mtype": 1, "adid": "adid001", "cid": "cid001", "crid": "crid001", @@ -25,7 +26,7 @@ "cur": "USD", "ext": { "responsetimemillis": { - "nextmillennium" : "{{ nextmillennium.response_time_ms }}" + "nextmillennium": "{{ nextmillennium.response_time_ms }}" }, "prebid": { "auctiontimestamp": 0 diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json index 92191632e7c..24a61053439 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-request.json @@ -53,6 +53,12 @@ "prebid": { "storedrequest": { "id": "placement_id" + }, + "server": { + "externalurl": "http://localhost:8080", + "gvlid": 1, + "datacenter": "local", + "endpoint": "/openrtb2/auction" } } } diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-response.json index dede1455066..27756dd8f0a 100644 --- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-response.json +++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-nextmillennium-bid-response.json @@ -6,6 +6,7 @@ { "crid": "crid001", "adid": "adid001", + "mtype": 1, "price": 0.01, "id": "bid_id", "impid": "imp_id",