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