From cf2e6f7e030b39b1a27d7cd75ecc031a3a3e230c Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Sat, 31 Oct 2026 20:19:29 +0200 Subject: [PATCH 1/2] Floxis: convert non-USD floors and request currency to USD --- .../server/bidder/floxis/FloxisBidder.java | 39 ++++++++++++- .../config/bidder/FloxisConfiguration.java | 4 +- .../bidder/floxis/FloxisBidderTest.java | 58 +++++++++++++++++-- 3 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index aa987ea8a6c..faa796166db 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -13,7 +13,9 @@ 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; @@ -23,6 +25,7 @@ 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; @@ -43,12 +46,18 @@ public class FloxisBidder implements Bidder { 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); } @@ -59,19 +68,22 @@ public Result>> 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 currencies = resolveCurrencies(request.getCur()); final List> 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(); @@ -97,6 +109,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 resolveCurrencies(List 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)) diff --git a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java index 896ac465fbe..3217e4aa9d4 100644 --- a/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/bidder/FloxisConfiguration.java @@ -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; @@ -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(); } } diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index 8345748f850..35801e005c9 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -12,7 +12,11 @@ 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; @@ -20,10 +24,12 @@ 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 java.math.BigDecimal; import java.util.List; import java.util.function.UnaryOperator; @@ -33,17 +39,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 @@ -158,7 +176,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 @@ -174,7 +192,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 @@ -190,7 +208,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( @@ -405,6 +423,36 @@ public void makeHttpRequestsShouldForwardRequestBodyUnchanged() { .containsExactly(bidRequest); } + @Test + public void makeHttpRequestsShouldConvertBidFloorAndRequestCurrencyToUsd() { + // given + given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) + .willReturn(BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(imp -> imp + .bidfloor(BigDecimal.ONE) + .bidfloorcur("EUR") + .ext(givenImpExt("abc", "us-e"))) + .toBuilder() + .cur(singletonList("EUR")) + .build(); + + // when + final Result>> 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 makeBidsShouldReturnErrorWhenResponseBodyCouldNotBeParsed() { // given From db88417770307f63f6480ca8c00a748022f6e3af Mon Sep 17 00:00:00 2001 From: floxis-admin Date: Tue, 3 Nov 2026 22:42:18 +0200 Subject: [PATCH 2/2] Floxis: add advertiser domains to bid meta and cover a request currency that already includes USD Co-Authored-By: Claude Opus 5.5 --- .../server/bidder/floxis/FloxisBidder.java | 46 +++++++++++- .../bidder/floxis/FloxisBidderTest.java | 71 ++++++++++++++++--- 2 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java index faa796166db..c7dc604f794 100644 --- a/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java +++ b/src/main/java/org/prebid/server/bidder/floxis/FloxisBidder.java @@ -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; @@ -22,6 +23,8 @@ 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; @@ -34,12 +37,16 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; public class FloxisBidder implements Bidder { private static final TypeReference> FLOXIS_EXT_TYPE_REFERENCE = new TypeReference<>() { }; + private static final TypeReference> EXT_PREBID_TYPE_REFERENCE = + new TypeReference<>() { + }; private static final String HOST_MACRO = "Host"; private static final String SEAT_MACRO = "SeatId"; @@ -156,7 +163,7 @@ public Result> makeBids(BidderCall httpCall, BidRequ } } - private static List extractBids(BidResponse bidResponse, List imps, List errors) { + private List extractBids(BidResponse bidResponse, List imps, List errors) { if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { return Collections.emptyList(); } @@ -172,15 +179,48 @@ private static List extractBids(BidResponse bidResponse, List im .toList(); } - private static BidderBid makeBidderBid(Bid bid, List imps, String currency, List errors) { + private BidderBid makeBidderBid(Bid bid, List imps, String currency, List 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 imps, Bid bid) { final Imp imp = imps.stream() .filter(currentImp -> Objects.equals(currentImp.getId(), bid.getImpid())) diff --git a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java index 35801e005c9..a7d3c60c6a2 100644 --- a/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/floxis/FloxisBidderTest.java @@ -28,6 +28,8 @@ 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; @@ -429,13 +431,9 @@ public void makeHttpRequestsShouldConvertBidFloorAndRequestCurrencyToUsd() { given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString())) .willReturn(BigDecimal.TEN); - final BidRequest bidRequest = givenBidRequest(imp -> imp - .bidfloor(BigDecimal.ONE) - .bidfloorcur("EUR") - .ext(givenImpExt("abc", "us-e"))) - .toBuilder() - .cur(singletonList("EUR")) - .build(); + final BidRequest bidRequest = givenBidRequest( + request -> request.cur(singletonList("EUR")), + givenImp(imp -> imp.bidfloor(BigDecimal.ONE).bidfloorcur("EUR"))); // when final Result>> result = target.makeHttpRequests(bidRequest); @@ -453,6 +451,24 @@ public void makeHttpRequestsShouldConvertBidFloorAndRequestCurrencyToUsd() { .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>> 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 @@ -627,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 httpCall = givenHttpCall(givenBidRequest(identity()), + mapper.writeValueAsString(bidResponse)); + + // when + final Result> 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 @@ -659,10 +702,16 @@ public void makeBidsShouldReturnErrorWhenBidImpIdIsNotInOutgoingRequest() throws } private static BidRequest givenBidRequest(UnaryOperator 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 bidRequestCustomizer, + Imp... imps) { + + return bidRequestCustomizer.apply(BidRequest.builder() + .id("req-1") + .imp(asList(imps)) + .site(Site.builder().id("271").build())) .build(); }