From 3df8a886051318bce6a561e3bf806cfa667dd050 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 13 May 2025 14:18:58 +0200 Subject: [PATCH 1/5] Support bid rounding options --- .../server/auction/BidResponseCreator.java | 3 +- .../org/prebid/server/auction/CpmRange.java | 35 +++- .../auction/TargetingKeywordsCreator.java | 15 +- .../BasicCategoryMappingService.java | 14 +- .../CategoryMappingService.java | 2 + .../NoOpCategoryMappingService.java | 2 + .../model/AccountAuctionBidRoundingMode.java | 20 ++ .../settings/model/AccountAuctionConfig.java | 3 + .../BasicCategoryMappingServiceTest.java | 83 ++++---- .../auction/BidResponseCreatorTest.java | 8 +- .../prebid/server/auction/CpmRangeTest.java | 177 +++++++++++++++--- .../auction/TargetingKeywordsCreatorTest.java | 38 ++-- 12 files changed, 295 insertions(+), 105 deletions(-) create mode 100644 src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 73dcc6b404a..ee28ddbe9d7 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -594,6 +594,7 @@ private Future createCategoryMapping(AuctionContext aucti return categoryMappingService.createCategoryMapping( bidderResponses, auctionContext.getBidRequest(), + auctionContext.getAccount(), auctionContext.getTimeoutContext().getTimeout()) .map(categoryMappingResult -> addCategoryMappingErrors(categoryMappingResult, auctionContext)); @@ -1561,7 +1562,7 @@ private Bid toBid(BidInfo bidInfo, final String categoryDuration = bidInfo.getCategory(); targetingKeywords = keywordsCreator != null ? keywordsCreator.makeFor( - bid, seat, isWinningBid, cacheId, bidType.getName(), videoCacheId, categoryDuration) + bid, seat, isWinningBid, cacheId, bidType.getName(), videoCacheId, categoryDuration, account) : null; } else { targetingKeywords = null; diff --git a/src/main/java/org/prebid/server/auction/CpmRange.java b/src/main/java/org/prebid/server/auction/CpmRange.java index 11ad6652438..0fe864efe4d 100644 --- a/src/main/java/org/prebid/server/auction/CpmRange.java +++ b/src/main/java/org/prebid/server/auction/CpmRange.java @@ -3,11 +3,16 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.settings.model.AccountAuctionBidRoundingMode; import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; /** * Class for price operating with rules defined in {@link PriceGranularity} @@ -23,8 +28,8 @@ private CpmRange() { /** * Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in string format */ - public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity) { - final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity); + public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity, Account account) { + final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity, account); return value != null ? format(value, priceGranularity.getPrecision()) : StringUtils.EMPTY; } @@ -47,7 +52,7 @@ private static NumberFormat numberFormat(int precision) { * Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in {@link BigDecimal} * format */ - public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity) { + public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity, Account account) { if (cpm.compareTo(BigDecimal.ZERO) <= 0) { return null; } @@ -69,14 +74,32 @@ public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceG min = max; } - return increment != null ? calculate(cpm, min, increment) : null; + return increment != null ? calculate(cpm, min, increment, resolveRoundingMode(account)) : null; } - private static BigDecimal calculate(BigDecimal cpm, BigDecimal min, BigDecimal increment) { + private static BigDecimal calculate(BigDecimal cpm, + BigDecimal min, + BigDecimal increment, + RoundingMode roundingMode) { + return cpm .subtract(min) - .divide(increment, 0, RoundingMode.FLOOR) + .divide(increment, 0, roundingMode) .multiply(increment) .add(min); } + + private static RoundingMode resolveRoundingMode(Account account) { + final AccountAuctionBidRoundingMode accountRoundingMode = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidRoundingMode) + .orElse(AccountAuctionBidRoundingMode.DOWN); + + return switch (accountRoundingMode) { + case DOWN -> RoundingMode.FLOOR; + case UP -> RoundingMode.CEILING; + case TRUE -> RoundingMode.HALF_UP; + case TIMESPLIT -> ThreadLocalRandom.current().nextBoolean() ? RoundingMode.FLOOR : RoundingMode.CEILING; + }; + } } diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 4fb92910ebf..b873210c1f3 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -3,6 +3,7 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.util.ArrayList; @@ -152,7 +153,8 @@ Map makeFor(Bid bid, String cacheId, String format, String vastCacheId, - String categoryDuration) { + String categoryDuration, + Account account) { final Map keywords = makeFor( bidder, @@ -164,7 +166,8 @@ Map makeFor(Bid bid, vastCacheId, categoryDuration, format, - bid.getDealid()); + bid.getDealid(), + account); if (resolver == null) { return truncateKeys(keywords); @@ -188,7 +191,8 @@ private Map makeFor(String bidder, String vastCacheId, String categoryDuration, String format, - String dealId) { + String dealId, + Account account) { final boolean includeDealBid = alwaysIncludeDeals && StringUtils.isNotEmpty(dealId); final KeywordMap keywordMap = new KeywordMap( @@ -198,7 +202,10 @@ private Map makeFor(String bidder, includeBidderKeys || includeDealBid, Collections.emptySet()); - final String roundedCpm = isPriceGranularityValid() ? CpmRange.fromCpm(price, priceGranularity) : DEFAULT_CPM; + final String roundedCpm = isPriceGranularityValid() + ? CpmRange.fromCpm(price, priceGranularity, account) + : DEFAULT_CPM; + keywordMap.put(this.keyPrefix + PB_KEY, roundedCpm); keywordMap.put(this.keyPrefix + BIDDER_KEY, bidder); diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java index 823c29d5bd2..be359586d97 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -42,6 +42,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; import org.prebid.server.util.ObjectUtil; import java.math.BigDecimal; @@ -83,6 +84,7 @@ public BasicCategoryMappingService(ApplicationSettings applicationSettings, Jack @Override public Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout) { final ExtRequestTargeting targeting = targeting(bidRequest); @@ -111,8 +113,8 @@ public Future createCategoryMapping(List return makeBidderToBidCategory( bidderResponses, withCategory, translateCategories, primaryAdServer, publisher, rejectedBids, timeout) - .map(categoryBidContexts -> resolveBidsCategoriesDurations( - bidderResponses, categoryBidContexts, bidRequest, targeting, withCategory, rejectedBids)); + .map(categoryBidContexts -> resolveBidsCategoriesDurations(bidderResponses, categoryBidContexts, + account, bidRequest, targeting, withCategory, rejectedBids)); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { @@ -326,6 +328,7 @@ private static void collectCategoryFetchResults(CompositeFuture compositeFuture, */ private CategoryMappingResult resolveBidsCategoriesDurations(List bidderResponses, List categoryBidContexts, + Account account, BidRequest bidRequest, ExtRequestTargeting targeting, boolean withCategory, @@ -342,8 +345,8 @@ private CategoryMappingResult resolveBidsCategoriesDurations(List> uniqueCatKeysToCategoryBids = categoryBidContexts.stream() - .map(categoryBidContext -> enrichCategoryBidContext(categoryBidContext, durations, priceGranularity, - withCategory, appendBidderNames, impIdToBiddersDealTear, rejectedBids)) + .map(categoryBidContext -> enrichCategoryBidContext(categoryBidContext, account, durations, + priceGranularity, withCategory, appendBidderNames, impIdToBiddersDealTear, rejectedBids)) .filter(Objects::nonNull) .collect(Collectors.groupingBy(CategoryBidContext::getCategoryUniqueKey, Collectors.mapping(Function.identity(), Collectors.toSet()))); @@ -504,6 +507,7 @@ private static boolean isNotRejected(String bidId, String bidder, List durations, PriceGranularity priceGranularity, boolean withCategory, @@ -522,7 +526,7 @@ private CategoryBidContext enrichCategoryBidContext(CategoryBidContext categoryB return null; } - final BigDecimal price = CpmRange.fromCpmAsNumber(bid.getPrice(), priceGranularity); + final BigDecimal price = CpmRange.fromCpmAsNumber(bid.getPrice(), priceGranularity, account); final String rowPrice = CpmRange.format(price, priceGranularity.getPrecision()); final String category = categoryBidContext.getCategory(); final String categoryUniqueKey = createCategoryUniqueKey(withCategory, category, rowPrice, duration); diff --git a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java index 2c3b3f369b0..e99f05dd815 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java @@ -5,6 +5,7 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; import java.util.List; @@ -12,5 +13,6 @@ public interface CategoryMappingService { Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout); } diff --git a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java index 88ac988c521..4bf54857955 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java @@ -5,6 +5,7 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.model.CategoryMappingResult; import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.settings.model.Account; import java.util.List; @@ -13,6 +14,7 @@ public class NoOpCategoryMappingService implements CategoryMappingService { @Override public Future createCategoryMapping(List bidderResponses, BidRequest bidRequest, + Account account, Timeout timeout) { return Future.succeededFuture(CategoryMappingResult.of(bidderResponses)); diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java new file mode 100644 index 00000000000..839602540aa --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionBidRoundingMode.java @@ -0,0 +1,20 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum AccountAuctionBidRoundingMode { + + @JsonProperty("down") + @JsonEnumDefaultValue + DOWN, + + @JsonProperty("true") + TRUE, + + @JsonProperty("timesplit") + TIMESPLIT, + + @JsonProperty("up") + UP +} diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index ac7da04dd31..551b3eae4bc 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -45,6 +45,9 @@ public class AccountAuctionConfig { AccountTargetingConfig targeting; + @JsonAlias("bid-rounding") + AccountAuctionBidRoundingMode bidRoundingMode; + @JsonProperty("preferredmediatype") Map preferredMediaTypes; diff --git a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java index 3c05d00af37..7be988f887a 100644 --- a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java +++ b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java @@ -32,6 +32,7 @@ import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.settings.ApplicationSettings; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.time.Clock; @@ -101,7 +102,7 @@ public void applyCategoryMappingShouldReturnFilteredBidsWithCategory() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -141,7 +142,7 @@ public void applyCategoryMappingShouldTolerateBidsWithSameIdWithingDifferentBidd // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -170,7 +171,7 @@ public void applyCategoryMappingShouldNotCallFetchCategoryWhenTranslateCategorie // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then verifyNoInteractions(applicationSettings); @@ -191,7 +192,7 @@ public void applyCategoryMappingShouldReturnFailedFutureWhenTranslateTrueAndAdSe // when assertThatThrownBy(() -> categoryMappingService.createCategoryMapping( - bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), timeout)) + bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout)) .isInstanceOf(InvalidRequestException.class) .hasMessage("Primary ad server required but was not defined when translate category is enabled"); } @@ -207,7 +208,7 @@ public void applyCategoryMappingShouldReturnFailedFutureWhenTranslateTrueAndAdSe // when and then assertThatThrownBy(() -> categoryMappingService.createCategoryMapping( - bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), timeout)) + bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout)) .isInstanceOf(InvalidRequestException.class) .hasMessage("Primary ad server `3` is not recognized"); } @@ -226,7 +227,10 @@ public void applyCategoryMappingShouldReturnUseFreewheelAdServerWhenAdServerIs1( Future.succeededFuture(singletonMap("cat1", "fetchedCat1"))); // when - categoryMappingService.createCategoryMapping(bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), + categoryMappingService.createCategoryMapping( + bidderResponses, + givenBidRequestWithTargeting(extRequestTargeting), + Account.empty("id"), timeout); // then @@ -247,7 +251,10 @@ public void applyCategoryMappingShouldReturnUseDpfAdServerWhenAdServerIs2() { Future.succeededFuture(singletonMap("cat1", "fetchedCat1"))); // when - categoryMappingService.createCategoryMapping(bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), + categoryMappingService.createCategoryMapping( + bidderResponses, + givenBidRequestWithTargeting(extRequestTargeting), + Account.empty("id"), timeout); // then @@ -271,7 +278,7 @@ public void applyCategoryMappingShouldRejectBidsWithFailedCategoryFetch() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -298,7 +305,7 @@ public void applyCategoryMappingShouldRejectBidsWithCatLengthMoreThanOne() { Future.succeededFuture(singletonMap("cat2", "fetchedCat2"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -326,7 +333,7 @@ public void applyCategoryMappingShouldRejectBidsWithWhenCatIsNull() { Future.succeededFuture(singletonMap("cat2", "fetchedCat2"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -354,7 +361,7 @@ public void applyCategoryMappingShouldRejectBidWhenNullCategoryReturnedFromSourc Future.succeededFuture(null)); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -385,7 +392,7 @@ public void applyCategoryMappingShouldUseMediaTypePriceGranularityIfDefined() { Future.succeededFuture(singletonMap("cat1", "fetchedCat1"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -410,7 +417,7 @@ public void applyCategoryMappingShouldRejectBidIfItsDurationLargerThanTargetingM Future.succeededFuture(singletonMap("cat2", "fetchedCat2"))); // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -433,7 +440,7 @@ public void applyCategoryMappingShouldReturnEmptyCategoryMappingResult() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -462,7 +469,7 @@ public void applyCategoryMappingShouldReturnFirstVideoCategoryIfPresent() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -487,7 +494,7 @@ public void applyCategoryMappingShouldReturnEmptyCategoryIfNotWithCategory() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -512,7 +519,7 @@ public void applyCategoryMappingShouldReturnFirstIabBidCategoryIfWithCategoryAnd // when final Future resultFuture = categoryMappingService.createCategoryMapping( - bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), timeout); + bidderResponses, givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -540,7 +547,7 @@ public void applyCategoryMappingShouldReturnFetchedCategoryIfWithCategoryAndTran // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -562,7 +569,7 @@ public void applyCategoryMappingShouldSetFirstDurationFromRangeIfDurationIsNull( // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -588,7 +595,7 @@ public void applyCategoryMappingShouldDeduplicateBidsByFetchedCategoryWhenWithCa // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -615,7 +622,7 @@ public void applyCategoryMappingShouldDeduplicateBidsByBidCatWhenWithCategoryIsT // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -643,7 +650,7 @@ public void applyCategoryMappingShouldDeduplicateBidsByPriceAndDurationIfWithCat // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -667,7 +674,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndFetchedCatego // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -689,7 +696,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndBidCatAndDura // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -712,7 +719,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndBidCatAndDura // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -734,7 +741,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriceAndDuration() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -764,7 +771,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriorityAndDuration() // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -801,7 +808,7 @@ public void applyCategoryMappingShouldUseDealTierFromImpExtPrebidBidders() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -840,7 +847,7 @@ public void applyCategoryMappingShouldPrecedencePriorityAndDurationFromPrebidOve // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -875,7 +882,7 @@ public void applyCategoryMappingShouldReturnDurCatBuiltFromPriorityCatAndDuratio // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -912,7 +919,7 @@ public void applyCategoryMappingShouldBuildCatDurFromPriceCatAndDurIfSupportDeal // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -947,7 +954,7 @@ public void applyCategoryMappingShouldBuildCatDurFromPriceCatAndDurIfBidPriority // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -987,7 +994,7 @@ public void applyCategoryMappingShouldIgnoreContextAndPrebidInImpExt() { // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1022,7 +1029,7 @@ public void applyCategoryMappingShouldAddErrorIfImpBidderDoesNotHaveDealTierAndC // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1060,7 +1067,7 @@ public void applyCategoryMappingShouldAddErrorIfPrefixIsNullAndCreateRegularCatD // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1099,7 +1106,7 @@ public void applyCategoryMappingShouldAddErrorIfMinDealTierIsNullAndCreateRegula // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1136,7 +1143,7 @@ public void applyCategoryMappingShouldAddErrorIfMinDealTierLessThanZeroAndCreate // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - bidRequest, timeout); + bidRequest, Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); @@ -1167,7 +1174,7 @@ public void applyCategoryMappingShouldRejectAllBidsFromBidderInDifferentReasons( // when final Future resultFuture = categoryMappingService.createCategoryMapping(bidderResponses, - givenBidRequestWithTargeting(extRequestTargeting), timeout); + givenBidRequestWithTargeting(extRequestTargeting), Account.empty("id"), timeout); // then assertThat(resultFuture.succeeded()).isTrue(); diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 7410c387a22..f62c6a44651 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -229,7 +229,7 @@ public void setUp() { given(cacheDefaultProperties.getAudioTtl()).willReturn(null); given(cacheDefaultProperties.getNativeTtl()).willReturn(null); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willAnswer(invocationOnMock -> Future.succeededFuture( CategoryMappingResult.of(emptyMap(), emptyMap(), invocationOnMock.getArgument(0), null))); @@ -1102,7 +1102,7 @@ public void shouldUseBidsReturnedInCategoryMapperResultAndUpdateErrors() { bidRequest, contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willReturn(Future.succeededFuture(CategoryMappingResult.of(emptyMap(), emptyMap(), singletonList(BidderResponse.of( "bidder1", @@ -1133,7 +1133,7 @@ public void shouldThrowExceptionWhenCategoryMappingThrowsPrebidException() { givenBidRequest(identity(), identity(), givenImp()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willReturn(Future.failedFuture(new InvalidRequestException("category exception"))); // when @@ -2456,7 +2456,7 @@ public void shouldAddDealTierSatisfiedToExtBidPrebidWhenBidsPrioritySatisfiedMin givenBidRequest(Imp.builder().id("i1").build()), contextBuilder -> contextBuilder.auctionParticipations(toAuctionParticipant(bidderResponses))); - given(categoryMappingService.createCategoryMapping(any(), any(), any())) + given(categoryMappingService.createCategoryMapping(any(), any(), any(), any())) .willReturn(Future.succeededFuture(CategoryMappingResult.of(emptyMap(), Collections.singletonMap(bid, true), bidderResponses, emptyList()))); diff --git a/src/test/java/org/prebid/server/auction/CpmRangeTest.java b/src/test/java/org/prebid/server/auction/CpmRangeTest.java index f3e72b5483d..5dc07003b25 100644 --- a/src/test/java/org/prebid/server/auction/CpmRangeTest.java +++ b/src/test/java/org/prebid/server/auction/CpmRangeTest.java @@ -1,21 +1,29 @@ package org.prebid.server.auction; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionBidRoundingMode; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.auction.PriceGranularity.createFromExtPriceGranularity; +import static org.prebid.server.auction.PriceGranularity.createFromString; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.DOWN; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.TIMESPLIT; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.TRUE; +import static org.prebid.server.settings.model.AccountAuctionBidRoundingMode.UP; public class CpmRangeTest { @Test public void fromCpmShouldReturnMaxRangeIfCpmExceedsIt() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(21), PriceGranularity.createFromString("auto"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(21), createFromString("auto"), givenAccount())) .isEqualTo("20.00"); } @@ -27,7 +35,7 @@ public void fromCpmShouldReturnPriceWithCorrectPrecision() { ExtGranularityRange.of(BigDecimal.valueOf(10), BigDecimal.valueOf(0.1))))); // when - final String cpm = CpmRange.fromCpm(BigDecimal.valueOf(5.1245), priceGranularity); + final String cpm = CpmRange.fromCpm(BigDecimal.valueOf(5.1245), priceGranularity, givenAccount()); // then assertThat(cpm).isEqualTo("5.1"); @@ -35,88 +43,191 @@ public void fromCpmShouldReturnPriceWithCorrectPrecision() { @Test public void fromCpmShouldReturnCpmGivenLowGranularity() { - Assertions.assertThat( - CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("low"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount())) + .isEqualTo("3.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount(DOWN))) + .isEqualTo("3.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount(UP))) + .isEqualTo("4.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.74), createFromString("low"), givenAccount(UP))) + .isEqualTo("4.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.75), createFromString("low"), givenAccount(TRUE))) + .isEqualTo("4.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.74), createFromString("low"), givenAccount(TRUE))) .isEqualTo("3.50"); } @Test public void fromCpmShouldReturnCpmGivenMedGranularity() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("med"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount())) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount(DOWN))) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("med"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("med"), givenAccount(TRUE))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("med"), givenAccount(TRUE))) .isEqualTo("3.80"); - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("medium"))) + + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount())) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount(DOWN))) + .isEqualTo("3.80"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("medium"), givenAccount(UP))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("medium"), givenAccount(TRUE))) + .isEqualTo("3.90"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("medium"), givenAccount(TRUE))) .isEqualTo("3.80"); } @Test public void fromCpmShouldReturnCpmGivenHighGranularity() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("high"))) - .isEqualTo("3.87"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount())) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(DOWN))) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(UP))) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("high"), givenAccount(UP))) + .isEqualTo("3.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(TRUE))) + .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.84), createFromString("high"), givenAccount(TRUE))) + .isEqualTo("3.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.85), createFromString("high"), givenAccount(TIMESPLIT))) + .isEqualTo("3.85"); + } @Test public void fromCpmShouldReturnCpmGivenAutoGranularityAndFirstRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.87), PriceGranularity.createFromString("auto"))) - .isEqualTo("3.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount())) + .isEqualTo("3.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("3.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount(UP))) + .isEqualTo("3.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.32), createFromString("dense"), givenAccount(UP))) + .isEqualTo("3.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.33), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("3.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(3.32), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("3.30"); } @Test public void fromCpmShouldReturnCpmGivenAutoGranularityAndSecondRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.32), PriceGranularity.createFromString("auto"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount())) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount(DOWN))) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount(UP))) + .isEqualTo("5.40"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.34), createFromString("auto"), givenAccount(UP))) + .isEqualTo("5.40"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.35), createFromString("auto"), givenAccount(TRUE))) + .isEqualTo("5.40"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.34), createFromString("auto"), givenAccount(TRUE))) .isEqualTo("5.30"); } @Test public void fromCpmShouldReturnCpmGivenAutoGranularityAndThirdRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.59), PriceGranularity.createFromString("auto"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount())) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount(DOWN))) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("auto"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("auto"), givenAccount(TRUE))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("auto"), givenAccount(TRUE))) .isEqualTo("13.50"); } @Test public void fromCpmShouldReturnCpmGivenDenseGranularityAndFirstRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.87), PriceGranularity.createFromString("dense"))) - .isEqualTo("2.87"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount())) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(UP))) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.84), createFromString("dense"), givenAccount(UP))) + .isEqualTo("2.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("2.85"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.84), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("2.84"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.85), createFromString("dense"), givenAccount(TIMESPLIT))) + .isEqualTo("2.85"); } @Test public void fromCpmShouldReturnCpmGivenDenseGranularityAndSecondRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.36), PriceGranularity.createFromString("dense"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount())) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("5.30"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount(UP))) .isEqualTo("5.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.32), createFromString("dense"), givenAccount(UP))) + .isEqualTo("5.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.33), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("5.35"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(5.32), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("5.30"); } @Test public void fromCpmShouldReturnCpmGivenDenseGranularityAndThirdRange() { - Assertions.assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.69), PriceGranularity.createFromString("dense"))) + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount())) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount(DOWN))) + .isEqualTo("13.50"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("dense"), givenAccount(UP))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.75), createFromString("dense"), givenAccount(TRUE))) + .isEqualTo("14.00"); + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(13.74), createFromString("dense"), givenAccount(TRUE))) .isEqualTo("13.50"); } @Test public void fromCpmShouldReturnResultWithDefaultPrecisionTwoIfRangePrecisionInNull() { - Assertions.assertThat( - CpmRange.fromCpm(BigDecimal.valueOf(2.3333), PriceGranularity.createFromExtPriceGranularity( + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.3333), createFromExtPriceGranularity( ExtPriceGranularity.of(null, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), - BigDecimal.valueOf(0.01))))))) + BigDecimal.valueOf(0.01))))), givenAccount())) .isEqualTo("2.33"); } @Test public void fromCpmShouldReturnResultWithPrecisionZero() { - Assertions.assertThat( - CpmRange.fromCpm(BigDecimal.valueOf(2.3333), PriceGranularity.createFromExtPriceGranularity( + assertThat(CpmRange.fromCpm(BigDecimal.valueOf(2.3333), createFromExtPriceGranularity( ExtPriceGranularity.of(0, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), - BigDecimal.valueOf(0.01))))))) + BigDecimal.valueOf(0.01))))), givenAccount())) .isEqualTo("2"); } @Test public void fromCpmAsNumberShouldReturnExpectedResult() { // given - final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity( + final PriceGranularity priceGranularity = createFromExtPriceGranularity( ExtPriceGranularity.of(null, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), BigDecimal.valueOf(0.01))))); // when - final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2.333), priceGranularity); + final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2.333), priceGranularity, givenAccount()); // then assertThat(result.compareTo(BigDecimal.valueOf(2.33))).isEqualTo(0); @@ -125,13 +236,13 @@ public void fromCpmAsNumberShouldReturnExpectedResult() { @Test public void fromCpmAsNumberShouldReturnExpectedResultForMultipleRanges() { // given - final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity( + final PriceGranularity priceGranularity = createFromExtPriceGranularity( ExtPriceGranularity.of(2, asList( ExtGranularityRange.of(BigDecimal.valueOf(1.5), BigDecimal.ONE), ExtGranularityRange.of(BigDecimal.valueOf(2.5), BigDecimal.valueOf(1.2))))); // when - final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2), priceGranularity); + final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2), priceGranularity, givenAccount()); // then assertThat(result.compareTo(BigDecimal.valueOf(1.5))).isEqualTo(0); @@ -140,14 +251,22 @@ public void fromCpmAsNumberShouldReturnExpectedResultForMultipleRanges() { @Test public void fromCpmAsNumberShouldRetunNullIfPriceDoesNotFitToRange() { // given - final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity( + final PriceGranularity priceGranularity = createFromExtPriceGranularity( ExtPriceGranularity.of(null, singletonList(ExtGranularityRange.of(BigDecimal.valueOf(3), BigDecimal.valueOf(0.01))))); // when - final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(-2.0), priceGranularity); + final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(-2.0), priceGranularity, givenAccount()); // then assertThat(result).isNull(); } + + private static Account givenAccount(AccountAuctionBidRoundingMode mode) { + return Account.builder().auction(AccountAuctionConfig.builder().bidRoundingMode(mode).build()).build(); + } + + private static Account givenAccount() { + return Account.builder().build(); + } } diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index 879bd7873c2..49ab351205a 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -6,6 +6,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; +import org.prebid.server.settings.model.Account; import java.math.BigDecimal; import java.util.Map; @@ -45,7 +46,7 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", false, null, null, null, null); + .makeFor(bid, "bidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsOnly( @@ -76,7 +77,7 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "veryververyverylongbidder1", false, null, null, null, null); + .makeFor(bid, "veryververyverylongbidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsOnly( @@ -111,7 +112,8 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, "cacheId1", "banner", "videoCacheId1", "categoryDuration"); + .makeFor(bid, "bidder1", true, "cacheId1", "banner", + "videoCacheId1", "categoryDuration", Account.empty("accountId")); // then assertThat(keywords).containsOnly( @@ -154,7 +156,7 @@ public void shouldIncludeFormatOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "", true, null, "banner", null, null); + .makeFor(bid, "", true, null, "banner", null, null, Account.empty("accountId")); // then assertThat(keywords).contains(entry("hb_format", "banner")); @@ -180,7 +182,7 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_cache_id_bidder", "hb_deal_bidder", "hb_size_bidder", @@ -207,7 +209,7 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).contains( @@ -235,7 +237,7 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_bidder", "hb_pb"); @@ -261,7 +263,7 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsKeys("hb_bidder", "hb_pb"); @@ -287,7 +289,7 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_bidder_bidder1", "hb_pb_bidder1"); @@ -313,7 +315,7 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsKeys("hb_bidder_bidder1", "hb_pb_bidder1"); @@ -339,7 +341,7 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() null, null, defaultKeyPrefix) - .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null); + .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).hasSize(2) @@ -366,7 +368,7 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).hasSize(2) @@ -393,7 +395,7 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder", true, null, null, null, null); + .makeFor(bid, "bidder", true, null, null, null, null, Account.empty("accountId")); // then // Without truncating: "hb_bidder", "hb_bidder_bidder", "hb_env", "hb_env_bidder", "hb_pb", "hb_pb_bidder" @@ -421,7 +423,7 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() null, null, defaultKeyPrefix) - .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null); + .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).hasSize(2) @@ -454,7 +456,7 @@ public void shouldTruncateKeysFromResolver() { null, resolver, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).contains(entry("key_longer_than_twen", "value1")); @@ -486,7 +488,7 @@ public void shouldIncludeKeywordsFromResolver() { null, resolver, defaultKeyPrefix) - .makeFor(bid, "bidder1", true, null, null, null, null); + .makeFor(bid, "bidder1", true, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).contains(entry("keyword1", "value1")); @@ -512,7 +514,7 @@ public void shouldIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsTrue() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", false, null, null, null, null); + .makeFor(bid, "bidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).containsOnlyKeys("hb_bidder_bidder1", "hb_deal_bidder1", "hb_pb_bidder1"); @@ -538,7 +540,7 @@ public void shouldNotIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsFalse() { null, null, defaultKeyPrefix) - .makeFor(bid, "bidder1", false, null, null, null, null); + .makeFor(bid, "bidder1", false, null, null, null, null, Account.empty("accountId")); // then assertThat(keywords).doesNotContainKeys("hb_bidder_bidder1", "hb_deal_bidder1", "hb_pb_bidder1"); From 951f2be004bb3393c1950f5812c291e15e0de0d0 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 13 May 2025 14:26:58 +0200 Subject: [PATCH 2/5] Docs Update --- docs/application-settings.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/application-settings.md b/docs/application-settings.md index d99607a1477..6dc2b8aba1e 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -25,6 +25,11 @@ There are two ways to configure application settings: database and file. This do - `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment - `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment - `auction.events.enabled` - enables events for account if true +- `auction.bid-rounding` - bid rounding options are: + - **down** - rounding down to the lower price bucket + - **up** - rounding up to the higher price bucket + - **timesplit** - 50% of the time rounding down to the lower PB and 50% of the time rounding up to the higher price bucket + - **true** - if the price >= 50% of the range, rounding up to the higher price bucket, otherwise rounding down - `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true. - `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false. - `auction.price-floors.fetch.url` - url to fetch price floors data from. From 83f53df84ec48557fcd4b261d8012f1def2f693f Mon Sep 17 00:00:00 2001 From: antonbabak Date: Thu, 15 May 2025 11:56:09 +0200 Subject: [PATCH 3/5] Small fix --- src/main/java/org/prebid/server/auction/CpmRange.java | 2 +- .../org/prebid/server/settings/model/AccountAuctionConfig.java | 2 +- src/test/java/org/prebid/server/auction/CpmRangeTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/CpmRange.java b/src/main/java/org/prebid/server/auction/CpmRange.java index 0fe864efe4d..00133373474 100644 --- a/src/main/java/org/prebid/server/auction/CpmRange.java +++ b/src/main/java/org/prebid/server/auction/CpmRange.java @@ -92,7 +92,7 @@ private static BigDecimal calculate(BigDecimal cpm, private static RoundingMode resolveRoundingMode(Account account) { final AccountAuctionBidRoundingMode accountRoundingMode = Optional.ofNullable(account) .map(Account::getAuction) - .map(AccountAuctionConfig::getBidRoundingMode) + .map(AccountAuctionConfig::getBidRounding) .orElse(AccountAuctionBidRoundingMode.DOWN); return switch (accountRoundingMode) { diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java index 551b3eae4bc..a9230017517 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -46,7 +46,7 @@ public class AccountAuctionConfig { AccountTargetingConfig targeting; @JsonAlias("bid-rounding") - AccountAuctionBidRoundingMode bidRoundingMode; + AccountAuctionBidRoundingMode bidRounding; @JsonProperty("preferredmediatype") Map preferredMediaTypes; diff --git a/src/test/java/org/prebid/server/auction/CpmRangeTest.java b/src/test/java/org/prebid/server/auction/CpmRangeTest.java index 5dc07003b25..48028175363 100644 --- a/src/test/java/org/prebid/server/auction/CpmRangeTest.java +++ b/src/test/java/org/prebid/server/auction/CpmRangeTest.java @@ -263,7 +263,7 @@ public void fromCpmAsNumberShouldRetunNullIfPriceDoesNotFitToRange() { } private static Account givenAccount(AccountAuctionBidRoundingMode mode) { - return Account.builder().auction(AccountAuctionConfig.builder().bidRoundingMode(mode).build()).build(); + return Account.builder().auction(AccountAuctionConfig.builder().bidRounding(mode).build()).build(); } private static Account givenAccount() { From 50117543c469b9687d61a49fdba69e0e279e7445 Mon Sep 17 00:00:00 2001 From: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Date: Fri, 23 May 2025 18:35:06 +0300 Subject: [PATCH 4/5] Test: Bid rounding (#3903) Co-authored-by: osulzhenko --- .../model/config/AccountAuctionConfig.groovy | 4 + .../model/request/auction/BidRounding.groovy | 24 ++++ .../server/functional/tests/AmpSpec.groovy | 2 +- .../server/functional/tests/BaseSpec.groovy | 20 +++- .../functional/tests/BidRoundingSpec.groovy | 111 ++++++++++++++++++ .../PriceFloorsEnforcementSpec.groovy | 2 +- 6 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy index 8dc9831e3fa..5fdd53ac481 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.request.auction.BidAdjustment +import org.prebid.server.functional.model.request.auction.BidRounding import org.prebid.server.functional.model.request.auction.PaaFormat import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.MediaType @@ -31,6 +32,7 @@ class AccountAuctionConfig { PrivacySandbox privacySandbox @JsonProperty("bidadjustments") BidAdjustment bidAdjustments + BidRounding bidRounding @JsonProperty("price_granularity") PriceGranularityType priceGranularitySnakeCase @@ -48,4 +50,6 @@ class AccountAuctionConfig { AccountBidValidationConfig bidValidationsSnakeCase @JsonProperty("price_floors") AccountPriceFloorsConfig priceFloorsSnakeCase + @JsonProperty("bid_rounding") + BidRounding bidRoundingSnakeCase } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy new file mode 100644 index 00000000000..ba612ce9f87 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRounding.groovy @@ -0,0 +1,24 @@ +package org.prebid.server.functional.model.request.auction + +import com.fasterxml.jackson.annotation.JsonValue + +enum BidRounding { + + UP("up"), + DOWN("down"), + TRUE("true"), + TIME_SPLIT("timesplit"), + UNKNOWN("unknown"), + + private String value + + BidRounding(String value) { + this.value = value + } + + @Override + @JsonValue + String toString() { + return value + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy index 78d6b03016d..fa72326fe7c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy @@ -86,7 +86,7 @@ class AmpSpec extends BaseSpec { then: "Response should contain information from stored response" def price = storedAuctionResponse.bid[0].price - assert response.targeting["hb_pb"] == getRoundedTargetingValueWithDefaultPrecision(price) + assert response.targeting["hb_pb"] == getRoundedTargetingValueWithDownPrecision(price) assert response.targeting["hb_size"] == "${storedAuctionResponse.bid[0].weight}x${storedAuctionResponse.bid[0].height}" and: "PBS not send request to bidder" diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index 63ba2516ff4..123dbb25505 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -19,7 +19,11 @@ import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils import spock.lang.Specification +import java.math.RoundingMode + import static java.math.RoundingMode.DOWN +import static java.math.RoundingMode.HALF_UP +import static java.math.RoundingMode.UP import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.util.SystemProperties.DEFAULT_TIMEOUT @@ -80,8 +84,16 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { logs.findAll { it.contains(text) } } - protected static String getRoundedTargetingValueWithDefaultPrecision(BigDecimal value) { - "${value.setScale(DEFAULT_TARGETING_PRECISION, DOWN)}0" + protected static String getRoundedTargetingValueWithDownPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, DOWN) + } + + protected static String getRoundedTargetingValueWithHalfUpPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, HALF_UP) + } + + protected static String getRoundedTargetingValueWithUpPrecision(BigDecimal value) { + roundWithDefaultPrecisionAndRoundingType(value, UP) } protected static Map> getRequests(BidResponse bidResponse) { @@ -100,4 +112,8 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { List bidderCalls) { [(bidderName): bidderCalls.collect { bidderCall -> decode(bidderCall.requestBody as String, BidderRequest) }] } + + private static GString roundWithDefaultPrecisionAndRoundingType(BigDecimal value, RoundingMode roundingMode) { + "${value.setScale(DEFAULT_TARGETING_PRECISION, roundingMode)}0" + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy new file mode 100644 index 00000000000..15095aaa3e8 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/BidRoundingSpec.groovy @@ -0,0 +1,111 @@ +package org.prebid.server.functional.tests + +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.AccountStatus.ACTIVE +import static org.prebid.server.functional.model.request.auction.BidRounding.DOWN +import static org.prebid.server.functional.model.request.auction.BidRounding.TRUE +import static org.prebid.server.functional.model.request.auction.BidRounding.UNKNOWN +import static org.prebid.server.functional.model.request.auction.BidRounding.UP + +class BidRoundingSpec extends BaseSpec { + + def "PBS should round bid value to the down when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.randomFloorValue + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithDownPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: null), + new AccountAuctionConfig(bidRounding: UNKNOWN), + new AccountAuctionConfig(bidRounding: DOWN), + new AccountAuctionConfig(bidRoundingSnakeCase: DOWN)] + } + + def "PBS should round bid value to the up when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.getRandomFloorValue() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithUpPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: UP), + new AccountAuctionConfig(bidRoundingSnakeCase: UP)] + } + + def "PBS should round bid value to the up or down when account bid rounding setting is #bidRoundingValue"() { + given: "Default bid request" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + enableCache() + } + + and: "Account in the DB" + def account = getAccountWithBidRounding(bidRequest.accountId, bidRoundingValue) + accountDao.save(account) + + and: "Default bid response" + def bidPrice = PBSUtils.getRandomFloorValue() + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid[0].price = bidPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Targeting hb_pb should be round" + def targeting = response.seatbid[0].bid[0].ext.prebid.targeting + assert targeting["hb_pb"] == getRoundedTargetingValueWithHalfUpPrecision(bidPrice) + + where: + bidRoundingValue << [new AccountAuctionConfig(bidRounding: TRUE), + new AccountAuctionConfig(bidRoundingSnakeCase: TRUE)] + } + + private static final Account getAccountWithBidRounding(String accountId, AccountAuctionConfig accountAuctionConfig) { + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy index 4d47947670a..cec8bb8fa41 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsEnforcementSpec.groovy @@ -77,7 +77,7 @@ class PriceFloorsEnforcementSpec extends PriceFloorsBaseSpec { def response = floorsPbsService.sendAmpRequest(ampRequest) then: "PBS should suppress bids lower than floorRuleValue" - def bidPrice = getRoundedTargetingValueWithDefaultPrecision(floorValue) + def bidPrice = getRoundedTargetingValueWithDownPrecision(floorValue) verifyAll(response) { targeting["hb_pb_generic"] == bidPrice targeting["hb_pb"] == bidPrice From abb3009c05fa77f946263078920c0f0160037d5d Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 2 Jun 2025 10:09:10 +0200 Subject: [PATCH 5/5] Fix comments --- .../BasicCategoryMappingService.java | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java index be359586d97..480f8c9dd49 100644 --- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java +++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java @@ -112,9 +112,21 @@ public Future createCategoryMapping(List final List rejectedBids = new ArrayList<>(); return makeBidderToBidCategory( - bidderResponses, withCategory, translateCategories, primaryAdServer, publisher, rejectedBids, timeout) - .map(categoryBidContexts -> resolveBidsCategoriesDurations(bidderResponses, categoryBidContexts, - account, bidRequest, targeting, withCategory, rejectedBids)); + bidderResponses, + withCategory, + translateCategories, + primaryAdServer, + publisher, + rejectedBids, + timeout) + .map(categoryBidContexts -> resolveBidsCategoriesDurations( + bidderResponses, + categoryBidContexts, + account, + bidRequest, + targeting, + withCategory, + rejectedBids)); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { @@ -345,8 +357,15 @@ private CategoryMappingResult resolveBidsCategoriesDurations(List> uniqueCatKeysToCategoryBids = categoryBidContexts.stream() - .map(categoryBidContext -> enrichCategoryBidContext(categoryBidContext, account, durations, - priceGranularity, withCategory, appendBidderNames, impIdToBiddersDealTear, rejectedBids)) + .map(categoryBidContext -> enrichCategoryBidContext( + categoryBidContext, + account, + durations, + priceGranularity, + withCategory, + appendBidderNames, + impIdToBiddersDealTear, + rejectedBids)) .filter(Objects::nonNull) .collect(Collectors.groupingBy(CategoryBidContext::getCategoryUniqueKey, Collectors.mapping(Function.identity(), Collectors.toSet())));