diff --git a/src/main/java/org/prebid/server/auction/PriceGranularity.java b/src/main/java/org/prebid/server/auction/PriceGranularity.java index 81cec03fd62..75620e4d953 100644 --- a/src/main/java/org/prebid/server/auction/PriceGranularity.java +++ b/src/main/java/org/prebid/server/auction/PriceGranularity.java @@ -72,6 +72,12 @@ public static PriceGranularity createFromString(String stringPriceGranularity) { } } + public static PriceGranularity createFromStringOrDefault(String stringPriceGranularity) { + return isValidStringPriceGranularityType(stringPriceGranularity) + ? STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.valueOf(stringPriceGranularity)) + : PriceGranularity.DEFAULT; + } + /** * Returns list of {@link ExtGranularityRange}s. */ diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java index 0084afc7aca..b014c508678 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java @@ -52,6 +52,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import java.util.ArrayList; @@ -60,6 +61,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; public class AmpRequestFactory { @@ -407,7 +409,7 @@ private Future updateBidRequest(AuctionContext auctionContext) { .map(ortbVersionConversionManager::convertToAuctionSupportedVersion) .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext)) .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest)) - .map(this::fillExplicitParameters) + .map(bidRequest -> fillExplicitParameters(bidRequest, account)) .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors())) .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true)) .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings())) @@ -459,7 +461,7 @@ private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidR * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext} * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters */ - private BidRequest fillExplicitParameters(BidRequest bidRequest) { + private BidRequest fillExplicitParameters(BidRequest bidRequest, Account account) { final List imps = bidRequest.getImp(); // Force HTTPS as AMP requires it, but pubs can forget to set it. final Imp imp = imps.getFirst(); @@ -496,6 +498,7 @@ private BidRequest fillExplicitParameters(BidRequest bidRequest) { .imp(setSecure ? Collections.singletonList(imps.getFirst().toBuilder().secure(1).build()) : imps) .ext(extRequest( bidRequest, + account, setDefaultTargeting, setDefaultCache)) .build(); @@ -692,6 +695,7 @@ private static List parseMultiSizeParam(String ms) { * Creates updated bidrequest.ext {@link ObjectNode}. */ private ExtRequest extRequest(BidRequest bidRequest, + Account account, boolean setDefaultTargeting, boolean setDefaultCache) { @@ -704,7 +708,7 @@ private ExtRequest extRequest(BidRequest bidRequest, : ExtRequestPrebid.builder(); if (setDefaultTargeting) { - prebidBuilder.targeting(createTargetingWithDefaults(prebid)); + prebidBuilder.targeting(createTargetingWithDefaults(prebid, account)); } if (setDefaultCache) { prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null), @@ -727,15 +731,14 @@ private ExtRequest extRequest(BidRequest bidRequest, * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's * child properties is missed or entire targeting does not exist. */ - private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) { + private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNull = targeting == null; final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity(); final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull(); - final JsonNode outgoingPriceGranularityNode - = isPriceGranularityNull - ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)) + final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull + ? mapper.mapper().valueToTree(ExtPriceGranularity.from(getDefaultPriceGranularity(account))) : priceGranularityNode; final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull @@ -759,6 +762,14 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) .build(); } + private static PriceGranularity getDefaultPriceGranularity(Account account) { + return Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + } + @Value(staticConstructor = "of") private static class GppSidExtraction { diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java index 8b377c08bf9..5bcabe413db 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java @@ -56,6 +56,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ObjectUtil; import org.prebid.server.util.StreamUtil; @@ -187,7 +189,11 @@ public BidRequest resolve(BidRequest bidRequest, final ExtRequest ext = bidRequest.getExt(); final List imps = bidRequest.getImp(); final ExtRequest populatedExt = populateRequestExt( - ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps), endpoint); + ext, + bidRequest, + ObjectUtils.defaultIfNull(populatedImps, imps), + endpoint, + auctionContext.getAccount()); final Source source = bidRequest.getSource(); final Source populatedSource = populateSource(source, populatedExt, hasStoredBidRequest); @@ -713,10 +719,15 @@ private static boolean isUniqueIds(List imps) { return impIdsSet.size() == impIdsList.size(); } - private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List imps, String endpoint) { + private ExtRequest populateRequestExt(ExtRequest ext, + BidRequest bidRequest, + List imps, + String endpoint, + Account account) { + final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(ext, ExtRequest::getPrebid); - final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps); + final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps, account); final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid); final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest, endpoint); @@ -783,7 +794,7 @@ private static void resolveImpMediaTypes(Imp imp, Set impsMediaTypes) { /** * Returns populated {@link ExtRequestTargeting} or null if no changes were applied. */ - private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps) { + private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List imps, Account account) { final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null; final boolean isTargetingNotNull = targeting != null; @@ -796,8 +807,12 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List i if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) { return targeting.toBuilder() - .pricegranularity(resolvePriceGranularity(targeting, isPriceGranularityNull, - isPriceGranularityTextual, imps)) + .pricegranularity(resolvePriceGranularity( + targeting, + isPriceGranularityNull, + isPriceGranularityTextual, + imps, + account)) .includewinners(isIncludeWinnersNull || targeting.getIncludewinners()) .includebidderkeys(isIncludeBidderKeysNull ? !isWinningOnly(prebid.getCache()) @@ -822,14 +837,22 @@ private boolean isWinningOnly(ExtRequestPrebidCache cache) { * In case of valid string price granularity replaced it with appropriate custom view. * In case of invalid string value throws {@link InvalidRequestException}. */ - private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull, - boolean isPriceGranularityTextual, List imps) { + private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, + boolean isPriceGranularityNull, + boolean isPriceGranularityTextual, + List imps, + Account account) { final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity()) .containsAll(getImpMediaTypes(imps)); if (isPriceGranularityNull && !hasAllMediaTypes) { - return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT)); + final PriceGranularity defaultPriceGranularity = Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getPriceGranularity) + .map(PriceGranularity::createFromStringOrDefault) + .orElse(PriceGranularity.DEFAULT); + return mapper.mapper().valueToTree(ExtPriceGranularity.from(defaultPriceGranularity)); } final JsonNode priceGranularityNode = targeting.getPricegranularity(); 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 63d27073805..2983ac3f731 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 @@ -12,7 +12,7 @@ import org.prebid.server.functional.model.response.auction.MediaType @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) class AccountAuctionConfig { - String priceGranularity + PriceGranularityType priceGranularity Integer bannerCacheTtl Integer videoCacheTtl Integer truncateTargetAttr @@ -28,7 +28,7 @@ class AccountAuctionConfig { PrivacySandbox privacySandbox @JsonProperty("price_granularity") - String priceGranularitySnakeCase + PriceGranularityType priceGranularitySnakeCase @JsonProperty("banner_cache_ttl") Integer bannerCacheTtlSnakeCase @JsonProperty("video_cache_ttl") diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy new file mode 100644 index 00000000000..957a2d880bf --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy @@ -0,0 +1,28 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.Range + +enum PriceGranularityType { + + LOW(2, [Range.getDefault(5, 0.5)]), + MEDIUM(2, [Range.getDefault(20, 0.1)]), + MED(2, [Range.getDefault(20, 0.1)]), + HIGH(2, [Range.getDefault(20, 0.01)]), + AUTO(2, [Range.getDefault(5, 0.05), Range.getDefault(10, 0.1), Range.getDefault(20, 0.5)]), + DENSE(2, [Range.getDefault(3, 0.01), Range.getDefault(8, 0.05), Range.getDefault(20, 0.5)]), + UNKNOWN(null, []) + + final Integer precision + final List ranges + + PriceGranularityType(Integer precision, List ranges) { + this.precision = precision + this.ranges = ranges + } + + @JsonValue + String toLowerCase() { + return name().toLowerCase() + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy index 29f4472cab2..873c686a578 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy @@ -1,10 +1,21 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.config.PriceGranularityType @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class PriceGranularity { Integer precision List ranges + + static PriceGranularity getDefault(PriceGranularityType granularity) { + new PriceGranularity(precision: granularity.precision, ranges: granularity.ranges) + } + + static PriceGranularity getDefault() { + getDefault(PriceGranularityType.MED) + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy index c5fa8cb2220..1b106b67faa 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy @@ -1,10 +1,16 @@ package org.prebid.server.functional.model.request.auction +import groovy.transform.EqualsAndHashCode import groovy.transform.ToString @ToString(includeNames = true, ignoreNulls = true) +@EqualsAndHashCode class Range { BigDecimal max BigDecimal increment + + static Range getDefault(Integer max, BigDecimal increment) { + new Range(max: max, increment: increment) + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy index 67ff7907901..e24d22b4b8f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,6 +4,7 @@ import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.bidder.Openx import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.PriceGranularityType import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse @@ -17,13 +18,17 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode import java.nio.charset.StandardCharsets +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST +import static org.prebid.server.functional.model.AccountStatus.ACTIVE import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.config.PriceGranularityType.UNKNOWN import static org.prebid.server.functional.model.response.auction.ErrorType.TARGETING import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer @@ -1121,6 +1126,230 @@ class TargetingSpec extends BaseSpec { assert targeting["hb_env"] == HB_ENV_AMP } + def "PBS auction should throw error when price granularity from original request is empty"() { + given: "Default bidRequest with empty price granularity" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS auction should prioritize price granularity from original request over account config"() { + given: "Default bidRequest with price granularity" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity as PriceGranularityType) + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + } + + and: "Account in the DB" + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: PBSUtils.getRandomEnum(PriceGranularityType)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS amp should prioritize price granularity from original request over account config"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity) + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from bidRequest" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from account config with different name case when original request doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def "PBS auction should include price granularity from default account config when original request doesn't contain price granularity"() { + given: "Pbs with default account that include privacySandbox configuration" + def priceGranularity = PBSUtils.getRandomEnum(PriceGranularityType, [UNKNOWN]) + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def pbsService = pbsServiceFactory.getService( + ["settings.default-account-config": encode(accountConfig)]) + + and: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + when: "PBS processes auction request" + pbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + } + + def "PBS auction should include include default price granularity when original request and account config doesn't contain price granularity"() { + given: "Default basic BidRequest" + def bidRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAuctionRequest(bidRequest) + + then: "BidderRequest should include default price granularity" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.default + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(priceGranularity: UNKNOWN)] + } + + def "PBS amp should throw error when price granularity from original request is empty"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest with empty price granularity" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN)) + setAccountId(ampRequest.account) + } + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType)) + accountDao.save(account) + + when: "PBS processes auction request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "Request should fail with an error" + def exception = thrown(PrebidServerException) + assert exception.statusCode == BAD_REQUEST.code() + assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied' + } + + def "PBS amp should include price granularity from account config when original request doesn't contain price granularity"() { + given: "Default AmpRequest" + def ampRequest = AmpRequest.defaultAmpRequest + + and: "Default ampStoredRequest" + def ampStoredRequest = BidRequest.defaultBidRequest.tap { + ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false) + setAccountId(ampRequest.account) + } + + and: "Account in the DB" + def account = createAccountWithPriceGranularity(ampRequest.account, priceGranularity) + accountDao.save(account) + + and: "Create and save stored request into DB" + def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest) + storedRequestDao.save(storedRequest) + + when: "PBS processes amp request" + defaultPbsService.sendAmpRequest(ampRequest) + + then: "BidderRequest should include price granularity from account config" + def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id) + assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) + + where: + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + } + + def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + return new Account(uuid: accountId, config: accountConfig) + } + private static PrebidServerService getEnabledWinBidsPbsService() { pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"]) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy index 8316ee54233..22e5b27a04f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy @@ -17,7 +17,6 @@ import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.JPY -import static org.prebid.server.functional.model.bidder.BidderName.GENERIC import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy index 0d72dcfe4c6..2db485448b4 100644 --- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy +++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy @@ -107,9 +107,9 @@ class PBSUtils implements ObjectMapperWrapper { getRandomDecimal(min, max).setScale(scale, HALF_UP) } - static > T getRandomEnum(Class anEnum) { - def values = anEnum.enumConstants - values[getRandomNumber(0, values.length - 1)] + static > T getRandomEnum(Class anEnum, List exclude = []) { + def values = anEnum.enumConstants.findAll { !exclude.contains(it) } as T[] + values[getRandomNumber(0, values.size() - 1)] } static String convertCase(String input, Case caseType) { diff --git a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java index 8c3fd9ee24b..338af30f830 100644 --- a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java +++ b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java @@ -26,6 +26,19 @@ public void createFromStringShouldThrowPrebidExceptionIfInvalidStringType() { assertThatExceptionOfType(PreBidException.class).isThrownBy(() -> PriceGranularity.createFromString("invalid")); } + @Test + public void createFromStringOrDefaultShouldCreateMedPriceGranularityWhenInvalidStringType() { + // given and when + final PriceGranularity defaultPriceGranularity = PriceGranularity.createFromStringOrDefault( + "invalid"); + + // then + assertThat(defaultPriceGranularity.getRangesMax()).isEqualByComparingTo(BigDecimal.valueOf(20)); + assertThat(defaultPriceGranularity.getPrecision()).isEqualTo(2); + assertThat(defaultPriceGranularity.getRanges()).containsOnly( + ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))); + } + @Test public void createCustomPriceGranularityByStringLow() { // given and when diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java index 7318d7906c2..9b716645aa7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java @@ -62,6 +62,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.request.Targeting; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import java.util.ArrayList; @@ -540,6 +542,33 @@ public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestE ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))))); } + @Test + public void shouldReturnBidRequestWithAccountPriceGranularityIfStoredBidRequestExtTargetingHasNoPriceGranularity() { + // given + givenBidRequest( + builder -> builder + .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())), + Imp.builder().build()); + + given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(Account.builder() + .auction(AccountAuctionConfig.builder().priceGranularity("low").build()) + .build())); + + // when + final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest(); + + // then + assertThat(singletonList(request)) + .extracting(BidRequest::getExt).isNotNull() + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity) + // assert that priceGranularity was set with default value and includeWinners remained unchanged + .containsExactly( + tuple(false, mapper.valueToTree(ExtPriceGranularity.of(2, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))))); + } + @Test public void shouldReturnBidRequestWithNotChangedExtRequestPrebidTargetingFields() { // given diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java index 81680a5ac7e..41fa98842e7 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java @@ -58,6 +58,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtSite; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; import java.math.BigDecimal; import java.util.ArrayList; @@ -1836,6 +1838,33 @@ public void shouldSetDefaultPriceGranularityIfPriceGranularityAndMediaTypePriceG BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)))))); } + @Test + public void shouldSetAccountPriceGranularityIfPriceGranularityAndMediaTypePriceGranularityIsMissing() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().video(Video.builder().build()).ext(mapper.createObjectNode()).build())) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder().build()) + .build())) + .build(); + + final AuctionContext givenAuctionContext = auctionContext.with(Account.builder() + .auction(AccountAuctionConfig.builder().priceGranularity("low").build()) + .build()); + + // when + final BidRequest result = target.resolve(bidRequest, givenAuctionContext, ENDPOINT, false); + + // then + assertThat(singletonList(result)) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getTargeting) + .extracting(ExtRequestTargeting::getPricegranularity) + .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of( + BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))))); + } + @Test public void shouldNotSetDefaultPriceGranularityIfThereIsAMediaTypePriceGranularityForImpType() { // given