diff --git a/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java new file mode 100644 index 00000000000..15344b28576 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/AnalyticsTagsEnricher.java @@ -0,0 +1,125 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; +import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAnalyticsConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class AnalyticsTagsEnricher { + + private AnalyticsTagsEnricher() { + } + + public static AuctionContext enrichWithAnalyticsTags(AuctionContext context) { + final boolean clientDetailsEnabled = isClientDetailsEnabled(context); + if (!clientDetailsEnabled) { + return context; + } + + final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) + .map(Account::getAnalytics) + .map(AccountAnalyticsConfig::isAllowClientDetails) + .orElse(false); + + if (!allowClientDetails) { + return addClientDetailsWarning(context); + } + + final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); + + if (extAnalyticsTags == null) { + return context; + } + + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); + + final ExtBidResponsePrebid updatedExtPrebid = extPrebid + .map(ExtBidResponsePrebid::toBuilder) + .orElse(ExtBidResponsePrebid.builder()) + .analytics(ExtAnalytics.of(extAnalyticsTags)) + .build(); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .prebid(updatedExtPrebid) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } + + private static boolean isClientDetailsEnabled(AuctionContext context) { + final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getAnalytics) + .orElse(null); + + if (notObjectNode(analytics)) { + return false; + } + + final JsonNode options = analytics.get("options"); + if (notObjectNode(options)) { + return false; + } + + final JsonNode enableClientDetails = options.get("enableclientdetails"); + return enableClientDetails != null + && enableClientDetails.isBoolean() + && enableClientDetails.asBoolean(); + } + + private static boolean notObjectNode(JsonNode jsonNode) { + return jsonNode == null || !jsonNode.isObject(); + } + + private static AuctionContext addClientDetailsWarning(AuctionContext context) { + final BidResponse bidResponse = context.getBidResponse(); + final Optional ext = Optional.ofNullable(bidResponse.getExt()); + + final Map> warnings = ext + .map(ExtBidResponse::getWarnings) + .orElse(Collections.emptyMap()); + final List prebidWarnings = ObjectUtils.defaultIfNull( + warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), + Collections.emptyList()); + + final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); + updatedPrebidWarnings.add(ExtBidderError.of( + BidderError.Type.generic.getCode(), + "analytics.options.enableclientdetails not enabled for account")); + final Map> updatedWarnings = new HashMap<>(warnings); + updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); + + final ExtBidResponse updatedExt = ext + .map(ExtBidResponse::toBuilder) + .orElse(ExtBidResponse.builder()) + .warnings(updatedWarnings) + .build(); + + final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); + return context.with(updatedBidResponse); + } +} diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java new file mode 100644 index 00000000000..ec6b1c9d2f3 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -0,0 +1,241 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.Bid; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.PbsUtil; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class BidsAdjuster { + + private static final String ORIGINAL_BID_CPM = "origbidcpm"; + private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; + + private final ResponseBidValidator responseBidValidator; + private final CurrencyConversionService currencyService; + private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final PriceFloorEnforcer priceFloorEnforcer; + private final DsaEnforcer dsaEnforcer; + private final JacksonMapper mapper; + + public BidsAdjuster(ResponseBidValidator responseBidValidator, + CurrencyConversionService currencyService, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + JacksonMapper mapper) { + + this.responseBidValidator = Objects.requireNonNull(responseBidValidator); + this.currencyService = Objects.requireNonNull(currencyService); + this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); + this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); + this.mapper = Objects.requireNonNull(mapper); + } + + public List validateAndAdjustBids(List auctionParticipations, + AuctionContext auctionContext, + BidderAliases aliases) { + + return auctionParticipations.stream() + .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) + .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) + .map(auctionParticipation -> priceFloorEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getAccount(), + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .map(auctionParticipation -> dsaEnforcer.enforce( + auctionContext.getBidRequest(), + auctionParticipation, + auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) + .toList(); + } + + private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, + AuctionContext auctionContext, + BidderAliases aliases) { + + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidRequest bidRequest = auctionContext.getBidRequest(); + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + final List errors = new ArrayList<>(seatBid.getErrors()); + final List warnings = new ArrayList<>(seatBid.getWarnings()); + + final List requestCurrencies = bidRequest.getCur(); + if (requestCurrencies.size() > 1) { + errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" + .formatted(requestCurrencies.getFirst()))); + } + + final List bids = seatBid.getBids(); + final List validBids = new ArrayList<>(bids.size()); + + for (final BidderBid bid : bids) { + final ValidationResult validationResult = responseBidValidator.validate( + bid, + bidderResponse.getBidder(), + auctionContext, + aliases); + + if (validationResult.hasWarnings() || validationResult.hasErrors()) { + errors.add(makeValidationBidderError(bid.getBid(), validationResult)); + } + + if (!validationResult.hasErrors()) { + validBids.add(bid); + } + } + + final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() + ? bidderResponse + : bidderResponse.with( + seatBid.toBuilder() + .bids(validBids) + .errors(errors) + .warnings(warnings) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { + final String validationErrors = Stream.concat( + validationResult.getErrors().stream().map(message -> "Error: " + message), + validationResult.getWarnings().stream().map(message -> "Warning: " + message)) + .collect(Collectors.joining(". ")); + + final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); + return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); + } + + private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, + BidRequest bidRequest) { + if (auctionParticipation.isRequestBlocked()) { + return auctionParticipation; + } + + final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); + final BidderSeatBid seatBid = bidderResponse.getSeatBid(); + + final List bidderBids = seatBid.getBids(); + if (bidderBids.isEmpty()) { + return auctionParticipation; + } + + final List updatedBidderBids = new ArrayList<>(bidderBids.size()); + final List errors = new ArrayList<>(seatBid.getErrors()); + final String adServerCurrency = bidRequest.getCur().getFirst(); + + for (final BidderBid bidderBid : bidderBids) { + try { + final BidderBid updatedBidderBid = + updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); + updatedBidderBids.add(updatedBidderBid); + } catch (PreBidException e) { + errors.add(BidderError.generic(e.getMessage())); + } + } + + final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() + .bids(updatedBidderBids) + .errors(errors) + .build()); + return auctionParticipation.with(resultBidderResponse); + } + + private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, + BidderResponse bidderResponse, + BidRequest bidRequest, + String adServerCurrency) { + final Bid bid = bidderBid.getBid(); + final String bidCurrency = bidderBid.getBidCurrency(); + final BigDecimal price = bid.getPrice(); + + final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( + price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); + + final BigDecimal priceAdjustmentFactor = + bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); + final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); + + final ObjectNode bidExt = bid.getExt(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + + updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); + + final Bid.BidBuilder bidBuilder = bid.toBuilder(); + if (adjustedPrice.compareTo(price) != 0) { + bidBuilder.price(adjustedPrice); + } + + if (!updatedBidExt.isEmpty()) { + bidBuilder.ext(updatedBidExt); + } + + return bidderBid.toBuilder().bid(bidBuilder.build()).build(); + } + + private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { + final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); + if (adjustmentFactors == null) { + return null; + } + final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( + bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); + + return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); + } + + private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); + return prebid != null ? prebid.getBidadjustmentfactors() : null; + } + + private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { + return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 + ? price.multiply(priceAdjustmentFactor) + : price; + } + + private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); + if (StringUtils.isNotBlank(bidCurrency)) { + addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); + } + } + + private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { + node.set(propertyName, propertyValue); + } +} diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 87953ff2177..66b34a8df46 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -2,9 +2,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; @@ -31,7 +29,6 @@ import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -58,13 +55,11 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.ExecutionAction; @@ -94,7 +89,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; @@ -105,19 +99,11 @@ 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.request.ExtUser; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; -import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics; -import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; -import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; -import org.prebid.server.proto.openrtb.ext.response.ExtBidderError; import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAnalyticsConfig; import org.prebid.server.util.HttpUtil; -import org.prebid.server.util.ObjectUtil; +import org.prebid.server.util.ListUtil; +import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.math.BigDecimal; import java.time.Clock; @@ -134,7 +120,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; public class ExchangeService { @@ -144,8 +129,6 @@ public class ExchangeService { private static final String PREBID_EXT = "prebid"; private static final String BIDDER_EXT = "bidder"; private static final String TID_EXT = "tid"; - private static final String ORIGINAL_BID_CPM = "origbidcpm"; - private static final String ORIGINAL_BID_CURRENCY = "origbidcur"; private static final String ALL_BIDDERS_CONFIG = "*"; private static final Integer DEFAULT_MULTIBID_LIMIT_MIN = 1; private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9; @@ -166,17 +149,13 @@ public class ExchangeService { private final TimeoutFactory timeoutFactory; private final BidRequestOrtbVersionConversionManager ortbVersionConversionManager; private final HttpBidderRequester httpBidderRequester; - private final ResponseBidValidator responseBidValidator; - private final CurrencyConversionService currencyService; private final BidResponseCreator bidResponseCreator; private final BidResponsePostProcessor bidResponsePostProcessor; private final HookStageExecutor hookStageExecutor; private final HttpInteractionLogger httpInteractionLogger; private final PriceFloorAdjuster priceFloorAdjuster; - private final PriceFloorEnforcer priceFloorEnforcer; private final PriceFloorProcessor priceFloorProcessor; - private final DsaEnforcer dsaEnforcer; - private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private final BidsAdjuster bidsAdjuster; private final Metrics metrics; private final Clock clock; private final JacksonMapper mapper; @@ -197,17 +176,13 @@ public ExchangeService(double logSamplingRate, TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager ortbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, PriceFloorProcessor priceFloorProcessor, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -228,17 +203,13 @@ public ExchangeService(double logSamplingRate, this.timeoutFactory = Objects.requireNonNull(timeoutFactory); this.ortbVersionConversionManager = Objects.requireNonNull(ortbVersionConversionManager); this.httpBidderRequester = Objects.requireNonNull(httpBidderRequester); - this.responseBidValidator = Objects.requireNonNull(responseBidValidator); - this.currencyService = Objects.requireNonNull(currencyService); this.bidResponseCreator = Objects.requireNonNull(bidResponseCreator); this.bidResponsePostProcessor = Objects.requireNonNull(bidResponsePostProcessor); this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor); this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger); this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); - this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer); this.priceFloorProcessor = Objects.requireNonNull(priceFloorProcessor); - this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer); - this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); + this.bidsAdjuster = Objects.requireNonNull(bidsAdjuster); this.metrics = Objects.requireNonNull(metrics); this.clock = Objects.requireNonNull(clock); this.mapper = Objects.requireNonNull(mapper); @@ -249,7 +220,7 @@ public ExchangeService(double logSamplingRate, public Future holdAuction(AuctionContext context) { return processAuctionRequest(context) .compose(this::invokeResponseHooks) - .map(this::enrichWithAnalyticsTags) + .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags) .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo) .map(this::updateHooksMetrics); } @@ -303,7 +274,8 @@ private Future runAuction(AuctionContext receivedContext) { bidRequest.getImp(), context.getBidRejectionTrackers())) .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings)) - .map(auctionParticipations -> validateAndAdjustBids(auctionParticipations, context, aliases)) + .map(auctionParticipations -> + bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases)) .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases)) .map(context::with)) // produce response from bidder results @@ -319,20 +291,20 @@ private Future runAuction(AuctionContext receivedContext) { } private BidderAliases aliases(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final Map aliases = prebid != null ? prebid.getAliases() : null; final Map aliasgvlids = prebid != null ? prebid.getAliasgvlids() : null; return BidderAliases.of(aliases, aliasgvlids, bidderCatalog); } private static ExtRequestTargeting targeting(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); return prebid != null ? prebid.getTargeting() : null; } private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { final ExtRequestTargeting targeting = targeting(bidRequest); - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ExtRequestPrebidCache cache = prebid != null ? prebid.getCache() : null; if (targeting != null && cache != null) { @@ -367,13 +339,8 @@ private static BidRequestCacheInfo bidRequestCacheInfo(BidRequest bidRequest) { return BidRequestCacheInfo.noCache(); } - private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { - final ExtRequest requestExt = bidRequest.getExt(); - return requestExt != null ? requestExt.getPrebid() : null; - } - private static Map bidderToMultiBids(BidRequest bidRequest, List debugWarnings) { - final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid extRequestPrebid = PbsUtil.extRequestPrebid(bidRequest); final Collection multiBids = extRequestPrebid != null ? CollectionUtils.emptyIfNull(extRequestPrebid.getMultibid()) : Collections.emptyList(); @@ -629,7 +596,7 @@ private User prepareUser(String bidder, userBuilder.buyeruid(buyerUidUpdateResult.getValue()); if (shouldUpdateUserEids) { - userBuilder.eids(nullIfEmpty(allowedUserEids)); + userBuilder.eids(ListUtil.nullIfEmpty(allowedUserEids)); } if (shouldUpdateUserExt) { @@ -706,7 +673,7 @@ private List getAuctionParticipation( * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ private static Map bidderToPrebidBidders(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); + final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); if (bidders == null || bidders.isNull()) { @@ -1338,185 +1305,6 @@ private boolean isZeroNonDealBids(BigDecimal price, String dealId) { || (price.compareTo(BigDecimal.ZERO) == 0 && StringUtils.isBlank(dealId)); } - private List validateAndAdjustBids(List auctionParticipations, - AuctionContext auctionContext, - BidderAliases aliases) { - - return auctionParticipations.stream() - .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases)) - .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest())) - .map(auctionParticipation -> priceFloorEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getAccount(), - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .map(auctionParticipation -> dsaEnforcer.enforce( - auctionContext.getBidRequest(), - auctionParticipation, - auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) - .toList(); - } - - private AuctionParticipation validBidderResponse(AuctionParticipation auctionParticipation, - AuctionContext auctionContext, - BidderAliases aliases) { - - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidRequest bidRequest = auctionContext.getBidRequest(); - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final List errors = new ArrayList<>(seatBid.getErrors()); - final List warnings = new ArrayList<>(seatBid.getWarnings()); - - final List requestCurrencies = bidRequest.getCur(); - if (requestCurrencies.size() > 1) { - errors.add(BidderError.badInput("Cur parameter contains more than one currency. %s will be used" - .formatted(requestCurrencies.getFirst()))); - } - - final List bids = seatBid.getBids(); - final List validBids = new ArrayList<>(bids.size()); - - for (final BidderBid bid : bids) { - final ValidationResult validationResult = responseBidValidator.validate( - bid, - bidderResponse.getBidder(), - auctionContext, - aliases); - - if (validationResult.hasWarnings() || validationResult.hasErrors()) { - errors.add(makeValidationBidderError(bid.getBid(), validationResult)); - } - - if (!validationResult.hasErrors()) { - validBids.add(bid); - } - } - - final BidderResponse resultBidderResponse = errors.size() == seatBid.getErrors().size() - ? bidderResponse - : bidderResponse.with( - seatBid.toBuilder() - .bids(validBids) - .errors(errors) - .warnings(warnings) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) { - final String validationErrors = Stream.concat( - validationResult.getErrors().stream().map(message -> "Error: " + message), - validationResult.getWarnings().stream().map(message -> "Warning: " + message)) - .collect(Collectors.joining(". ")); - - final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown"); - return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors); - } - - private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation, - BidRequest bidRequest) { - if (auctionParticipation.isRequestBlocked()) { - return auctionParticipation; - } - - final BidderResponse bidderResponse = auctionParticipation.getBidderResponse(); - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - - final List bidderBids = seatBid.getBids(); - if (bidderBids.isEmpty()) { - return auctionParticipation; - } - - final List updatedBidderBids = new ArrayList<>(bidderBids.size()); - final List errors = new ArrayList<>(seatBid.getErrors()); - final String adServerCurrency = bidRequest.getCur().getFirst(); - - for (final BidderBid bidderBid : bidderBids) { - try { - final BidderBid updatedBidderBid = - updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency); - updatedBidderBids.add(updatedBidderBid); - } catch (PreBidException e) { - errors.add(BidderError.generic(e.getMessage())); - } - } - - final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder() - .bids(updatedBidderBids) - .errors(errors) - .build()); - return auctionParticipation.with(resultBidderResponse); - } - - private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid, - BidderResponse bidderResponse, - BidRequest bidRequest, - String adServerCurrency) { - final Bid bid = bidderBid.getBid(); - final String bidCurrency = bidderBid.getBidCurrency(); - final BigDecimal price = bid.getPrice(); - - final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency( - price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency); - - final BigDecimal priceAdjustmentFactor = - bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid); - final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency); - - final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); - - updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency); - - final Bid.BidBuilder bidBuilder = bid.toBuilder(); - if (adjustedPrice.compareTo(price) != 0) { - bidBuilder.price(adjustedPrice); - } - - if (!updatedBidExt.isEmpty()) { - bidBuilder.ext(updatedBidExt); - } - - return bidderBid.toBuilder().bid(bidBuilder.build()).build(); - } - - private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) { - final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - final ImpMediaType mediaType = ImpMediaTypeResolver.resolve( - bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType()); - - return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); - } - - private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); - return prebid != null ? prebid.getBidadjustmentfactors() : null; - } - - private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) { - return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0 - ? price.multiply(priceAdjustmentFactor) - : price; - } - - private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price)); - if (StringUtils.isNotBlank(bidCurrency)) { - addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency)); - } - } - - private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) { - node.set(propertyName, propertyValue); - } - private int responseTime(long startTime) { return Math.toIntExact(clock.millis() - startTime); } @@ -1580,101 +1368,6 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) { }; } - private AuctionContext enrichWithAnalyticsTags(AuctionContext context) { - final boolean clientDetailsEnabled = isClientDetailsEnabled(context); - if (!clientDetailsEnabled) { - return context; - } - - final boolean allowClientDetails = Optional.ofNullable(context.getAccount()) - .map(Account::getAnalytics) - .map(AccountAnalyticsConfig::isAllowClientDetails) - .orElse(false); - - if (!allowClientDetails) { - return addClientDetailsWarning(context); - } - - final List extAnalyticsTags = HookDebugInfoEnricher.toExtAnalyticsTags(context); - - if (extAnalyticsTags == null) { - return context; - } - - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - final Optional extPrebid = ext.map(ExtBidResponse::getPrebid); - - final ExtBidResponsePrebid updatedExtPrebid = extPrebid - .map(ExtBidResponsePrebid::toBuilder) - .orElse(ExtBidResponsePrebid.builder()) - .analytics(ExtAnalytics.of(extAnalyticsTags)) - .build(); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .prebid(updatedExtPrebid) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - - private static boolean isClientDetailsEnabled(AuctionContext context) { - final JsonNode analytics = Optional.ofNullable(context.getBidRequest()) - .map(BidRequest::getExt) - .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getAnalytics) - .orElse(null); - - if (notObjectNode(analytics)) { - return false; - } - - final JsonNode options = analytics.get("options"); - if (notObjectNode(options)) { - return false; - } - - final JsonNode enableClientDetails = options.get("enableclientdetails"); - return enableClientDetails != null - && enableClientDetails.isBoolean() - && enableClientDetails.asBoolean(); - } - - private static boolean notObjectNode(JsonNode jsonNode) { - return jsonNode == null || !jsonNode.isObject(); - } - - private static AuctionContext addClientDetailsWarning(AuctionContext context) { - final BidResponse bidResponse = context.getBidResponse(); - final Optional ext = Optional.ofNullable(bidResponse.getExt()); - - final Map> warnings = ext - .map(ExtBidResponse::getWarnings) - .orElse(Collections.emptyMap()); - final List prebidWarnings = ObjectUtils.defaultIfNull( - warnings.get(BidResponseCreator.DEFAULT_DEBUG_KEY), - Collections.emptyList()); - - final List updatedPrebidWarnings = new ArrayList<>(prebidWarnings); - updatedPrebidWarnings.add(ExtBidderError.of( - BidderError.Type.generic.getCode(), - "analytics.options.enableclientdetails not enabled for account")); - final Map> updatedWarnings = new HashMap<>(warnings); - updatedWarnings.put(BidResponseCreator.DEFAULT_DEBUG_KEY, updatedPrebidWarnings); - - final ExtBidResponse updatedExt = ext - .map(ExtBidResponse::toBuilder) - .orElse(ExtBidResponse.builder()) - .warnings(updatedWarnings) - .build(); - - final BidResponse updatedBidResponse = bidResponse.toBuilder().ext(updatedExt).build(); - return context.with(updatedBidResponse); - } - private AuctionContext updateHooksMetrics(AuctionContext context) { final EnumMap> stageOutcomes = context.getHookExecutionContext().getStageOutcomes(); @@ -1727,8 +1420,4 @@ private void updateHookInvocationMetrics(Account account, Stage stage, HookExecu metrics.updateAccountHooksMetrics(account, moduleCode, status, action); } } - - private List nullIfEmpty(List value) { - return CollectionUtils.isEmpty(value) ? null : value; - } } diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 8e21fe77b4b..d248264a59d 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -13,6 +13,7 @@ import org.prebid.server.auction.AmpResponsePostProcessor; import org.prebid.server.auction.BidResponseCreator; import org.prebid.server.auction.BidResponsePostProcessor; +import org.prebid.server.auction.BidsAdjuster; import org.prebid.server.auction.DebugResolver; import org.prebid.server.auction.DsaEnforcer; import org.prebid.server.auction.ExchangeService; @@ -835,17 +836,13 @@ ExchangeService exchangeService( TimeoutFactory timeoutFactory, BidRequestOrtbVersionConversionManager bidRequestOrtbVersionConversionManager, HttpBidderRequester httpBidderRequester, - ResponseBidValidator responseBidValidator, - CurrencyConversionService currencyConversionService, BidResponseCreator bidResponseCreator, BidResponsePostProcessor bidResponsePostProcessor, HookStageExecutor hookStageExecutor, HttpInteractionLogger httpInteractionLogger, PriceFloorAdjuster priceFloorAdjuster, - PriceFloorEnforcer priceFloorEnforcer, PriceFloorProcessor priceFloorProcessor, - DsaEnforcer dsaEnforcer, - BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + BidsAdjuster bidsAdjuster, Metrics metrics, Clock clock, JacksonMapper mapper, @@ -867,23 +864,36 @@ ExchangeService exchangeService( timeoutFactory, bidRequestOrtbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyConversionService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, priceFloorProcessor, - dsaEnforcer, - bidAdjustmentFactorResolver, + bidsAdjuster, metrics, clock, mapper, criteriaLogManager, enabledStrictAppSiteDoohValidation); } + @Bean + BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator, + CurrencyConversionService currencyConversionService, + PriceFloorEnforcer priceFloorEnforcer, + DsaEnforcer dsaEnforcer, + BidAdjustmentFactorResolver bidAdjustmentFactorResolver, + JacksonMapper mapper) { + + return new BidsAdjuster( + responseBidValidator, + currencyConversionService, + bidAdjustmentFactorResolver, + priceFloorEnforcer, + dsaEnforcer, + mapper); + } + @Bean StoredRequestProcessor storedRequestProcessor( @Value("${auction.stored-requests-timeout-ms}") long defaultTimeoutMs, diff --git a/src/main/java/org/prebid/server/util/ListUtil.java b/src/main/java/org/prebid/server/util/ListUtil.java index e31aaa453ad..66efeaa1858 100644 --- a/src/main/java/org/prebid/server/util/ListUtil.java +++ b/src/main/java/org/prebid/server/util/ListUtil.java @@ -1,5 +1,6 @@ package org.prebid.server.util; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.util.algorithms.ListsUnionView; import java.util.List; @@ -12,4 +13,8 @@ private ListUtil() { public static List union(List first, List second) { return new ListsUnionView<>(first, second); } + + public static List nullIfEmpty(List value) { + return CollectionUtils.isEmpty(value) ? null : value; + } } diff --git a/src/main/java/org/prebid/server/util/PbsUtil.java b/src/main/java/org/prebid/server/util/PbsUtil.java new file mode 100644 index 00000000000..bc81f1ed2b5 --- /dev/null +++ b/src/main/java/org/prebid/server/util/PbsUtil.java @@ -0,0 +1,16 @@ +package org.prebid.server.util; + +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +public class PbsUtil { + + private PbsUtil() { + } + + public static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { + final ExtRequest requestExt = bidRequest.getExt(); + return requestExt != null ? requestExt.getPrebid() : null; + } +} diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java new file mode 100644 index 00000000000..2691b629544 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -0,0 +1,1010 @@ +package org.prebid.server.auction; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Video; +import com.iab.openrtb.response.Bid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.AuctionParticipation; +import org.prebid.server.auction.model.BidRejectionTracker; +import org.prebid.server.auction.model.BidderRequest; +import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.bidder.model.BidderError; +import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.floors.PriceFloorEnforcer; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.validation.ResponseBidValidator; +import org.prebid.server.validation.model.ValidationResult; + +import java.math.BigDecimal; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; +import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; +import static org.prebid.server.proto.openrtb.ext.response.BidType.video; +import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; + +@ExtendWith(MockitoExtension.class) +public class BidsAdjusterTest extends VertxTest { + + @Mock(strictness = LENIENT) + private ResponseBidValidator responseBidValidator; + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyService; + + @Mock(strictness = LENIENT) + private PriceFloorEnforcer priceFloorEnforcer; + + @Mock(strictness = LENIENT) + private DsaEnforcer dsaEnforcer; + + @Mock(strictness = LENIENT) + private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + + private BidsAdjuster target; + + @BeforeEach + public void setUp() { + given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); + + given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); + given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); + given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null); + + givenTarget(); + } + + private void givenTarget() { + target = new BidsAdjuster( + responseBidValidator, + currencyService, + bidAdjustmentFactorResolver, + priceFloorEnforcer, + dsaEnforcer, + jacksonMapper); + } + + @Test + public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(updatedPrice); + } + + @Test + public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2.0)); + } + + @Test + public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD"); + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).isEmpty(); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.TEN); + given(currencyService.convertCurrency(any(), any(), any(), any())) + .willReturn(BigDecimal.TEN); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + final BigDecimal updatedPrice = BigDecimal.valueOf(100); + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()) + .extracting(BidderBid::getBid) + .flatExtracting(Bid::getPrice) + .containsOnly(updatedPrice); + assertThat(firstSeatBid.getErrors()).isEmpty(); + } + + @Test + public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) + .willThrow( + new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); + + assertThat(result).hasSize(1); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "CUR1"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1"); + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); + + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { + // given + final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); + final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"), + givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR") + )) + .build(), + 1); + + final BidRequest bidRequest = BidRequest.builder() + .cur(singletonList("BAD")) + .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), + identity()))).build(); + + final BigDecimal updatedPrice = BigDecimal.valueOf(20); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD"))) + .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD")); + verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD")); + + assertThat(result).hasSize(1); + + final ObjectNode expectedBidExt = mapper.createObjectNode(); + expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); + expectedBidExt.put("origbidcur", "USD"); + final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); + final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD"); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsOnly(expectedBidderBid); + + final BidderError expectedError = + BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD"); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCurrency() { + // given + final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(bidderPrice).build(), "USD") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2"))); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1")); + + assertThat(result).hasSize(1); + + final BidderError expectedError = BidderError.badInput("Cur parameter contains more than one currency." + + " CUR1 will be used"); + final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid(); + assertThat(firstSeatBid.getBids()) + .extracting(BidderBid::getBid) + .flatExtracting(Bid::getPrice) + .containsOnly(updatedPrice); + assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); + } + + @Test + public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { + // given + final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); + final BigDecimal bidder2Price = BigDecimal.valueOf(2); + final BigDecimal bidder3Price = BigDecimal.valueOf(3); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"), + givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"), + givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD") + )) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest( + singletonList(givenImp(Map.of("bidder1", 1), identity())), + builder -> builder.cur(singletonList("USD"))); + + final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); + given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); + given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); + verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); + verifyNoMoreInteractions(currencyService); + + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsOnly(bidder3Price, updatedPrice, updatedPrice); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { + // given + final BidderResponse bidderResponse = givenBidderResponse( + Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build()); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(2.468)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(4.936)); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(1).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + } + + @Test + public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD", video) + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> + impBuilder.id("123").video(Video.builder().placement(10).build()))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(2)); + } + + @Test + public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative), + givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio))) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); + } + + @Test + public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.valueOf(2)).build(), + "USD") + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() + .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, + singletonMap("bidder", BigDecimal.valueOf(3.456))))) + .build(); + givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) + .willReturn(BigDecimal.valueOf(3.456)); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .bidadjustmentfactors(givenAdjustments) + .auctiontimestamp(1000L) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.valueOf(6.912)); + } + + @Test + public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of( + givenBidderBid(Bid.builder() + .impid("123") + .price(BigDecimal.ONE).build(), + "USD") + )) + .build(), + 1); + + final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); + givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(emptyMap()) + .auctiontimestamp(1000L) + .currency(ExtRequestCurrency.of(null, false)) + .bidadjustmentfactors(givenAdjustments) + .build()))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .extracting(BidderBid::getBid) + .extracting(Bid::getPrice) + .containsExactly(BigDecimal.ONE); + } + + @Test + public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { + // given + final BidderBid bidToAccept = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderBid bidToReject = + givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.TEN).build(), "USD"); + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(bidToAccept, bidToReject)) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(List.of( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), + identity()); + + given(priceFloorEnforcer.enforce(any(), any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(bidToAccept); + } + + @Test + public void shouldReturnBidsAcceptedByDsaEnforcer() { + // given + final BidderBid bidToAccept = + givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD"); + final BidderBid bidToReject = + givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(BigDecimal.TEN).build(), "USD"); + + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(bidToAccept, bidToReject)) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(List.of( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), + identity()); + + given(dsaEnforcer.enforce(any(), any(), any())) + .willReturn(AuctionParticipation.builder() + .bidder("bidder1") + .bidderResponse(BidderResponse.of( + "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) + .build()); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .containsExactly(bidToAccept); + } + + @Test + public void shouldTolerateResponseBidValidationErrors() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(givenBidderBid( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + when(responseBidValidator.validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.error("Error: bid validation error.")); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .isEmpty(); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly( + BidderError.invalidBid( + "BidId `bidId1` validation messages: Error: Error: bid validation error.")); + } + + @Test + public void shouldTolerateResponseBidValidationWarnings() { + // given + final BidderResponse bidderResponse = BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(List.of(givenBidderBid( + Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build()))) + .build(), + 1); + + final BidRequest bidRequest = givenBidRequest(singletonList( + // imp ids are not really used for matching, included them here for clarity + givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), + builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() + .auctiontimestamp(1000L) + .build()))); + + when(responseBidValidator.validate(any(), any(), any(), any())) + .thenReturn(ValidationResult.warning(singletonList("Error: bid validation warning."))); + + final List auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest); + final AuctionContext auctionContext = givenAuctionContext(bidRequest); + + // when + final List result = target + .validateAndAdjustBids(auctionParticipations, auctionContext, null); + + // then + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getBids) + .hasSize(1); + assertThat(result) + .extracting(AuctionParticipation::getBidderResponse) + .extracting(BidderResponse::getSeatBid) + .flatExtracting(BidderSeatBid::getErrors) + .containsOnly(BidderError.invalidBid( + "BidId `bidId1` validation messages: Warning: Error: bid validation warning.")); + } + + private BidderResponse givenBidderResponse(Bid bid) { + return BidderResponse.of( + "bidder", + BidderSeatBid.builder() + .bids(singletonList(givenBidderBid(bid))) + .build(), + 1); + } + + private List givenAuctionParticipation( + BidderResponse bidderResponse, BidRequest bidRequest) { + + final BidderRequest bidderRequest = BidderRequest.builder() + .bidRequest(bidRequest) + .build(); + + return List.of(AuctionParticipation.builder() + .bidder("bidder") + .bidderRequest(bidderRequest) + .bidderResponse(bidderResponse) + .build()); + } + + private AuctionContext givenAuctionContext(BidRequest bidRequest) { + return AuctionContext.builder() + .bidRequest(bidRequest) + .bidRejectionTrackers(Map.of("bidder", new BidRejectionTracker("bidder", Set.of(), 1))) + .build(); + } + + private static BidRequest givenBidRequest(List imp, + UnaryOperator bidRequestBuilderCustomizer) { + + return bidRequestBuilderCustomizer + .apply(BidRequest.builder().cur(singletonList("USD")).imp(imp).tmax(500L)) + .build(); + } + + private static Imp givenImp(T ext, Function impBuilderCustomizer) { + return impBuilderCustomizer.apply(Imp.builder() + .id(UUID.randomUUID().toString()) + .ext(mapper.valueToTree(singletonMap( + "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap())))) + .build(); + } + + private static BidderBid givenBidderBid(Bid bid) { + return BidderBid.of(bid, banner, null); + } + + private static BidderBid givenBidderBid(Bid bid, String currency) { + return BidderBid.of(bid, banner, currency); + } + + private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) { + return BidderBid.of(bid, type, currency); + } + + private static Map doubleMap(K key1, V value1, K key2, V value2) { + final Map map = new HashMap<>(); + map.put(key1, value1); + map.put(key2, value2); + return map; + } +} diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 0c7710dbc74..e058d43b0df 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -25,7 +25,6 @@ import com.iab.openrtb.request.SupplyChain; import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; -import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; @@ -43,7 +42,6 @@ import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessingResult; import org.prebid.server.auction.mediatypeprocessor.MediaTypeProcessor; import org.prebid.server.auction.model.AuctionContext; @@ -70,13 +68,11 @@ import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.bidder.model.Price; import org.prebid.server.cookie.UidsCookie; -import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.exception.PreBidException; import org.prebid.server.execution.Timeout; import org.prebid.server.execution.TimeoutFactory; import org.prebid.server.floors.PriceFloorAdjuster; -import org.prebid.server.floors.PriceFloorEnforcer; import org.prebid.server.floors.PriceFloorProcessor; import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.ExecutionAction; @@ -113,7 +109,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtImpAuctionEnvironment; import org.prebid.server.proto.openrtb.ext.request.ExtPriceGranularity; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors; import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; @@ -128,7 +123,6 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.ExtUserPrebid; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; 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.ExtAnalytics; @@ -156,8 +150,6 @@ import org.prebid.server.settings.model.AccountEventsConfig; import org.prebid.server.spring.config.bidder.model.CompressionType; import org.prebid.server.spring.config.bidder.model.Ortb; -import org.prebid.server.validation.ResponseBidValidator; -import org.prebid.server.validation.model.ValidationResult; import java.io.IOException; import java.math.BigDecimal; @@ -209,11 +201,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.prebid.server.proto.openrtb.ext.response.BidType.audio; import static org.prebid.server.proto.openrtb.ext.response.BidType.banner; import static org.prebid.server.proto.openrtb.ext.response.BidType.video; -import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative; @ExtendWith(MockitoExtension.class) public class ExchangeServiceTest extends VertxTest { @@ -257,12 +246,6 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private HttpBidderRequester httpBidderRequester; - @Mock(strictness = LENIENT) - private ResponseBidValidator responseBidValidator; - - @Mock(strictness = LENIENT) - private CurrencyConversionService currencyService; - @Mock(strictness = LENIENT) private BidResponseCreator bidResponseCreator; @@ -278,17 +261,11 @@ public class ExchangeServiceTest extends VertxTest { @Mock(strictness = LENIENT) private PriceFloorAdjuster priceFloorAdjuster; - @Mock(strictness = LENIENT) - private PriceFloorEnforcer priceFloorEnforcer; - @Mock(strictness = LENIENT) private PriceFloorProcessor priceFloorProcessor; @Mock(strictness = LENIENT) - private DsaEnforcer dsaEnforcer; - - @Mock(strictness = LENIENT) - private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private BidsAdjuster bidsAdjuster; @Mock private Metrics metrics; @@ -374,14 +351,12 @@ public void setUp() { false, AuctionResponsePayloadImpl.of(invocation.getArgument(0))))); + given(bidsAdjuster.validateAndAdjustBids(any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + given(mediaTypeProcessor.process(any(), anyString(), any(), any())) .willAnswer(invocation -> MediaTypeProcessingResult.succeeded(invocation.getArgument(0), emptyList())); - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - given(uidUpdater.updateUid(any(), any(), any())) .willAnswer(inv -> Optional.ofNullable((AuctionContext) inv.getArgument(1)) .map(AuctionContext::getBidRequest) @@ -397,14 +372,11 @@ public void setUp() { given(storedResponseProcessor.updateStoredBidResponse(any())) .willAnswer(inv -> inv.getArgument(0)); - given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); - given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); given(priceFloorAdjuster.adjustForImp(any(), any(), any(), any(), any())) .willAnswer(inv -> Price.of( ((Imp) inv.getArgument(0)).getBidfloorcur(), ((Imp) inv.getArgument(0)).getBidfloor())); - given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null); given(priceFloorProcessor.enrichWithPriceFloors(any(), any(), any(), any(), any())) .willAnswer(inv -> inv.getArgument(0)); @@ -1512,12 +1484,10 @@ public void shouldCallBidResponseCreatorWithExpectedParamsAndUpdateDebugErrors() verify(bidResponseCreator) .create(contextArgumentCaptor.capture(), eq(expectedCacheInfo), eq(expectedMultiBidMap)); - final ObjectNode expectedBidExt = mapper.createObjectNode().put("origbidcpm", new BigDecimal("7.89")); final Bid expectedThirdBid = Bid.builder() .id("bidId3") .impid("impId3") .price(BigDecimal.valueOf(7.89)) - .ext(expectedBidExt) .build(); final List auctionParticipations = contextArgumentCaptor.getValue().getAuctionParticipations(); @@ -1655,81 +1625,6 @@ public void shouldTolerateNullRequestExtPrebidTargeting() { .allSatisfy(map -> assertThat(map).isNull()); } - @Test - public void shouldTolerateResponseBidValidationErrors() { - // given - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .auctiontimestamp(1000L) - .build()))); - - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.error( - singletonList("bid validation warning"), - "bid validation error")); - - givenBidResponseCreator(singletonList(Bid.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .isEmpty(); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly( - BidderError.invalidBid("BidId `bidId1` validation messages: Error: bid validation error." - + " Warning: bid validation warning")); - } - - @Test - public void shouldTolerateResponseBidValidationWarnings() { - // given - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.valueOf(1.23)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .auctiontimestamp(1000L) - .build()))); - - given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success( - singletonList("bid validation warning"))); - - givenBidResponseCreator(singletonList(Bid.builder().build())); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .hasSize(1); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(BidderError.invalidBid( - "BidId `bidId1` validation messages: Warning: bid validation warning")); - } - @Test public void shouldRejectBidIfCurrencyIsNotValid() { // given @@ -1744,9 +1639,6 @@ public void shouldRejectBidIfCurrencyIsNotValid() { .auctiontimestamp(1000L) .build()))); - given(responseBidValidator.validate(any(), any(), any(), any())) - .willReturn(ValidationResult.error("BidResponse currency is not valid: USDD")); - final List bidderErrors = singletonList(ExtBidderError.of(BidderError.Type.generic.getCode(), "BidResponse currency is not valid: USDD")); givenBidResponseCreator(singletonMap("bidder1", bidderErrors)); @@ -3052,347 +2944,6 @@ public void holdAuctionShouldFailWhenSiteAppAndDoohArePresentInBidRequestAndStri verify(metrics).updateAlertsMetrics(MetricName.general); } - @Test - public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(5.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - givenBidResponseCreator(singletonList(Bid.builder().price(updatedPrice).build())); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice).containsExactly(updatedPrice); - } - - @Test - public void shouldApplyStoredBidResponseAdjustments() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(TEN); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - verify(storedResponseProcessor).updateStoredBidResponse(any()); - } - - @Test - public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - // returns the same price as in argument - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice).containsExactly(BigDecimal.ONE); - } - - @Test - public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { - // given - final Bidder bidder = mock(Bidder.class); - final List bids = List.of( - Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), - Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), - Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), - Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); - final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); - - givenBidder("bidder", bidder, seatBid); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - final AuctionContext givenContext = givenRequestContext(bidRequest); - - // when - final AuctionContext result = target.holdAuction(givenContext).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid).hasSize(1); - assertThat(givenContext.getDebugWarnings()) - .containsExactlyInAnyOrder( - "Dropped bid 'invalid_bid_1'. Does not contain a positive (or zero if there is a deal) 'price'", - "Dropped bid 'invalid_bid_2'. Does not contain a positive (or zero if there is a deal) 'price'", - "Dropped bid 'invalid_bid_3'. Does not contain a positive (or zero if there is a deal) 'price'" - ); - verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); - } - - @Test - public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2.0)).build(), "CUR")))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD"); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).isEmpty(); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", TEN); - - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - given(currencyService.convertCurrency(any(), any(), any(), any())) - .willReturn(TEN); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List auctionParticipations = captureAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BigDecimal updatedPrice = BigDecimal.valueOf(100); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).isEmpty(); - } - - @Test - public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() { - // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0); - givenBidder("bidder", mock(Bidder.class), givenSeatBid(asList( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"), - givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2")))); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - identity()); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice) - .willThrow( - new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD")); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any()); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "CUR1"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1"); - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD"); - - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() { - // given - final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0); - final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0); - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD")))); - givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId2").price(BigDecimal.valueOf(10.0)).build(), "CUR")))); - - final BidRequest bidRequest = BidRequest.builder().cur(singletonList("BAD")) - .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), - identity()))).build(); - - final BigDecimal updatedPrice = BigDecimal.valueOf(20); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD"))) - .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD")); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD")); - verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD")); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(2); - - final ObjectNode expectedBidExt = mapper.createObjectNode(); - expectedBidExt.put("origbidcpm", new BigDecimal("2.0")); - expectedBidExt.put("origbidcur", "USD"); - final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build(); - final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD"); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsOnly(expectedBidderBid); - - final BidderError expectedError = - BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD"); - assertThat(auctionParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getErrors) - .containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCurrency() { - // given - final BigDecimal bidderPrice = BigDecimal.valueOf(2.0); - givenBidder("bidder", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(bidderPrice).build(), "USD")))); - - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.cur(asList("CUR1", "CUR2", "CUR2"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1")); - - final List auctionParticipations = - contextArgumentCaptor.getValue().getAuctionParticipations(); - assertThat(auctionParticipations).hasSize(1); - - final BidderError expectedError = BidderError.badInput("Cur parameter contains more than one currency." - + " CUR1 will be used"); - final BidderSeatBid firstSeatBid = auctionParticipations.getFirst().getBidderResponse().getSeatBid(); - assertThat(firstSeatBid.getBids()) - .extracting(BidderBid::getBid) - .flatExtracting(Bid::getPrice) - .containsOnly(updatedPrice); - assertThat(firstSeatBid.getErrors()).containsOnly(expectedError); - } - - @Test - public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { - // given - final BigDecimal bidder1Price = BigDecimal.valueOf(1.5); - final BigDecimal bidder2Price = BigDecimal.valueOf(2); - final BigDecimal bidder3Price = BigDecimal.valueOf(3); - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR")))); - givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP")))); - givenBidder("bidder3", mock(Bidder.class), givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD")))); - - final Map impBidders = new HashMap<>(); - impBidders.put("bidder1", 1); - impBidders.put("bidder2", 2); - impBidders.put("bidder3", 3); - final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(impBidders, identity())), builder -> builder.cur(singletonList("USD"))); - - final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); - given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); - given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final ArgumentCaptor contextArgumentCaptor = ArgumentCaptor.forClass(AuctionContext.class); - verify(bidResponseCreator).create(contextArgumentCaptor.capture(), any(), any()); - verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD")); - verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); - verifyNoMoreInteractions(currencyService); - - assertThat(contextArgumentCaptor.getValue().getAuctionParticipations()) - .hasSize(3) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsOnly(bidder3Price, updatedPrice, updatedPrice); - } - @Test public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsService() { // given @@ -3541,351 +3092,6 @@ public void shouldPassResponseToPostProcessor() { .build())); } - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(2.468)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(4.936)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(1).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - BidderBid.of(Bid.builder().impid("1234").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - BidderBid.of(Bid.builder().impid("123").price(BigDecimal.valueOf(2)).build(), video, null)))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> - impBuilder.id("123").video(Video.builder().placement(10).build()))), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(2)); - } - - @Test - public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(List.of( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build()), - BidderBid.builder().type(xNative).bid(givenBid(identity())).build(), - BidderBid.builder().type(audio).bid(givenBid(identity())).build()))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - } - - @Test - public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { - // given - final Bidder bidder = mock(Bidder.class); - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder() - .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner, - singletonMap("bidder", BigDecimal.valueOf(3.456))))) - .build(); - givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468)); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) - .willReturn(BigDecimal.valueOf(3.456)); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .bidadjustmentfactors(givenAdjustments) - .auctiontimestamp(1000L) - .build()))); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .extracting(BidderBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.valueOf(6.912)); - } - - @Test - public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() { - // given - final Bidder bidder = mock(Bidder.class); - - givenBidder("bidder", bidder, givenSeatBid(singletonList( - givenBidderBid(Bid.builder().impid("impId").price(BigDecimal.ONE).build())))); - - final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build(); - givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN); - - final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), - builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .aliases(emptyMap()) - .auctiontimestamp(1000L) - .currency(ExtRequestCurrency.of(null, false)) - .bidadjustmentfactors(givenAdjustments) - .build()))); - - // when - final AuctionContext result = target.holdAuction(givenRequestContext(bidRequest)).result(); - - // then - assertThat(result.getBidResponse().getSeatbid()) - .flatExtracting(SeatBid::getBid) - .extracting(Bid::getPrice) - .containsExactly(BigDecimal.ONE); - } - - @Test - public void shouldReturnBidsAcceptedByPriceFloorEnforcer() { - // given - final BidderBid bidToAccept = - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(ONE).build(), "USD"); - final BidderBid bidToReject = - givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(TEN).build(), "USD"); - - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(List.of(bidToAccept, bidToReject))); - - final BidRequest bidRequest = givenBidRequest(List.of( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), - identity()); - - given(priceFloorEnforcer.enforce(any(), any(), any(), any())) - .willReturn(AuctionParticipation.builder() - .bidder("bidder1") - .bidderResponse(BidderResponse.of( - "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) - .build()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsExactly(bidToAccept); - } - - @Test - public void shouldReturnBidsAcceptedByDsaEnforcer() { - // given - final BidderBid bidToAccept = - givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(ONE).build(), "USD"); - final BidderBid bidToReject = - givenBidderBid(Bid.builder().id("bidId2").impid("impId2").price(TEN).build(), "USD"); - - givenBidder("bidder1", mock(Bidder.class), givenSeatBid(List.of(bidToAccept, bidToReject))); - - final BidRequest bidRequest = givenBidRequest(List.of( - // imp ids are not really used for matching, included them here for clarity - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1")), - givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId2"))), - identity()); - - given(dsaEnforcer.enforce(any(), any(), any())) - .willReturn(AuctionParticipation.builder() - .bidder("bidder1") - .bidderResponse(BidderResponse.of( - "bidder1", BidderSeatBid.of(singletonList(bidToAccept)), 0)) - .build()); - - // when - target.holdAuction(givenRequestContext(bidRequest)); - - // then - final List capturedParticipations = captureAuctionParticipations(); - assertThat(capturedParticipations) - .extracting(AuctionParticipation::getBidderResponse) - .extracting(BidderResponse::getSeatBid) - .flatExtracting(BidderSeatBid::getBids) - .containsExactly(bidToAccept); - } - @Test public void shouldReturnBidResponseModifiedByAuctionResponseHooks() { // given @@ -4590,9 +3796,6 @@ public void shouldReduceBidsHavingDealIdWithSameImpIdByBidderWithToleratingNotOb givenBidder(givenSingleSeatBid(bidderBid)); - given(responseBidValidator.validate(any(), any(), any(), any())) - .willReturn(ValidationResult.success()); - // when target.holdAuction(auctionContext); @@ -4786,6 +3989,38 @@ public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() { assertThat(timeoutCaptor.getAllValues()).containsExactly(450L); } + @Test + public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() { + // given + final Bidder bidder = mock(Bidder.class); + final List bids = List.of( + Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(), + Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(), + Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(), + Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build()); + final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList()); + + givenBidder("bidder", bidder, seatBid); + + final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())), + identity()); + final AuctionContext givenContext = givenRequestContext(bidRequest); + + // when + final AuctionContext result = target.holdAuction(givenContext).result(); + + // then + assertThat(result.getBidResponse().getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(1); + assertThat(givenContext.getDebugWarnings()) + .containsExactlyInAnyOrder( + "Dropped bid 'invalid_bid_1'. Does not contain a positive (or zero if there is a deal) 'price'", + "Dropped bid 'invalid_bid_2'. Does not contain a positive (or zero if there is a deal) 'price'", + "Dropped bid 'invalid_bid_3'. Does not contain a positive (or zero if there is a deal) 'price'" + ); + verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error); + } + private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { target = new ExchangeService( 0, @@ -4801,17 +4036,13 @@ private void givenTarget(boolean enabledStrictAppSiteDoohValidation) { timeoutFactory, ortbVersionConversionManager, httpBidderRequester, - responseBidValidator, - currencyService, bidResponseCreator, bidResponsePostProcessor, hookStageExecutor, httpInteractionLogger, priceFloorAdjuster, - priceFloorEnforcer, priceFloorProcessor, - dsaEnforcer, - bidAdjustmentFactorResolver, + bidsAdjuster, metrics, clock, jacksonMapper,