diff --git a/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java b/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java index 5de8002241e..f77a9a5c58a 100644 --- a/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java +++ b/src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java @@ -10,18 +10,23 @@ import io.vertx.core.http.HttpMethod; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.bidder.Bidder; +import org.prebid.server.bidder.model.Price; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpCall; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; +import org.prebid.server.currency.CurrencyConversionService; +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.adview.ExtImpAdview; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -35,12 +40,18 @@ public class AdviewBidder implements Bidder { new TypeReference>() { }; private static final String ACCOUNT_ID_MACRO = "{{AccountId}}"; + private static final String BIDDER_CURRENCY = "USD"; private final String endpointUrl; + private final CurrencyConversionService currencyConversionService; private final JacksonMapper mapper; - public AdviewBidder(String endpointUrl, JacksonMapper mapper) { + public AdviewBidder(String endpointUrl, + CurrencyConversionService currencyConversionService, + JacksonMapper mapper) { + this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); + this.currencyConversionService = Objects.requireNonNull(currencyConversionService); this.mapper = Objects.requireNonNull(mapper); } @@ -48,39 +59,73 @@ public AdviewBidder(String endpointUrl, JacksonMapper mapper) { public Result>> makeHttpRequests(BidRequest request) { final Imp firstImp = request.getImp().get(0); final ExtImpAdview extImpAdview; + final BidRequest modifiedBidRequest; try { - extImpAdview = mapper.mapper().convertValue(firstImp.getExt(), ADVIEW_EXT_TYPE_REFERENCE).getBidder(); - } catch (IllegalArgumentException e) { - return Result.withError(BidderError.badInput("invalid imp.ext")); + extImpAdview = parseExtImp(firstImp); + final Price bidFloorPrice = resolveBidFloor(firstImp, request); + modifiedBidRequest = modifyRequest(request, extImpAdview.getMasterTagId(), bidFloorPrice); + } catch (PreBidException e) { + return Result.withError(BidderError.badInput(e.getMessage())); } - final BidRequest modifiedRequest = modifyRequest(request, extImpAdview.getMasterTagId()); return Result.withValue( HttpRequest.builder() .method(HttpMethod.POST) .uri(resolveEndpoint(extImpAdview.getAccountId())) .headers(HttpUtil.headers()) - .body(mapper.encodeToBytes(modifiedRequest)) - .payload(modifiedRequest) + .body(mapper.encodeToBytes(modifiedBidRequest)) + .payload(modifiedBidRequest) .build()); } - private static BidRequest modifyRequest(BidRequest bidRequest, String masterTagId) { + private ExtImpAdview parseExtImp(Imp imp) { + try { + return mapper.mapper().convertValue(imp.getExt(), ADVIEW_EXT_TYPE_REFERENCE).getBidder(); + } catch (IllegalArgumentException e) { + throw new PreBidException("invalid imp.ext"); + } + } + + private Price resolveBidFloor(Imp imp, BidRequest bidRequest) { + final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); + return BidderUtil.isValidPrice(initialBidFloorPrice) + ? convertBidFloor(initialBidFloorPrice, imp.getId(), bidRequest) + : initialBidFloorPrice; + } + + private Price convertBidFloor(Price bidFloorPrice, String impId, BidRequest bidRequest) { + final String bidFloorCur = bidFloorPrice.getCurrency(); + try { + final BigDecimal convertedPrice = currencyConversionService + .convertCurrency(bidFloorPrice.getValue(), bidRequest, bidFloorCur, BIDDER_CURRENCY); + + return Price.of(BIDDER_CURRENCY, convertedPrice); + } catch (PreBidException e) { + throw new PreBidException(String.format( + "Unable to convert provided bid floor currency from %s to %s for imp `%s`", + bidFloorCur, BIDDER_CURRENCY, impId)); + } + } + + private static BidRequest modifyRequest(BidRequest bidRequest, String masterTagId, Price bidFloorPrice) { return bidRequest.toBuilder() - .imp(modifyImps(bidRequest.getImp(), masterTagId)) + .imp(modifyImps(bidRequest.getImp(), masterTagId, bidFloorPrice)) + .cur(Collections.singletonList(BIDDER_CURRENCY)) .build(); } - private static List modifyImps(List imps, String masterTagId) { + private static List modifyImps(List imps, String masterTagId, Price bidFloorPrice) { final List modifiedImps = new ArrayList<>(imps); - modifiedImps.set(0, modifyImp(imps.get(0), masterTagId)); + modifiedImps.set(0, modifyImp(imps.get(0), masterTagId, bidFloorPrice)); return modifiedImps; } - private static Imp modifyImp(Imp imp, String masterTagId) { + private static Imp modifyImp(Imp imp, String masterTagId, Price bidFloorPrice) { return imp.toBuilder() .tagid(masterTagId) + .bidfloor(bidFloorPrice.getValue()) + .bidfloorcur(bidFloorPrice.getCurrency()) .banner(resolveBanner(imp.getBanner())) .build(); } diff --git a/src/main/java/org/prebid/server/bidder/model/Price.java b/src/main/java/org/prebid/server/bidder/model/Price.java new file mode 100644 index 00000000000..c2e55939791 --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/model/Price.java @@ -0,0 +1,13 @@ +package org.prebid.server.bidder.model; + +import lombok.Value; + +import java.math.BigDecimal; + +@Value(staticConstructor = "of") +public class Price { + + String currency; + + BigDecimal value; +} diff --git a/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java index 2dbbd154b63..40b81bf434f 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/AdviewConfiguration.java @@ -2,6 +2,7 @@ import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.adview.AdviewBidder; +import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.json.JacksonMapper; import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; @@ -30,12 +31,13 @@ BidderConfigurationProperties configurationProperties() { @Bean BidderDeps adviewBidderDeps(BidderConfigurationProperties adviewConfigurationProperties, @NotBlank @Value("${external-url}") String externalUrl, - JacksonMapper mapper) { + JacksonMapper mapper, + CurrencyConversionService currencyConversionService) { return BidderDepsAssembler.forBidder(BIDDER_NAME) .withConfig(adviewConfigurationProperties) .usersyncerCreator(UsersyncerCreator.create(externalUrl)) - .bidderCreator(config -> new AdviewBidder(config.getEndpoint(), mapper)) + .bidderCreator(config -> new AdviewBidder(config.getEndpoint(), currencyConversionService, mapper)) .assemble(); } } diff --git a/src/main/java/org/prebid/server/util/BidderUtil.java b/src/main/java/org/prebid/server/util/BidderUtil.java index 725400a1541..f806249c905 100644 --- a/src/main/java/org/prebid/server/util/BidderUtil.java +++ b/src/main/java/org/prebid/server/util/BidderUtil.java @@ -1,5 +1,8 @@ package org.prebid.server.util; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidder.model.Price; + import java.math.BigDecimal; public class BidderUtil { @@ -10,4 +13,8 @@ private BidderUtil() { public static boolean isValidPrice(BigDecimal price) { return price != null && price.compareTo(BigDecimal.ZERO) > 0; } + + public static boolean isValidPrice(Price price) { + return isValidPrice(price.getValue()) && StringUtils.isNotBlank(price.getCurrency()); + } } diff --git a/src/test/java/org/prebid/server/bidder/adview/AdviewBidderTest.java b/src/test/java/org/prebid/server/bidder/adview/AdviewBidderTest.java index fa587b581df..71aa7d6bd44 100644 --- a/src/test/java/org/prebid/server/bidder/adview/AdviewBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/adview/AdviewBidderTest.java @@ -11,7 +11,11 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import org.prebid.server.VertxTest; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; @@ -19,18 +23,24 @@ 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.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; import org.prebid.server.proto.openrtb.ext.ExtPrebid; import org.prebid.server.proto.openrtb.ext.request.adview.ExtImpAdview; +import java.math.BigDecimal; import java.util.List; -import java.util.function.Function; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; -import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static java.util.function.Function.identity; +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.assertj.core.api.AssertionsForClassTypes.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @@ -41,14 +51,21 @@ public class AdviewBidderTest extends VertxTest { private AdviewBidder adviewBidder; + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private CurrencyConversionService currencyConversionService; + @Before public void setUp() { - adviewBidder = new AdviewBidder(ENDPOINT_URL, jacksonMapper); + adviewBidder = new AdviewBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper); } @Test public void creationShouldFailOnInvalidEndpointUrl() { - assertThatIllegalArgumentException().isThrownBy(() -> new AdviewBidder("invalid_url", jacksonMapper)); + assertThatIllegalArgumentException().isThrownBy(() -> + new AdviewBidder("invalid_url", currencyConversionService, jacksonMapper)); } @Test @@ -105,6 +122,48 @@ public void makeHttpRequestsShouldModifyFirstImpBanner() { .containsExactly(banner.toBuilder().w(1).h(1).build()); } + @Test + public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBidderCurrency() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest( + impBuilder -> impBuilder.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")); + + // when + final Result>> result = adviewBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getValue()) + .extracting(HttpRequest::getPayload) + .flatExtracting(BidRequest::getImp) + .extracting(Imp::getBidfloor, Imp::getBidfloorcur) + .containsOnly(tuple(BigDecimal.TEN, "USD")); + } + + @Test + public void makeHttpRequestsShouldReturnErrorMessageOnFailedCurrencyConversion() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willThrow(PreBidException.class); + + final BidRequest bidRequest = givenBidRequest( + impCustomizer -> impCustomizer.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")); + + // when + Result>> result = adviewBidder.makeHttpRequests(bidRequest); + + // then + assertThat(result.getErrors()).allSatisfy(bidderError -> { + assertThat(bidderError.getType()) + .isEqualTo(BidderError.Type.bad_input); + assertThat(bidderError.getMessage()) + .isEqualTo("Unable to convert provided bid floor currency from EUR to USD for imp `123`"); + }); + } + @Test public void makeHttpRequestsShouldNotModifyFirstImpBannerIfFormatsAreAbsent() { // given @@ -126,8 +185,8 @@ public void makeHttpRequestsShouldNotModifyFirstImpBannerIfFormatsAreAbsent() { @Test public void makeHttpRequestsShouldNotModifyFirstImpBannerIfFirstFormatIsAbsent() { // given - final BidRequest bidRequest = givenBidRequest( - impBuilder -> impBuilder.banner(Banner.builder().format(singletonList(null)).w(2).h(2).build())); + final BidRequest bidRequest = givenBidRequest(impBuilder -> + impBuilder.banner(Banner.builder().format(singletonList(null)).w(2).h(2).build())); // when final Result>> result = adviewBidder.makeHttpRequests(bidRequest); @@ -144,7 +203,9 @@ public void makeHttpRequestsShouldNotModifyFirstImpBannerIfFirstFormatIsAbsent() @Test public void makeHttpRequestsShouldModifyOnlyFirstImp() { // given - final BidRequest bidRequest = givenBidRequest(identity(), impBuilder -> impBuilder.id("456").ext(null)); + final BidRequest bidRequest = givenBidRequest(identity(), List.of( + identity(), + impBuilder -> impBuilder.id("456").ext(null))); // when final Result>> result = adviewBidder.makeHttpRequests(bidRequest); @@ -270,8 +331,8 @@ public void makeBidsShouldReturnBannerBidIfBannerAndVideoAreAbsentInRequestImp() } private static BidRequest givenBidRequest( - Function bidRequestCustomizer, - List> impCustomizers) { + UnaryOperator bidRequestCustomizer, + List> impCustomizers) { return bidRequestCustomizer.apply(BidRequest.builder() .imp(impCustomizers.stream() @@ -280,11 +341,11 @@ private static BidRequest givenBidRequest( .build(); } - private static BidRequest givenBidRequest(Function... impCustomizers) { - return givenBidRequest(identity(), asList(impCustomizers)); + private static BidRequest givenBidRequest(UnaryOperator impCustomizer) { + return givenBidRequest(identity(), singletonList(impCustomizer)); } - private static Imp givenImp(Function impCustomizer) { + private static Imp givenImp(UnaryOperator impCustomizer) { return impCustomizer.apply(Imp.builder() .id("123") .banner(Banner.builder().w(23).h(25).build()) @@ -293,9 +354,10 @@ private static Imp givenImp(Function impCustomiz .build(); } - private static BidResponse givenBidResponse(Function bidCustomizer) { + private static BidResponse givenBidResponse(UnaryOperator bidCustomizer) { return BidResponse.builder() - .seatbid(singletonList(SeatBid.builder().bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) + .seatbid(singletonList(SeatBid.builder() + .bid(singletonList(bidCustomizer.apply(Bid.builder()).build())) .build())) .build(); }