Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
85 changes: 79 additions & 6 deletions src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see some missed changes from GO version:

			if len(seatBid.Bid[i].ADomain) > 0 {
				typedBid.BidMeta = &openrtb_ext.ExtBidPrebidMeta{AdvertiserDomains: seatBid.Bid[i].ADomain}
			}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in db88417: a bid with adomain now gets ext.prebid.meta.advertiserDomains, keeping any other bid.ext fields, covered by makeBidsShouldAddAdvertiserDomainsToBidMetaAndKeepBidExt. The branch is also rebased on master after #4676.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.prebid.server.bidder.floxis;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.Bid;
Expand All @@ -13,16 +14,21 @@
import org.prebid.server.bidder.model.BidderCall;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.HttpRequest;
import org.prebid.server.bidder.model.Price;
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.floxis.ExtImpFloxis;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.Uri;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
Expand All @@ -31,24 +37,34 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class FloxisBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpFloxis>> FLOXIS_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};
private static final TypeReference<ExtPrebid<ExtBidPrebid, ?>> EXT_PREBID_TYPE_REFERENCE =
new TypeReference<>() {
};

private static final String HOST_MACRO = "Host";
private static final String SEAT_MACRO = "SeatId";

private static final String DEFAULT_REGION = "us-e";
private static final String DEFAULT_PARTNER = "floxis";
private static final String BIDDER_CURRENCY = "USD";

private final Uri endpointUrl;
private final CurrencyConversionService currencyConversionService;
private final JacksonMapper mapper;

public FloxisBidder(String endpointUrl, JacksonMapper mapper) {
public FloxisBidder(String endpointUrl,
CurrencyConversionService currencyConversionService,
JacksonMapper mapper) {

this.endpointUrl = Uri.of(endpointUrl);
this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
this.mapper = Objects.requireNonNull(mapper);
}

Expand All @@ -59,19 +75,22 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request

for (Imp imp : request.getImp()) {
final ExtImpFloxis impExt;
final Imp modifiedImp;
try {
impExt = parseImpExt(imp);
modifiedImp = modifyImp(imp, request);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
continue;
}

impsByUrl.computeIfAbsent(resolveUrl(impExt), key -> new ArrayList<>()).add(imp);
impsByUrl.computeIfAbsent(resolveUrl(impExt), key -> new ArrayList<>()).add(modifiedImp);
}

final List<String> currencies = resolveCurrencies(request.getCur());
final List<HttpRequest<BidRequest>> httpRequests = impsByUrl.entrySet().stream()
.map(entry -> BidderUtil.defaultRequest(
request.toBuilder().imp(entry.getValue()).build(),
request.toBuilder().imp(entry.getValue()).cur(currencies).build(),
entry.getKey(),
mapper))
.toList();
Expand All @@ -97,6 +116,27 @@ private ExtImpFloxis parseImpExt(Imp imp) {
return ExtImpFloxis.of(impExt.getSeat(), region, partner);
}

private Imp modifyImp(Imp imp, BidRequest request) {
final Price bidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
if (!BidderUtil.shouldConvertBidFloor(bidFloorPrice, BIDDER_CURRENCY)) {
return imp;
}

final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
bidFloorPrice.getValue(), request, bidFloorPrice.getCurrency(), BIDDER_CURRENCY);

return imp.toBuilder()
.bidfloor(convertedPrice)
.bidfloorcur(BIDDER_CURRENCY)
.build();
}

private static List<String> resolveCurrencies(List<String> currencies) {
return CollectionUtils.isEmpty(currencies) || currencies.contains(BIDDER_CURRENCY)
? currencies
: Collections.singletonList(BIDDER_CURRENCY);
}

private String resolveUrl(ExtImpFloxis impExt) {
return endpointUrl
.replaceMacro(HOST_MACRO, resolveBidHost(impExt))
Expand All @@ -123,7 +163,7 @@ public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequ
}
}

private static List<BidderBid> extractBids(BidResponse bidResponse, List<Imp> imps, List<BidderError> errors) {
private List<BidderBid> extractBids(BidResponse bidResponse, List<Imp> imps, List<BidderError> errors) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}
Expand All @@ -139,15 +179,48 @@ private static List<BidderBid> extractBids(BidResponse bidResponse, List<Imp> im
.toList();
}

private static BidderBid makeBidderBid(Bid bid, List<Imp> imps, String currency, List<BidderError> errors) {
private BidderBid makeBidderBid(Bid bid, List<Imp> imps, String currency, List<BidderError> errors) {
try {
return BidderBid.of(bid, getMediaTypeForBid(imps, bid), currency);
return BidderBid.of(addAdvertiserDomains(bid), getMediaTypeForBid(imps, bid), currency);
} catch (PreBidException e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
return null;
}
}

private Bid addAdvertiserDomains(Bid bid) {
if (CollectionUtils.isEmpty(bid.getAdomain())) {
return bid;
}

final ExtBidPrebid prebid = parseExtBidPrebid(bid);
final ExtBidPrebidMeta meta = Optional.ofNullable(prebid)
.map(ExtBidPrebid::getMeta)
.map(ExtBidPrebidMeta::toBuilder)
.orElseGet(ExtBidPrebidMeta::builder)
.advertiserDomains(bid.getAdomain())
.build();
final ExtBidPrebid modifiedPrebid = Optional.ofNullable(prebid)
.map(ExtBidPrebid::toBuilder)
.orElseGet(ExtBidPrebid::builder)
.meta(meta)
.build();

final ObjectNode ext = bid.getExt() != null ? bid.getExt().deepCopy() : mapper.mapper().createObjectNode();
ext.set("prebid", mapper.mapper().valueToTree(modifiedPrebid));
return bid.toBuilder().ext(ext).build();
}

private ExtBidPrebid parseExtBidPrebid(Bid bid) {
try {
return Optional.ofNullable(mapper.mapper().convertValue(bid.getExt(), EXT_PREBID_TYPE_REFERENCE))
.map(ExtPrebid::getPrebid)
.orElse(null);
} catch (IllegalArgumentException e) {
return null;
}
}

private static BidType getMediaTypeForBid(List<Imp> imps, Bid bid) {
final Imp imp = imps.stream()
.filter(currentImp -> Objects.equals(currentImp.getId(), bid.getImpid()))
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.floxis.FloxisBidder;
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 All @@ -25,11 +26,12 @@ BidderConfigurationProperties configurationProperties() {

@Bean
BidderDeps floxisBidderDeps(BidderConfigurationProperties floxisConfigurationProperties,
CurrencyConversionService currencyConversionService,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(floxisConfigurationProperties)
.bidderCreator(config -> new FloxisBidder(config.getEndpoint(), mapper))
.bidderCreator(config -> new FloxisBidder(config.getEndpoint(), currencyConversionService, mapper))
.assemble();
}
}
115 changes: 106 additions & 9 deletions src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test that cur array won't be changed if it contains USD

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in db88417: makeHttpRequestsShouldNotChangeRequestCurrencyWhenItContainsUsd sends cur ["EUR", "USD"] and expects it unchanged.

Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,26 @@
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.VertxTest;
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.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.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.floxis.ExtImpFloxis;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta;

import java.math.BigDecimal;
import java.util.List;
import java.util.function.UnaryOperator;

Expand All @@ -33,17 +41,29 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.tuple;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;

@ExtendWith(MockitoExtension.class)
public class FloxisBidderTest extends VertxTest {

private static final String ENDPOINT_URL = "https://{Host}.floxis.tech/pbs?seat={SeatId}";

private final FloxisBidder target = new FloxisBidder(ENDPOINT_URL, jacksonMapper);
@Mock
private CurrencyConversionService currencyConversionService;

private FloxisBidder target;

@BeforeEach
public void setUp() {
target = new FloxisBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper);
}

@Test
public void creationShouldFailOnInvalidEndpointUrl() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new FloxisBidder("invalid_url", jacksonMapper));
.isThrownBy(() -> new FloxisBidder("invalid_url", currencyConversionService, jacksonMapper));
}

@Test
Expand Down Expand Up @@ -158,7 +178,7 @@ public void makeHttpRequestsShouldUsePartnerPrefixedHostWhenPartnerProvided() {
@Test
public void makeHttpRequestsShouldLeaveEndpointUnchangedWhenItDeclaresNoMacros() {
// given
final FloxisBidder bidder = new FloxisBidder("http://localhost:8090/floxis-exchange", jacksonMapper);
final FloxisBidder bidder = new FloxisBidder("http://localhost:8090/floxis-exchange", currencyConversionService, jacksonMapper);
final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "eu")));

// when
Expand All @@ -174,7 +194,7 @@ public void makeHttpRequestsShouldLeaveEndpointUnchangedWhenItDeclaresNoMacros()
@Test
public void makeHttpRequestsShouldIgnoreRegionAndPartnerWhenAliasEndpointDeclaresNoHostMacro() {
// given
final FloxisBidder bidder = new FloxisBidder("https://hb.adapex.io/pbs?seat={SeatId}", jacksonMapper);
final FloxisBidder bidder = new FloxisBidder("https://hb.adapex.io/pbs?seat={SeatId}", currencyConversionService, jacksonMapper);
final BidRequest bidRequest = givenBidRequest(imp -> imp.ext(givenImpExt("abc", "eu", "acme")));

// when
Expand All @@ -190,7 +210,7 @@ public void makeHttpRequestsShouldIgnoreRegionAndPartnerWhenAliasEndpointDeclare
@Test
public void makeHttpRequestsShouldRouteOnceWhenImpsDifferOnlyInRegionAndPartnerOnAliasEndpoint() {
// given
final FloxisBidder bidder = new FloxisBidder("https://hb.adapex.io/pbs?seat={SeatId}", jacksonMapper);
final FloxisBidder bidder = new FloxisBidder("https://hb.adapex.io/pbs?seat={SeatId}", currencyConversionService, jacksonMapper);
final BidRequest bidRequest = BidRequest.builder()
.id("req-1")
.imp(asList(
Expand Down Expand Up @@ -405,6 +425,50 @@ public void makeHttpRequestsShouldForwardRequestBodyUnchanged() {
.containsExactly(bidRequest);
}

@Test
public void makeHttpRequestsShouldConvertBidFloorAndRequestCurrencyToUsd() {
// given
given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
.willReturn(BigDecimal.TEN);

final BidRequest bidRequest = givenBidRequest(
request -> request.cur(singletonList("EUR")),
givenImp(imp -> imp.bidfloor(BigDecimal.ONE).bidfloorcur("EUR")));

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getCur)
.containsExactly(singletonList("USD"));
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.flatExtracting(BidRequest::getImp)
.extracting(Imp::getBidfloor, Imp::getBidfloorcur)
.containsExactly(tuple(BigDecimal.TEN, "USD"));
}

@Test
public void makeHttpRequestsShouldNotChangeRequestCurrencyWhenItContainsUsd() {
// given
final BidRequest bidRequest = givenBidRequest(
request -> request.cur(asList("EUR", "USD")),
givenImp(identity()));

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getCur)
.containsExactly(asList("EUR", "USD"));
}

@Test
public void makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() {
// given
Expand Down Expand Up @@ -579,6 +643,33 @@ public void makeBidsShouldReturnErrorForImpWithoutFormatAndWithoutMType() throws
"unable to resolve a single media type for impression imp-1; set bid.mtype"));
}

@Test
public void makeBidsShouldAddAdvertiserDomainsToBidMetaAndKeepBidExt() throws Exception {
// given
final BidResponse bidResponse = givenBidResponse(Bid.builder()
.impid("imp-1")
.mtype(1)
.adomain(singletonList("advertiser.com"))
.ext(mapper.createObjectNode().put("dsa", "value"))
.build());
final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidRequest(identity()),
mapper.writeValueAsString(bidResponse));

// when
final Result<List<BidderBid>> result = target.makeBids(httpCall, givenBidRequest(identity()));

// then
final ObjectNode expectedExt = mapper.createObjectNode().put("dsa", "value");
expectedExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder()
.meta(ExtBidPrebidMeta.builder().advertiserDomains(singletonList("advertiser.com")).build())
.build()));
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.extracting(BidderBid::getBid)
.extracting(Bid::getExt)
.containsExactly(expectedExt);
}

@Test
public void makeBidsShouldReturnErrorWhenBidImpIdIsNotInOutgoingRequest() throws Exception {
// given
Expand Down Expand Up @@ -611,10 +702,16 @@ public void makeBidsShouldReturnErrorWhenBidImpIdIsNotInOutgoingRequest() throws
}

private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
return BidRequest.builder()
.id("req-1")
.imp(singletonList(givenImp(impCustomizer)))
.site(Site.builder().id("271").build())
return givenBidRequest(identity(), givenImp(impCustomizer));
}

private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
Imp... imps) {

return bidRequestCustomizer.apply(BidRequest.builder()
.id("req-1")
.imp(asList(imps))
.site(Site.builder().id("271").build()))
.build();
}

Expand Down