Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add currency conversion to Adview bidder #1609

Merged
merged 16 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 83 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 @@ -9,19 +9,24 @@
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.http.HttpMethod;
import org.apache.commons.collections4.CollectionUtils;
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.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,118 @@ 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 String resolvedBidFloorCurrency;
final BigDecimal resolvedBidFloor;
if (shouldConvertBidFloor(firstImp)) {
resolvedBidFloorCurrency = BIDDER_CURRENCY;
resolvedBidFloor = resolveBidFloor(request, firstImp, resolvedBidFloorCurrency);
} else {
resolvedBidFloorCurrency = firstImp.getBidfloorcur();
resolvedBidFloor = firstImp.getBidfloor();
}

modifiedBidRequest =
modifyRequest(request, extImpAdview.getMasterTagId(), resolvedBidFloor, resolvedBidFloorCurrency);
} 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 static boolean shouldConvertBidFloor(Imp imp) {
return BidderUtil.isValidPrice(imp.getBidfloor())
&& !StringUtils.equalsIgnoreCase(imp.getBidfloorcur(), BIDDER_CURRENCY);
}

private BigDecimal resolveBidFloor(BidRequest request, Imp imp, String bidFloorCurrency) {
final BigDecimal bidFloor = imp.getBidfloor();
final String bidFloorCur = imp.getBidfloorcur();

return !bidFloorCurrency.equals(bidFloorCur)
? convertBidFloor(bidFloor, bidFloorCur, imp.getId(), request)
: bidFloor;
}

private BigDecimal convertBidFloor(BigDecimal bidFloor,
String bidFloorCur,
String impId,
BidRequest bidRequest) {
try {
return currencyConversionService
.convertCurrency(bidFloor, bidRequest, bidFloorCur, BIDDER_CURRENCY);
} 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,
BigDecimal bidFloor,
String bidFloorCur) {
return bidRequest.toBuilder()
.imp(modifyImps(bidRequest.getImp(), masterTagId))
.imp(modifyImps(bidRequest.getImp(), masterTagId, bidFloor, bidFloorCur))
.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,
BigDecimal bidFloor,
String bidFloorCur) {
final List<Imp> modifiedImps = new ArrayList<>(imps);
modifiedImps.set(0, modifyImp(imps.get(0), masterTagId));
modifiedImps.set(0, modifyImp(imps.get(0), masterTagId, bidFloor, bidFloorCur));
return modifiedImps;
}

private static Imp modifyImp(Imp imp, String masterTagId) {
private static Imp modifyImp(Imp imp,
String masterTagId,
BigDecimal bidFloor,
String bidFloorCur) {
return imp.toBuilder()
.tagid(masterTagId)
.bidfloor(bidFloor)
.bidfloorcur(bidFloorCur)
.banner(resolveBanner(imp.getBanner()))
.build();
}
Expand Down
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();
}
}
100 changes: 85 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,56 @@ public void makeHttpRequestsShouldModifyFirstImpBanner() {
.containsExactly(banner.toBuilder().w(1).h(1).build());
}

@Test
public void makeHttpRequestsShouldConvertCurrencyIfIncorrect() {
// 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"));
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getImp)
.containsOnly(singletonList(
givenImp(impBuilder -> impBuilder
.bidfloorcur("USD")
.bidfloor(BigDecimal.TEN)
.tagid("publisherId"))));
}

@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 +193,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 +211,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 +339,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 +349,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 +362,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