diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index ee28ddbe9d7..dd8f79c1920 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -23,6 +23,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.AuctionParticipation; @@ -101,6 +102,7 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountAuctionEventConfig; +import org.prebid.server.settings.model.AccountBidRankingConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.AccountTargetingConfig; import org.prebid.server.settings.model.VideoStoredDataResult; @@ -114,9 +116,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -124,6 +126,7 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; public class BidResponseCreator { @@ -644,7 +647,9 @@ private Future cacheBidsAndCreateResponse(List final ExtRequestTargeting targeting = targeting(bidRequest); final List bidderResponseInfos = toBidderResponseWithTargetingBidInfos( - bidderResponses, bidderToMultiBids, preferDeals(targeting)); + bidderResponses, + bidderToMultiBids, + preferDeals(targeting)); final Set bidInfos = bidderResponseInfos.stream() .map(BidderResponseInfo::getSeatBid) @@ -687,76 +692,81 @@ private List toBidderResponseWithTargetingBidInfos( Map bidderToMultiBids, boolean preferDeals) { - final Map> bidderResponseToReducedBidInfos = bidderResponses.stream() - .collect(Collectors.toMap( - Function.identity(), - bidderResponse -> toSortedMultiBidInfo(bidderResponse, bidderToMultiBids, preferDeals))); - - final Map>> impIdToBidderToBidInfos = bidderResponseToReducedBidInfos.values() - .stream() - .flatMap(Collection::stream) - .collect(Collectors.groupingBy( - bidInfo -> bidInfo.getCorrespondingImp().getId(), - Collectors.groupingBy(BidInfo::getBidder))); - - // Best bids from bidders for imp - final Set winningBids = new HashSet<>(); - // All bids from bidder for imp - final Set winningBidsByBidder = new HashSet<>(); - - for (final Map> bidderToBidInfos : impIdToBidderToBidInfos.values()) { + final Comparator comparator = winningBidComparatorFactory.create(preferDeals).reversed(); - bidderToBidInfos.values().forEach(winningBidsByBidder::addAll); - - bidderToBidInfos.values().stream() - .flatMap(Collection::stream) - .max(winningBidComparatorFactory.create(preferDeals)) - .ifPresent(winningBids::add); - } + final List> bidInfosPerBidder = bidderResponses.stream() + .map(bidderResponse -> limitMultiBid(bidderResponse, bidderToMultiBids, comparator)) + .toList(); + final List> rankedBidInfos = applyRanking(bidInfosPerBidder, comparator); - return bidderResponseToReducedBidInfos.entrySet().stream() - .map(responseToBidInfos -> injectBidInfoWithTargeting( - responseToBidInfos.getKey(), - responseToBidInfos.getValue(), - bidderToMultiBids, - winningBids, - winningBidsByBidder)) + return IntStream.range(0, bidderResponses.size()) + .mapToObj(i -> enrichBidInfoWithTargeting( + bidderResponses.get(i), + rankedBidInfos.get(i), + bidderToMultiBids)) .toList(); } - private List toSortedMultiBidInfo(BidderResponseInfo bidderResponse, + private static List limitMultiBid(BidderResponseInfo bidderResponse, Map bidderToMultiBids, - boolean preferDeals) { + Comparator comparator) { + + final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); + final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; final List bidInfos = bidderResponse.getSeatBid().getBidsInfos(); final Map> impIdToBidInfos = bidInfos.stream() .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); - final MultiBidConfig multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); - final Integer bidLimit = multiBid != null ? multiBid.getMaxBids() : DEFAULT_BID_LIMIT_MIN; - return impIdToBidInfos.values().stream() - .map(infos -> sortReducedBidInfo(infos, bidLimit, preferDeals)) - .flatMap(Collection::stream) + .flatMap(infos -> infos.stream() + .sorted(comparator) + .limit(bidLimit)) .toList(); } - private List sortReducedBidInfo(List bidInfos, int limit, boolean preferDeals) { - return bidInfos.stream() - .sorted(winningBidComparatorFactory.create(preferDeals).reversed()) - .limit(limit) - .toList(); + private static List> applyRanking(List> bidInfosPerBidder, + Comparator comparator) { + + final Map>> impIdToBidderBidInfo = new HashMap<>(); + for (int bidderIndex = 0; bidderIndex < bidInfosPerBidder.size(); bidderIndex++) { + final List bidInfos = bidInfosPerBidder.get(bidderIndex); + + for (BidInfo bidInfo : bidInfos) { + impIdToBidderBidInfo + .computeIfAbsent(bidInfo.getCorrespondingImp().getId(), ignore -> new ArrayList<>()) + .add(Pair.of(bidderIndex, bidInfo)); + } + } + + for (List> bidderToBidInfo : impIdToBidderBidInfo.values()) { + bidderToBidInfo.sort(Comparator.comparing(Pair::getRight, comparator)); + } + + final List> rankedBidInfosPerBidder = new ArrayList<>(); + for (int i = 0; i < bidInfosPerBidder.size(); i++) { + rankedBidInfosPerBidder.add(new ArrayList<>()); + } + + for (List> sortedBidderToBidInfo : impIdToBidderBidInfo.values()) { + for (int rank = 0; rank < sortedBidderToBidInfo.size(); rank++) { + final Pair bidderToBidInfo = sortedBidderToBidInfo.get(rank); + final BidInfo bidInfo = bidderToBidInfo.getRight(); + + rankedBidInfosPerBidder.get(bidderToBidInfo.getLeft()) + .add(bidInfo.toBuilder().rank(rank + 1).build()); + } + } + + return rankedBidInfosPerBidder; } - private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, + private static BidderResponseInfo enrichBidInfoWithTargeting(BidderResponseInfo bidderResponseInfo, List bidderBidInfos, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + Map bidderToMultiBids) { final String bidder = bidderResponseInfo.getBidder(); - final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids, - winningBids, winningBidsByBidder); + final List bidInfosWithTargeting = toBidInfoWithTargeting(bidderBidInfos, bidder, bidderToMultiBids); final BidderSeatBidInfo seatBid = bidderResponseInfo.getSeatBid(); final BidderSeatBidInfo modifiedSeatBid = seatBid.with(bidInfosWithTargeting); @@ -765,24 +775,20 @@ private static BidderResponseInfo injectBidInfoWithTargeting(BidderResponseInfo private static List toBidInfoWithTargeting(List bidderBidInfos, String bidder, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + Map bidderToMultiBids) { final Map> impIdToBidInfos = bidderBidInfos.stream() .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); return impIdToBidInfos.values().stream() - .map(bidInfos -> injectTargeting(bidInfos, bidder, bidderToMultiBids, winningBids, winningBidsByBidder)) + .map(bidInfos -> enrichWithTargeting(bidInfos, bidder, bidderToMultiBids)) .flatMap(Collection::stream) .toList(); } - private static List injectTargeting(List bidderImpIdBidInfos, - String bidder, - Map bidderToMultiBids, - Set winningBids, - Set winningBidsByBidder) { + private static List enrichWithTargeting(List bidderImpIdBidInfos, + String bidder, + Map bidderToMultiBids) { final List result = new ArrayList<>(); @@ -797,8 +803,7 @@ private static List injectTargeting(List bidderImpIdBidInfos, final TargetingInfo targetingInfo = TargetingInfo.builder() .isTargetingEnabled(targetingBidderCode != null) - .isBidderWinningBid(winningBidsByBidder.contains(bidInfo)) - .isWinningBid(winningBids.contains(bidInfo)) + .isWinningBid(bidInfo.getRank() == 1) .isAddTargetBidderCode(targetingBidderCode != null && multiBidSize > 1) .bidderCode(targetingBidderCode) .seat(targetingCode(bidInfo.getSeat(), bidderCodePrefix, i)) @@ -819,10 +824,6 @@ private static String targetingCode(String base, String prefix, int i) { return prefix != null ? prefix + (i + 1) : null; } - /** - * Returns {@link ExtBidResponse} object, populated with response time, errors and debug info (if requested) - * from all bidders. - */ private ExtBidResponse toExtBidResponse(List bidderResponseInfos, AuctionContext auctionContext, CacheServiceResult cacheResult, @@ -1544,6 +1545,7 @@ private Bid toBid(BidInfo bidInfo, BidRequest bidRequest, Account account, Map> bidWarnings) { + final TargetingInfo targetingInfo = bidInfo.getTargetingInfo(); final BidType bidType = bidInfo.getBidType(); final Bid bid = bidInfo.getBid(); @@ -1575,6 +1577,8 @@ private Bid toBid(BidInfo bidInfo, final ObjectNode originalBidExt = bid.getExt(); final Boolean dealsTierSatisfied = bidInfo.getSatisfiedPriority(); + final boolean bidRankingEnabled = isBidRankingEnabled(account); + final ExtBidPrebid updatedExtBidPrebid = getExtPrebid(originalBidExt, ExtBidPrebid.class) .map(ExtBidPrebid::toBuilder) @@ -1584,6 +1588,7 @@ private Bid toBid(BidInfo bidInfo, .dealTierSatisfied(dealsTierSatisfied) .cache(cache) .passThrough(extractPassThrough(bidInfo.getCorrespondingImp())) + .rank(bidRankingEnabled ? bidInfo.getRank() : null) .build(); final ObjectNode updatedBidExt = @@ -1601,7 +1606,6 @@ private Bid toBid(BidInfo bidInfo, private boolean shouldIncludeTargetingInResponse(ExtRequestTargeting targeting, TargetingInfo targetingInfo) { return targeting != null && targetingInfo.isTargetingEnabled() - && targetingInfo.isBidderWinningBid() && (Objects.equals(targeting.getIncludebidderkeys(), true) || Objects.equals(targeting.getIncludewinners(), true) || Objects.equals(targeting.getIncludeformat(), true)); @@ -1614,6 +1618,13 @@ private JsonNode extractPassThrough(Imp imp) { .orElse(null); } + private static boolean isBidRankingEnabled(Account account) { + return Optional.ofNullable(account.getAuction()) + .map(AccountAuctionConfig::getRanking) + .map(AccountBidRankingConfig::getEnabled) + .orElse(false); + } + private String createNativeMarkup(String bidAdm, Imp correspondingImp) { final Response nativeMarkup; try { @@ -1740,9 +1751,6 @@ private static boolean eventsAllowedByRequest(AuctionContext auctionContext) { return prebid != null && prebid.getEvents() != null; } - /** - * Extracts auction timestamp from {@link ExtRequest} or get it from {@link Clock} if it is null. - */ private long auctionTimestamp(AuctionContext auctionContext) { final ExtRequest ext = auctionContext.getBidRequest().getExt(); final ExtRequestPrebid prebid = ext != null ? ext.getPrebid() : null; @@ -1872,9 +1880,6 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe resolveKeyPrefix); } - /** - * Returns max targeting keyword length. - */ private int resolveTruncateAttrChars(ExtRequestTargeting targeting, Account account) { final AccountAuctionConfig accountAuctionConfig = account.getAuction(); final Integer accountTruncateTargetAttr = @@ -1929,11 +1934,6 @@ private static boolean isCachedDebugEnabled(CachedDebugLog cachedDebugLog) { return cachedDebugLog != null && cachedDebugLog.isEnabled(); } - /** - * Parse {@link JsonNode} to {@link List} of {@link ExtPriceGranularity}. - *

- * Throws {@link PreBidException} in case of errors during decoding price granularity. - */ private ExtPriceGranularity parsePriceGranularity(JsonNode priceGranularity) { try { return mapper.mapper().treeToValue(priceGranularity, ExtPriceGranularity.class); @@ -1969,9 +1969,6 @@ private static BidResponse populateSeatNonBid(AuctionContext auctionContext, Bid return bidResponse.toBuilder().ext(updatedExtBidResponse).build(); } - /** - * Creates {@link CacheAsset} for the given cache ID. - */ private CacheAsset toCacheAsset(String cacheId) { return CacheAsset.of(cacheAssetUrlTemplate.concat(cacheId), cacheId); } @@ -1983,9 +1980,6 @@ private static Set nullIfEmpty(Set set) { return Collections.unmodifiableSet(set); } - /** - * Creates {@link ExtBidPrebidVideo} from bid extension. - */ private Optional getExtBidPrebidVideo(ObjectNode bidExt) { return getExtPrebid(bidExt, ExtBidPrebid.class) .map(ExtBidPrebid::getVideo); diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java index f2e3fc7b438..224b6344f34 100644 --- a/src/main/java/org/prebid/server/auction/model/BidInfo.java +++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java @@ -37,6 +37,8 @@ public class BidInfo { Integer vastTtl; + Integer rank; + public String getBidId() { final ObjectNode extNode = bid != null ? bid.getExt() : null; final JsonNode bidIdNode = extNode != null ? extNode.path("prebid").path("bidid") : null; diff --git a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java index dbd3d7fd2b3..60f453a942f 100644 --- a/src/main/java/org/prebid/server/auction/model/TargetingInfo.java +++ b/src/main/java/org/prebid/server/auction/model/TargetingInfo.java @@ -15,7 +15,5 @@ public class TargetingInfo { boolean isWinningBid; - boolean isBidderWinningBid; - boolean isAddTargetBidderCode; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java index d116447caed..afc46770799 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java @@ -40,4 +40,6 @@ public class ExtBidPrebid { @JsonProperty("passthrough") JsonNode passThrough; + + Integer rank; } 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 82bf01afb75..e41f005df54 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java @@ -58,4 +58,6 @@ public class AccountAuctionConfig { PaaFormat paaFormat; AccountCacheConfig cache; + + AccountBidRankingConfig ranking; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java b/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java new file mode 100644 index 00000000000..361ff4c9781 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/AccountBidRankingConfig.java @@ -0,0 +1,9 @@ +package org.prebid.server.settings.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class AccountBidRankingConfig { + + Boolean enabled; +} 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 d0b3ee586d2..bf49ce7c874 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 @@ -24,6 +24,7 @@ class AccountAuctionConfig { AccountBidValidationConfig bidValidations AccountEventsConfig events AccountCacheConfig cache + AccountRankingConfig ranking AccountPriceFloorsConfig priceFloors Targeting targeting PaaFormat paaformat diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy new file mode 100644 index 00000000000..64103717127 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountRankingConfig.groovy @@ -0,0 +1,9 @@ +package org.prebid.server.functional.model.config + +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +class AccountRankingConfig { + + Boolean enabled +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index 353f5516935..18bc588d1d3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -72,7 +72,7 @@ class Bid implements ObjectMapperWrapper { } } - static List getDefaultMultyTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { + static List getDefaultMultiTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { List bids = [] if (imp.banner) bids << createBid(imp, BidMediaType.BANNER) { adm = null } if (imp.video) bids << createBid(imp, BidMediaType.VIDEO) diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy index 35fa5b6a540..d8accbf82ac 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Prebid.groovy @@ -15,4 +15,5 @@ class Prebid { Meta meta Map passThrough Video storedRequestAttributes + Integer rank } 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 e24d22b4b8f..081c578ecfa 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy @@ -4,20 +4,34 @@ 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.AccountRankingConfig import org.prebid.server.functional.model.config.PriceGranularityType import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.db.StoredImp import org.prebid.server.functional.model.db.StoredRequest import org.prebid.server.functional.model.db.StoredResponse import org.prebid.server.functional.model.request.amp.AmpRequest import org.prebid.server.functional.model.request.auction.AdServerTargeting import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.MultiBid +import org.prebid.server.functional.model.request.auction.Native import org.prebid.server.functional.model.request.auction.PrebidCache +import org.prebid.server.functional.model.request.auction.PrebidStoredRequest import org.prebid.server.functional.model.request.auction.PriceGranularity import org.prebid.server.functional.model.request.auction.Range +import org.prebid.server.functional.model.request.auction.StoredAuctionResponse import org.prebid.server.functional.model.request.auction.StoredBidResponse import org.prebid.server.functional.model.request.auction.Targeting +import org.prebid.server.functional.model.request.auction.Video import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidMediaType import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.ErrorType +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.model.response.auction.Prebid +import org.prebid.server.functional.model.response.auction.SeatBid import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils @@ -28,8 +42,11 @@ 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.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD 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.model.response.auction.MediaType.VIDEO import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class TargetingSpec extends BaseSpec { @@ -40,7 +57,10 @@ class TargetingSpec extends BaseSpec { private static final String DEFAULT_TARGETING_PREFIX = "hb" private static final Integer TARGETING_PREFIX_LENGTH = 11 private static final Integer MAX_TRUNCATE_ATTR_CHARS = 255 + private static final Integer MAX_BIDS_RANKING = 3 private static final String HB_ENV_AMP = "amp" + private static final Integer MAIN_RANK = 1 + private static final Integer SUBORDINATE_RANK = 2 def "PBS should include targeting bidder specific keys when alwaysIncludeDeals is true and deal bid wins"() { given: "Bid request with alwaysIncludeDeals = true" @@ -1166,7 +1186,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) } def "PBS amp should prioritize price granularity from original request over account config"() { @@ -1196,7 +1216,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) } def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() { @@ -1217,7 +1237,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + 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"() { @@ -1238,7 +1258,7 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + 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"() { @@ -1301,7 +1321,6 @@ class TargetingSpec extends BaseSpec { 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) @@ -1341,13 +1360,549 @@ class TargetingSpec extends BaseSpec { assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity) where: - priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) + priceGranularity << (PriceGranularityType.values() - UNKNOWN as List) } - def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) { + def "PBS shouldn't add bid ranked for request when account config for auction.ranking disabled or default"() { + given: "Bid request with enabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + 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) + + and: "Bid response with 3 bids where deal bid has higher price" + def imp = bidRequest.imp.first + def bids = [Bid.getDefaultBid(imp), Bid.getDefaultBid(imp), Bid.getDefaultBid(imp)] + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = bids + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS bids in response shouldn't contain ranks" + assert response?.seatbid?.bid?.ext?.prebid?.rank?.flatten() == [null] * MAX_BIDS_RANKING + + where: + accountAuctionConfig << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(ranking: new AccountRankingConfig()), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: null)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: false)) + ] + } + + def "PBS should add bid ranked and rank by deals for default request when auction.ranking and preferDeals are enabled"() { + given: "Bid request with enabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = true + } + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidWithDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidWithDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank single bid" + verifyAll(response.seatbid.first.bid) { + it.id == [bidWithDeal.id] + it.price == [bidWithDeal.price] + it.ext.prebid.rank == [MAIN_RANK] + } + } + + def "PBS should add bid ranked and rank by price for default request when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidWithDealId = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidWithDealId] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank single bid" + verifyAll(response.seatbid.first.bid) { + it.id == [bidBiggerPrice.id] + it.price == [bidBiggerPrice.price] + it.ext.prebid.rank == [MAIN_RANK] + } + } + + def "PBS should add bid ranked and rank by price for request with multiBid when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should add bid ranked and rank by price for multiple media types request when auction.ranking is enabled and preferDeals disabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.video = Video.getDefaultVideo() + it.imp.first.nativeObj = Native.getDefaultNative() + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultMultiTypesBids(bidRequest.imp.first).first.tap { + it.price = bidPrice + 1 + } + def bidBDeal = Bid.getDefaultMultiTypesBids(bidRequest.imp.first).last.tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + assert !response.ext.warnings + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should properly rank bids when request with multibid contains some invalid bid"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultVideoRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def higherPriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 2 + } + + def middlePriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 1 + adm = null + } + + def lowerPriceBid = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [lowerPriceBid, middlePriceBid, higherPriceBid] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == higherPriceBid.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == lowerPriceBid.id).ext.prebid.rank == 2 + + and: "PBS should contain error for invalid bid" + response.ext.errors[ErrorType.GENERIC]?.message == + ["BidId `${middlePriceBid.id}` validation messages: Error: Bid \"${middlePriceBid.id}\" with video type missing adm and nurl"] + } + + def "PBS should assign bid ranks across all seatbids combined when the request contains imps with multiple bidders"() { + given: "PBS config with openX bidder" + def pbsConfig = ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Bid request with multiple bidders" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = true + } + it.ext.prebid.multibid = [new MultiBid(bidder: WILDCARD, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def genericBid = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + } + def openxBid = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + it.seatbid = [new SeatBid(bid: [genericBid], seat: GENERIC), + new SeatBid(bid: [openxBid], seat: OPENX)] + } + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS should rank OpenX bid higher than Generic bid" + assert response.seatbid.findAll { it.seat == OPENX }.bid.ext.prebid.rank.flatten() == [MAIN_RANK] + assert response.seatbid.findAll { it.seat == GENERIC }.bid.ext.prebid.rank.flatten() == [SUBORDINATE_RANK] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should assign bid ranks for each imp separately when request has multiple imps and multiBid is configured"() { + given: "Bid request with multiple imps" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.imp.first.nativeObj = Native.getDefaultNative() + imp.add(Imp.getDefaultImpression(VIDEO)) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = requestPreferDeals + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with multiple bids" + def bidPrice = PBSUtils.randomPrice + def bidLowerPrice = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + mediaType = BidMediaType.NATIVE + } + def bidHigherPrice = Bid.getDefaultBid(bidRequest.imp.first).tap { + price = bidPrice + 1 + } + def bidWithDeal = Bid.getDefaultBid(bidRequest.imp.last).tap { + dealid = PBSUtils.randomNumber + price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidLowerPrice, bidHigherPrice, bidWithDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bids for first imp" + def bids = response.seatbid.first.bid + def firstImpBidders = bids.findAll { it.impid == bidRequest.imp.id.first() } + assert firstImpBidders.find { it.id == bidHigherPrice.id }.ext.prebid.rank == 1 + assert firstImpBidders.find { it.id == bidLowerPrice.id }.ext.prebid.rank == 2 + + and: "should separately rank bids for second imp" + def secondImpBidders = bids.findAll { it.impid == bidRequest.imp.id.last() } + assert secondImpBidders*.ext.prebid.rank == [MAIN_RANK] + + where: + requestPreferDeals << [null, false, true] + } + + def "PBS should ignore bid ranked from original response when auction.ranking enabled"() { + given: "Bid request with disabled preferDeals" + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + it.ext = new BidExt(prebid: new Prebid(rank: PBSUtils.randomNumber)) + } + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + it.ext = new BidExt(prebid: new Prebid(rank: PBSUtils.randomNumber)) + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS should add bid ranked and rank by price for request with stored imp when auction.ranking enabled"() { + given: "Bid request with disabled preferDeals" + def storedRequestId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap { + preferDeals = false + } + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def account = getAccountConfigWithAuctionRanking(bidRequest.accountId) + accountDao.save(account) + + and: "Save storedImp into DB" + def impression = Imp.getDefaultImpression(MediaType.BANNER).tap { + id = storedRequestId + video = Video.getDefaultVideo() + } + def storedImp = StoredImp.getStoredImp(bidRequest.accountId, impression) + storedImpDao.save(storedImp) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPrice = Bid.getDefaultMultiTypesBids(impression).first.tap { + it.price = bidPrice + 1 + impid = bidRequest.imp.id.first + } + def bidBDeal = Bid.getDefaultMultiTypesBids(impression).last.tap { + impid = bidRequest.imp.id.first + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + } + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = [bidBiggerPrice, bidBDeal] + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should rank bid with higher price as top priority" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == 1 + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == 2 + } + + def "PBS shouldn't rank bids for request with stored imp when auction.ranking default"() { + given: "Bid request with enabled preferDeals" + def storedRequestId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + imp.first.ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId) + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: new AccountAuctionConfig()) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Save storedImp into DB" + def impression = Imp.getDefaultImpression(MediaType.BANNER).tap { + id = storedRequestId + video = Video.getDefaultVideo() + nativeObj = Native.getDefaultNative() + } + def storedImp = StoredImp.getStoredImp(bidRequest.accountId, impression) + storedImpDao.save(storedImp) + + and: "Bid response with 2 bids where deal bid has lower price" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + seatbid[0].bid = Bid.getDefaultMultiTypesBids(impression) { impid = bidRequest.imp.id.first } + } + + and: "Set bidder response" + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS bids in response shouldn't contain ranks" + assert response?.seatbid?.bid?.ext?.prebid?.rank?.flatten() == [null] * MAX_BIDS_RANKING + } + + def "PBS should copy bid ranked from stored response when auction.ranking #auction"() { + given: "Bid request with enabled preferDeals" + def storedResponseId = PBSUtils.randomNumber + def bidRequest = BidRequest.getDefaultBidRequest().tap { + it.ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true) + it.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: MAX_BIDS_RANKING)] + enableCache() + ext.prebid.storedAuctionResponse = new StoredAuctionResponse(id: storedResponseId) + } + + and: "Account in the DB" + def accountConfig = new AccountConfig(status: ACTIVE, auction: auction) + def account = new Account(uuid: bidRequest.accountId, config: accountConfig) + accountDao.save(account) + + and: "Stored response in DB" + def bidPrice = PBSUtils.randomPrice + def bidBiggerPriceRanking = PBSUtils.randomNumber + def bidBiggerPrice = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.price = bidPrice + 1 + it.ext = new BidExt(prebid: new Prebid(rank: bidBiggerPriceRanking)) + } + def bidBDealRanking = PBSUtils.randomNumber + def bidBDeal = Bid.getDefaultBid(bidRequest.imp[0]).tap { + it.dealid = PBSUtils.randomNumber + it.price = bidPrice + it.ext = new BidExt(prebid: new Prebid(rank: bidBDealRanking)) + } + def storedResponse = new StoredResponse(responseId: storedResponseId, + storedAuctionResponse: new SeatBid(bid: [bidBiggerPrice, bidBDeal], seat: GENERIC)) + storedResponseDao.save(storedResponse) + + when: "PBS processes auction request" + def response = defaultPbsService.sendAuctionRequest(bidRequest) + + then: "PBS should copy bid ranked from stored response" + def bids = response.seatbid.first.bid + assert bids.find(it -> it.id == bidBiggerPrice.id).ext.prebid.rank == bidBiggerPriceRanking + assert bids.find(it -> it.id == bidBDeal.id).ext.prebid.rank == bidBDealRanking + + where: + auction << [ + null, + new AccountAuctionConfig(), + new AccountAuctionConfig(ranking: new AccountRankingConfig()), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: null)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: false)), + new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: true)) + ] + } + + Account 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) + new Account(uuid: accountId, config: accountConfig) + } + + Account getAccountConfigWithAuctionRanking(String accountId, Boolean auctionRankingEnablement = true) { + def accountAuctionConfig = new AccountAuctionConfig(ranking: new AccountRankingConfig(enabled: auctionRankingEnablement)) + def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig) + new Account(uuid: accountId, config: accountConfig) } private static PrebidServerService getEnabledWinBidsPbsService() { diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy index a1fa69e487e..c04e4d263d7 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy @@ -636,7 +636,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { def originalPrice = PBSUtils.getRandomDecimal() def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = currency - seatbid.first.bid = Bid.getDefaultMultyTypesBids(bidRequest.imp.first) { + seatbid.first.bid = Bid.getDefaultMultiTypesBids(bidRequest.imp.first) { price = originalPrice ext = new BidExt() } diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index f62c6a44651..83deee74ce8 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -88,7 +88,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.request.TraceLevel; -import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.CacheAsset; import org.prebid.server.proto.openrtb.ext.response.Events; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; @@ -118,6 +117,7 @@ import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountAuctionEventConfig; +import org.prebid.server.settings.model.AccountBidRankingConfig; import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.settings.model.VideoStoredDataResult; import org.prebid.server.spring.config.model.CacheDefaultTtlProperties; @@ -161,7 +161,6 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic; import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; @@ -304,8 +303,22 @@ public void shouldPassBidWithGeneratedIdAndPreserveExtFieldsWhenIdGeneratorTypeU .put("origbidcur", "test") .set("prebid", mapper.valueToTree(extBidPrebid))) .build(); - final BidInfo bidInfo = toBidInfo(expectedBid, imp, bidder, "seat", banner, true); - verify(coreCacheService).cacheBidsOpenrtb(eq(singletonList(bidInfo)), any(), any(), any()); + final BidInfo expectedBidInfo = BidInfo.builder() + .bid(expectedBid) + .correspondingImp(imp) + .bidder("bidder1") + .seat("seat") + .bidType(banner) + .targetingInfo(TargetingInfo.builder() + .bidderCode("bidder1") + .seat("seat") + .isTargetingEnabled(true) + .isWinningBid(true) + .isAddTargetBidderCode(false) + .build()) + .rank(1) + .build(); + verify(coreCacheService).cacheBidsOpenrtb(eq(singletonList(expectedBidInfo)), any(), any(), any()); } @Test @@ -955,7 +968,22 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenIdGeneratorIsUUIDAndEvent .ext(mapper.createObjectNode().set("prebid", mapper.valueToTree(extBidPrebid))) .build(); - final BidInfo expectedBidInfo = toBidInfo(expectedBid, imp, bidder, "seat", banner, true); + final BidInfo expectedBidInfo = BidInfo.builder() + .bid(expectedBid) + .correspondingImp(imp) + .bidder("bidder1") + .seat("seat") + .bidType(banner) + .targetingInfo(TargetingInfo.builder() + .bidderCode("bidder1") + .seat("seat") + .isTargetingEnabled(true) + .isWinningBid(true) + .isAddTargetBidderCode(false) + .build()) + .rank(1) + .build(); + verify(coreCacheService).cacheBidsOpenrtb(eq(singletonList(expectedBidInfo)), any(), any(), any()); verify(eventsService).createEvent(eq(generatedBidId), anyString(), anyString(), anyBoolean(), any()); @@ -985,7 +1013,7 @@ public void shouldSetExpectedResponseSeatBidAndBidFields() { .ext(bidExt) .build(); - final String bidder = "bidder1"; + final String bidder = "bidder"; final List bidderResponses = singletonList(BidderResponse.of( bidder, givenSeatBid( @@ -1042,6 +1070,189 @@ public void shouldSetExpectedResponseSeatBidAndBidFields() { .build()); } + @Test + public void shouldSetExpectedBidsWithRanksWhenBidRankingEnabled() { + // given + final ObjectNode bidExt = mapper.createObjectNode() + .put("origbidcpm", BigDecimal.ONE) + .put("origbidcur", "USD"); + + final Bid bid1 = Bid.builder() + .id("bidId1") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .impid("impId1") + .ext(bidExt) + .build(); + + final Bid bid2 = Bid.builder() + .id("bidId2") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .impid("impId2") + .ext(bidExt) + .build(); + + final String bidder = "bidder"; + final List bidderResponses = singletonList(BidderResponse.of( + bidder, + givenSeatBid( + BidderBid.of(bid1, banner, "seat1", "USD"), + BidderBid.of(bid2, banner, "seat2", "USD")), + 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(givenImp("impId1"), givenImp("impId2")), + contextBuilder -> contextBuilder + .auctionParticipations(toAuctionParticipant(bidderResponses)) + .account(Account.builder().auction(AccountAuctionConfig.builder() + .ranking(AccountBidRankingConfig.of(true)).build()) + .build())); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); + givenCacheServiceResult(emptyList()); + + // when + final BidResponse bidResponse = target.create(auctionContext, cacheInfo, MULTI_BIDS).result(); + + // then + final ObjectNode expectedBidExtToCache = mapper.createObjectNode(); + expectedBidExtToCache.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build())); + expectedBidExtToCache.put("origbidcpm", BigDecimal.ONE); + expectedBidExtToCache.put("origbidcur", "USD"); + + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + verify(coreCacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any()); + + assertThat(bidsArgumentCaptor.getValue()) + .extracting(bidInfo -> bidInfo.getBid().getExt()) + .containsOnly(expectedBidExtToCache); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().rank(1).type(banner).build())); + expectedBidExt.put("origbidcpm", BigDecimal.ONE); + expectedBidExt.put("origbidcur", "USD"); + + assertThat(bidResponse.getSeatbid()) + .containsOnly( + SeatBid.builder() + .seat("seat1") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId1") + .impid("impId1") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .ext(expectedBidExt) + .build())) + .build(), + SeatBid.builder() + .seat("seat2") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId2") + .impid("impId2") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .ext(expectedBidExt) + .build())) + .build()); + } + + @Test + public void shouldSetExpectedBidsWithRanksWhenBidHasSameImpIdAndBidRankingEnabled() { + // given + final ObjectNode bidExt = mapper.createObjectNode() + .put("origbidcpm", BigDecimal.ONE) + .put("origbidcur", "USD"); + + final Bid bid1 = Bid.builder() + .id("bidId1") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .impid("impId") + .ext(bidExt) + .build(); + + final Bid bid2 = Bid.builder() + .id("bidId2") + .price(BigDecimal.TWO) + .adm(BID_ADM) + .impid("impId") + .ext(bidExt) + .build(); + + final String bidder = "bidder"; + final List bidderResponses = singletonList(BidderResponse.of( + bidder, + givenSeatBid( + BidderBid.of(bid1, banner, "seat1", "USD"), + BidderBid.of(bid2, banner, "seat2", "USD")), + 100)); + + final AuctionContext auctionContext = givenAuctionContext( + givenBidRequest(givenImp("impId")), + contextBuilder -> contextBuilder + .auctionParticipations(toAuctionParticipant(bidderResponses)) + .account(Account.builder().auction(AccountAuctionConfig.builder() + .ranking(AccountBidRankingConfig.of(true)).build()) + .build())); + + final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder().doCaching(true).build(); + givenCacheServiceResult(emptyList()); + + // when + final BidResponse bidResponse = target.create(auctionContext, cacheInfo, MULTI_BIDS).result(); + + // then + final ObjectNode expectedBidExtToCache = mapper.createObjectNode(); + expectedBidExtToCache.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().type(banner).build())); + expectedBidExtToCache.put("origbidcpm", BigDecimal.ONE); + expectedBidExtToCache.put("origbidcur", "USD"); + + final ArgumentCaptor> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class); + verify(coreCacheService).cacheBidsOpenrtb(bidsArgumentCaptor.capture(), any(), any(), any()); + + assertThat(bidsArgumentCaptor.getValue()) + .extracting(bidInfo -> bidInfo.getBid().getExt()) + .containsOnly(expectedBidExtToCache); + + final ObjectNode expectedBidExtWithRank1 = mapper.createObjectNode(); + expectedBidExtWithRank1.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().rank(1).type(banner).build())); + expectedBidExtWithRank1.put("origbidcpm", BigDecimal.ONE); + expectedBidExtWithRank1.put("origbidcur", "USD"); + + final ObjectNode expectedBidExtWithRank2 = mapper.createObjectNode(); + expectedBidExtWithRank2.set("prebid", mapper.valueToTree(ExtBidPrebid.builder().rank(2).type(banner).build())); + expectedBidExtWithRank2.put("origbidcpm", BigDecimal.ONE); + expectedBidExtWithRank2.put("origbidcur", "USD"); + + assertThat(bidResponse.getSeatbid()) + .containsOnly( + SeatBid.builder() + .seat("seat1") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId1") + .impid("impId") + .price(BigDecimal.ONE) + .adm(BID_ADM) + .ext(expectedBidExtWithRank2) + .build())) + .build(), + SeatBid.builder() + .seat("seat2") + .group(0) + .bid(singletonList(Bid.builder() + .id("bidId2") + .impid("impId") + .price(BigDecimal.TWO) + .adm(BID_ADM) + .ext(expectedBidExtWithRank1) + .build())) + .build()); + } + @Test public void shouldUpdateCacheDebugLogWithExtBidResponseWhenEnabledAndBidsReturned() { // given @@ -1647,7 +1858,7 @@ public void shouldPassPreferDealsToWinningComparatorFactoryWhenBidRequestTrue() target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - verify(winningBidComparatorFactory, times(2)).create(eq(true)); + verify(winningBidComparatorFactory).create(eq(true)); } @Test @@ -1667,7 +1878,7 @@ public void shouldPassPreferDealsFalseWhenBidRequestPreferDealsIsNotDefined() { target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result(); // then - verify(winningBidComparatorFactory, times(2)).create(eq(false)); + verify(winningBidComparatorFactory).create(eq(false)); } @Test @@ -5456,30 +5667,6 @@ private static Map zipBidsWithCacheInfos(List bidInfos, .collect(Collectors.toMap(i -> bidInfos.get(i).getBid(), cacheInfos::get)); } - private static BidInfo toBidInfo(Bid bid, - Imp correspondingImp, - String bidder, - String seat, - BidType bidType, - boolean isWinningBid) { - - return BidInfo.builder() - .bid(bid) - .correspondingImp(correspondingImp) - .bidder(bidder) - .seat(seat) - .bidType(bidType) - .targetingInfo(TargetingInfo.builder() - .bidderCode(bidder) - .seat(seat) - .isTargetingEnabled(true) - .isWinningBid(isWinningBid) - .isBidderWinningBid(true) - .isAddTargetBidderCode(false) - .build()) - .build(); - } - private static Imp givenImp() { return givenImp(IMP_ID); }