Skip to content

Commit

Permalink
Add currency conversion to Adview bidder (prebid#1609)
Browse files Browse the repository at this point in the history
  • Loading branch information
yevhenii-viktorov authored Dec 20, 2021
1 parent 9b98143 commit 69091b2
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 29 deletions.
69 changes: 57 additions & 12 deletions src/main/java/org/prebid/server/bidder/adview/AdviewBidder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,52 +40,92 @@ public class AdviewBidder implements Bidder<BidRequest> {
new TypeReference<ExtPrebid<?, ExtImpAdview>>() {
};
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);
}

@Override
public Result<List<HttpRequest<BidRequest>>> 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.<BidRequest>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<Imp> modifyImps(List<Imp> imps, String masterTagId) {
private static List<Imp> modifyImps(List<Imp> imps, String masterTagId, Price bidFloorPrice) {
final List<Imp> 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();
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/prebid/server/bidder/model/Price.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
7 changes: 7 additions & 0 deletions src/main/java/org/prebid/server/util/BidderUtil.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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());
}
}
92 changes: 77 additions & 15 deletions src/test/java/org/prebid/server/bidder/adview/AdviewBidderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,36 @@
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;
import org.prebid.server.bidder.model.HttpCall;
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;
Expand All @@ -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
Expand Down Expand Up @@ -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<List<HttpRequest<BidRequest>>> 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<List<HttpRequest<BidRequest>>> 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
Expand All @@ -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<List<HttpRequest<BidRequest>>> result = adviewBidder.makeHttpRequests(bidRequest);
Expand All @@ -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<List<HttpRequest<BidRequest>>> result = adviewBidder.makeHttpRequests(bidRequest);
Expand Down Expand Up @@ -270,8 +331,8 @@ public void makeBidsShouldReturnBannerBidIfBannerAndVideoAreAbsentInRequestImp()
}

private static BidRequest givenBidRequest(
Function<BidRequest.BidRequestBuilder, BidRequest.BidRequestBuilder> bidRequestCustomizer,
List<Function<Imp.ImpBuilder, Imp.ImpBuilder>> impCustomizers) {
UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
List<UnaryOperator<Imp.ImpBuilder>> impCustomizers) {

return bidRequestCustomizer.apply(BidRequest.builder()
.imp(impCustomizers.stream()
Expand All @@ -280,11 +341,11 @@ private static BidRequest givenBidRequest(
.build();
}

private static BidRequest givenBidRequest(Function<Imp.ImpBuilder, Imp.ImpBuilder>... impCustomizers) {
return givenBidRequest(identity(), asList(impCustomizers));
private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
return givenBidRequest(identity(), singletonList(impCustomizer));
}

private static Imp givenImp(Function<Imp.ImpBuilder, Imp.ImpBuilder> impCustomizer) {
private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
return impCustomizer.apply(Imp.builder()
.id("123")
.banner(Banner.builder().w(23).h(25).build())
Expand All @@ -293,9 +354,10 @@ private static Imp givenImp(Function<Imp.ImpBuilder, Imp.ImpBuilder> impCustomiz
.build();
}

private static BidResponse givenBidResponse(Function<Bid.BidBuilder, Bid.BidBuilder> bidCustomizer) {
private static BidResponse givenBidResponse(UnaryOperator<Bid.BidBuilder> 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();
}
Expand Down

0 comments on commit 69091b2

Please sign in to comment.