From 6c7e5fe8fe5cf3aef94485b3a199029ee482537a Mon Sep 17 00:00:00 2001 From: antonbabak Date: Tue, 15 Apr 2025 15:33:04 +0200 Subject: [PATCH 1/8] Adjust Floors for Bidadjustments --- .../prebid/server/auction/BidsAdjuster.java | 8 +- .../server/auction/ExchangeService.java | 25 +- .../server/auction/model/AuctionContext.java | 12 - .../server/auction/model/BidderRequest.java | 5 + .../requestfactory/AuctionRequestFactory.java | 10 +- .../BidAdjustmentFactorResolver.java | 3 +- .../BidAdjustmentRulesValidator.java | 20 +- .../BidAdjustmentsEnricher.java | 103 +++++ .../BidAdjustmentsProcessor.java | 27 +- .../BidAdjustmentsResolver.java | 61 +-- .../BidAdjustmentsRetriever.java | 86 ---- .../BidAdjustmentsRulesResolver.java | 79 ++++ .../FloorAdjustmentFactorResolver.java | 3 +- .../FloorAdjustmentsResolver.java | 90 ++++ .../bidadjustments/model/BidAdjustments.java | 43 +- .../model/BidAdjustmentsRule.java} | 5 +- .../model/BidAdjustmentsRules.java | 50 +++ .../floors/BasicPriceFloorAdjuster.java | 69 +-- .../floors/BasicPriceFloorEnforcer.java | 37 +- .../NoSignalBidderPriceFloorAdjuster.java | 5 - .../server/floors/PriceFloorAdjuster.java | 7 - .../ext/request/ExtRequestBidAdjustments.java | 15 - .../config/PriceFloorsConfiguration.java | 24 +- .../spring/config/ServiceConfiguration.java | 27 +- .../server/auction/BidsAdjusterTest.java | 4 +- .../server/auction/ExchangeServiceTest.java | 2 + .../AuctionRequestFactoryTest.java | 43 +- .../BidAdjustmentFactorResolverTest.java | 12 +- .../BidAdjustmentRulesValidatorTest.java | 123 +++--- ...t.java => BidAdjustmentsEnricherTest.java} | 169 +++---- .../BidAdjustmentsProcessorTest.java | 130 +++--- .../BidAdjustmentsResolverTest.java | 95 ++-- .../BidAdjustmentsRulesResolverTest.java | 411 ++++++++++++++++++ .../FloorAdjustmentFactorResolverTest.java | 2 +- .../FloorAdjustmentsResolverTest.java | 242 +++++++++++ ...Test.java => BidAdjustmentsRulesTest.java} | 24 +- .../floors/BasicPriceFloorAdjusterTest.java | 233 +++------- .../floors/BasicPriceFloorEnforcerTest.java | 62 +-- .../NoSignalBidderPriceFloorAdjusterTest.java | 19 - 39 files changed, 1512 insertions(+), 873 deletions(-) rename src/main/java/org/prebid/server/{auction/adjustment => bidadjustments}/BidAdjustmentFactorResolver.java (91%) create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java delete mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java create mode 100644 src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java rename src/main/java/org/prebid/server/{auction/adjustment => bidadjustments}/FloorAdjustmentFactorResolver.java (94%) create mode 100644 src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java rename src/main/java/org/prebid/server/{proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java => bidadjustments/model/BidAdjustmentsRule.java} (71%) create mode 100644 src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java delete mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java rename src/test/java/org/prebid/server/{auction/adjustment => bidadjustments}/BidAdjustmentFactorResolverTest.java (80%) rename src/test/java/org/prebid/server/bidadjustments/{BidAdjustmentsRetrieverTest.java => BidAdjustmentsEnricherTest.java} (65%) create mode 100644 src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java rename src/test/java/org/prebid/server/{auction/adjustment => bidadjustments}/FloorAdjustmentFactorResolverTest.java (99%) create mode 100644 src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java rename src/test/java/org/prebid/server/bidadjustments/model/{BidAdjustmentsTest.java => BidAdjustmentsRulesTest.java} (67%) diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java index 4ae7a6e3e3e..9be728b8cc2 100644 --- a/src/main/java/org/prebid/server/auction/BidsAdjuster.java +++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java @@ -42,22 +42,22 @@ public List validateAndAdjustBids(List validBidderResponse(auctionParticipation, auctionContext, aliases)) .map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids( auctionParticipation, - auctionContext.getBidRequest(), - auctionContext.getBidAdjustments())) + bidRequest)) .map(auctionParticipation -> priceFloorEnforcer.enforce( - auctionContext.getBidRequest(), + bidRequest, auctionParticipation, auctionContext.getAccount(), auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) .map(auctionParticipation -> dsaEnforcer.enforce( - auctionContext.getBidRequest(), + bidRequest, auctionParticipation, auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder()))) .toList(); diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 56cd8192501..facc0c85447 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -89,6 +89,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.settings.model.Account; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import org.prebid.server.util.ListUtil; import org.prebid.server.util.PbsUtil; @@ -713,20 +714,34 @@ private AuctionParticipation createAuctionParticipation( final String storedBidResponse = impBidderToStoredBidResponse.size() == 1 ? impBidderToStoredBidResponse.get(imps.getFirst().getId()).get(bidder) : null; + + final BidRequest enrichedWithPriceFloors = priceFloorProcessor.enrichWithPriceFloors( + context.getBidRequest().toBuilder().imp(imps).build(), + context.getAccount(), + bidder, + context.getPrebidErrors(), + context.getDebugWarnings()); + final BidRequest preparedBidRequest = prepareBidRequest( bidderPrivacyResult, - imps, + enrichedWithPriceFloors, bidderToMultiBid, biddersToConfigs, bidderToPrebidBidders, bidderAliases, context); + final Map originalPriceFloors = enrichedWithPriceFloors.getImp().stream() + .filter(imp -> BidderUtil.isValidPrice(imp.getBidfloor()) + && StringUtils.isNotBlank(imp.getBidfloorcur())) + .collect(Collectors.toMap(Imp::getId, imp -> Price.of(imp.getBidfloorcur(), imp.getBidfloor()))); + final BidderRequest bidderRequest = BidderRequest.builder() .bidder(bidder) .ortbVersion(ortbVersion) .storedResponse(storedBidResponse) .bidRequest(preparedBidRequest) + .originalPriceFloors(originalPriceFloors) .build(); return AuctionParticipation.builder() @@ -742,7 +757,7 @@ private OrtbVersion bidderSupportedOrtbVersion(String bidder, BidderAliases alia } private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, - List imps, + BidRequest bidRequest, Map bidderToMultiBid, Map biddersToConfigs, Map bidderToPrebidBidders, @@ -750,12 +765,6 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, AuctionContext context) { final String bidder = bidderPrivacyResult.getRequestBidder(); - final BidRequest bidRequest = priceFloorProcessor.enrichWithPriceFloors( - context.getBidRequest().toBuilder().imp(imps).build(), - context.getAccount(), - bidder, - context.getPrebidErrors(), - context.getDebugWarnings()); final boolean transmitTid = transmitTransactionId(bidder, context); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); final boolean useFirstPartyData = firstPartyDataBidders == null || firstPartyDataBidders.stream() diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java index 5dbe83c3ff2..3ee60aab4fa 100644 --- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java +++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java @@ -8,7 +8,6 @@ import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.debug.DebugContext; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cache.model.DebugHttpCall; import org.prebid.server.cookie.UidsCookie; import org.prebid.server.geolocation.model.GeoInfo; @@ -18,7 +17,6 @@ import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.settings.model.Account; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -73,10 +71,6 @@ public class AuctionContext { CachedDebugLog cachedDebugLog; - @JsonIgnore - @Builder.Default - BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap()); - public AuctionContext with(Account account) { return this.toBuilder().account(account).build(); } @@ -130,12 +124,6 @@ public AuctionContext with(GeoInfo geoInfo) { .build(); } - public AuctionContext with(BidAdjustments bidAdjustments) { - return this.toBuilder() - .bidAdjustments(bidAdjustments) - .build(); - } - public AuctionContext withRequestRejected() { return this.toBuilder() .requestRejected(true) diff --git a/src/main/java/org/prebid/server/auction/model/BidderRequest.java b/src/main/java/org/prebid/server/auction/model/BidderRequest.java index ad60230e54b..1e2ac58a8db 100644 --- a/src/main/java/org/prebid/server/auction/model/BidderRequest.java +++ b/src/main/java/org/prebid/server/auction/model/BidderRequest.java @@ -4,6 +4,9 @@ import lombok.Builder; import lombok.Value; import org.prebid.server.auction.versionconverter.OrtbVersion; +import org.prebid.server.bidder.model.Price; + +import java.util.Map; @Builder(toBuilder = true) @Value @@ -17,6 +20,8 @@ public class BidderRequest { BidRequest bidRequest; + Map originalPriceFloors; + public BidderRequest with(BidRequest bidRequest) { return toBuilder().bidRequest(bidRequest).build(); } diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java index 0eb24dface1..9407451efc9 100644 --- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java +++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java @@ -17,7 +17,7 @@ import org.prebid.server.auction.model.AuctionStoredResult; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; -import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.json.JacksonMapper; @@ -51,7 +51,7 @@ public class AuctionRequestFactory { private final JacksonMapper mapper; private final OrtbTypesResolver ortbTypesResolver; private final GeoLocationServiceWrapper geoLocationServiceWrapper; - private final BidAdjustmentsRetriever bidAdjustmentsRetriever; + private final BidAdjustmentsEnricher bidAdjustmentsEnricher; private static final String ENDPOINT = Endpoint.openrtb2_auction.value(); @@ -69,7 +69,7 @@ public AuctionRequestFactory(long maxRequestSize, DebugResolver debugResolver, JacksonMapper mapper, GeoLocationServiceWrapper geoLocationServiceWrapper, - BidAdjustmentsRetriever bidAdjustmentsRetriever) { + BidAdjustmentsEnricher bidAdjustmentsEnricher) { this.maxRequestSize = maxRequestSize; this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory); @@ -85,7 +85,7 @@ public AuctionRequestFactory(long maxRequestSize, this.debugResolver = Objects.requireNonNull(debugResolver); this.mapper = Objects.requireNonNull(mapper); this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper); - this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever); + this.bidAdjustmentsEnricher = Objects.requireNonNull(bidAdjustmentsEnricher); } /** @@ -146,7 +146,7 @@ public Future enrichAuctionContext(AuctionContext initialContext .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext) .map(auctionContext::with)) - .map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext))) + .map(auctionContext -> auctionContext.with(bidAdjustmentsEnricher.enrichBidRequest(auctionContext))) .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext) .map(auctionContext::with)) diff --git a/src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java similarity index 91% rename from src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java rename to src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java index fa0013d683f..39027596c01 100644 --- a/src/main/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolver.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -28,6 +28,7 @@ public BigDecimal resolve(ImpMediaType impMediaType, } return Optional.ofNullable(impMediaType) + .map(type -> type == ImpMediaType.video_instream ? ImpMediaType.video : type) .map(adjustmentFactorsByMediaTypes::get) .flatMap(factors -> factors.entrySet().stream() .filter(entry -> StringUtils.equalsIgnoreCase(entry.getKey(), bidder)) diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java index 9ddeefb6e2e..34495d2cea3 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java @@ -3,8 +3,8 @@ import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidadjustments.model.BidAdjustmentType; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.validation.ValidationException; @@ -16,7 +16,7 @@ public class BidAdjustmentRulesValidator { public static final Set SUPPORTED_MEDIA_TYPES = Set.of( - BidAdjustmentsResolver.WILDCARD, + BidAdjustmentsRulesResolver.WILDCARD, ImpMediaType.banner.toString(), ImpMediaType.audio.toString(), ImpMediaType.video_instream.toString(), @@ -27,13 +27,13 @@ private BidAdjustmentRulesValidator() { } - public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException { + public static void validate(BidAdjustments bidAdjustments) throws ValidationException { if (bidAdjustments == null) { return; } - final Map>>> mediatypes = - bidAdjustments.getMediatype(); + final Map>>> mediatypes = + bidAdjustments.getRules(); if (MapUtils.isEmpty(mediatypes)) { return; @@ -41,12 +41,12 @@ public static void validate(ExtRequestBidAdjustments bidAdjustments) throws Vali for (String mediatype : mediatypes.keySet()) { if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) { - final Map>> bidders = mediatypes.get(mediatype); + final Map>> bidders = mediatypes.get(mediatype); if (MapUtils.isEmpty(bidders)) { throw new ValidationException("no bidders found in %s".formatted(mediatype)); } for (String bidder : bidders.keySet()) { - final Map> deals = bidders.get(bidder); + final Map> deals = bidders.get(bidder); if (MapUtils.isEmpty(deals)) { throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder)); @@ -61,14 +61,14 @@ public static void validate(ExtRequestBidAdjustments bidAdjustments) throws Vali } } - private static void validateRules(List rules, + private static void validateRules(List rules, String path) throws ValidationException { if (rules == null) { throw new ValidationException("no bid adjustment rules found in %s".formatted(path)); } - for (ExtRequestBidAdjustmentsRule rule : rules) { + for (BidAdjustmentsRule rule : rules) { final BidAdjustmentType type = rule.getAdjType(); final String currency = rule.getCurrency(); final BigDecimal value = rule.getValue(); diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java new file mode 100644 index 00000000000..b0feb290e48 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java @@ -0,0 +1,103 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountAuctionConfig; +import org.prebid.server.validation.ValidationException; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsEnricher { + + private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsEnricher.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final ObjectMapper mapper; + private final JacksonMapper jacksonMapper; + private final JsonMerger jsonMerger; + private final double samplingRate; + + public BidAdjustmentsEnricher(JacksonMapper mapper, JsonMerger jsonMerger, double samplingRate) { + this.jacksonMapper = Objects.requireNonNull(mapper); + this.mapper = mapper.mapper(); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.samplingRate = samplingRate; + } + + public BidRequest enrichBidRequest(AuctionContext auctionContext) { + final BidRequest bidRequest = auctionContext.getBidRequest(); + final List debugWarnings = auctionContext.getDebugWarnings(); + final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); + + final JsonNode requestNode = Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode accountNode = Optional.ofNullable(auctionContext.getAccount()) + .map(Account::getAuction) + .map(AccountAuctionConfig::getBidAdjustments) + .orElseGet(mapper::createObjectNode); + + final JsonNode mergedNode = jsonMerger.merge(requestNode, accountNode); + + final List resolvedWarnings = debugEnabled ? debugWarnings : null; + final JsonNode resolvedBidAdjustments = convertAndValidate(mergedNode, resolvedWarnings, "request") + .or(() -> convertAndValidate(accountNode, resolvedWarnings, "account")) + .orElse(null); + + return bidRequest.toBuilder() + .ext(updateExtRequestWithBidAdjustments(bidRequest, resolvedBidAdjustments)) + .build(); + } + + private Optional convertAndValidate(JsonNode bidAdjustmentsNode, + List debugWarnings, + String errorLocation) { + + if (bidAdjustmentsNode.isEmpty()) { + return Optional.empty(); + } + + try { + final BidAdjustments bidAdjustments = mapper.convertValue(bidAdjustmentsNode, BidAdjustments.class); + + BidAdjustmentRulesValidator.validate(bidAdjustments); + return Optional.of(bidAdjustmentsNode); + } catch (IllegalArgumentException | ValidationException e) { + final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); + if (debugWarnings != null) { + debugWarnings.add(message); + } + conditionalLogger.error(message, samplingRate); + return Optional.empty(); + } + } + + private ExtRequest updateExtRequestWithBidAdjustments(BidRequest bidRequest, JsonNode bidAdjustments) { + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid prebid = extRequest != null ? extRequest.getPrebid() : null; + final ExtRequestPrebid updatedPrebid = (prebid != null ? prebid.toBuilder() : ExtRequestPrebid.builder()) + .bidadjustments((ObjectNode) bidAdjustments) + .build(); + + final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid); + return extRequest == null + ? updatedExtRequest + : jacksonMapper.fillExtension(updatedExtRequest, extRequest.getProperties()); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java index 1136876c7f6..3adacf4bf07 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -1,5 +1,6 @@ package org.prebid.server.bidadjustments; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -7,10 +8,8 @@ import com.iab.openrtb.response.Bid; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.ImpMediaTypeResolver; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; @@ -37,7 +36,7 @@ public class BidAdjustmentsProcessor { private final CurrencyConversionService currencyService; private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver; private final BidAdjustmentsResolver bidAdjustmentsResolver; - private final JacksonMapper mapper; + private final ObjectMapper mapper; public BidAdjustmentsProcessor(CurrencyConversionService currencyService, BidAdjustmentFactorResolver bidAdjustmentFactorResolver, @@ -47,12 +46,11 @@ public BidAdjustmentsProcessor(CurrencyConversionService currencyService, this.currencyService = Objects.requireNonNull(currencyService); this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver); this.bidAdjustmentsResolver = Objects.requireNonNull(bidAdjustmentsResolver); - this.mapper = Objects.requireNonNull(mapper); + this.mapper = Objects.requireNonNull(mapper).mapper(); } public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionParticipation, - BidRequest bidRequest, - BidAdjustments bidAdjustments) { + BidRequest bidRequest) { if (auctionParticipation.isRequestBlocked()) { return auctionParticipation; @@ -70,7 +68,7 @@ public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionP final String bidder = auctionParticipation.getBidder(); final List updatedBidderBids = bidderBids.stream() - .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, bidAdjustments, errors)) + .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, errors)) .filter(Objects::nonNull) .collect(Collectors.toList()); @@ -85,7 +83,6 @@ public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionP private BidderBid applyBidAdjustments(BidderBid bidderBid, BidRequest bidRequest, String bidder, - BidAdjustments bidAdjustments, List errors) { try { final Price originalPrice = getOriginalPrice(bidderBid); @@ -105,7 +102,6 @@ private BidderBid applyBidAdjustments(BidderBid bidderBid, priceWithFactorsApplied, bidder, bidRequest, - bidAdjustments, mediaType, bidderBid.getBid().getDealid()); @@ -119,7 +115,7 @@ private BidderBid applyBidAdjustments(BidderBid bidderBid, private BidderBid updateBid(Price originalPrice, Price adjustedPrice, BidderBid bidderBid, BidRequest bidRequest) { final Bid bid = bidderBid.getBid(); final ObjectNode bidExt = bid.getExt(); - final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode(); + final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.createObjectNode(); final BigDecimal originalBidPrice = originalPrice.getValue(); final String originalBidCurrency = originalPrice.getCurrency(); @@ -168,12 +164,9 @@ private Price applyBidAdjustmentFactors(Price bidPrice, private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, ImpMediaType mediaType) { final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest); - if (adjustmentFactors == null) { - return null; - } - - final ImpMediaType targetMediaType = mediaType == ImpMediaType.video_instream ? ImpMediaType.video : mediaType; - return bidAdjustmentFactorResolver.resolve(targetMediaType, adjustmentFactors, bidder); + return adjustmentFactors == null + ? null + : bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder); } private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) { @@ -190,14 +183,12 @@ private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecim private Price applyBidAdjustmentRules(Price bidPrice, String bidder, BidRequest bidRequest, - BidAdjustments bidAdjustments, ImpMediaType mediaType, String dealId) { return bidAdjustmentsResolver.resolve( bidPrice, bidRequest, - bidAdjustments, mediaType, bidder, dealId); diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java index ffac1cbc51a..45b1f83e607 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java @@ -1,88 +1,49 @@ package org.prebid.server.bidadjustments; import com.iab.openrtb.request.BidRequest; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.bidadjustments.model.BidAdjustmentType; -import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.bidder.model.Price; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import org.prebid.server.util.BidderUtil; -import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; -import org.prebid.server.util.dsl.config.PrebidConfigParameter; -import org.prebid.server.util.dsl.config.PrebidConfigParameters; -import org.prebid.server.util.dsl.config.PrebidConfigSource; -import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; -import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; -import org.prebid.server.util.dsl.config.impl.SimpleParameters; -import org.prebid.server.util.dsl.config.impl.SimpleSource; import java.math.BigDecimal; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Objects; public class BidAdjustmentsResolver { - public static final String WILDCARD = "*"; - public static final String DELIMITER = "|"; - - private final PrebidConfigMatchingStrategy matchingStrategy; private final CurrencyConversionService currencyService; + private final BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + + public BidAdjustmentsResolver(CurrencyConversionService currencyService, + BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver) { - public BidAdjustmentsResolver(CurrencyConversionService currencyService) { this.currencyService = Objects.requireNonNull(currencyService); - this.matchingStrategy = new MostAccurateCombinationStrategy(); + this.bidAdjustmentsRulesResolver = Objects.requireNonNull(bidAdjustmentsRulesResolver); } public Price resolve(Price initialPrice, BidRequest bidRequest, - BidAdjustments bidAdjustments, ImpMediaType targetMediaType, String targetBidder, String targetDealId) { - final List adjustmentsRules = findRules( - bidAdjustments, - targetMediaType, - targetBidder, - targetDealId); - - return adjustPrice(initialPrice, adjustmentsRules, bidRequest); - } - - private List findRules(BidAdjustments bidAdjustments, - ImpMediaType targetMediaType, - String targetBidder, - String targetDealId) { - - final Map> rules = bidAdjustments.getRules(); - final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); - final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId); - - final String rule = matchingStrategy.match(source, parameters); - return rule == null ? Collections.emptyList() : rules.get(rule); - } - - private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) { - final List conditionsMatchers = List.of( - SimpleDirectParameter.of(mediaType.toString()), - SimpleDirectParameter.of(bidder), - StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard()); + final List rules = bidAdjustmentsRulesResolver.resolve( + bidRequest, targetMediaType, targetBidder, targetDealId); - return SimpleParameters.of(conditionsMatchers); + return adjustPrice(initialPrice, rules, bidRequest); } private Price adjustPrice(Price price, - List bidAdjustmentRules, + List bidAdjustmentRules, BidRequest bidRequest) { String resolvedCurrency = price.getCurrency(); BigDecimal resolvedPrice = price.getValue(); - for (ExtRequestBidAdjustmentsRule rule : bidAdjustmentRules) { + for (BidAdjustmentsRule rule : bidAdjustmentRules) { final BidAdjustmentType adjustmentType = rule.getAdjType(); final BigDecimal adjustmentValue = rule.getValue(); final String adjustmentCurrency = rule.getCurrency(); diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java deleted file mode 100644 index 6a151754bb2..00000000000 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.prebid.server.bidadjustments; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.iab.openrtb.request.BidRequest; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.bidadjustments.model.BidAdjustments; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.JsonMerger; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.Logger; -import org.prebid.server.log.LoggerFactory; -import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; -import org.prebid.server.settings.model.Account; -import org.prebid.server.settings.model.AccountAuctionConfig; -import org.prebid.server.validation.ValidationException; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class BidAdjustmentsRetriever { - - private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsRetriever.class); - private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); - - private final ObjectMapper mapper; - private final JsonMerger jsonMerger; - private final double samplingRate; - - public BidAdjustmentsRetriever(JacksonMapper mapper, - JsonMerger jsonMerger, - double samplingRate) { - this.mapper = Objects.requireNonNull(mapper).mapper(); - this.jsonMerger = Objects.requireNonNull(jsonMerger); - this.samplingRate = samplingRate; - } - - public BidAdjustments retrieve(AuctionContext auctionContext) { - final List debugWarnings = auctionContext.getDebugWarnings(); - final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled(); - - final JsonNode requestBidAdjustmentsNode = Optional.ofNullable(auctionContext.getBidRequest()) - .map(BidRequest::getExt) - .map(ExtRequest::getPrebid) - .map(ExtRequestPrebid::getBidadjustments) - .orElseGet(mapper::createObjectNode); - - final JsonNode accountBidAdjustmentsNode = Optional.ofNullable(auctionContext.getAccount()) - .map(Account::getAuction) - .map(AccountAuctionConfig::getBidAdjustments) - .orElseGet(mapper::createObjectNode); - - final JsonNode mergedBidAdjustmentsNode = jsonMerger.merge( - requestBidAdjustmentsNode, - accountBidAdjustmentsNode); - - final List resolvedWarnings = debugEnabled ? debugWarnings : null; - return convertAndValidate(mergedBidAdjustmentsNode, resolvedWarnings, "request") - .or(() -> convertAndValidate(accountBidAdjustmentsNode, resolvedWarnings, "account")) - .orElse(BidAdjustments.of(Collections.emptyMap())); - } - - private Optional convertAndValidate(JsonNode bidAdjustmentsNode, - List debugWarnings, - String errorLocation) { - try { - final ExtRequestBidAdjustments accountBidAdjustments = mapper.convertValue( - bidAdjustmentsNode, - ExtRequestBidAdjustments.class); - - BidAdjustmentRulesValidator.validate(accountBidAdjustments); - return Optional.of(BidAdjustments.of(accountBidAdjustments)); - } catch (IllegalArgumentException | ValidationException e) { - final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage(); - if (debugWarnings != null) { - debugWarnings.add(message); - } - conditionalLogger.error(message, samplingRate); - return Optional.empty(); - } - } -} diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java new file mode 100644 index 00000000000..748e5aab6d7 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java @@ -0,0 +1,79 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; +import org.prebid.server.json.JacksonMapper; +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.request.ImpMediaType; +import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy; +import org.prebid.server.util.dsl.config.PrebidConfigParameter; +import org.prebid.server.util.dsl.config.PrebidConfigParameters; +import org.prebid.server.util.dsl.config.PrebidConfigSource; +import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy; +import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter; +import org.prebid.server.util.dsl.config.impl.SimpleParameters; +import org.prebid.server.util.dsl.config.impl.SimpleSource; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class BidAdjustmentsRulesResolver { + + public static final String WILDCARD = "*"; + public static final String DELIMITER = "|"; + + private final PrebidConfigMatchingStrategy matchingStrategy; + private final ObjectMapper mapper; + + public BidAdjustmentsRulesResolver(JacksonMapper mapper) { + this.matchingStrategy = new MostAccurateCombinationStrategy(); + this.mapper = Objects.requireNonNull(mapper).mapper(); + } + + public List resolve(BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetBidder, + String targetDealId) { + + final BidAdjustmentsRules bidAdjustments = BidAdjustmentsRules.of(extractBidAdjustments(bidRequest)); + return findRules(bidAdjustments, targetMediaType, targetBidder, targetDealId); + } + + private BidAdjustments extractBidAdjustments(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustments) + .map(node -> mapper.convertValue(node, BidAdjustments.class)) + .orElse(null); + } + + private List findRules(BidAdjustmentsRules bidAdjustments, + ImpMediaType targetMediaType, + String targetBidder, + String targetDealId) { + + final Map> rules = bidAdjustments.getRules(); + final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); + final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId); + + final String rule = matchingStrategy.match(source, parameters); + return rule == null ? Collections.emptyList() : rules.get(rule); + } + + private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) { + final List conditionsMatchers = List.of( + SimpleDirectParameter.of(mediaType.toString()), + SimpleDirectParameter.of(bidder), + StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard()); + + return SimpleParameters.of(conditionsMatchers); + } +} diff --git a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java similarity index 94% rename from src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java rename to src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java index 77bbb7372ce..86dd863d1c3 100644 --- a/src/main/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolver.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -32,6 +32,7 @@ public BigDecimal resolve(Set impMediaTypes, } final BigDecimal mediaTypeMinFactor = impMediaTypes.stream() + .map(type -> type == ImpMediaType.video_instream ? ImpMediaType.video : type) .map(adjustmentFactorsByMediaTypes::get) .map(bidderToFactor -> MapUtils.isNotEmpty(bidderToFactor) ? bidderToFactor.entrySet().stream() diff --git a/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java new file mode 100644 index 00000000000..79d1c4eea63 --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java @@ -0,0 +1,90 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.bidadjustments.model.BidAdjustmentType; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; +import org.prebid.server.util.BidderUtil; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class FloorAdjustmentsResolver { + + private final BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + private final CurrencyConversionService currencyService; + + public FloorAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + this.bidAdjustmentsRulesResolver = Objects.requireNonNull(bidAdjustmentsRulesResolver); + this.currencyService = Objects.requireNonNull(currencyService); + } + + public Price resolve(Price initialBidFloorPrice, + BidRequest bidRequest, + Set targetMediaTypes, + String targetBidder) { + + final String currency = bidRequest.getCur().getFirst(); + Price minimalBidFloorPrice = null; + BigDecimal minimalPriceBidFloorValue = new BigDecimal(Integer.MAX_VALUE); + + for (ImpMediaType targetMediaType : targetMediaTypes) { + final Price resolvedPrice = resolve(initialBidFloorPrice, bidRequest, targetMediaType, targetBidder); + final BigDecimal convertedResolvedValue = currencyService.convertCurrency( + resolvedPrice.getValue(), bidRequest, resolvedPrice.getCurrency(), currency); + if (convertedResolvedValue.compareTo(minimalPriceBidFloorValue) < 0) { + minimalBidFloorPrice = resolvedPrice; + minimalPriceBidFloorValue = convertedResolvedValue; + } + } + + return ObjectUtils.firstNonNull(minimalBidFloorPrice, initialBidFloorPrice); + } + + public Price resolve(Price initialBidFloorPrice, + BidRequest bidRequest, + ImpMediaType targetMediaType, + String targetBidder) { + + final List rules = bidAdjustmentsRulesResolver.resolve( + bidRequest, targetMediaType, targetBidder, null); + + return reversePrice(initialBidFloorPrice, rules, bidRequest); + } + + private Price reversePrice(Price price, + List bidAdjustmentRules, + BidRequest bidRequest) { + + final List reversedRules = bidAdjustmentRules.reversed(); + final String resolvedCurrency = price.getCurrency(); + BigDecimal resolvedPrice = price.getValue(); + + for (BidAdjustmentsRule rule : reversedRules) { + final BidAdjustmentType adjustmentType = rule.getAdjType(); + final BigDecimal adjustmentValue = rule.getValue(); + final String adjustmentCurrency = rule.getCurrency(); + + switch (adjustmentType) { + case MULTIPLIER -> resolvedPrice = resolvedPrice.divide(adjustmentValue, 4, RoundingMode.HALF_EVEN); + case CPM -> { + final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency( + adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency); + resolvedPrice = BidderUtil.roundFloor(resolvedPrice.add(convertedAdjustmentValue)); + } + case STATIC -> throw new PreBidException("STATIC type can't be applied to a floor price"); + } + } + + return Price.of(resolvedCurrency, resolvedPrice); + } +} diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java index 385a7644811..c57f5e7b26a 100644 --- a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java @@ -1,52 +1,15 @@ package org.prebid.server.bidadjustments.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; -import org.apache.commons.collections4.MapUtils; -import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; -import org.prebid.server.bidadjustments.BidAdjustmentsResolver; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @Value(staticConstructor = "of") public class BidAdjustments { - private static final String RULE_SCHEME = - "%s" + BidAdjustmentsResolver.DELIMITER + "%s" + BidAdjustmentsResolver.DELIMITER + "%s"; - - Map> rules; - - public static BidAdjustments of(ExtRequestBidAdjustments bidAdjustments) { - if (bidAdjustments == null) { - return BidAdjustments.of(Collections.emptyMap()); - } - - final Map> rules = new HashMap<>(); - - final Map>>> mediatypes = - bidAdjustments.getMediatype(); - - if (MapUtils.isEmpty(mediatypes)) { - return BidAdjustments.of(Collections.emptyMap()); - } - - for (String mediatype : mediatypes.keySet()) { - if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { - final Map>> bidders = mediatypes.get(mediatype); - for (String bidder : bidders.keySet()) { - final Map> deals = bidders.get(bidder); - for (String dealId : deals.keySet()) { - rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); - } - } - } - } - - return BidAdjustments.of(MapUtils.unmodifiableMap(rules)); - } + @JsonProperty("mediatype") + Map>>> rules; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java similarity index 71% rename from src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java rename to src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java index a857575a85f..dec501d71b2 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRule.java @@ -1,15 +1,14 @@ -package org.prebid.server.proto.openrtb.ext.request; +package org.prebid.server.bidadjustments.model; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Builder; import lombok.Value; -import org.prebid.server.bidadjustments.model.BidAdjustmentType; import java.math.BigDecimal; @Builder(toBuilder = true) @Value -public class ExtRequestBidAdjustmentsRule { +public class BidAdjustmentsRule { @JsonProperty("adjtype") BidAdjustmentType adjType; diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java new file mode 100644 index 00000000000..1192a66ae9c --- /dev/null +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java @@ -0,0 +1,50 @@ +package org.prebid.server.bidadjustments.model; + +import lombok.Value; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Value(staticConstructor = "of") +public class BidAdjustmentsRules { + + private static final String RULE_SCHEME = + "%s" + BidAdjustmentsRulesResolver.DELIMITER + "%s" + BidAdjustmentsRulesResolver.DELIMITER + "%s"; + + Map> rules; + + public static BidAdjustmentsRules of(BidAdjustments bidAdjustments) { + if (bidAdjustments == null) { + return BidAdjustmentsRules.of(Collections.emptyMap()); + } + + final Map> rules = new HashMap<>(); + + final Map>>> mediatypes = + bidAdjustments.getRules(); + + if (MapUtils.isEmpty(mediatypes)) { + return BidAdjustmentsRules.of(Collections.emptyMap()); + } + + for (String mediatype : mediatypes.keySet()) { + if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) { + final Map>> bidders = mediatypes.get(mediatype); + for (String bidder : bidders.keySet()) { + final Map> deals = bidders.get(bidder); + for (String dealId : deals.keySet()) { + rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId)); + } + } + } + } + + return BidAdjustmentsRules.of(MapUtils.unmodifiableMap(rules)); + } + +} diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java index 6238b17eec2..cf919abb368 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java @@ -3,8 +3,10 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Imp; import org.apache.commons.lang3.ObjectUtils; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.bidder.model.Price; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -22,6 +24,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -30,12 +33,15 @@ public class BasicPriceFloorAdjuster implements PriceFloorAdjuster { private static final int ADJUSTMENT_SCALE = 4; private static final BiFunction DIVIDE_FUNCTION = (priceFloor, factor) -> priceFloor.divide(factor, ADJUSTMENT_SCALE, RoundingMode.HALF_EVEN); - private static final BiFunction MULTIPLY_FUNCTION = BigDecimal::multiply; private final FloorAdjustmentFactorResolver floorAdjustmentFactorResolver; + private final FloorAdjustmentsResolver floorAdjustmentsResolver; + + public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver, + FloorAdjustmentsResolver floorAdjustmentsResolver) { - public BasicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { this.floorAdjustmentFactorResolver = Objects.requireNonNull(floorAdjustmentFactorResolver); + this.floorAdjustmentsResolver = Objects.requireNonNull(floorAdjustmentsResolver); } @Override @@ -45,36 +51,38 @@ public Price adjustForImp(Imp imp, Account account, List debugWarnings) { - return adjust(imp, bidder, bidRequest, account, DIVIDE_FUNCTION); - } - - @Override - public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return adjust(imp, bidder, bidRequest, account, MULTIPLY_FUNCTION); - } - - private Price adjust(Imp imp, - String bidder, - BidRequest bidRequest, - Account account, - BiFunction function) { - - final ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); + final ExtRequestBidAdjustmentFactors bidAdjustmentFactors = extractBidAdjustmentFactors(bidRequest); final BigDecimal impBidFloor = imp.getBidfloor(); - if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null || extractBidAdjustmentFactors == null) { + if (!shouldAdjustBidFloor(bidRequest, account) || impBidFloor == null) { return Price.of(imp.getBidfloorcur(), impBidFloor); } - final Set impMediaTypes = retrieveImpMediaTypes(imp); - final BigDecimal factor = floorAdjustmentFactorResolver.resolve( - impMediaTypes, extractBidAdjustmentFactors, bidder); + final Set mediaTypes = retrieveImpMediaTypes(imp); + + Price adjustedBidFloor = Price.of(imp.getBidfloorcur(), impBidFloor); + if (bidAdjustmentFactors != null) { + final BigDecimal factor = floorAdjustmentFactorResolver.resolve(mediaTypes, bidAdjustmentFactors, bidder); + + final BigDecimal adjustedBidFloorValue = factor != null && factor.compareTo(BigDecimal.ONE) != 0 + ? BidderUtil.roundFloor(DIVIDE_FUNCTION.apply(impBidFloor, factor)) + : impBidFloor; + + adjustedBidFloor = Price.of(imp.getBidfloorcur(), adjustedBidFloorValue); + } - final BigDecimal adjustedBidFloor = factor != null && factor.compareTo(BigDecimal.ONE) != 0 - ? BidderUtil.roundFloor(function.apply(impBidFloor, factor)) - : impBidFloor; + try { + return floorAdjustmentsResolver.resolve(adjustedBidFloor, bidRequest, mediaTypes, bidder); + } catch (PreBidException e) { + return adjustedBidFloor; + } + } - return Price.of(imp.getBidfloorcur(), adjustedBidFloor); + private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getExt()) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getBidadjustmentfactors) + .orElse(null); } private static boolean shouldAdjustBidFloor(BidRequest bidRequest, Account account) { @@ -95,7 +103,7 @@ private static Set retrieveImpMediaTypes(Imp imp) { if (imp.getVideo() != null) { final Integer placement = imp.getVideo().getPlacement(); if (placement == null || Objects.equals(placement, 1)) { - availableMediaTypes.add(ImpMediaType.video); + availableMediaTypes.add(ImpMediaType.video_instream); } else { availableMediaTypes.add(ImpMediaType.video_outstream); } @@ -126,11 +134,4 @@ private static Boolean shouldAdjustBidFloorByAccount(Account account) { return ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getAdjustForBidAdjustment); } - - private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extPrebid = ObjectUtil.getIfNotNull(extRequest, ExtRequest::getPrebid); - - return ObjectUtil.getIfNotNull(extPrebid, ExtRequestPrebid::getBidadjustmentfactors); - } } diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java index 132bf86e782..1b9c7425385 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java @@ -1,10 +1,8 @@ package org.prebid.server.floors; import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Imp; import com.iab.openrtb.response.Bid; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -36,7 +34,9 @@ import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; @@ -50,15 +50,12 @@ public class BasicPriceFloorEnforcer implements PriceFloorEnforcer { private static final int ENFORCE_RATE_MAX = 100; private final CurrencyConversionService currencyConversionService; - private final PriceFloorAdjuster priceFloorAdjuster; private final Metrics metrics; public BasicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, - PriceFloorAdjuster priceFloorAdjuster, Metrics metrics) { this.currencyConversionService = Objects.requireNonNull(currencyConversionService); - this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster); this.metrics = Objects.requireNonNull(metrics); } @@ -165,9 +162,12 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest, } final BigDecimal price = bid.getPrice(); + final Map originalPriceFloors = Optional.ofNullable(auctionParticipation.getBidderRequest()) + .map(BidderRequest::getOriginalPriceFloors) + .orElse(Collections.emptyMap()); + final BigDecimal floor = resolveFloor( - bidderResponse.getBidder(), - account, + originalPriceFloors, bidderBid, bidderBidRequest, bidRequest, @@ -211,8 +211,7 @@ private static boolean enforceDealFloors(AuctionParticipation auctionParticipati return BooleanUtils.isTrue(requestEnforceDealFloors) && BooleanUtils.isTrue(accountEnforceDealFloors); } - private BigDecimal resolveFloor(String bidder, - Account account, + private BigDecimal resolveFloor(Map originalPriceFloors, BidderBid bidderBid, BidRequest bidderBidRequest, BidRequest bidRequest, @@ -226,14 +225,15 @@ private BigDecimal resolveFloor(String bidder, return convertIfRequired(customBidderFloor, priceFloorInfo.getCurrency(), bidderBidRequest, bidRequest); } - final Imp imp = correspondingImp(bidderBid.getBid(), bidderBidRequest.getImp()); - final Price correctedImpFloor = priceFloorAdjuster.revertAdjustmentForImp(imp, bidder, bidRequest, account); final String bidRequestCurrency = resolveBidRequestCurrency(bidRequest); + final Price originalFloorPrice = originalPriceFloors.get(bidderBid.getBid().getImpid()); - return convertCurrency( - correctedImpFloor.getValue(), + return originalFloorPrice == null + ? null + : convertCurrency( + originalFloorPrice.getValue(), bidRequest, - correctedImpFloor.getCurrency(), + originalFloorPrice.getCurrency(), bidRequestCurrency); } catch (PreBidException e) { final String logMessage = "Price floors enforcement failed for request id: %s, reason: %s" @@ -286,15 +286,6 @@ private static String resolveBidRequestCurrency(BidRequest bidRequest) { return CollectionUtils.isEmpty(currencies) ? null : currencies.getFirst(); } - private static Imp correspondingImp(Bid bid, List imps) { - final String impId = bid.getImpid(); - return ListUtils.emptyIfNull(imps).stream() - .filter(imp -> Objects.equals(impId, imp.getId())) - .findFirst() - // Should never happen, see ResponseBidValidator usage. - .orElseThrow(() -> new PreBidException("Bid with impId %s doesn't have matched imp".formatted(impId))); - } - private static boolean isPriceBelowFloor(BigDecimal price, BigDecimal bidFloor) { return bidFloor != null && price.compareTo(bidFloor) < 0; } diff --git a/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java index 07c05134877..8256b72222c 100644 --- a/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjuster.java @@ -67,11 +67,6 @@ public Price adjustForImp(Imp imp, .orElseGet(() -> delegate.adjustForImp(imp, bidder, bidRequest, account, debugWarnings)); } - @Override - public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return delegate.revertAdjustmentForImp(imp, bidder, bidRequest, account); - } - private static boolean isNoSignalBidder(String bidder, List noSignalBidders) { return noSignalBidders.stream().anyMatch(noSignalBidder -> StringUtils.equalsIgnoreCase(noSignalBidder, bidder)) || noSignalBidders.contains(ALL_BIDDERS); diff --git a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java index 89a2ac7bc24..0654e6b523f 100644 --- a/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/PriceFloorAdjuster.java @@ -12,8 +12,6 @@ public interface PriceFloorAdjuster { Price adjustForImp(Imp imp, String bidder, BidRequest bidRequest, Account account, List debugWarnings); - Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account); - static NoOpPriceFloorAdjuster noOp() { return new NoOpPriceFloorAdjuster(); } @@ -29,10 +27,5 @@ public Price adjustForImp(Imp imp, return ObjectUtil.getIfNotNull(imp, i -> Price.of(i.getBidfloorcur(), i.getBidfloor())); } - - @Override - public Price revertAdjustmentForImp(Imp imp, String bidder, BidRequest bidRequest, Account account) { - return ObjectUtil.getIfNotNull(imp, i -> Price.of(i.getBidfloorcur(), i.getBidfloor())); - } } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java deleted file mode 100644 index ab0565ce44e..00000000000 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.proto.openrtb.ext.request; - -import lombok.Builder; -import lombok.Value; - -import java.util.List; -import java.util.Map; - -@Builder(toBuilder = true) -@Value -public class ExtRequestBidAdjustments { - - Map>>> mediatype; - -} diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java index 8a483e92a4d..b408a67b12a 100644 --- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java @@ -1,7 +1,9 @@ package org.prebid.server.spring.config; import io.vertx.core.Vertx; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.currency.CurrencyConversionService; import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.floors.BasicPriceFloorAdjuster; @@ -52,10 +54,8 @@ PriceFloorFetcher priceFloorFetcher( @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") - PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, - PriceFloorAdjuster priceFloorAdjuster, - Metrics metrics) { - return new BasicPriceFloorEnforcer(currencyConversionService, priceFloorAdjuster, metrics); + PriceFloorEnforcer basicPriceFloorEnforcer(CurrencyConversionService currencyConversionService, Metrics metrics) { + return new BasicPriceFloorEnforcer(currencyConversionService, metrics); } @Bean @@ -103,8 +103,18 @@ FloorAdjustmentFactorResolver floorsAdjustmentFactorResolver() { @Bean @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") - BasicPriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver) { - return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver); + FloorAdjustmentsResolver floorAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + return new FloorAdjustmentsResolver(bidAdjustmentsRulesResolver, currencyService); + } + + @Bean + @ConditionalOnProperty(prefix = "price-floors", name = "enabled", havingValue = "true") + BasicPriceFloorAdjuster basicPriceFloorAdjuster(FloorAdjustmentFactorResolver floorAdjustmentFactorResolver, + FloorAdjustmentsResolver floorAdjustmentsResolver) { + + return new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver, floorAdjustmentsResolver); } @Bean 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 10fd17ad162..a62de5a4fbb 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -34,7 +34,7 @@ import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; import org.prebid.server.auction.WinningBidComparatorFactory; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.BidAdjustmentFactorResolver; import org.prebid.server.auction.categorymapping.BasicCategoryMappingService; import org.prebid.server.auction.categorymapping.CategoryMappingService; import org.prebid.server.auction.categorymapping.NoOpCategoryMappingService; @@ -66,7 +66,8 @@ import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory; import org.prebid.server.bidadjustments.BidAdjustmentsProcessor; import org.prebid.server.bidadjustments.BidAdjustmentsResolver; -import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; +import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.BidderErrorNotifier; @@ -431,7 +432,7 @@ AuctionRequestFactory auctionRequestFactory( DebugResolver debugResolver, JacksonMapper mapper, GeoLocationServiceWrapper geoLocationServiceWrapper, - BidAdjustmentsRetriever bidAdjustmentsRetriever) { + BidAdjustmentsEnricher bidAdjustmentsEnricher) { return new AuctionRequestFactory( maxRequestSize, @@ -448,7 +449,7 @@ AuctionRequestFactory auctionRequestFactory( debugResolver, mapper, geoLocationServiceWrapper, - bidAdjustmentsRetriever); + bidAdjustmentsEnricher); } @Bean @@ -907,7 +908,8 @@ ExchangeService exchangeService( metrics, clock, mapper, - criteriaLogManager, enabledStrictAppSiteDoohValidation); + criteriaLogManager, + enabledStrictAppSiteDoohValidation); } @Bean @@ -1192,13 +1194,20 @@ SkippedAuctionService skipAuctionService(StoredResponseProcessor storedResponseP } @Bean - BidAdjustmentsRetriever bidAdjustmentsRetriever(JacksonMapper mapper, JsonMerger jsonMerger) { - return new BidAdjustmentsRetriever(mapper, jsonMerger, logSamplingRate); + BidAdjustmentsEnricher bidAdjustmentsEnricher(JacksonMapper mapper, JsonMerger jsonMerger) { + return new BidAdjustmentsEnricher(mapper, jsonMerger, logSamplingRate); } @Bean - BidAdjustmentsResolver bidAdjustmentsResolver(CurrencyConversionService currencyService) { - return new BidAdjustmentsResolver(currencyService); + BidAdjustmentsResolver bidAdjustmentsResolver(BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver, + CurrencyConversionService currencyService) { + + return new BidAdjustmentsResolver(currencyService, bidAdjustmentsRulesResolver); + } + + @Bean + BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver(JacksonMapper mapper) { + return new BidAdjustmentsRulesResolver(mapper); } @Bean diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java index 9bfbc9cb143..4d0dc685827 100644 --- a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java +++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java @@ -65,7 +65,7 @@ public void setUp() { given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1)); - given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any())) .willAnswer(inv -> inv.getArgument(0)); target = new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer); @@ -88,7 +88,7 @@ public void shouldReturnBidsAdjustedByBidAdjustmentsProcessor() { final BidderBid adjustedBid = givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.TEN).build(), "USD"); - given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any())) + given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any())) .willReturn(AuctionParticipation.builder() .bidder("bidder1") .bidderResponse(BidderResponse.of( diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 2e8c9a5e2c1..d98d1c7235c 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -1231,6 +1231,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .build())))) + .originalPriceFloors(Collections.emptyMap()) .build()), any(), any(), @@ -1253,6 +1254,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .auctiontimestamp(1000L) .build())))) + .originalPriceFloors(Collections.emptyMap()) .build()), any(), any(), diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java index 60a9f35c603..6ab3a5cce77 100644 --- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java +++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java @@ -36,9 +36,8 @@ import org.prebid.server.auction.model.debug.DebugContext; import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory; import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager; -import org.prebid.server.bidadjustments.BidAdjustmentsRetriever; +import org.prebid.server.bidadjustments.BidAdjustmentsEnricher; import org.prebid.server.bidadjustments.model.BidAdjustmentType; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.cookie.CookieDeprecationService; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.geolocation.model.GeoInfo; @@ -52,7 +51,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions; @@ -63,7 +62,6 @@ import java.util.Map; import static java.util.Collections.emptyList; -import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.apache.commons.lang3.StringUtils.EMPTY; @@ -110,7 +108,7 @@ public class AuctionRequestFactoryTest extends VertxTest { @Mock(strictness = LENIENT) private GeoLocationServiceWrapper geoLocationServiceWrapper; @Mock(strictness = LENIENT) - private BidAdjustmentsRetriever bidAdjustmentsRetriever; + private BidAdjustmentsEnricher bidAdjustmentsEnricher; private AuctionRequestFactory target; @@ -197,7 +195,7 @@ public void setUp() { .will(invocationOnMock -> invocationOnMock.getArgument(0)); given(geoLocationServiceWrapper.lookup(any())) .willReturn(Future.succeededFuture(GeoInfo.builder().vendor("vendor").build())); - given(bidAdjustmentsRetriever.retrieve(any())).willReturn(BidAdjustments.of(emptyMap())); + given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(defaultBidRequest); target = new AuctionRequestFactory( Integer.MAX_VALUE, @@ -214,7 +212,7 @@ public void setUp() { debugResolver, jacksonMapper, geoLocationServiceWrapper, - bidAdjustmentsRetriever); + bidAdjustmentsEnricher); } @Test @@ -250,7 +248,7 @@ public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() { debugResolver, jacksonMapper, geoLocationServiceWrapper, - bidAdjustmentsRetriever); + bidAdjustmentsEnricher); given(routingContext.getBodyAsString()).willReturn("body"); @@ -693,6 +691,7 @@ public void shouldReturnModifiedBidRequestInAuctionContextWhenRequestWasPopulate final BidRequest updatedBidRequest = defaultBidRequest.toBuilder().id("updated").build(); given(paramsResolver.resolve(any(), any(), any(), anyBoolean())).willReturn(updatedBidRequest); + given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(updatedBidRequest); // when final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); @@ -729,22 +728,30 @@ public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementRetur @Test public void shouldReturnPopulatedBidAdjustments() { // given - givenValidBidRequest(); - - final BidAdjustments bidAdjustments = BidAdjustments.of(Map.of( - "rule1", List.of( - ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()), - "rule2", List.of( - ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(), - ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build()))); + final ObjectNode bidAdjustments = mapper.valueToTree(Map.of( + "mediaType1", Map.of("bidder1", Map.of("dealId1", List.of( + BidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()))), + "mediaType2", Map.of("bidder2", Map.of("dealId2", List.of( + BidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(), + BidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build()))))); + + final BidRequest givenBidRequest = defaultBidRequest.toBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(bidAdjustments).build())) + .build(); - given(bidAdjustmentsRetriever.retrieve(any())).willReturn(bidAdjustments); + givenValidBidRequest(givenBidRequest); + given(bidAdjustmentsEnricher.enrichBidRequest(any())).willReturn(givenBidRequest); // when final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result(); // then - assertThat(result.getBidAdjustments()).isEqualTo(bidAdjustments); + assertThat(result) + .extracting(AuctionContext::getBidRequest) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(bidAdjustments); } @Test diff --git a/src/test/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolverTest.java similarity index 80% rename from src/test/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolverTest.java rename to src/test/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolverTest.java index 7ff8439a960..ef572a69e50 100644 --- a/src/test/java/org/prebid/server/auction/adjustment/BidAdjustmentFactorResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentFactorResolverTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -13,17 +13,17 @@ public class BidAdjustmentFactorResolverTest { - private BidAdjustmentFactorResolver bidAdjustmentFactorResolver; + private BidAdjustmentFactorResolver target; @BeforeEach public void setUp() { - bidAdjustmentFactorResolver = new BidAdjustmentFactorResolver(); + target = new BidAdjustmentFactorResolver(); } @Test public void resolveShouldReturnOneIfAdjustmentsByMediaTypeAndBidderAreAbsent() { // when - final BigDecimal result = bidAdjustmentFactorResolver.resolve( + final BigDecimal result = target.resolve( ImpMediaType.video, ExtRequestBidAdjustmentFactors.builder().build(), "bidder"); @@ -40,7 +40,7 @@ public void resolveShouldReturnBidderAdjustmentFactorIfAdjustmentsByTypeAreAbsen adjustmentFactors.addFactor("BIDder", BigDecimal.valueOf(3.456)); // when - final BigDecimal result = bidAdjustmentFactorResolver.resolve(ImpMediaType.video, adjustmentFactors, "bidDER"); + final BigDecimal result = target.resolve(ImpMediaType.video, adjustmentFactors, "bidDER"); // then assertThat(result).isEqualTo(BigDecimal.valueOf(3.456)); @@ -60,7 +60,7 @@ public void resolveShouldReturnAdjustmentByMediaTypeIfPresentIgnoringCase() { .build(); // when - final BigDecimal result = bidAdjustmentFactorResolver.resolve(ImpMediaType.video, adjustmentFactors, "bidDER"); + final BigDecimal result = target.resolve(ImpMediaType.video_instream, adjustmentFactors, "bidDER"); // then assertThat(result).isEqualTo(BigDecimal.valueOf(1.234)); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java index 0c98ff6af3b..a9634e568fd 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java @@ -1,8 +1,8 @@ package org.prebid.server.bidadjustments; import org.junit.jupiter.api.Test; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.validation.ValidationException; import java.math.BigDecimal; @@ -28,28 +28,26 @@ public void validateShouldDoNothingWhenBidAdjustmentsIsNull() throws ValidationE @Test public void validateShouldDoNothingWhenMediatypesIsEmpty() throws ValidationException { // when & then - BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder().build()); + BidAdjustmentRulesValidator.validate(BidAdjustments.of(Collections.emptyMap())); } @Test public void validateShouldSkipMediatypeValidationWhenMediatypesIsNotSupported() throws ValidationException { // given - final ExtRequestBidAdjustmentsRule invalidRule = ExtRequestBidAdjustmentsRule.builder() + final BidAdjustmentsRule invalidRule = BidAdjustmentsRule.builder() .value(new BigDecimal("-999")) .build(); // when & then - BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() - .mediatype(Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule))))) - .build()); + BidAdjustmentRulesValidator.validate(BidAdjustments.of( + Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule)))))); } @Test public void validateShouldFailWhenBiddersAreAbsent() { // given - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Collections.emptyMap())) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Collections.emptyMap())); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -60,9 +58,8 @@ public void validateShouldFailWhenBiddersAreAbsent() { @Test public void validateShouldFailWhenDealsAreAbsent() { // given - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", Collections.emptyMap()))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", Collections.emptyMap()))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -73,12 +70,11 @@ public void validateShouldFailWhenDealsAreAbsent() { @Test public void validateShouldFailWhenRulesIsEmpty() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", null); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -89,24 +85,23 @@ public void validateShouldFailWhenRulesIsEmpty() { @Test public void validateShouldDoNothingWhenRulesAreEmpty() throws ValidationException { // when & then - BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder() - .mediatype(Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of())))) - .build()); + BidAdjustmentRulesValidator.validate(BidAdjustments.of( + Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of()))))); + } @Test public void validateShouldFailWhenRuleHasUnknownType() { // given - final Map> rules = new HashMap<>(); - rules.put("*", List.of(ExtRequestBidAdjustmentsRule.builder() + final Map> rules = new HashMap<>(); + rules.put("*", List.of(BidAdjustmentsRule.builder() .adjType(UNKNOWN) .value(BigDecimal.ONE) .currency("USD") .build())); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -118,12 +113,11 @@ public void validateShouldFailWhenRuleHasUnknownType() { @Test public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenCpm("1", "USD"), givenCpm("1", null))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -134,12 +128,11 @@ public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() { @Test public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("-1", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -150,12 +143,11 @@ public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() { @Test public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("2147483647", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -167,12 +159,11 @@ public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() { @Test public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenStatic("1", "USD"), givenStatic("1", null))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -184,12 +175,11 @@ public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() { @Test public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("-1", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -201,12 +191,11 @@ public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() { @Test public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("2147483647", "USD"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -218,12 +207,11 @@ public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() { @Test public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("-1"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -235,12 +223,11 @@ public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() { @Test public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() { // given - final Map> rules = new HashMap<>(); + final Map> rules = new HashMap<>(); rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("100"))); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of("banner", Map.of("bidderName", rules))) - .build(); + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of("banner", Map.of("bidderName", rules))); // when & then assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments)) @@ -252,17 +239,16 @@ public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() { @Test public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationException { // given - final List givenRules = List.of( + final List givenRules = List.of( givenMultiplier("1"), givenCpm("2", "USD"), givenStatic("3", "EUR")); - final Map>> givenRulesMap = Map.of( + final Map>> givenRulesMap = Map.of( "bidderName", Map.of("dealId", givenRules)); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of( + final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( "audio", givenRulesMap, "native", givenRulesMap, "video-instream", givenRulesMap, @@ -274,31 +260,30 @@ public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationExcep "*", Map.of("*", givenRules), "bidderName", Map.of( "*", givenRules, - "dealId", givenRules)))) - .build(); + "dealId", givenRules)))); //when & then BidAdjustmentRulesValidator.validate(givenBidAdjustments); } - private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenStatic(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(STATIC) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenCpm(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(CPM) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenMultiplier(String value) { + return BidAdjustmentsRule.builder() .adjType(MULTIPLIER) .value(new BigDecimal(value)) .build(); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricherTest.java similarity index 65% rename from src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java rename to src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricherTest.java index df6caa05abd..e5997dbfd6c 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricherTest.java @@ -1,6 +1,7 @@ package org.prebid.server.bidadjustments; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import org.junit.jupiter.api.BeforeEach; @@ -8,49 +9,47 @@ import org.prebid.server.VertxTest; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.debug.DebugContext; -import org.prebid.server.bidadjustments.model.BidAdjustments; import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountAuctionConfig; -import java.math.BigDecimal; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; -import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; -import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; -public class BidAdjustmentsRetrieverTest extends VertxTest { +public class BidAdjustmentsEnricherTest extends VertxTest { - private BidAdjustmentsRetriever target; + private BidAdjustmentsEnricher target; @BeforeEach public void before() { - target = new BidAdjustmentsRetriever(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d); + target = new BidAdjustmentsEnricher(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d); } @Test - public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() { + public void enrichBidRequestShouldReturnEmptyAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() { // given final List debugMessages = new ArrayList<>(); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( null, null, debugMessages, true)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages).isEmpty(); } @Test - public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountAdjustmentsAreAbsent() + public void enrichBidRequestShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountRequestAreAbsent() throws JsonProcessingException { // given @@ -63,7 +62,7 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccoun "invalid": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -76,18 +75,22 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccoun final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, null, debugMessages, true)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages) .containsOnly("bid adjustment from request was invalid: the found rule " + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.invalid.invalid is invalid"); } @Test - public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAbsent() + public void enrichBidRequestShouldReturnRequestBidAdjustmentsWhenAccountRequestAreAbsent() throws JsonProcessingException { // given @@ -100,7 +103,7 @@ public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAb "*": [ { "adjtype": "cpm", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -113,24 +116,22 @@ public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAb final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, null, debugMessages, true)); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( - "banner|*|*", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(CPM) - .currency("USD") - .value(new BigDecimal("0.1")) - .build()))); - - assertThat(actual).isEqualTo(expected); + final JsonNode expected = givenRule("banner", "*", "*", "cpm", "0.1", "USD"); + + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(expected); assertThat(debugMessages).isEmpty(); } @Test - public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAbsent() + public void enrichBidRequestShouldReturnAccountBidAdjustmentsWhenRequestRequestAreAbsent() throws JsonProcessingException { // given @@ -143,7 +144,7 @@ public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAb "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -161,7 +162,7 @@ public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAb "*": [ { "adjtype": "static", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -175,26 +176,24 @@ public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAb final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( - "audio|bidder|*", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(STATIC) - .currency("USD") - .value(new BigDecimal("0.1")) - .build()))); - - assertThat(actual).isEqualTo(expected); + final JsonNode expected = givenRule("audio", "bidder", "*", "static", "0.1", "USD"); + + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(expected); assertThat(debugMessages) .containsOnly("bid adjustment from request was invalid: the found rule " + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.*.* is invalid"); } @Test - public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustmentsAreInvalid() + public void enrichBidRequestShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestRequestAreInvalid() throws JsonProcessingException { // given @@ -207,7 +206,7 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustme "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -225,7 +224,7 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustme "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -239,20 +238,24 @@ public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustme final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages).containsExactlyInAnyOrder( - "bid adjustment from request was invalid: the found rule " - + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid", - "bid adjustment from account was invalid: the found rule " - + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid"); + "bid adjustment from request was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid", + "bid adjustment from account was invalid: the found rule " + + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid"); } @Test - public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException { + public void enrichBidRequestShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException { // given final List debugMessages = new ArrayList<>(); final String requestAdjustments = """ @@ -263,7 +266,7 @@ public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws Js "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -281,7 +284,7 @@ public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws Js "*": [ { "adjtype": "invalid", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -295,16 +298,20 @@ public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws Js final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, false)); // then - assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap())); + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isNull(); assertThat(debugMessages).isEmpty(); } @Test - public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws JsonProcessingException { + public void enrichBidRequestShouldReturnMergedAccountIntoRequestRequest() throws JsonProcessingException { // given final List debugMessages = new ArrayList<>(); final String requestAdjustments = """ @@ -315,7 +322,7 @@ public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws Jso "*": [ { "adjtype": "cpm", - "value": 0.1, + "value": "0.1", "currency": "USD" } ] @@ -333,14 +340,14 @@ public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws Jso "dealId": [ { "adjtype": "cpm", - "value": 0.3, + "value": "0.3", "currency": "USD" } ], "*": [ { "adjtype": "static", - "value": 0.2, + "value": "0.2", "currency": "USD" } ] @@ -354,25 +361,22 @@ public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws Jso final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments); // when - final BidAdjustments actual = target.retrieve(givenAuctionContext( + final BidRequest actual = target.enrichBidRequest(givenAuctionContext( givenRequestAdjustments, givenAccountAdjustments, debugMessages, true)); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( - "banner|*|dealId", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(CPM) - .currency("USD") - .value(new BigDecimal("0.3")) - .build()), - "banner|*|*", - List.of(ExtRequestBidAdjustmentsRule.builder() - .adjType(CPM) - .currency("USD") - .value(new BigDecimal("0.1")) - .build()))); - - assertThat(actual).isEqualTo(expected); + final JsonNode expected = mapper.valueToTree( + Map.of("mediatype", Map.of("banner", Map.of("*", Map.of( + "*", mapper.createArrayNode().add(mapper.createObjectNode() + .put("adjtype", "cpm").put("value", "0.1").put("currency", "USD")), + "dealId", mapper.createArrayNode().add(mapper.createObjectNode() + .put("adjtype", "cpm").put("value", "0.3").put("currency", "USD"))))))); + + assertThat(actual) + .extracting(BidRequest::getExt) + .extracting(ExtRequest::getPrebid) + .extracting(ExtRequestPrebid::getBidadjustments) + .isEqualTo(expected); assertThat(debugMessages).isEmpty(); } @@ -393,4 +397,19 @@ private static AuctionContext givenAuctionContext(ObjectNode requestBidAdjustmen .build(); } + private static JsonNode givenRule(String mediaType, + String bidder, + String dealId, + String adjtype, + String value, + String currency) { + + return mapper.valueToTree( + Map.of("mediatype", Map.of(mediaType, Map.of(bidder, Map.of(dealId, mapper.createArrayNode() + .add(mapper.createObjectNode() + .put("adjtype", adjtype) + .put("value", value) + .put("currency", currency))))))); + } + } diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java index 2affb167eef..dea75ee343d 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java @@ -13,11 +13,10 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.prebid.server.VertxTest; -import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver; import org.prebid.server.auction.model.AuctionParticipation; import org.prebid.server.auction.model.BidderRequest; import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; @@ -26,13 +25,14 @@ import org.prebid.server.exception.PreBidException; 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.ExtRequestBidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustments; 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 java.math.BigDecimal; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; @@ -74,7 +74,7 @@ public class BidAdjustmentsProcessorTest extends VertxTest { public void before() { given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); target = new BidAdjustmentsProcessor( @@ -89,20 +89,22 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { // given final BidderResponse bidderResponse = givenBidderResponse( Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build()); + final BidRequest bidRequest = givenBidRequest( - singletonList(givenImp(singletonMap("bidder", 2), identity())), identity()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())).willReturn(adjustedPrice); final BigDecimal expectedPrice = new BigDecimal("123.5"); given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -112,7 +114,6 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2.0))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), eq("bidder"), eq("dealId")); @@ -123,19 +124,21 @@ 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()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocation -> invocation.getArgument(0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -149,8 +152,11 @@ 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()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); @@ -158,8 +164,7 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then final BidderError expectedError = BidderError.generic( @@ -175,18 +180,19 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving( 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()); + singletonList(givenImp(singletonMap("bidder", 2), identity())), + request -> request.ext(ExtRequest.of( + ExtRequestPrebid.builder().bidadjustments(mapper.valueToTree(givenBidAdjustments())).build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocation -> invocation.getArgument(0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then final BidderError expectedError = BidderError.generic( @@ -209,6 +215,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); @@ -217,13 +224,12 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder")) .willReturn(BigDecimal.TEN); final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice); + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())).willReturn(adjustedPrice); final BigDecimal expectedPrice = new BigDecimal("123.5"); given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then final BidderSeatBid seatBid = result.getBidderResponse().getSeatBid(); @@ -235,7 +241,6 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(20.0))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), eq("bidder"), eq("dealId")); @@ -267,8 +272,7 @@ public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForS final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, null); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any()); @@ -302,10 +306,12 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported .build(), 1); + final ObjectNode bidAdjustments = mapper.valueToTree(givenBidAdjustments()); final BidRequest bidRequest = BidRequest.builder() .cur(singletonList("CUR")) - .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), - identity()))).build(); + .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3), identity()))) + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(bidAdjustments).build())) + .build(); final BigDecimal updatedPrice = BigDecimal.valueOf(20); given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); @@ -316,7 +322,7 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported // when final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + auctionParticipation, bidRequest); // then verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("CUR")); @@ -353,7 +359,11 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { final BidRequest bidRequest = givenBidRequest( singletonList(givenImp(Map.of("bidder1", 1), identity())), - builder -> builder.cur(singletonList("USD"))); + builder -> builder + .cur(singletonList("USD")) + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) + .build()))); final BigDecimal updatedPrice = BigDecimal.valueOf(10.0); given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice); @@ -362,8 +372,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD")); @@ -376,7 +385,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { .extracting(Bid::getPrice) .containsOnly(bidder3Price, updatedPrice, updatedPrice); - verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any()); + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any()); } @Test @@ -394,14 +403,14 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -412,7 +421,6 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(4.936))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), eq("bidder"), eq("dealId")); @@ -438,7 +446,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, singletonMap("bidder", BigDecimal.valueOf(3.456))))) .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_instream, givenAdjustments, "bidder")) .willReturn(BigDecimal.valueOf(3.456)); final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> @@ -446,14 +454,14 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -464,7 +472,6 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_instream), eq("bidder"), eq("dealId")); @@ -490,7 +497,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video, singletonMap("bidder", BigDecimal.valueOf(3.456))))) .build(); - given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder")) + given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_instream, givenAdjustments, "bidder")) .willReturn(BigDecimal.valueOf(3.456)); final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder -> @@ -498,14 +505,14 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -516,7 +523,6 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_instream), eq("bidder"), eq("dealId")); @@ -550,13 +556,13 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideo builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -567,7 +573,6 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideo verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_outstream), eq("bidder"), eq("dealId")); @@ -599,14 +604,14 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidIm builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -618,7 +623,6 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidIm verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_outstream), eq("bidder"), eq("dealId")); @@ -650,14 +654,14 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpId builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -668,7 +672,6 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpId verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.video_outstream), eq("bidder"), eq("dealId")); @@ -698,14 +701,14 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -713,8 +716,7 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() .extracting(Bid::getPrice) .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - verify(bidAdjustmentsResolver, times(3)) - .resolve(any(), any(), any(), any(), any(), any()); + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any()); } @Test @@ -745,14 +747,14 @@ public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() .aliases(emptyMap()) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .auctiontimestamp(1000L) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target.enrichWithAdjustedBids( - auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -763,7 +765,6 @@ public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), eq("bidder"), eq("dealId")); @@ -794,13 +795,13 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent .auctiontimestamp(1000L) .currency(ExtRequestCurrency.of(null, false)) .bidadjustmentfactors(givenAdjustments) + .bidadjustments(mapper.valueToTree(givenBidAdjustments())) .build()))); final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); // when - final AuctionParticipation result = target - .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments()); + final AuctionParticipation result = target.enrichWithAdjustedBids(auctionParticipation, bidRequest); // then assertThat(result.getBidderResponse().getSeatBid().getBids()) @@ -811,7 +812,6 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent verify(bidAdjustmentsResolver).resolve( eq(Price.of("USD", BigDecimal.ONE)), eq(bidRequest), - eq(givenBidAdjustments()), eq(ImpMediaType.banner), eq("bidder"), eq("dealId")); @@ -848,8 +848,8 @@ private static Map doubleMap(K key1, V value1, K key2, V value2) { return map; } - private static BidAdjustments givenBidAdjustments() { - return BidAdjustments.of(ExtRequestBidAdjustments.builder().build()); + private static BidAdjustmentsRules givenBidAdjustments() { + return BidAdjustmentsRules.of(BidAdjustments.of(Collections.emptyMap())); } private BidderResponse givenBidderResponse(Bid bid) { diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java index 97ca68e939e..d0033f384af 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java @@ -7,15 +7,13 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.bidadjustments.model.BidAdjustments; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; import org.prebid.server.bidder.model.Price; import org.prebid.server.currency.CurrencyConversionService; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; -import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.math.BigDecimal; +import java.util.Collections; import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -26,6 +24,8 @@ import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.banner; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.video_outstream; @ExtendWith(MockitoExtension.class) public class BidAdjustmentsResolverTest extends VertxTest { @@ -33,11 +33,14 @@ public class BidAdjustmentsResolverTest extends VertxTest { @Mock(strictness = LENIENT) private CurrencyConversionService currencyService; + @Mock(strictness = LENIENT) + private BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + private BidAdjustmentsResolver target; @BeforeEach public void before() { - target = new BidAdjustmentsResolver(currencyService); + target = new BidAdjustmentsResolver(currencyService, bidAdjustmentsRulesResolver); given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> { final BigDecimal initialPrice = (BigDecimal) invocation.getArguments()[0]; @@ -48,16 +51,15 @@ public void before() { @Test public void resolveShouldPickAndApplyRulesBySpecificMediaType() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "banner|*|*", List.of(givenStatic("15", "EUR")), - "*|*|*", List.of(givenStatic("25", "UAH")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + .willReturn(List.of(givenStatic("15", "EUR"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), - BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + givenBidRequest, + banner, "bidderName", "dealId"); @@ -69,18 +71,15 @@ public void resolveShouldPickAndApplyRulesBySpecificMediaType() { @Test public void resolveShouldPickAndApplyRulesByWildcardMediaType() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "banner|*|*", List.of(givenCpm("15", "EUR")), - "*|*|*", List.of(givenCpm("25", "UAH")))); - final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, video_outstream, "bidderName", "dealId")) + .willReturn(List.of(givenCpm("25", "UAH"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.video_outstream, + video_outstream, "bidderName", "dealId"); @@ -92,16 +91,15 @@ public void resolveShouldPickAndApplyRulesByWildcardMediaType() { @Test public void resolveShouldPickAndApplyRulesBySpecificBidder() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|bidderName|*", List.of(givenMultiplier("15")), - "*|*|*", List.of(givenMultiplier("25")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + .willReturn(List.of(givenMultiplier("15"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + banner, "bidderName", "dealId"); @@ -113,16 +111,15 @@ public void resolveShouldPickAndApplyRulesBySpecificBidder() { @Test public void resolveShouldPickAndApplyRulesByWildcardBidder() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|bidderName|*", List.of(givenStatic("15", "EUR"), givenMultiplier("15")), - "*|*|*", List.of(givenStatic("25", "UAH"), givenMultiplier("25")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "anotherBidderName", "dealId")) + .willReturn(List.of(givenStatic("25", "UAH"), givenMultiplier("25"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + banner, "anotherBidderName", "dealId"); @@ -134,17 +131,15 @@ public void resolveShouldPickAndApplyRulesByWildcardBidder() { @Test public void resolveShouldPickAndApplyRulesBySpecificDealId() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR")), - "*|*|*", List.of(givenCpm("25", "JPY"), givenStatic("25", "UAH")))); final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + .willReturn(List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.banner, + banner, "bidderName", "dealId"); @@ -156,17 +151,15 @@ public void resolveShouldPickAndApplyRulesBySpecificDealId() { @Test public void resolveShouldPickAndApplyRulesByWildcardDealId() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenMultiplier("15"), givenCpm("15", "EUR")), - "*|*|*", List.of(givenMultiplier("25"), givenCpm("25", "UAH")))); final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "anotherDealId")) + .willReturn(List.of(givenMultiplier("25"), givenCpm("25", "UAH"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.banner, + banner, "bidderName", "anotherDealId"); @@ -178,17 +171,15 @@ public void resolveShouldPickAndApplyRulesByWildcardDealId() { @Test public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenCpm("15", "EUR"), givenCpm("15", "JPY")), - "*|*|*", List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY")))); final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", null)) + .willReturn(List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY"))); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), givenBidRequest, - givenBidAdjustments, - ImpMediaType.banner, + banner, "bidderName", null); @@ -201,15 +192,15 @@ public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { @Test public void resolveShouldReturnEmptyListWhenNoMatchFound() { // given - final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of( - "*|*|dealId", List.of(givenStatic("15", "EUR")))); + final BidRequest givenBidRequest = BidRequest.builder().build(); + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", null)) + .willReturn(Collections.emptyList()); // when final Price actual = target.resolve( Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), - givenBidAdjustments, - ImpMediaType.banner, + banner, "bidderName", null); @@ -218,24 +209,24 @@ public void resolveShouldReturnEmptyListWhenNoMatchFound() { verifyNoInteractions(currencyService); } - private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenStatic(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(STATIC) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenCpm(String value, String currency) { + return BidAdjustmentsRule.builder() .adjType(CPM) .currency(currency) .value(new BigDecimal(value)) .build(); } - private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenMultiplier(String value) { + return BidAdjustmentsRule.builder() .adjType(MULTIPLIER) .value(new BigDecimal(value)) .build(); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java new file mode 100644 index 00000000000..6b6b440ddd9 --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java @@ -0,0 +1,411 @@ +package org.prebid.server.bidadjustments; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.Test; +import org.prebid.server.VertxTest; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; +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.request.ImpMediaType; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; + +public class BidAdjustmentsRulesResolverTest extends VertxTest { + + private final BidAdjustmentsRulesResolver target = new BidAdjustmentsRulesResolver(jacksonMapper); + + @Test + public void resolveShouldPickAndApplyRulesBySpecificMediaType() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ] + } + }, + "*": { + "*": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("15", "EUR")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardMediaType() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "banner": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": "15", + "currency": "EUR" + } + ] + } + }, + "*": { + "*": { + "*": [ + { + "adjtype": "cpm", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.video_outstream, + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedCpm("25", "UAH")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidder() throws JsonProcessingException { + // given + final BidAdjustmentsRules givenBidAdjustments = BidAdjustmentsRules.of(Map.of( + "*|bidderName|*", List.of(expectedMultiplier("15")), + "*|*|*", List.of(expectedMultiplier("25")))); + + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardBidder() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "anotherBidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "UAH"), expectedMultiplier("25")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificDealId() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "cpm", + "value": "15", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ], + "*": [ + { + "adjtype": "cpm", + "value": "25", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedCpm("15", "JPY"), expectedStatic("15", "EUR")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealId() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "multiplier", + "value": "15" + }, + { + "adjtype": "cpm", + "value": "15", + "currency": "EUR" + } + ], + "*": [ + { + "adjtype": "multiplier", + "value": "25" + }, + { + "adjtype": "cpm", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "anotherDealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("25"), expectedCpm("25", "UAH")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "cpm", + "value": "15", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ], + "*": [ + { + "adjtype": "cpm", + "value": "25", + "currency": "JPY" + }, + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + null); + + // then + assertThat(actual).containsExactly(expectedCpm("25", "JPY"), expectedStatic("25", "UAH")); + } + + @Test + public void resolveShouldReturnEmptyListWhenNoMatchFound() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "*": { + "dealId": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + null); + + // then + assertThat(actual).isEmpty(); + } + + private static BidRequest givenBidRequest(String adjustmentsString) throws JsonProcessingException { + final ObjectNode adjustmetsNode = (ObjectNode) mapper.readTree(adjustmentsString); + return BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(adjustmetsNode).build())) + .build(); + } + + private static BidAdjustmentsRule expectedStatic(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule expectedCpm(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule expectedMultiplier(String value) { + return BidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolverTest.java similarity index 99% rename from src/test/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolverTest.java rename to src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolverTest.java index 0c0ab6b155f..9f78d514a3a 100644 --- a/src/test/java/org/prebid/server/auction/adjustment/FloorAdjustmentFactorResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentFactorResolverTest.java @@ -1,4 +1,4 @@ -package org.prebid.server.auction.adjustment; +package org.prebid.server.bidadjustments; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java new file mode 100644 index 00000000000..64f4fcaa982 --- /dev/null +++ b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java @@ -0,0 +1,242 @@ +package org.prebid.server.bidadjustments; + +import com.iab.openrtb.request.BidRequest; +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.bidadjustments.model.BidAdjustmentsRule; +import org.prebid.server.bidder.model.Price; +import org.prebid.server.currency.CurrencyConversionService; +import org.prebid.server.exception.PreBidException; +import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER; +import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.banner; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.video_outstream; + +@ExtendWith(MockitoExtension.class) +public class FloorAdjustmentsResolverTest extends VertxTest { + + private static final String BIDDER_NAME = "testBidder"; + private static final String USD = "USD"; + private static final String EUR = "EUR"; + private static final String UAH = "UAH"; + + @Mock(strictness = LENIENT) + private CurrencyConversionService currencyService; + + @Mock(strictness = LENIENT) + private BidAdjustmentsRulesResolver bidAdjustmentsRulesResolver; + + private FloorAdjustmentsResolver target; + + @BeforeEach + public void before() { + target = new FloorAdjustmentsResolver(bidAdjustmentsRulesResolver, currencyService); + + given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> { + final BigDecimal amount = invocation.getArgument(0); + final String fromCurrency = invocation.getArgument(2); + final String toCurrency = invocation.getArgument(3); + + if (fromCurrency.equals(toCurrency)) { + return amount; + } else if (toCurrency.equals(USD)) { + return amount.divide(BigDecimal.TEN, 4, RoundingMode.HALF_EVEN); + } else if (fromCurrency.equals(USD)) { + return amount.multiply(BigDecimal.TEN); + } else { + return amount; + } + }); + } + + @Test + public void resolveShouldReturnInitialPriceWhenNoRulesFoundForAnyMediaType() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.TEN); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner, video_outstream); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + .willReturn(Collections.emptyList()); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME, null)) + .willReturn(Collections.emptyList()); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(initialPrice); + verify(currencyService, times(2)).convertCurrency(BigDecimal.TEN, bidRequest, USD, USD); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldApplyMultiplierRuleInReverse() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(20)); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule multiplierRule = givenMultiplier("2"); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + .willReturn(singletonList(multiplierRule)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("10.0000"))); + + verify(currencyService).convertCurrency(eq(new BigDecimal("10.0000")), eq(bidRequest), eq(USD), eq(USD)); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldApplyCpmRuleInReverse() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(50)); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule cpmRule = givenCpm("5", EUR); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + .willReturn(singletonList(cpmRule)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + final Price expectedPrice = Price.of(USD, new BigDecimal("50.5")); + assertThat(actual).isEqualTo(expectedPrice); + + verify(currencyService).convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD); + verify(currencyService).convertCurrency(new BigDecimal("50.5"), bidRequest, USD, USD); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldThrowExceptionWhenStaticRuleIsEncountered() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.TEN); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule staticRule = givenStatic("5", USD); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + .willReturn(singletonList(staticRule)); + + // when and then + assertThatThrownBy(() -> target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME)) + .isInstanceOf(PreBidException.class) + .hasMessage("STATIC type can't be applied to a floor price"); + + verifyNoInteractions(currencyService); + } + + @Test + public void resolveShouldApplyMultipleRulesInReverseOrder() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(100)); + final BidRequest bidRequest = givenBidRequest(USD); + final Set mediaTypes = Set.of(banner); + + final BidAdjustmentsRule rule1 = givenMultiplier("2"); + final BidAdjustmentsRule rule2 = givenCpm("5", EUR); + + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + .willReturn(List.of(rule1, rule2)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("50.2500"))); + + verify(currencyService).convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD); + verify(currencyService).convertCurrency(new BigDecimal("50.2500"), bidRequest, USD, USD); + verifyNoMoreInteractions(currencyService); + } + + @Test + public void resolveShouldChooseMinimalFloorAcrossMediaTypesAfterConversion() { + // given + final Price initialPrice = Price.of(USD, BigDecimal.valueOf(100)); + final BidRequest bidRequest = givenBidRequest(EUR); + final Set mediaTypes = Set.of(banner, video_outstream); + + final BidAdjustmentsRule bannerRule = givenMultiplier("4"); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + .willReturn(singletonList(bannerRule)); + + final BidAdjustmentsRule videoRule = givenCpm("500", UAH); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME, null)) + .willReturn(singletonList(videoRule)); + + // when + final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); + + // then + assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("25.0000"))); + + verify(currencyService).convertCurrency(new BigDecimal("25.0000"), bidRequest, USD, EUR); + verify(currencyService).convertCurrency(new BigDecimal("500"), bidRequest, UAH, USD); + verify(currencyService).convertCurrency(new BigDecimal("150"), bidRequest, USD, EUR); + verifyNoMoreInteractions(currencyService); + } + + private static BidRequest givenBidRequest(String currency) { + return BidRequest.builder() + .cur(singletonList(currency)) + .build(); + } + + private static BidAdjustmentsRule givenStatic(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(STATIC) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule givenCpm(String value, String currency) { + return BidAdjustmentsRule.builder() + .adjType(CPM) + .currency(currency) + .value(new BigDecimal(value)) + .build(); + } + + private static BidAdjustmentsRule givenMultiplier(String value) { + return BidAdjustmentsRule.builder() + .adjType(MULTIPLIER) + .value(new BigDecimal(value)) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java similarity index 67% rename from src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java rename to src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java index 6bc26d7ef1a..45de1a0c64c 100644 --- a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java @@ -1,8 +1,6 @@ package org.prebid.server.bidadjustments.model; import org.junit.jupiter.api.Test; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments; -import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule; import java.math.BigDecimal; import java.util.List; @@ -11,18 +9,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; -public class BidAdjustmentsTest { +public class BidAdjustmentsRulesTest { @Test public void shouldBuildRulesSet() { // given - final List givenRules = List.of(givenRule("1"), givenRule("2")); - final Map>> givenRulesMap = Map.of( + final List givenRules = List.of(givenRule("1"), givenRule("2")); + final Map>> givenRulesMap = Map.of( "bidderName", Map.of("dealId", givenRules)); - final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder() - .mediatype(Map.of( + final BidAdjustments givenBidAdjustments = BidAdjustments.of( + Map.of( "audio", givenRulesMap, "native", givenRulesMap, "video-instream", givenRulesMap, @@ -34,14 +32,13 @@ public void shouldBuildRulesSet() { "*", Map.of("*", givenRules), "bidderName", Map.of( "*", givenRules, - "dealId", givenRules)))) - .build(); + "dealId", givenRules)))); // when - final BidAdjustments actual = BidAdjustments.of(givenBidAdjustments); + final BidAdjustmentsRules actual = BidAdjustmentsRules.of(givenBidAdjustments); // then - final BidAdjustments expected = BidAdjustments.of(Map.of( + final BidAdjustmentsRules expected = BidAdjustmentsRules.of(Map.of( "audio|bidderName|dealId", givenRules, "native|bidderName|dealId", givenRules, "video-instream|bidderName|dealId", givenRules, @@ -55,11 +52,12 @@ public void shouldBuildRulesSet() { } - private static ExtRequestBidAdjustmentsRule givenRule(String value) { - return ExtRequestBidAdjustmentsRule.builder() + private static BidAdjustmentsRule givenRule(String value) { + return BidAdjustmentsRule.builder() .adjType(CPM) .currency("USD") .value(new BigDecimal(value)) .build(); } + } diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java index 1aecc95bdf8..20d7308a271 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorAdjusterTest.java @@ -9,8 +9,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; -import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentFactorResolver; +import org.prebid.server.bidadjustments.FloorAdjustmentsResolver; import org.prebid.server.bidder.model.Price; +import org.prebid.server.exception.PreBidException; import org.prebid.server.floors.model.PriceFloorEnforcement; import org.prebid.server.floors.model.PriceFloorRules; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; @@ -25,15 +27,19 @@ import java.util.ArrayList; import java.util.EnumMap; import java.util.Map; +import java.util.Set; import java.util.function.UnaryOperator; 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.anySet; +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.verifyNoInteractions; +import static org.prebid.server.proto.openrtb.ext.request.ImpMediaType.video_instream; @ExtendWith(MockitoExtension.class) public class BasicPriceFloorAdjusterTest extends VertxTest { @@ -43,35 +49,48 @@ public class BasicPriceFloorAdjusterTest extends VertxTest { @Mock(strictness = LENIENT) private FloorAdjustmentFactorResolver floorAdjustmentFactorResolver; + @Mock(strictness = LENIENT) + private FloorAdjustmentsResolver floorAdjustmentsResolver; + private BasicPriceFloorAdjuster target; @BeforeEach public void setUp() { given(floorAdjustmentFactorResolver.resolve(anySet(), any(), any())).willReturn(BigDecimal.ONE); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); - target = new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver); + target = new BasicPriceFloorAdjuster(floorAdjustmentFactorResolver, floorAdjustmentsResolver); } @Test - public void adjustForImpShouldCallAdjustmentFactorResolverAndApplyFactor() { + public void adjustForImpShouldApplyAllAdjustments() { // given given(floorAdjustmentFactorResolver.resolve(anySet(), any(), any())).willReturn(new BigDecimal("0.1")); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willReturn(Price.of("UAH", new BigDecimal("117.00"))); + final BidRequest givenBidRequest = givenBidRequest(identity()); // when final Price adjustedBidPrice = target.adjustForImp( givenImp(identity()), RUBICON, - givenBidRequest(identity()), + givenBidRequest, null, new ArrayList<>()); // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", new BigDecimal(100))); - verify(floorAdjustmentFactorResolver).resolve(anySet(), any(), any()); + assertThat(adjustedBidPrice).isEqualTo(Price.of("UAH", new BigDecimal("117.00"))); + verify(floorAdjustmentFactorResolver).resolve(eq(Set.of(video_instream)), any(), eq(RUBICON)); + verify(floorAdjustmentsResolver).resolve( + eq(Price.of("USD", new BigDecimal(100))), + eq(givenBidRequest), + eq(Set.of(video_instream)), + eq(RUBICON)); } @Test - public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByAccount() { + public void adjustForImpShouldNotApplyAdjustmentsWhenAdjustmentDisabledByAccount() { // given final Account account = Account.builder() .auction(AccountAuctionConfig.builder() @@ -91,10 +110,11 @@ public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByAccount() { // then assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); + verifyNoInteractions(floorAdjustmentsResolver, floorAdjustmentFactorResolver); } @Test - public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByRequest() { + public void adjustForImpShouldNotApplyAdjustmentsWhenAdjustmentDisabledByRequest() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() @@ -120,13 +140,16 @@ public void adjustForImpShouldNotApplyFactorIfAdjustmentDisabledByRequest() { // then assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); + verifyNoInteractions(floorAdjustmentsResolver, floorAdjustmentFactorResolver); } @Test - public void adjustForImpShouldApplyNoAdjustmentsIfBidAdjustmentsFactorIsNotPresent() { + public void adjustForImpShouldApplyNoFactorAdjustmentsWhenBidAdjustmentsFactorIsNotPresent() { // given final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustmentfactors(null).build()))); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willReturn(Price.of("UAH", new BigDecimal("117.00"))); final Imp imp = givenImp(identity()); // when @@ -138,7 +161,13 @@ public void adjustForImpShouldApplyNoAdjustmentsIfBidAdjustmentsFactorIsNotPrese new ArrayList<>()); // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", imp.getBidfloor())); + assertThat(adjustedBidPrice).isEqualTo(Price.of("UAH", new BigDecimal("117.00"))); + verifyNoInteractions(floorAdjustmentFactorResolver); + verify(floorAdjustmentsResolver).resolve( + eq(Price.of(imp.getBidfloorcur(), imp.getBidfloor())), + eq(bidRequest), + eq(Set.of(video_instream)), + eq(RUBICON)); } @Test @@ -156,6 +185,7 @@ public void adjustForImpShouldReturnNullIfImpBidFloorIsNotPresent() { // then assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", null)); + verifyNoInteractions(floorAdjustmentFactorResolver, floorAdjustmentsResolver); } @Test @@ -247,188 +277,23 @@ public void adjustForImpShouldSkipMediaTypeIfNoMediaTypesOfImpFound() { } @Test - public void revertAdjustmentForImpShouldCallAdjustmentFactorResolverAndApplyFactor() { + public void adjustForImpShouldSkipBidAdjustmentsWhenResolverThrowsException() { // given given(floorAdjustmentFactorResolver.resolve(anySet(), any(), any())).willReturn(new BigDecimal("0.1")); + given(floorAdjustmentsResolver.resolve(any(), any(), anySet(), any())) + .willThrow(new PreBidException("Exception")); + final BidRequest givenBidRequest = givenBidRequest(identity()); // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - givenBidRequest(identity()), - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.ONE)); - verify(floorAdjustmentFactorResolver).resolve(anySet(), any(), any()); - } - - @Test - public void revertAdjustmentForImpShouldNotApplyFactorIfAdjustmentDisabledByAccount() { - // given - final Account account = Account.builder() - .auction(AccountAuctionConfig.builder() - .priceFloors(AccountPriceFloorsConfig.builder() - .adjustForBidAdjustment(false) - .build()) - .build()) - .build(); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - givenBidRequest(identity()), - account); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldNotApplyFactorIfAdjustmentDisabledByRequest() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(givenMediaTypes(Map.of( - ImpMediaType.video, - Map.of(RUBICON, BigDecimal.valueOf(0.85D))))) - .build()) - .floors(PriceFloorRules.builder() - .enforcement(PriceFloorEnforcement.builder() - .bidAdjustment(false) - .build()) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldApplyNoAdjustmentsIfBidAdjustmentsFactorIsNotPresent() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustmentfactors(null).build()))); - final Imp imp = givenImp(identity()); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - imp, - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", imp.getBidfloor())); - } - - @Test - public void revertAdjustmentForImpShouldReturnNullIfImpBidFloorIsNotPresent() { - // given - final Imp imp = givenImp(impBuilder -> impBuilder.bidfloor(null)); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - imp, - RUBICON, - givenBidRequest(identity()), - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", null)); - } - - @Test - public void revertAdjustmentForImpShouldReturnBidFloorNotFactoredByOtherBidder() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(givenMediaTypes(Map.of( - ImpMediaType.video, - Map.of("bidder", BigDecimal.valueOf(0.8D))))) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldReturnFactoredOfOneIfExtBidAdjustmentsFactorMediaTypesIsNull() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(null) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - givenImp(identity()), - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldReturnFactorOfOneIfNoMediaTypeInImpression() { - // given - final BidRequest bidRequest = givenBidRequest(identity()); - final Imp imp = givenImp(impBuilder -> impBuilder.video(null)); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( - imp, - RUBICON, - bidRequest, - null); - - // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); - } - - @Test - public void revertAdjustmentForImpShouldSkipMediaTypeIfNoMediaTypesOfImpFound() { - // given - final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> - bidRequestBuilder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .bidadjustmentfactors(ExtRequestBidAdjustmentFactors.builder() - .mediatypes(givenMediaTypes(Map.of( - ImpMediaType.video_outstream, - Map.of(RUBICON, BigDecimal.valueOf(0.8D))))) - .build()) - .build()))); - - // when - final Price adjustedBidPrice = target.revertAdjustmentForImp( + final Price adjustedBidPrice = target.adjustForImp( givenImp(identity()), RUBICON, - bidRequest, - null); + givenBidRequest, + null, + new ArrayList<>()); // then - assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", BigDecimal.TEN)); + assertThat(adjustedBidPrice).isEqualTo(Price.of("USD", new BigDecimal(100))); } private static BidRequest givenBidRequest(UnaryOperator requestCustomizer) { diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java index 724aef1f391..ad4fdbe0470 100644 --- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java +++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java @@ -33,6 +33,7 @@ import java.math.BigDecimal; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.UnaryOperator; import static java.util.Collections.emptyList; @@ -45,7 +46,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) @@ -55,8 +55,6 @@ public class BasicPriceFloorEnforcerTest { private BidRejectionTracker rejectionTracker; @Mock(strictness = LENIENT) private CurrencyConversionService currencyConversionService; - @Mock(strictness = LENIENT) - private PriceFloorAdjuster priceFloorAdjuster; @Mock private Metrics metrics; @@ -64,11 +62,10 @@ public class BasicPriceFloorEnforcerTest { @BeforeEach public void setUp() { - given(priceFloorAdjuster.revertAdjustmentForImp(any(), any(), any(), any())).willAnswer(invocation -> { - final Imp argument = invocation.getArgument(0); - return Price.of(argument.getBidfloorcur(), argument.getBidfloor()); - }); - priceFloorEnforcer = new BasicPriceFloorEnforcer(currencyConversionService, priceFloorAdjuster, metrics); + given(currencyConversionService.convertCurrency(any(), any(), any(), any())) + .willAnswer(invocation -> invocation.getArgument(0)); + + priceFloorEnforcer = new BasicPriceFloorEnforcer(currencyConversionService, metrics); } @Test @@ -320,33 +317,24 @@ public void shouldRejectBidsHavingPriceBelowFloor() { request.imp(givenImps(imp -> imp.id("impId1"), imp -> imp.id("impId2"))).cur(singletonList("USD"))); final BidRequest bidderBidRequest = givenBidRequest(request -> - request.imp(givenImps( - imp -> imp.id("impId1").bidfloor(new BigDecimal("0.11")).bidfloorcur("USD"), - imp -> imp.id("impId2").bidfloor(new BigDecimal("0.22")).bidfloorcur("USD")))); + request.imp(givenImps(imp -> imp.id("impId1"), imp -> imp.id("impId2")))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidderBidRequest, givenBidderSeatBid( bid -> bid.id("bidId1").impid("impId1").price(BigDecimal.ONE), - bid -> bid.id("bidId2").impid("impId2").price(BigDecimal.TEN))); + bid -> bid.id("bidId2").impid("impId2").price(BigDecimal.TEN)), + Map.of( + "impId1", Price.of("USD", new BigDecimal("1.1")), + "impId2", Price.of("USD", new BigDecimal("2.2")))); final Account account = givenAccount(identity()); - final String givenBidder = auctionParticipation.getBidderResponse().getBidder(); - final List givenImps = bidderBidRequest.getImp(); - - given(priceFloorAdjuster.revertAdjustmentForImp(givenImps.get(0), givenBidder, bidRequest, account)) - .willReturn(Price.of("USD", new BigDecimal("1.1"))); - given(priceFloorAdjuster.revertAdjustmentForImp(givenImps.get(1), givenBidder, bidRequest, account)) - .willReturn(Price.of("USD", new BigDecimal("2.2"))); - // when final AuctionParticipation result = priceFloorEnforcer.enforce( bidRequest, auctionParticipation, account, rejectionTracker); // then - verify(priceFloorAdjuster, times(2)).revertAdjustmentForImp(any(), any(), any(), any()); - final BidderBid rejectedBid = BidderBid.of( Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), null, null); verify(rejectionTracker).rejectBid(rejectedBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR); @@ -368,12 +356,13 @@ public void shouldRejectBidsHavingPriceBelowFloor() { public void shouldRejectBidsHavingPriceBelowFloorAndRequestEnforceFloorsRateIs100() { // given final BidRequest bidRequest = givenBidRequest( - request -> request.imp(givenImps(imp -> imp.bidfloor(BigDecimal.TEN))), + request -> request.imp(givenImps(identity())), enforcement -> enforcement.enforceRate(100)); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.ONE))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.ONE)), + Map.of("impId", Price.of("USD", BigDecimal.TEN))); final Account account = givenAccount(identity()); @@ -495,12 +484,13 @@ public void shouldRemainBidsEvenCurrencyConversionForFloorIsFailed() { .willThrow(new PreBidException("error")); final BidRequest bidRequest = givenBidRequest(request -> request - .imp(givenImps(imp -> imp.bidfloorcur("USD").bidfloor(BigDecimal.ONE))) + .imp(givenImps(identity())) .cur(singletonList("EUR"))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN)), + Map.of("impId", Price.of("USD", BigDecimal.ONE))); final Account account = givenAccount(identity()); @@ -528,12 +518,13 @@ public void shouldRemainBidsHavingPriceGreaterThenConvertedFloor() { given(currencyConversionService.convertCurrency(any(), any(), any(), any())).willReturn(BigDecimal.TEN); final BidRequest bidRequest = givenBidRequest(request -> request - .imp(givenImps(imp -> imp.bidfloorcur("USD").bidfloor(BigDecimal.ONE))) + .imp(givenImps(identity())) .cur(singletonList("EUR"))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN)), + Map.of("impId", Price.of("USD", BigDecimal.ONE))); final Account account = givenAccount(identity()); @@ -558,14 +549,15 @@ public void shouldRemainBidsHavingPriceGreaterThenConvertedFloorInAnotherCurrenc given(currencyConversionService.convertCurrency(any(), any(), any(), any())).willReturn(BigDecimal.TEN); final BidRequest bidRequest = givenBidRequest(request -> request - .imp(givenImps(imp -> imp.bidfloor(BigDecimal.ONE).bidfloorcur("JPY"))) + .imp(givenImps(identity())) .cur(singletonList("EUR"))); final BidRequest bidderRequest = givenBidRequest(request -> bidRequest.toBuilder().cur(singletonList("USD"))); final AuctionParticipation auctionParticipation = givenAuctionParticipation( bidderRequest, - givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN))); + givenBidderSeatBid(bid -> bid.price(BigDecimal.TEN)), + Map.of("impId", Price.of("JPY", BigDecimal.ONE))); final Account account = givenAccount(identity()); @@ -649,16 +641,24 @@ public void shouldRemainBidsHavingPriceGreaterThenConvertedCustomBidderFloorInAn .hasSize(1); } - private static AuctionParticipation givenAuctionParticipation(BidRequest bidRequest, BidderSeatBid bidderSeatBid) { + private static AuctionParticipation givenAuctionParticipation(BidRequest bidRequest, + BidderSeatBid bidderSeatBid, + Map priceFloors) { + return AuctionParticipation.builder() .bidderRequest(BidderRequest.builder() .bidder("bidder1") .bidRequest(bidRequest) + .originalPriceFloors(priceFloors) .build()) .bidderResponse(BidderResponse.of("bidder", bidderSeatBid, 0)) .build(); } + private static AuctionParticipation givenAuctionParticipation(BidRequest bidRequest, BidderSeatBid bidderSeatBid) { + return givenAuctionParticipation(bidRequest, bidderSeatBid, null); + } + private static Account givenAccount( UnaryOperator accountFloorsCustomizer) { diff --git a/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java b/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java index db15a1c8c1e..7dd8fdf575a 100644 --- a/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java +++ b/src/test/java/org/prebid/server/floors/NoSignalBidderPriceFloorAdjusterTest.java @@ -516,25 +516,6 @@ public void adjustForImpShouldCallDelegateWhenModelHasBidderSetAndTakesPrecenden verify(delegate).adjustForImp(givenImp, "bidder", givenBidRequest, givenAccount, debugWarnings); } - @Test - public void revertAdjustmentForImpShouldAlwaysAndOnlyCallDelegate() { - // given - final BidRequest givenBidRequest = BidRequest.builder().build(); - final Imp givenImp = givenImp(); - final Account givenAccount = Account.builder().build(); - - final Price expectedPrice = Price.of("EUR", BigDecimal.ONE); - - given(delegate.revertAdjustmentForImp(givenImp, "bidder", givenBidRequest, givenAccount)) - .willReturn(expectedPrice); - - // when - final Price actual = target.revertAdjustmentForImp(givenImp, "bidder", givenBidRequest, givenAccount); - - // then - assertThat(actual).isSameAs(expectedPrice); - } - private static BidRequest givenBidRequest(List modelGroupBidders, List dataBidders, List enforcementBidders) { From 5a5794124697d3acafec4055fbafc7e9073b6438 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 28 Apr 2025 10:33:13 +0200 Subject: [PATCH 2/8] Fix test names --- .../BidAdjustmentsResolverTest.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java index d0033f384af..8c81aa4adb7 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java @@ -49,7 +49,7 @@ public void before() { } @Test - public void resolveShouldPickAndApplyRulesBySpecificMediaType() { + public void resolveShouldApplyStaticRule() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) @@ -69,7 +69,7 @@ public void resolveShouldPickAndApplyRulesBySpecificMediaType() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardMediaType() { + public void resolveShouldApplyCpmRule() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, video_outstream, "bidderName", "dealId")) @@ -89,7 +89,7 @@ public void resolveShouldPickAndApplyRulesByWildcardMediaType() { } @Test - public void resolveShouldPickAndApplyRulesBySpecificBidder() { + public void resolveShouldApplyMultiplierRule() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) @@ -109,10 +109,10 @@ public void resolveShouldPickAndApplyRulesBySpecificBidder() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardBidder() { + public void resolveShouldApplyStaticAndCpmRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "anotherBidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) .willReturn(List.of(givenStatic("25", "UAH"), givenMultiplier("25"))); // when @@ -120,7 +120,7 @@ public void resolveShouldPickAndApplyRulesByWildcardBidder() { Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), banner, - "anotherBidderName", + "bidderName", "dealId"); // then @@ -129,7 +129,7 @@ public void resolveShouldPickAndApplyRulesByWildcardBidder() { } @Test - public void resolveShouldPickAndApplyRulesBySpecificDealId() { + public void resolveShouldApplyCpmAndStaticRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) @@ -149,10 +149,10 @@ public void resolveShouldPickAndApplyRulesBySpecificDealId() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardDealId() { + public void resolveShouldApplyMultiplierAdnCpmRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "anotherDealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) .willReturn(List.of(givenMultiplier("25"), givenCpm("25", "UAH"))); // when @@ -161,7 +161,7 @@ public void resolveShouldPickAndApplyRulesByWildcardDealId() { givenBidRequest, banner, "bidderName", - "anotherDealId"); + "dealId"); // then assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-225"))); @@ -169,7 +169,7 @@ public void resolveShouldPickAndApplyRulesByWildcardDealId() { } @Test - public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { + public void resolveShouldApplyTwoCpmRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", null)) @@ -190,7 +190,7 @@ public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() { } @Test - public void resolveShouldReturnEmptyListWhenNoMatchFound() { + public void resolveShouldNotApplyAnyRulesWhenNoMatchFound() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", null)) From b9b42bf480b93e221b79e5a9fcb928965aa12d35 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 28 Apr 2025 10:40:40 +0200 Subject: [PATCH 3/8] Remove dead code --- .../bidadjustments/BidAdjustmentsRulesResolverTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java index 6b6b440ddd9..1b35b34a273 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java @@ -6,14 +6,12 @@ import org.junit.jupiter.api.Test; import org.prebid.server.VertxTest; import org.prebid.server.bidadjustments.model.BidAdjustmentsRule; -import org.prebid.server.bidadjustments.model.BidAdjustmentsRules; 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.request.ImpMediaType; import java.math.BigDecimal; import java.util.List; -import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM; @@ -113,10 +111,6 @@ public void resolveShouldPickAndApplyRulesByWildcardMediaType() throws JsonProce @Test public void resolveShouldPickAndApplyRulesBySpecificBidder() throws JsonProcessingException { // given - final BidAdjustmentsRules givenBidAdjustments = BidAdjustmentsRules.of(Map.of( - "*|bidderName|*", List.of(expectedMultiplier("15")), - "*|*|*", List.of(expectedMultiplier("25")))); - final String givenAdjustments = """ { "mediatype": { From 2a3d7e8cd210431b0a702da21cead141507b39e6 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Fri, 30 May 2025 22:38:59 +0300 Subject: [PATCH 4/8] Tests: adjust floors for bidadjustments (#3868) --- .../request/auction/BidAdjustmentRule.groovy | 1 + .../model/request/auction/BidRequest.groovy | 5 + .../model/response/auction/Bid.groovy | 30 + .../response/auction/BidMediaType.groovy | 13 + .../server/functional/tests/BaseSpec.groovy | 6 + .../functional/tests/BidAdjustmentSpec.groovy | 234 +++- .../PriceFloorsAdjustmentSpec.groovy | 1248 +++++++++++++++++ .../pricefloors/PriceFloorsBaseSpec.groovy | 21 +- .../PriceFloorsCurrencySpec.groovy | 18 +- 9 files changed, 1522 insertions(+), 54 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy index 4fcfc1125e1..cdffc92e7e5 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy @@ -12,5 +12,6 @@ class BidAdjustmentRule { @JsonProperty('*') Map> wildcardBidder Map> generic + Map> openx Map> alias } diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy index aa9da45a4b6..26e9ddc2057 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.response.auction.MediaType import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP @@ -57,6 +58,10 @@ class BidRequest { getDefaultRequest(channel, Imp.getDefaultImpression(AUDIO)) } + static BidRequest getDefaultBidRequest(MediaType mediaType, DistributionChannel channel = SITE) { + getDefaultRequest(channel, Imp.getDefaultImpression(mediaType)) + } + static BidRequest getDefaultStoredRequest() { getDefaultBidRequest().tap { site = null diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy index 127ed32bfd9..353f5516935 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/Bid.groovy @@ -8,6 +8,8 @@ import org.prebid.server.functional.model.request.auction.Imp import org.prebid.server.functional.util.ObjectMapperWrapper import org.prebid.server.functional.util.PBSUtils +import static groovy.lang.Closure.DELEGATE_FIRST + @ToString(includeNames = true, ignoreNulls = true) @EqualsAndHashCode class Bid implements ObjectMapperWrapper { @@ -70,6 +72,23 @@ class Bid implements ObjectMapperWrapper { } } + static List getDefaultMultyTypesBids(Imp imp, @DelegatesTo(Bid) Closure commonInit = null) { + List bids = [] + if (imp.banner) bids << createBid(imp, BidMediaType.BANNER) { adm = null } + if (imp.video) bids << createBid(imp, BidMediaType.VIDEO) + if (imp.nativeObj) bids << createBid(imp, BidMediaType.NATIVE) + if (imp.audio) bids << createBid(imp, BidMediaType.AUDIO) { adm = null } + + if (commonInit) { + bids.each { bid -> + commonInit.delegate = bid + commonInit.resolveStrategy = DELEGATE_FIRST + commonInit() + } + } + bids + } + void setAdm(Object adm) { if (adm instanceof Adm) { this.adm = encode(adm) @@ -79,4 +98,15 @@ class Bid implements ObjectMapperWrapper { this.adm = null } } + + private static Bid createBid(Imp imp, BidMediaType type, @DelegatesTo(Bid) Closure init = null) { + def bid = getDefaultBid(imp) + bid.mediaType = type + if (init) { + init.delegate = bid + init.resolveStrategy = DELEGATE_FIRST + init() + } + bid + } } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy index 76aa2a558f9..84b5a5d9953 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidMediaType.groovy @@ -1,6 +1,7 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.annotation.JsonValue +import org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType enum BidMediaType { @@ -15,4 +16,16 @@ enum BidMediaType { BidMediaType(Integer value) { this.value = value } + + static BidMediaType from(BidAdjustmentMediaType mediaType) { + return switch (mediaType) { + case BidAdjustmentMediaType.BANNER -> BANNER + case BidAdjustmentMediaType.VIDEO -> VIDEO + case BidAdjustmentMediaType.VIDEO_IN_STREAM -> VIDEO + case BidAdjustmentMediaType.VIDEO_OUT_STREAM -> VIDEO + case BidAdjustmentMediaType.AUDIO -> AUDIO + case BidAdjustmentMediaType.NATIVE -> NATIVE + default -> throw new IllegalArgumentException("Unknown media type: " + mediaType); + }; + } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy index a4fda13f829..2c329468a7c 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BaseSpec.groovy @@ -2,6 +2,8 @@ package org.prebid.server.functional.tests import org.prebid.server.functional.model.bidderspecific.BidderRequest import org.prebid.server.functional.model.response.amp.AmpResponse +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidMediaType import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.BidderCall import org.prebid.server.functional.repository.HibernateRepositoryService @@ -91,6 +93,10 @@ abstract class BaseSpec extends Specification implements ObjectMapperWrapper { } } + protected static List getMediaTypedBids(BidResponse bidResponse, BidMediaType mediaType) { + bidResponse.seatbid*.bid.collectMany { it }.findAll { it.mediaType == mediaType } + } + protected static Map> getRequests(AmpResponse ampResponse) { ampResponse.ext.debug.bidders.collectEntries { bidderName, bidderCalls -> collectRequestByBidderName(bidderName, bidderCalls) diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 8effa51b231..0a016703245 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -79,6 +79,10 @@ class BidAdjustmentSpec extends BaseSpec { "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig + AMX_CONFIG) + def cleanupSpec() { + pbsServiceFactory.removeContainer(externalCurrencyConverterConfig + AMX_CONFIG) + } + def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment" def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { @@ -199,11 +203,95 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS should adjust bid price for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.randomPrice + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + MULTIPLIER | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + CPM | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | BidRequest.defaultBidRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM) + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | BidRequest.defaultAudioRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | BidRequest.defaultNativeRequest + STATIC | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | BidRequest.defaultBidRequest + } + + def "PBS should adjust bid price for matching bidder and left original bidderRequest with null floors when request has bidAdjustments config"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = null + bidRequest.imp.first.bidFloorCur = currency and: "Default bid response" def originalPrice = PBSUtils.randomPrice @@ -226,9 +314,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain default currency" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [null] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -279,10 +369,18 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def dealId = PBSUtils.randomString def currency = USD + def firstImpPrice = PBSUtils.randomPrice + def secondImpPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) - bidRequest.imp.add(Imp.defaultImpression) bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) and: "Default bid response" def originalPrice = PBSUtils.randomPrice @@ -297,7 +395,6 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted for big with dealId" - response.seatbid.first.bid.find { it.dealid == dealId } assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] and: "Price shouldn't be updated for bid with different dealId" @@ -311,9 +408,11 @@ class BidAdjustmentSpec extends BaseSpec { assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [firstImpPrice, secondImpPrice].sort() where: adjustmentType | ruleValue | mediaType | bidRequest @@ -361,9 +460,14 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS should adjust bid price for matching bidder when account config has bidAdjustments"() { - given: "Default bid response" - def originalPrice = PBSUtils.randomPrice + given: "BidRequest with floors" + def impPrice = PBSUtils.randomPrice def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = currency seatbid.first.bid.first.price = originalPrice @@ -389,9 +493,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -439,8 +545,13 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { - given: "Default BidRequest with ext.prebid.bidAdjustments" + given: "BidRequest with floors" + def impPrice = PBSUtils.randomPrice def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default BidRequest with ext.prebid.bidAdjustments" def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) bidRequest.cur = [currency] @@ -472,9 +583,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -524,11 +637,14 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS should prioritize exact bid price adjustment for matching bidder when request has exact and general bidAdjustment"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def exactRulePrice = PBSUtils.randomPrice + def impPrice = PBSUtils.randomPrice def currency = USD def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) } @@ -553,19 +669,24 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] } def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: PBSUtils.randomPrice, currency: currency) def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: PBSUtils.randomPrice, currency: currency) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) } @@ -592,9 +713,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: firstRuleType | secondRuleType @@ -613,8 +736,12 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.randomPrice, currency: GBP) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.randomPrice def bidRequest = BidRequest.defaultBidRequest.tap { cur = [EUR] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) } @@ -632,7 +759,7 @@ class BidAdjustmentSpec extends BaseSpec { then: "Final bid price should be adjusted" def convertedAdjustment = convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) - assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, bidRequest.cur.first) + assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, currency) and: "Original bid price and currency should be presented in bid.ext" verifyAll(response.seatbid.first.bid.first.ext) { @@ -640,21 +767,27 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert bidderRequest.cur == bidRequest.cur + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] } def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: GBP) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.randomPrice def bidRequest = BidRequest.defaultBidRequest.tap { - cur = [EUR] + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) } - and: "Default bid response with JPY currency" + and: "Default bid response with USD currency" def originalPrice = PBSUtils.randomPrice def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = USD @@ -666,7 +799,7 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted and converted to original request cur" - assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidRequest.cur.first) + assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, currency) assert response.cur == bidRequest.cur.first and: "Original bid price and currency should be presented in bid.ext" @@ -675,19 +808,24 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) - assert bidderRequest.cur == bidRequest.cur + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] } def "PBS should apply bidAdjustments after bidAdjustmentFactors when both are present"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def bidAdjustmentFactorsPrice = PBSUtils.randomPrice def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency) def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) } @@ -714,9 +852,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [MULTIPLIER, CPM, STATIC] @@ -728,9 +868,12 @@ class BidAdjustmentSpec extends BaseSpec { and: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency and: "Default bid response" def originalPrice = PBSUtils.randomPrice @@ -763,9 +906,11 @@ class BidAdjustmentSpec extends BaseSpec { def logs = pbsService.getLogsByTime(startTime) assert getLogsByText(logs, errorMessage) - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType | ruleValue | mediaType | bidRequest @@ -854,9 +999,12 @@ class BidAdjustmentSpec extends BaseSpec { def "PBS shouldn't adjust bid price for matching bidder when request has different bidder name in bidAdjustments config"() { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -884,9 +1032,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [MULTIPLIER, CPM, STATIC] @@ -898,10 +1048,13 @@ class BidAdjustmentSpec extends BaseSpec { and: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def adjustmentPrice = PBSUtils.randomPrice.toDouble() def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -936,9 +1089,11 @@ class BidAdjustmentSpec extends BaseSpec { def logs = pbsService.getLogsByTime(startTime) assert getLogsByText(logs, errorMessage) - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [CPM, STATIC] @@ -948,9 +1103,12 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def adjustmentPrice = PBSUtils.randomPrice def currency = USD + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) } @@ -978,9 +1136,11 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [MULTIPLIER, CPM, STATIC] @@ -992,10 +1152,13 @@ class BidAdjustmentSpec extends BaseSpec { and: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD + def impPrice = PBSUtils.randomPrice def adjustmentPrice = PBSUtils.randomPrice.toDouble() def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -1039,9 +1202,14 @@ class BidAdjustmentSpec extends BaseSpec { given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def adjustmentPrice = PBSUtils.randomPrice + def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) def bidRequest = BidRequest.defaultBidRequest.tap { cur = [currency] + imp.first.tap { + bidFloor = impPrice + bidFloorCur = currency + } ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) } @@ -1075,14 +1243,16 @@ class BidAdjustmentSpec extends BaseSpec { origbidcur == bidResponse.cur } - and: "Bidder request should contain currency from request" + and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] where: adjustmentType << [CPM, STATIC] } - + def "PBS should adjust bid price for matching bidder and alternate bidder code when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment and amx bidder" def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { @@ -1248,19 +1418,11 @@ class BidAdjustmentSpec extends BaseSpec { } private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { - BidRequest.defaultVideoRequest.tap { - imp.first.video.tap { - placement = videoPlacementSubtypes - } - } + getDefaultVideoRequestWithPlcmtAndPlacement(null, videoPlacementSubtypes) } private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { - BidRequest.defaultVideoRequest.tap { - imp.first.video.tap { - plcmt = videoPlcmtSubtype - } - } + getDefaultVideoRequestWithPlcmtAndPlacement(videoPlcmtSubtype, null) } private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy new file mode 100644 index 00000000000..cf0b7bac7d9 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy @@ -0,0 +1,1248 @@ +package org.prebid.server.functional.tests.pricefloors + +import org.prebid.server.functional.model.bidder.Openx +import org.prebid.server.functional.model.config.AccountAuctionConfig +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.pricefloors.PriceFloorField +import org.prebid.server.functional.model.pricefloors.Rule +import org.prebid.server.functional.model.request.auction.AdjustmentRule +import org.prebid.server.functional.model.request.auction.AdjustmentType +import org.prebid.server.functional.model.request.auction.Banner +import org.prebid.server.functional.model.request.auction.BidAdjustment +import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors +import org.prebid.server.functional.model.request.auction.BidAdjustmentRule +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.DistributionChannel +import org.prebid.server.functional.model.request.auction.ExtPrebidFloors +import org.prebid.server.functional.model.request.auction.Imp +import org.prebid.server.functional.model.request.auction.MultiBid +import org.prebid.server.functional.model.request.auction.Native +import org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes +import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype +import org.prebid.server.functional.model.response.auction.Bid +import org.prebid.server.functional.model.response.auction.BidExt +import org.prebid.server.functional.model.response.auction.BidMediaType +import org.prebid.server.functional.model.response.auction.BidResponse +import org.prebid.server.functional.model.response.auction.MediaType +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import java.time.Instant + +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.GENERIC +import static org.prebid.server.functional.model.bidder.BidderName.OPENX +import static org.prebid.server.functional.model.request.auction.AdjustmentType.CPM +import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER +import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM +import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM +import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE +import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM +import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM +import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID +import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer + +class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { + + private static final Integer MIN_ADJUST_VALUE = 0 + private static final Integer MAX_MULTIPLIER_ADJUST_VALUE = 99 + private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) + private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) + private static final Integer MAX_CPM_ADJUST_VALUE = 5 + private static final Integer MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE + private static final String WILDCARD = '*' + + private static final Map PBS_CONFIG = CURRENCY_CONVERTER_CONFIG + + FLOORS_CONFIG + + GENERIC_ALIAS_CONFIG + + ["adapters.openx.enabled" : "true", + "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + + ["adapter-defaults.ortb.multiformat-supported": "true"] + private static final PrebidServerService pbsService = pbsServiceFactory.getService(PBS_CONFIG) + + def cleanupSpec() { + pbsServiceFactory.removeContainer(PBS_CONFIG) + } + + def "PBS should reverse imp.floors for matching bidder when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.getRandomDecimal(impPrice) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain reversed floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should left original bidderRequest with null floors when request has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = null + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [null] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should reverse imp.floors for matching bidder when request with multiple imps has specific bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def firstImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def secondImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = PBSUtils.randomString + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.impid == bidRequest.imp.first.id }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.impid == bidRequest.imp.last.id }.price == [bidResponse.seatbid.first.bid.last.price] + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain reversed imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [getReverseAdjustedPrice(firstImpPrice, ruleValue as BigDecimal, adjustmentType), secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder with specific dealId when request with multiple imps has bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def dealId = PBSUtils.randomString + def currency = USD + def firstImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def secondImpPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = firstImpPrice + bidRequest.imp.first.bidFloorCur = currency + def secondImp = Imp.defaultImpression.tap { + bidFloor = secondImpPrice + bidFloorCur = currency + } + bidRequest.imp.add(secondImp) + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted for big with dealId" + assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)] + + and: "Price shouldn't be updated for bid with different dealId" + assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price + + and: "Response currency should stay the same" + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort() + assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur + assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency, currency] + assert bidderRequest.imp.bidFloor.sort() == [firstImpPrice, secondImpPrice].sort() + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should reverse imp.floors for matching bidder when account config has bidAdjustments"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Account in the DB with bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain reversed imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() { + given: "BidRequest with floors" + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + + and: "Account in the DB with bidAdjustments" + def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule)) + def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig)) + accountDao.save(account) + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to request config" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, ruleValue as BigDecimal, adjustmentType)] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | ANY | getBidRequestWithFloors(MediaType.BANNER) + } + + def "PBS should prioritize exact imp.floors reverser for matching bidder when request has exact and general bidAdjustment"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def exactRulePrice = PBSUtils.randomDecimal + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def currency = USD + def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, exactRulePrice, STATIC)] + } + + def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: firstRuleValue, currency: currency) + def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: secondRuleValue, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType) + def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType) + assert response.seatbid.first.bid.first.price == adjustedBidPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [applyReverseAdjustments(impPrice, [firstRule, secondRule])] + + where: + firstRuleType | secondRuleType | firstRuleValue | secondRuleValue + MULTIPLIER | CPM | PBSUtils.randomPrice | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + MULTIPLIER | STATIC | PBSUtils.randomPrice | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + MULTIPLIER | MULTIPLIER | PBSUtils.randomPrice | PBSUtils.randomPrice + CPM | CPM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, 1) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, 1) + CPM | STATIC | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + CPM | MULTIPLIER | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.randomPrice + STATIC | CPM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.randomPrice + STATIC | STATIC | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) + STATIC | MULTIPLIER | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE) | PBSUtils.randomPrice + } + + def "PBS should prioritize revert with lower resulting value for matching bidder when request has multiple media types"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def impPrice = PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + def currency = USD + def firstRule = new BidAdjustmentRule(openx: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: firstRulePrice, currency: currency)]]) + def secondRule = new BidAdjustmentRule(openx: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: secondRulePrice, currency: currency)]]) + def bidRequest = getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM).tap { + cur = [currency] + imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx + imp[0].ext.prebid.bidder.generic = null + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + imp.first.banner = Banner.getDefaultBanner() + imp.first.nativeObj = Native.getDefaultNative() + ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(primaryType): firstRule, (BANNER): secondRule]) + ext.prebid.multibid = [new MultiBid(bidder: OPENX, maxBids: 3)] + } + + and: "Default bid response" + def originalPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid = Bid.getDefaultMultyTypesBids(bidRequest.imp.first) { + price = originalPrice + ext = new BidExt() + } + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to first matched rule" + getMediaTypedBids(response, BidMediaType.from(primaryType)).price == [getAdjustedPrice(originalPrice, firstRulePrice, MULTIPLIER)] + getMediaTypedBids(response, BidMediaType.BANNER).price == [getAdjustedPrice(originalPrice, secondRulePrice, MULTIPLIER)] + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain revert imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, [firstRulePrice, secondRulePrice].max(), MULTIPLIER)] + + where: + primaryType | firstRulePrice | secondRulePrice + VIDEO_IN_STREAM | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + NATIVE | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + + VIDEO_IN_STREAM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + NATIVE | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + } + + def "PBS should convert CPM currency before adjustment when it different from original response currency"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE), currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Get currency rates" + def currencyRatesResponse = pbsService.sendCurrencyRatesRequest() + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def convertedAdjustment = getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur, currencyRatesResponse) + def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) + assert response.seatbid.first.bid.first.price == getPriceAfterCurrencyConversion(adjustedBidPrice, bidResponse.cur, currency, currencyRatesResponse) + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + def convertedReverseAdjustment = getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + def reversedAdjustBidPrice = getReverseAdjustedPrice(impPrice, convertedReverseAdjustment, adjustmentRule.adjustmentType) + assert bidderRequest.imp.bidFloor == [reversedAdjustBidPrice] + } + + def "PBS should change original currency when static bidAdjustments and original response have different currencies"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomDecimal, currency: GBP) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def currency = EUR + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + } + + and: "Default bid response with USD currency" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = USD + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + and: "Get currency rates" + def currencyRatesResponse = pbsService.sendCurrencyRatesRequest() + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted and converted to original request cur" + assert response.seatbid.first.bid.first.price == + getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + assert response.cur == bidRequest.cur.first + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + } + + def "PBS should apply bidAdjustments revert for imp.floors after bidAdjustmentFactors when both are present"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def bidAdjustmentFactorsPrice = PBSUtils.randomPrice + def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentValue, currency: currency) + def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule) + ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice]) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice + assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings or errors" + assert !response.ext.warnings + assert !response.ext.errors + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + def reversedBidPrice = impPrice / bidAdjustmentFactorsPrice + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(reversedBidPrice, adjustmentRule.value, adjustmentType)] + + where: + adjustmentType | impPrice | adjustmentValue + MULTIPLIER | PBSUtils.getRandomPrice() | PBSUtils.getRandomPrice() + CPM | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) + STATIC | PBSUtils.getRandomPrice(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE) | PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder when request has invalid value bidAdjustments config"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) + bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule) + bidRequest.cur = [currency] + bidRequest.imp.first.bidFloor = impPrice + bidRequest.imp.first.bidFloorCur = currency + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType | ruleValue | mediaType | bidRequest + MULTIPLIER | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + MULTIPLIER | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + + CPM | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + CPM | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + CPM | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + CPM | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + + STATIC | MIN_ADJUST_VALUE - 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MIN_ADJUST_VALUE - 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | MIN_ADJUST_VALUE - 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MIN_ADJUST_VALUE - 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | BANNER | getBidRequestWithFloors(MediaType.BANNER) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_IN_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | AUDIO | getBidRequestWithFloors(MediaType.AUDIO) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | NATIVE | getBidRequestWithFloors(MediaType.NATIVE) + STATIC | MAX_STATIC_ADJUST_VALUE + 1 | ANY | getBidRequestWithFloors(MediaType.NATIVE) + } + + def "PBS shouldn't reverse imp.floors for matching bidder when request has different bidder name in bidAdjustments config"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " + + "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown mediatype"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def adjustmentPrice = PBSUtils.randomPrice + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [impPrice] + + where: + adjustmentType << [MULTIPLIER, CPM, STATIC] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown adjustmentType"() { + given: "Start time" + def startTime = Instant.now() + + and: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def adjustmentPrice = PBSUtils.randomPrice.toDouble() + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.bidFloor = impPrice + imp.first.bidFloorCur = currency + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = impPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "PBS should ignore bidAdjustments for this request" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Should add a warning when in debug mode" + def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " + + "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String + assert response.ext.warnings[PREBID]?.code == [999] + assert response.ext.warnings[PREBID]?.message == [errorMessage] + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "PBS log should contain error" + def logs = pbsService.getLogsByTime(startTime) + assert getLogsByText(logs, errorMessage) + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + } + + def "PBS shouldn't reverse imp.floors for matching bidder when multiplier bidAdjustments doesn't have currency value"() { + given: "Default BidRequest with ext.prebid.bidAdjustments" + def currency = USD + def adjustmentPrice = PBSUtils.randomPrice + def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]]) + def bidRequest = getBidRequestWithFloors(MediaType.BANNER).tap { + cur = [currency] + imp.first.tap { + bidFloor = impPrice + bidFloorCur = currency + } + ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule) + } + + and: "Default bid response" + def originalPrice = PBSUtils.randomDecimal + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Response shouldn't contain any warnings" + assert !response.ext.warnings + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain original imp.floors" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + assert bidderRequest.imp.bidFloorCur == [currency] + assert bidderRequest.imp.bidFloor == [getReverseAdjustedPrice(impPrice, adjustmentPrice, MULTIPLIER)] + + where: + adjustmentType << [CPM, STATIC] + } + + private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) { + getDefaultVideoRequestWithPlcmtAndPlacement(null, videoPlacementSubtypes) + } + + private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) { + getDefaultVideoRequestWithPlcmtAndPlacement(videoPlcmtSubtype, null) + } + + private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype, + VideoPlacementSubtypes videoPlacementSubtypes) { + getBidRequestWithFloors(MediaType.VIDEO).tap { + imp.first.video.tap { + plcmt = videoPlcmtSubtype + placement = videoPlacementSubtypes + } + } + } + + private static BigDecimal getReverseAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice / adjustedValue, FLOOR_VALUE_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice + adjustedValue, FLOOR_VALUE_PRECISION) + case STATIC: + return PBSUtils.roundDecimal(originalPrice, FLOOR_VALUE_PRECISION) + default: + return adjustedValue + } + } + + private static BigDecimal applyReverseAdjustments(BigDecimal originalPrice, List rules) { + if (!rules || rules.any { it.adjustmentType == STATIC }) { + return originalPrice + } + def result = originalPrice + rules.reverseEach { + result = getReverseAdjustedPrice(result, it.value, it.adjustmentType) + } + result + } + + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, + BigDecimal adjustedValue, + AdjustmentType adjustmentType) { + switch (adjustmentType) { + case MULTIPLIER: + return PBSUtils.roundDecimal(originalPrice * adjustedValue, FLOOR_VALUE_PRECISION) + case CPM: + return PBSUtils.roundDecimal(originalPrice - adjustedValue, FLOOR_VALUE_PRECISION) + case STATIC: + return adjustedValue + default: + return originalPrice + } + } + + private static BidRequest getBidRequestWithFloors(MediaType type, + DistributionChannel channel = SITE) { + def floors = ExtPrebidFloors.extPrebidFloors.tap { + data.modelGroups.first.values = [(new Rule(channel: PBSUtils.randomString) + .getRule([PriceFloorField.CHANNEL])): PBSUtils.randomFloorValue] + } + BidRequest.getDefaultBidRequest(type, channel).tap { + ext.prebid.floors = floors + } + } +} diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index a604264b264..6edccfceddf 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -7,6 +7,7 @@ import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountPriceFloorsConfig import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType import org.prebid.server.functional.model.pricefloors.Rule @@ -20,12 +21,16 @@ import org.prebid.server.functional.model.request.auction.Prebid import org.prebid.server.functional.model.request.auction.Video import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.testcontainers.scaffolding.FloorsProvider import org.prebid.server.functional.tests.BaseSpec import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS @@ -59,9 +64,23 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { "Price floors processing failed: $reason. Following parsing of request price floors is failed: $details" } + protected static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, + (GBP): 0.793776804452961], + (GBP): [(USD): 1.2597999770088517, + (EUR): 1.1495574203931487], + (EUR): [(USD): 1.3429368029739777]] + protected static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { + setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) + } + protected static final Map CURRENCY_CONVERTER_CONFIG = ["auction.ad-server-currency" : "USD", + "currency-converter.external-rates.enabled" : "true", + "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), + "currency-converter.external-rates.default-timeout-ms": "4000", + "currency-converter.external-rates.refresh-period-ms" : "900000"] + + protected static final int FLOOR_VALUE_PRECISION = 4 private static final int DEFAULT_MODEL_WEIGHT = 1 private static final int CURRENCY_CONVERSION_PRECISION = 3 - private static final int FLOOR_VALUE_PRECISION = 4 protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index 69d8dce297d..f6b66721cb0 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -1,16 +1,13 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.config.AccountPriceFloorsConfig import org.prebid.server.functional.model.config.PriceFloorsFetch -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.pricefloors.PriceFloorData import org.prebid.server.functional.model.request.auction.ImpExtPrebidFloors import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService -import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.BOGUS @@ -22,24 +19,11 @@ import static org.prebid.server.functional.model.request.auction.FetchStatus.NON import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS import static org.prebid.server.functional.model.request.auction.Location.FETCH import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID -import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, - (GBP): 0.793776804452961], - (GBP): [(USD): 1.2597999770088517, - (EUR): 1.1495574203931487], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) - } private static final String GENERAL_ERROR_METRIC = "price-floors.general.err" - private static final Map CURRENCY_CONVERTER_CONFIG = ["auction.ad-server-currency" : "USD", - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] + private final PrebidServerService currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + CURRENCY_CONVERTER_CONFIG) From 10f4db3202c1c16bfb1557b5988bf22fa9e2fa32 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Mon, 2 Jun 2025 10:32:53 +0200 Subject: [PATCH 5/8] Fix comments --- .../BidAdjustmentsEnricher.java | 6 ++-- .../floors/BasicPriceFloorAdjuster.java | 30 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java index b0feb290e48..7ac48388c2e 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsEnricher.java @@ -90,8 +90,10 @@ private Optional convertAndValidate(JsonNode bidAdjustmentsNode, private ExtRequest updateExtRequestWithBidAdjustments(BidRequest bidRequest, JsonNode bidAdjustments) { final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid prebid = extRequest != null ? extRequest.getPrebid() : null; - final ExtRequestPrebid updatedPrebid = (prebid != null ? prebid.toBuilder() : ExtRequestPrebid.builder()) + final ExtRequestPrebid updatedPrebid = Optional.ofNullable(extRequest) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::toBuilder) + .orElse(ExtRequestPrebid.builder()) .bidadjustments((ObjectNode) bidAdjustments) .build(); diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java index cf919abb368..d943c6b92e9 100644 --- a/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java +++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorAdjuster.java @@ -59,17 +59,7 @@ public Price adjustForImp(Imp imp, } final Set mediaTypes = retrieveImpMediaTypes(imp); - - Price adjustedBidFloor = Price.of(imp.getBidfloorcur(), impBidFloor); - if (bidAdjustmentFactors != null) { - final BigDecimal factor = floorAdjustmentFactorResolver.resolve(mediaTypes, bidAdjustmentFactors, bidder); - - final BigDecimal adjustedBidFloorValue = factor != null && factor.compareTo(BigDecimal.ONE) != 0 - ? BidderUtil.roundFloor(DIVIDE_FUNCTION.apply(impBidFloor, factor)) - : impBidFloor; - - adjustedBidFloor = Price.of(imp.getBidfloorcur(), adjustedBidFloorValue); - } + final Price adjustedBidFloor = adjustPrice(imp, bidder, impBidFloor, bidAdjustmentFactors, mediaTypes); try { return floorAdjustmentsResolver.resolve(adjustedBidFloor, bidRequest, mediaTypes, bidder); @@ -78,6 +68,24 @@ public Price adjustForImp(Imp imp, } } + private Price adjustPrice(Imp imp, + String bidder, + BigDecimal impBidFloor, + ExtRequestBidAdjustmentFactors bidAdjustmentFactors, + Set mediaTypes) { + + if (bidAdjustmentFactors == null) { + return Price.of(imp.getBidfloorcur(), impBidFloor); + } + + final BigDecimal factor = floorAdjustmentFactorResolver.resolve(mediaTypes, bidAdjustmentFactors, bidder); + final BigDecimal adjustedBidFloorValue = factor != null && factor.compareTo(BigDecimal.ONE) != 0 + ? BidderUtil.roundFloor(DIVIDE_FUNCTION.apply(impBidFloor, factor)) + : impBidFloor; + + return Price.of(imp.getBidfloorcur(), adjustedBidFloorValue); + } + private static ExtRequestBidAdjustmentFactors extractBidAdjustmentFactors(BidRequest bidRequest) { return Optional.ofNullable(bidRequest.getExt()) .map(ExtRequest::getPrebid) From b06f248476311580de7a58e963c16891cd392a99 Mon Sep 17 00:00:00 2001 From: osulzhenko <125548596+osulzhenko@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:20:01 +0300 Subject: [PATCH 6/8] Functional tests/currency mock (#3982) --- .../service/PrebidServerService.groovy | 8 +- .../testcontainers/PbsConfig.groovy | 11 ++- .../scaffolding/CurrencyConversion.groovy | 4 +- .../functional/tests/BidAdjustmentSpec.groovy | 97 ++++--------------- .../functional/tests/CurrencySpec.groovy | 69 +++---------- .../tests/StoredResponseSpec.groovy | 12 ++- .../PriceFloorsAdjustmentSpec.groovy | 46 ++++----- .../pricefloors/PriceFloorsBaseSpec.groovy | 35 +------ .../PriceFloorsCurrencySpec.groovy | 57 +++++++++-- .../PriceFloorsSignalingSpec.groovy | 5 + .../functional/util/CurrencyUtil.groovy | 74 ++++++++++++++ 11 files changed, 211 insertions(+), 207 deletions(-) create mode 100644 src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy diff --git a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy index 31df1efc8d5..b9c173baa54 100644 --- a/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy +++ b/src/test/groovy/org/prebid/server/functional/service/PrebidServerService.groovy @@ -30,6 +30,7 @@ import org.prebid.server.functional.model.response.setuid.SetuidResponse import org.prebid.server.functional.model.response.status.StatusResponse import org.prebid.server.functional.testcontainers.container.PrebidServerContainer import org.prebid.server.functional.util.ObjectMapperWrapper +import org.prebid.server.functional.util.PBSUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -412,7 +413,12 @@ class PrebidServerService implements ObjectMapperWrapper { } Boolean isContainLogsByValue(String value) { - getPbsLogsByValue(value) != null + try { + PBSUtils.waitUntil({ getPbsLogsByValue(value) != null }) + true + } catch (IllegalStateException ignored) { + false + } } private String getPbsLogsByValue(String value) { diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy index 91ec927b1c2..052bcf2f69f 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/PbsConfig.groovy @@ -6,6 +6,7 @@ import org.testcontainers.containers.PostgreSQLContainer import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer import static org.prebid.server.functional.testcontainers.container.PrebidServerContainer.ADMIN_ENDPOINT_PASSWORD import static org.prebid.server.functional.testcontainers.container.PrebidServerContainer.ADMIN_ENDPOINT_USERNAME +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY final class PbsConfig { @@ -29,7 +30,7 @@ LIMIT 1 static final Map DEFAULT_ENV = [ "logging.sampling-rate" : "1.0", - "auction.ad-server-currency" : "USD", + "auction.ad-server-currency" : DEFAULT_CURRENCY.value, "auction.stored-requests-timeout-ms" : "1000", "metrics.prefix" : "prebid", "status-response" : "ok", @@ -136,5 +137,13 @@ LIMIT 1 "adapters.generic.aliases.adrino.meta-info.site-media-types" : ""] } + static Map getCurrencyConverterConfig() { + ["auction.ad-server-currency" : DEFAULT_CURRENCY.value, + "currency-converter.external-rates.enabled" : "true", + "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), + "currency-converter.external-rates.default-timeout-ms": "4000", + "currency-converter.external-rates.refresh-period-ms" : "900000"] + } + private PbsConfig() {} } diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy index 6f5b74bda61..6246f8c9f4d 100644 --- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy +++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/CurrencyConversion.groovy @@ -7,16 +7,18 @@ import org.testcontainers.containers.MockServerContainer import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.HttpStatusCode.OK_200 +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY_RATES class CurrencyConversion extends NetworkScaffolding { static final String CURRENCY_ENDPOINT_PATH = "/currency" + private static final CurrencyConversionRatesResponse DEFAULT_RATES_RESPONSE = CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES) CurrencyConversion(MockServerContainer mockServerContainer) { super(mockServerContainer, CURRENCY_ENDPOINT_PATH) } - void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse) { + void setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse conversionRatesResponse = DEFAULT_RATES_RESPONSE) { setResponse(request, conversionRatesResponse) } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 0a016703245..4a32f637e6f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -1,13 +1,12 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.Currency + import org.prebid.server.functional.model.bidder.Generic import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AlternateBidderCodes import org.prebid.server.functional.model.config.BidderConfig import org.prebid.server.functional.model.db.Account -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.request.auction.AdjustmentRule import org.prebid.server.functional.model.request.auction.AdjustmentType import org.prebid.server.functional.model.request.auction.Amx @@ -22,12 +21,11 @@ import org.prebid.server.functional.model.response.auction.BidExt import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerException import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils -import java.math.RoundingMode -import java.time.Instant - import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.GBP @@ -62,25 +60,21 @@ class BidAdjustmentSpec extends BaseSpec { private static final BigDecimal MAX_MULTIPLIER_ADJUST_VALUE = 99 private static final BigDecimal MAX_CPM_ADJUST_VALUE = Integer.MAX_VALUE private static final BigDecimal MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE - private static final Currency DEFAULT_CURRENCY = USD private static final int BID_ADJUST_PRECISION = 4 - private static final int PRICE_PRECISION = 3 private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM]) private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM]) - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, - (GBP): 0.793776804452961], - (GBP): [(USD): 1.2597999770088517, - (EUR): 1.1495574203931487], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) - } + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) private static final Map AMX_CONFIG = ["adapters.amx.enabled" : "true", "adapters.amx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] - private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig + AMX_CONFIG) + private static PrebidServerService pbsService + + def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() + pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig + AMX_CONFIG) + } def cleanupSpec() { - pbsServiceFactory.removeContainer(externalCurrencyConverterConfig + AMX_CONFIG) + pbsServiceFactory.removeContainer(PbsConfig.currencyConverterConfig + AMX_CONFIG) } def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() { @@ -757,9 +751,9 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - def convertedAdjustment = convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) + def convertedAdjustment = CurrencyUtil.convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur) def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) - assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, currency) + assert response.seatbid.first.bid.first.price == CurrencyUtil.convertCurrency(adjustedBidPrice, bidResponse.cur, currency) and: "Original bid price and currency should be presented in bid.ext" verifyAll(response.seatbid.first.bid.first.ext) { @@ -799,7 +793,7 @@ class BidAdjustmentSpec extends BaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted and converted to original request cur" - assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, currency) + assert response.seatbid.first.bid.first.price == CurrencyUtil.convertCurrency(adjustmentRule.value, adjustmentRule.currency, currency) assert response.cur == bidRequest.cur.first and: "Original bid price and currency should be presented in bid.ext" @@ -863,10 +857,7 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS shouldn't adjust bid price for matching bidder when request has invalid value bidAdjustments config"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def impPrice = PBSUtils.randomPrice def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) @@ -903,8 +894,7 @@ class BidAdjustmentSpec extends BaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -1043,10 +1033,7 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS shouldn't adjust bid price for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def impPrice = PBSUtils.randomPrice def adjustmentPrice = PBSUtils.randomPrice.toDouble() @@ -1086,8 +1073,7 @@ class BidAdjustmentSpec extends BaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -1147,10 +1133,7 @@ class BidAdjustmentSpec extends BaseSpec { } def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown adjustmentType"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def impPrice = PBSUtils.randomPrice def adjustmentPrice = PBSUtils.randomPrice.toDouble() @@ -1190,8 +1173,7 @@ class BidAdjustmentSpec extends BaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain currency from request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -1363,45 +1345,6 @@ class BidAdjustmentSpec extends BaseSpec { bidAdjustmentFactor << [0.9, 1.1] } - private static Map getExternalCurrencyConverterConfig() { - ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] - } - - private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { - return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) - } - - private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { - def conversionRate - if (fromCurrency == toCurrency) { - conversionRate = 1 - } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { - conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { - conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] - } else { - conversionRate = getCrossConversionRate(fromCurrency, toCurrency) - } - conversionRate - } - - private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { - for (Map rates : DEFAULT_CURRENCY_RATES.values()) { - def fromRate = rates?[fromCurrency] - def toRate = rates?[toCurrency] - - if (fromRate && toRate) { - return toRate / fromRate - } - } - - null - } - private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, BigDecimal adjustedValue, AdjustmentType adjustmentType) { diff --git a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy index df5bba70028..662f9423848 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/CurrencySpec.groovy @@ -1,13 +1,11 @@ package org.prebid.server.functional.tests -import org.prebid.server.functional.model.Currency -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.request.auction.BidRequest import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion - -import java.math.RoundingMode +import org.prebid.server.functional.util.CurrencyUtil import static org.prebid.server.functional.model.Currency.CAD import static org.prebid.server.functional.model.Currency.CHF @@ -16,21 +14,17 @@ import static org.prebid.server.functional.model.Currency.JPY import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer +import static org.prebid.server.functional.util.CurrencyUtil.DEFAULT_CURRENCY class CurrencySpec extends BaseSpec { - private static final Currency DEFAULT_CURRENCY = USD - private static final int PRICE_PRECISION = 3 - private static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(USD): 1, - (EUR): 0.9249838127832763, - (CHF): 0.9033391915641477, - (JPY): 151.1886041994265, - (CAD): 1.357136250115623], - (EUR): [(USD): 1.3429368029739777]] - private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) + private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) + private static PrebidServerService pbsService + + def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() + pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig) } - private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig) def "PBS should return currency rates"() { when: "PBS processes bidders params request" @@ -85,7 +79,7 @@ class CurrencySpec extends BaseSpec { then: "Auction response should contain bid in #requestCurrency currency" assert bidResponse.cur == requestCurrency def bidPrice = bidResponse.seatbid[0].bid[0].price - assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidPrice == CurrencyUtil.convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency @@ -109,7 +103,7 @@ class CurrencySpec extends BaseSpec { then: "Auction response should contain bid in #requestCurrency currency" assert bidResponse.cur == requestCurrency def bidPrice = bidResponse.seatbid[0].bid[0].price - assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidPrice == CurrencyUtil.convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency @@ -133,7 +127,7 @@ class CurrencySpec extends BaseSpec { then: "Auction response should contain bid in #requestCurrency currency" assert bidResponse.cur == requestCurrency def bidPrice = bidResponse.seatbid[0].bid[0].price - assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) + assert bidPrice == CurrencyUtil.convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency) assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency @@ -189,43 +183,4 @@ class CurrencySpec extends BaseSpec { and: "Bid response shouldn't contain warnings" assert !bidResponse.ext.warnings } - - private static Map getExternalCurrencyConverterConfig() { - ["auction.ad-server-currency" : DEFAULT_CURRENCY as String, - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] - } - - private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) { - return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) - } - - private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) { - def conversionRate - if (fromCurrency == toCurrency) { - conversionRate = 1 - } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { - conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] - } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { - conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] - } else { - conversionRate = getCrossConversionRate(fromCurrency, toCurrency) - } - conversionRate - } - - private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) { - for (Map rates : DEFAULT_CURRENCY_RATES.values()) { - def fromRate = rates?[fromCurrency] - def toRate = rates?[toCurrency] - - if (fromRate && toRate) { - return toRate / fromRate - } - } - - null - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy index 767c4b8e544..0b6f62923c7 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy @@ -372,10 +372,14 @@ class StoredResponseSpec extends BaseSpec { } } - private static final List convertToComparableSeatBid(List seatBid) { - seatBid*.tap { - it.bid*.ext = null - it.group = null + private static List convertToComparableSeatBid(List seatBids) { + seatBids*.tap { seatBid -> + seatBid.bid*.tap { bid -> + bid.ext = null + bid.price = bid.price.setScale(3) + } + seatBid.group = null } + seatBids } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy index cf0b7bac7d9..a1fa69e487e 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsAdjustmentSpec.groovy @@ -26,10 +26,10 @@ import org.prebid.server.functional.model.response.auction.BidMediaType import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.MediaType import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils -import java.time.Instant - import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.GBP import static org.prebid.server.functional.model.Currency.USD @@ -61,13 +61,17 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { private static final Integer MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE private static final String WILDCARD = '*' - private static final Map PBS_CONFIG = CURRENCY_CONVERTER_CONFIG + + private static final Map PBS_CONFIG = PbsConfig.currencyConverterConfig + FLOORS_CONFIG + GENERIC_ALIAS_CONFIG + ["adapters.openx.enabled" : "true", "adapters.openx.endpoint": "$networkServiceContainer.rootUri/auction".toString()] + ["adapter-defaults.ortb.multiformat-supported": "true"] - private static final PrebidServerService pbsService = pbsServiceFactory.getService(PBS_CONFIG) + private static PrebidServerService pbsService + + def setupSpec() { + pbsService = pbsServiceFactory.getService(PBS_CONFIG) + } def cleanupSpec() { pbsServiceFactory.removeContainer(PBS_CONFIG) @@ -629,7 +633,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } and: "Default bid response" - def originalPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) + def originalPrice = PBSUtils.getRandomDecimal() def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = currency seatbid.first.bid = Bid.getDefaultMultyTypesBids(bidRequest.imp.first) { @@ -695,9 +699,9 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { def response = pbsService.sendAuctionRequest(bidRequest) then: "Final bid price should be adjusted" - def convertedAdjustment = getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur, currencyRatesResponse) + def convertedAdjustment = CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur, currencyRatesResponse) def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType) - assert response.seatbid.first.bid.first.price == getPriceAfterCurrencyConversion(adjustedBidPrice, bidResponse.cur, currency, currencyRatesResponse) + assert response.seatbid.first.bid.first.price == CurrencyUtil.getPriceAfterCurrencyConversion(adjustedBidPrice, bidResponse.cur, currency, currencyRatesResponse) and: "Original bid price and currency should be presented in bid.ext" verifyAll(response.seatbid.first.bid.first.ext) { @@ -709,7 +713,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { def bidderRequest = bidder.getBidderRequest(bidRequest.id) assert bidderRequest.cur == [currency] assert bidderRequest.imp.bidFloorCur == [currency] - def convertedReverseAdjustment = getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + def convertedReverseAdjustment = CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) def reversedAdjustBidPrice = getReverseAdjustedPrice(impPrice, convertedReverseAdjustment, adjustmentRule.adjustmentType) assert bidderRequest.imp.bidFloor == [reversedAdjustBidPrice] } @@ -743,7 +747,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { then: "Final bid price should be adjusted and converted to original request cur" assert response.seatbid.first.bid.first.price == - getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) + CurrencyUtil.getPriceAfterCurrencyConversion(adjustmentRule.value, adjustmentRule.currency, currency, currencyRatesResponse) assert response.cur == bidRequest.cur.first and: "Original bid price and currency should be presented in bid.ext" @@ -814,10 +818,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } def "PBS shouldn't reverse imp.floors for matching bidder when request has invalid value bidAdjustments config"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]]) @@ -854,8 +855,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + assert pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -966,10 +966,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } def "PBS shouldn't reverse imp.floors for matching bidder when cpm or static bidAdjustments doesn't have currency value"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) def adjustmentPrice = PBSUtils.randomPrice.toDouble() @@ -1009,8 +1006,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain original imp.floors" def bidderRequest = bidder.getBidderRequest(bidRequest.id) @@ -1070,10 +1066,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } def "PBS shouldn't reverse imp.floors for matching bidder when bidAdjustments have unknown adjustmentType"() { - given: "Start time" - def startTime = Instant.now() - - and: "Default BidRequest with ext.prebid.bidAdjustments" + given: "Default BidRequest with ext.prebid.bidAdjustments" def currency = USD def impPrice = PBSUtils.getRandomPrice(MAX_CPM_ADJUST_VALUE) def adjustmentPrice = PBSUtils.randomPrice.toDouble() @@ -1113,8 +1106,7 @@ class PriceFloorsAdjustmentSpec extends PriceFloorsBaseSpec { } and: "PBS log should contain error" - def logs = pbsService.getLogsByTime(startTime) - assert getLogsByText(logs, errorMessage) + pbsService.isContainLogsByValue(errorMessage) and: "Bidder request should contain currency from request" def bidderRequest = bidder.getBidderRequest(bidRequest.id) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy index 6edccfceddf..95ef1318637 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy @@ -1,13 +1,11 @@ package org.prebid.server.functional.tests.pricefloors -import org.prebid.server.functional.model.Currency import org.prebid.server.functional.model.bidder.BidderName import org.prebid.server.functional.model.config.AccountAuctionConfig import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountPriceFloorsConfig import org.prebid.server.functional.model.config.PriceFloorsFetch import org.prebid.server.functional.model.db.Account -import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse import org.prebid.server.functional.model.pricefloors.Country import org.prebid.server.functional.model.pricefloors.MediaType import org.prebid.server.functional.model.pricefloors.Rule @@ -19,7 +17,6 @@ import org.prebid.server.functional.model.request.auction.ExtPrebidFloors import org.prebid.server.functional.model.request.auction.FetchStatus import org.prebid.server.functional.model.request.auction.Prebid import org.prebid.server.functional.model.request.auction.Video -import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion import org.prebid.server.functional.testcontainers.scaffolding.FloorsProvider @@ -28,9 +25,6 @@ import org.prebid.server.functional.util.PBSUtils import java.math.RoundingMode -import static org.prebid.server.functional.model.Currency.EUR -import static org.prebid.server.functional.model.Currency.GBP -import static org.prebid.server.functional.model.Currency.USD import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS @@ -64,27 +58,15 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { "Price floors processing failed: $reason. Following parsing of request price floors is failed: $details" } - protected static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626, - (GBP): 0.793776804452961], - (GBP): [(USD): 1.2597999770088517, - (EUR): 1.1495574203931487], - (EUR): [(USD): 1.3429368029739777]] - protected static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap { - setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES)) - } - protected static final Map CURRENCY_CONVERTER_CONFIG = ["auction.ad-server-currency" : "USD", - "currency-converter.external-rates.enabled" : "true", - "currency-converter.external-rates.url" : "$networkServiceContainer.rootUri/currency".toString(), - "currency-converter.external-rates.default-timeout-ms": "4000", - "currency-converter.external-rates.refresh-period-ms" : "900000"] + protected static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer) protected static final int FLOOR_VALUE_PRECISION = 4 private static final int DEFAULT_MODEL_WEIGHT = 1 - private static final int CURRENCY_CONVERSION_PRECISION = 3 protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG) def setupSpec() { + currencyConversion.setCurrencyConversionRatesResponse() floorsProvider.setResponse() } @@ -138,11 +120,6 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { PBSUtils.getRandomNumber(DEFAULT_MODEL_WEIGHT, MAX_MODEL_WEIGHT) } - static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { - def adjustedValue = floorValue / bidAdjustment - PBSUtils.roundDecimal(adjustedValue, FLOOR_VALUE_PRECISION) - } - static BidRequest getBidRequestWithMultipleMediaTypes() { BidRequest.defaultBidRequest.tap { imp[0].video = Video.defaultVideo } } @@ -177,12 +154,4 @@ abstract class PriceFloorsBaseSpec extends BaseSpec { protected BigDecimal getRoundedFloorValue(BigDecimal floorValue) { floorValue.setScale(FLOOR_VALUE_PRECISION, RoundingMode.HALF_EVEN) } - - protected BigDecimal getPriceAfterCurrencyConversion(BigDecimal value, - Currency currencyFrom, Currency currencyTo, - CurrencyRatesResponse currencyRatesResponse) { - def currencyRate = currencyRatesResponse.rates[currencyFrom.value][currencyTo.value] - def convertedValue = value * currencyRate - convertedValue.setScale(CURRENCY_CONVERSION_PRECISION, RoundingMode.HALF_EVEN) - } } diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy index f6b66721cb0..b569514d0c4 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy @@ -8,6 +8,8 @@ import org.prebid.server.functional.model.response.auction.Bid import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.ErrorType import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.testcontainers.PbsConfig +import org.prebid.server.functional.util.CurrencyUtil import org.prebid.server.functional.util.PBSUtils import static org.prebid.server.functional.model.Currency.BOGUS @@ -24,8 +26,15 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { private static final String GENERAL_ERROR_METRIC = "price-floors.general.err" - private final PrebidServerService currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + - CURRENCY_CONVERTER_CONFIG) + private static PrebidServerService currencyFloorsPbsService + + def setupSpec() { + currencyFloorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + PbsConfig.currencyConverterConfig) + } + + def cleanupSpec() { + pbsServiceFactory.removeContainer(FLOORS_CONFIG + PbsConfig.currencyConverterConfig) + } def "PBS should update bidFloor, bidFloorCur for signalling when request.cur is specified"() { given: "Default BidRequest with cur" @@ -37,6 +46,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response" def floorValue = PBSUtils.randomFloorValue def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -84,7 +97,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() and: "Bid response with 2 bids: price < floorMin, price = floorMin" - def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorValue, + def convertedMinFloorValue = CurrencyUtil.getPriceAfterCurrencyConversion(floorValue, floorsResponse.modelGroups[0].currency, bidRequest.cur[0], currencyRatesResponse) def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = EUR @@ -121,9 +134,13 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Get currency rates" def currencyRatesResponse = currencyFloorsPbsService.sendCurrencyRatesRequest() + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur, floorValur lower then floorMin" def floorProviderCur = EUR - def convertedMinFloorValue = getPriceAfterCurrencyConversion(floorMin, + def convertedMinFloorValue = CurrencyUtil.getPriceAfterCurrencyConversion(floorMin, bidRequest.ext.prebid.floors.floorMinCur, floorProviderCur, currencyRatesResponse) def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -168,6 +185,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = EUR def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -231,6 +252,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -267,6 +292,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -307,7 +336,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { and: "Bid response with 2 bids: price < floorMin, price = floorMin" def bidResponseCur = GBP - def convertedMinFloorValueGbp = getPriceAfterCurrencyConversion(floorValue, + def convertedMinFloorValueGbp = CurrencyUtil.getPriceAfterCurrencyConversion(floorValue, floorCur, bidResponseCur, currencyRatesResponse) def winBidPrice = convertedMinFloorValueGbp + 0.1 def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { @@ -329,7 +358,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { } and: "PBS should suppress bids lower than floorRuleValue" - def convertedFloorValueEur = getPriceAfterCurrencyConversion(winBidPrice, + def convertedFloorValueEur = CurrencyUtil.getPriceAfterCurrencyConversion(winBidPrice, bidResponseCur, requestCur, currencyRatesResponse) assert response.seatbid?.first()?.bid?.collect { it.price } == [convertedFloorValueEur] assert response.cur == bidRequest.cur[0] @@ -351,6 +380,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + and: "Set Floors Provider response with a currency different from the floorMinCur" def floorsProviderCur = BOGUS def floorsResponse = PriceFloorData.priceFloorData.tap { @@ -397,6 +430,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -454,6 +491,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) @@ -480,6 +521,10 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec { def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id) accountDao.save(account) + and: "Default bid response" + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest) + bidder.setResponse(bidRequest.id, bidResponse) + when: "PBS processes auction request" currencyFloorsPbsService.sendAuctionRequest(bidRequest) diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy index a885da9e86b..b06c2530242 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy @@ -18,6 +18,7 @@ import org.prebid.server.functional.model.response.auction.BidResponse import org.prebid.server.functional.model.response.auction.MediaType import org.prebid.server.functional.util.PBSUtils +import java.math.RoundingMode import java.time.Instant import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400 @@ -1145,4 +1146,8 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec { private static int getRuleSize(BidRequest bidRequest) { bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].values.size() } + + private static BigDecimal getAdjustedValue(BigDecimal floorValue, BigDecimal bidAdjustment) { + floorValue.divide(bidAdjustment, FLOOR_VALUE_PRECISION, RoundingMode.HALF_UP) + } } diff --git a/src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy b/src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy new file mode 100644 index 00000000000..172478ad5ba --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/util/CurrencyUtil.groovy @@ -0,0 +1,74 @@ +package org.prebid.server.functional.util + +import org.prebid.server.functional.model.Currency +import org.prebid.server.functional.model.response.currencyrates.CurrencyRatesResponse + +import java.math.RoundingMode + +import static org.prebid.server.functional.model.Currency.CAD +import static org.prebid.server.functional.model.Currency.CHF +import static org.prebid.server.functional.model.Currency.EUR +import static org.prebid.server.functional.model.Currency.GBP +import static org.prebid.server.functional.model.Currency.JPY +import static org.prebid.server.functional.model.Currency.USD + +class CurrencyUtil { + + public static final Currency DEFAULT_CURRENCY = USD + public static final Map> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9249838127832763, + (GBP): 0.793776804452961, + (EUR): 0.9249838127832763, + (CHF): 0.9033391915641477, + (JPY): 151.1886041994265, + (CAD): 1.357136250115623], + (GBP): [(USD): 1.2597999770088517, + (EUR): 1.1495574203931487], + (EUR): [(USD): 1.3429368029739777]] + public static final int PRICE_PRECISION = 3 + public static final int CURRENCY_CONVERSION_PRECISION = 3 + + static BigDecimal getPriceAfterCurrencyConversion(BigDecimal value, + Currency from, + Currency to, + CurrencyRatesResponse currencyRatesResponse) { + (value * currencyRatesResponse.rates[from.value][to.value]) + .setScale(CURRENCY_CONVERSION_PRECISION, RoundingMode.HALF_EVEN) + } + + static BigDecimal convertCurrency(BigDecimal price, + Currency fromCurrency, + Currency toCurrency, + Map> rates = DEFAULT_CURRENCY_RATES) { + return (price * getConversionRate(fromCurrency, toCurrency, rates)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN) + } + + private static BigDecimal getConversionRate(Currency fromCurrency, + Currency toCurrency, + Map> rates = DEFAULT_CURRENCY_RATES) { + def conversionRate + if (fromCurrency == toCurrency) { + conversionRate = 1 + } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) { + conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency] + } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) { + conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency] + } else { + conversionRate = getCrossConversionRate(fromCurrency, toCurrency, rates) + } + conversionRate + } + + private static BigDecimal getCrossConversionRate(Currency fromCurrency, + Currency toCurrency, + Map> rates) { + + for (Map rate : rates.values()) { + def fromRate = rate?[fromCurrency] + def toRate = rate?[toCurrency] + if (fromRate && toRate) { + return toRate / fromRate + } + } + null + } +} From 6a3ab7993989789d6a19c92f691477410a9bed58 Mon Sep 17 00:00:00 2001 From: antonbabak Date: Wed, 4 Jun 2025 11:56:10 +0200 Subject: [PATCH 7/8] Fix comments --- .../FloorAdjustmentsResolverTest.java | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java index 64f4fcaa982..14ea44e59dd 100644 --- a/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java @@ -14,7 +14,6 @@ import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Collections; import java.util.List; import java.util.Set; @@ -44,7 +43,7 @@ public class FloorAdjustmentsResolverTest extends VertxTest { private static final String EUR = "EUR"; private static final String UAH = "UAH"; - @Mock(strictness = LENIENT) + @Mock private CurrencyConversionService currencyService; @Mock(strictness = LENIENT) @@ -55,22 +54,6 @@ public class FloorAdjustmentsResolverTest extends VertxTest { @BeforeEach public void before() { target = new FloorAdjustmentsResolver(bidAdjustmentsRulesResolver, currencyService); - - given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> { - final BigDecimal amount = invocation.getArgument(0); - final String fromCurrency = invocation.getArgument(2); - final String toCurrency = invocation.getArgument(3); - - if (fromCurrency.equals(toCurrency)) { - return amount; - } else if (toCurrency.equals(USD)) { - return amount.divide(BigDecimal.TEN, 4, RoundingMode.HALF_EVEN); - } else if (fromCurrency.equals(USD)) { - return amount.multiply(BigDecimal.TEN); - } else { - return amount; - } - }); } @Test @@ -84,13 +67,14 @@ public void resolveShouldReturnInitialPriceWhenNoRulesFoundForAnyMediaType() { .willReturn(Collections.emptyList()); given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME, null)) .willReturn(Collections.emptyList()); + given(currencyService.convertCurrency(BigDecimal.TEN, bidRequest, USD, USD)).willReturn(BigDecimal.TEN); // when final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); // then assertThat(actual).isEqualTo(initialPrice); - verify(currencyService, times(2)).convertCurrency(BigDecimal.TEN, bidRequest, USD, USD); + verify(currencyService, times(2)).convertCurrency(any(), any(), any(), any()); verifyNoMoreInteractions(currencyService); } @@ -105,6 +89,8 @@ public void resolveShouldApplyMultiplierRuleInReverse() { given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) .willReturn(singletonList(multiplierRule)); + given(currencyService.convertCurrency(eq(new BigDecimal("10.0000")), eq(bidRequest), eq(USD), eq(USD))) + .willReturn(BigDecimal.valueOf(20)); // when final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); @@ -112,7 +98,7 @@ public void resolveShouldApplyMultiplierRuleInReverse() { // then assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("10.0000"))); - verify(currencyService).convertCurrency(eq(new BigDecimal("10.0000")), eq(bidRequest), eq(USD), eq(USD)); + verify(currencyService).convertCurrency(any(), any(), any(), any()); verifyNoMoreInteractions(currencyService); } @@ -127,6 +113,10 @@ public void resolveShouldApplyCpmRuleInReverse() { given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) .willReturn(singletonList(cpmRule)); + given(currencyService.convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD)) + .willReturn(BigDecimal.valueOf(0.5)); + given(currencyService.convertCurrency(new BigDecimal("50.5"), bidRequest, USD, USD)) + .willReturn(BigDecimal.valueOf(50)); // when final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); @@ -135,8 +125,7 @@ public void resolveShouldApplyCpmRuleInReverse() { final Price expectedPrice = Price.of(USD, new BigDecimal("50.5")); assertThat(actual).isEqualTo(expectedPrice); - verify(currencyService).convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD); - verify(currencyService).convertCurrency(new BigDecimal("50.5"), bidRequest, USD, USD); + verify(currencyService, times(2)).convertCurrency(any(), any(), any(), any()); verifyNoMoreInteractions(currencyService); } @@ -172,6 +161,10 @@ public void resolveShouldApplyMultipleRulesInReverseOrder() { given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) .willReturn(List.of(rule1, rule2)); + given(currencyService.convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD)) + .willReturn(BigDecimal.valueOf(0.5)); + given(currencyService.convertCurrency(new BigDecimal("50.2500"), bidRequest, USD, USD)) + .willReturn(BigDecimal.valueOf(100)); // when final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); @@ -179,8 +172,7 @@ public void resolveShouldApplyMultipleRulesInReverseOrder() { // then assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("50.2500"))); - verify(currencyService).convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD); - verify(currencyService).convertCurrency(new BigDecimal("50.2500"), bidRequest, USD, USD); + verify(currencyService, times(2)).convertCurrency(any(), any(), any(), any()); verifyNoMoreInteractions(currencyService); } @@ -199,15 +191,20 @@ public void resolveShouldChooseMinimalFloorAcrossMediaTypesAfterConversion() { given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME, null)) .willReturn(singletonList(videoRule)); + given(currencyService.convertCurrency(new BigDecimal("25.0000"), bidRequest, USD, EUR)) + .willReturn(new BigDecimal("250.0000")); + given(currencyService.convertCurrency(new BigDecimal("500"), bidRequest, UAH, USD)) + .willReturn(new BigDecimal("50")); + given(currencyService.convertCurrency(new BigDecimal("150"), bidRequest, USD, EUR)) + .willReturn(new BigDecimal("1500")); + // when final Price actual = target.resolve(initialPrice, bidRequest, mediaTypes, BIDDER_NAME); // then assertThat(actual).isEqualTo(Price.of(USD, new BigDecimal("25.0000"))); - verify(currencyService).convertCurrency(new BigDecimal("25.0000"), bidRequest, USD, EUR); - verify(currencyService).convertCurrency(new BigDecimal("500"), bidRequest, UAH, USD); - verify(currencyService).convertCurrency(new BigDecimal("150"), bidRequest, USD, EUR); + verify(currencyService, times(3)).convertCurrency(any(), any(), any(), any()); verifyNoMoreInteractions(currencyService); } From 950b608afee764ff199862a7459e87ec1b587496 Mon Sep 17 00:00:00 2001 From: Anton Babak <76536883+AntoxaAntoxic@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:29:55 +0200 Subject: [PATCH 8/8] Bid adjustments rules matching strategy (#3939) Co-authored-by: Markiyan Mykush <95693607+marki1an@users.noreply.github.com> Co-authored-by: markiian Co-authored-by: osulzhenko --- .../BidAdjustmentsProcessor.java | 3 + .../BidAdjustmentsResolver.java | 3 +- .../BidAdjustmentsRulesResolver.java | 25 +- .../FloorAdjustmentsResolver.java | 3 +- .../model/BidAdjustmentsRules.java | 4 +- .../impl/MostAccurateCombinationStrategy.java | 4 +- .../request/auction/BidAdjustmentRule.groovy | 8 +- .../functional/tests/BidAdjustmentSpec.groovy | 323 +++++++++++++++++- .../BidAdjustmentsProcessorTest.java | 24 +- .../BidAdjustmentsResolverTest.java | 24 +- .../BidAdjustmentsRulesResolverTest.java | 289 ++++++++++++++++ .../FloorAdjustmentsResolverTest.java | 22 +- .../model/BidAdjustmentsRulesTest.java | 6 +- 13 files changed, 691 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java index 5efec52a599..c161dcce193 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java @@ -106,6 +106,7 @@ private BidderBid applyBidAdjustments(BidderBid bidderBid, final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules( priceWithFactorsApplied, + bidderBid.getSeat(), bidder, bidRequest, mediaType, @@ -209,6 +210,7 @@ private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecim } private Price applyBidAdjustmentRules(Price bidPrice, + String seat, String bidder, BidRequest bidRequest, ImpMediaType mediaType, @@ -218,6 +220,7 @@ private Price applyBidAdjustmentRules(Price bidPrice, bidPrice, bidRequest, mediaType, + seat, bidder, dealId); } diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java index 45b1f83e607..46947c40e67 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java @@ -27,11 +27,12 @@ public BidAdjustmentsResolver(CurrencyConversionService currencyService, public Price resolve(Price initialPrice, BidRequest bidRequest, ImpMediaType targetMediaType, + String targetSeat, String targetBidder, String targetDealId) { final List rules = bidAdjustmentsRulesResolver.resolve( - bidRequest, targetMediaType, targetBidder, targetDealId); + bidRequest, targetMediaType, targetSeat, targetBidder, targetDealId); return adjustPrice(initialPrice, rules, bidRequest); } diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java index 748e5aab6d7..d133d997520 100644 --- a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolver.java @@ -38,13 +38,18 @@ public BidAdjustmentsRulesResolver(JacksonMapper mapper) { this.mapper = Objects.requireNonNull(mapper).mapper(); } + public List resolve(BidRequest bidRequest, ImpMediaType targetMediaType, String targetBidder) { + return resolve(bidRequest, targetMediaType, null, targetBidder, null); + } + public List resolve(BidRequest bidRequest, ImpMediaType targetMediaType, + String targetSeat, String targetBidder, String targetDealId) { final BidAdjustmentsRules bidAdjustments = BidAdjustmentsRules.of(extractBidAdjustments(bidRequest)); - return findRules(bidAdjustments, targetMediaType, targetBidder, targetDealId); + return findRules(bidAdjustments, targetMediaType, targetSeat, targetBidder, targetDealId); } private BidAdjustments extractBidAdjustments(BidRequest bidRequest) { @@ -57,22 +62,32 @@ private BidAdjustments extractBidAdjustments(BidRequest bidRequest) { private List findRules(BidAdjustmentsRules bidAdjustments, ImpMediaType targetMediaType, + String targetSeat, String targetBidder, String targetDealId) { final Map> rules = bidAdjustments.getRules(); final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet()); - final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId); + final PrebidConfigParameters parameters = createParameters( + targetMediaType, targetSeat, targetBidder, targetDealId); final String rule = matchingStrategy.match(source, parameters); return rule == null ? Collections.emptyList() : rules.get(rule); } - private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) { + private PrebidConfigParameters createParameters(ImpMediaType mediaType, + String seat, + String bidder, + String dealId) { + final List conditionsMatchers = List.of( SimpleDirectParameter.of(mediaType.toString()), - SimpleDirectParameter.of(bidder), - StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard()); + StringUtils.isBlank(seat) + ? SimpleDirectParameter.of(bidder) + : SimpleDirectParameter.of(List.of(seat, bidder)), + StringUtils.isBlank(dealId) + ? PrebidConfigParameter.wildcard() + : SimpleDirectParameter.of(dealId)); return SimpleParameters.of(conditionsMatchers); } diff --git a/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java index 79d1c4eea63..cb4485bfa35 100644 --- a/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java +++ b/src/main/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolver.java @@ -56,8 +56,7 @@ public Price resolve(Price initialBidFloorPrice, String targetBidder) { final List rules = bidAdjustmentsRulesResolver.resolve( - bidRequest, targetMediaType, targetBidder, null); - + bidRequest, targetMediaType, targetBidder); return reversePrice(initialBidFloorPrice, rules, bidRequest); } diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java index 1192a66ae9c..6d64ca1330e 100644 --- a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java +++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRules.java @@ -2,11 +2,11 @@ import lombok.Value; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator; import org.prebid.server.bidadjustments.BidAdjustmentsRulesResolver; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -23,7 +23,7 @@ public static BidAdjustmentsRules of(BidAdjustments bidAdjustments) { return BidAdjustmentsRules.of(Collections.emptyMap()); } - final Map> rules = new HashMap<>(); + final Map> rules = new CaseInsensitiveMap<>(); final Map>>> mediatypes = bidAdjustments.getRules(); diff --git a/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java b/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java index ea8a841df95..9cbcae877da 100644 --- a/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java +++ b/src/main/java/org/prebid/server/util/dsl/config/impl/MostAccurateCombinationStrategy.java @@ -14,10 +14,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.TreeSet; /** * Priority order for four column rule sets: @@ -174,7 +174,7 @@ private static List generateWildcardsIndices(Iterable toSet(Iterable iterable) { - return iterable instanceof Set set ? set : fill(new HashSet<>(), iterable); + return fill(new TreeSet<>(String.CASE_INSENSITIVE_ORDER), iterable); } private static > C fill(C destination, Iterable source) { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy index cdffc92e7e5..92af741601f 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy @@ -1,11 +1,8 @@ package org.prebid.server.functional.model.request.auction import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.databind.PropertyNamingStrategies -import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString -@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) @ToString(includeNames = true, ignoreNulls = true) class BidAdjustmentRule { @@ -14,4 +11,9 @@ class BidAdjustmentRule { Map> generic Map> openx Map> alias + @JsonProperty("ALIAS") + Map> aliasUpperCase + @JsonProperty("AlIaS") + Map> aliasCamelCase + Map> amx } diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy index 4a32f637e6f..7da1c89fffb 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy @@ -30,6 +30,7 @@ import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST import static org.prebid.server.functional.model.Currency.EUR import static org.prebid.server.functional.model.Currency.GBP import static org.prebid.server.functional.model.Currency.USD +import static org.prebid.server.functional.model.bidder.BidderName.ACUITYADS import static org.prebid.server.functional.model.bidder.BidderName.ALIAS import static org.prebid.server.functional.model.bidder.BidderName.AMX import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS @@ -73,6 +74,7 @@ class BidAdjustmentSpec extends BaseSpec { pbsService = pbsServiceFactory.getService(PbsConfig.currencyConverterConfig + AMX_CONFIG) } + @Override def cleanupSpec() { pbsServiceFactory.removeContainer(PbsConfig.currencyConverterConfig + AMX_CONFIG) } @@ -1234,7 +1236,7 @@ class BidAdjustmentSpec extends BaseSpec { where: adjustmentType << [CPM, STATIC] } - + def "PBS should adjust bid price for matching bidder and alternate bidder code when request has per-bidder bid adjustment factors"() { given: "Default bid request with bid adjustment and amx bidder" def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap { @@ -1345,6 +1347,325 @@ class BidAdjustmentSpec extends BaseSpec { bidAdjustmentFactor << [0.9, 1.1] } + def "PBS shouldn't adjust bid price when bid adjustment rule doesn't match with bidder code"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def bidAdjustmentRule = new BidAdjustmentRule((bidAdjustmentRuleBidder): [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): bidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ACUITYADS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == originalPrice + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ACUITYADS + + where: + bidAdjustmentRuleBidder << ["alias", "aliasUpperCase", "aliasCamelCase"] + } + + def "PBS should adjust bid price when two bid adjustment rules are compatible"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def dealId = PBSUtils.randomString + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def firstBidAdjustmentRule = new BidAdjustmentRule(amx: [(dealId): [adjustmentRule]]) + def secondBidAdjustmentRule = new BidAdjustmentRule(amx: [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): firstBidAdjustmentRule, + (ANY) : secondBidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code and dealId" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: AMX) + seatbid.first.bid.first.dealid = dealId + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + } + + def "PBS should adjust bid price when bid adjustment bidder and bidder code different"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency) + def bidAdjustmentRule = new BidAdjustmentRule((bidAdjustmentRuleBidder): [(WILDCARD): [adjustmentRule]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): bidAdjustmentRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + + where: + bidAdjustmentRuleBidder << ["alias", "aliasUpperCase", "aliasCamelCase"] + } + + def "PBS should adjust bid price when bid adjustment bidder and bidder code same as requested"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(amx: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: AMX) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == AMX + } + + def "PBS should adjust bid price when bid adjustment bidder is the same as bidder code"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + } + + def "PBS should adjust bid price when bid adjustment wildcard bidder and bidder code specified"() { + given: "Bid request with ext.prebid.bidAdjustments and ext.prebid.alternateBidderCode" + def exactRulePrice = PBSUtils.randomPrice + def currency = USD + def exactRule = new BidAdjustmentRule(wildcardBidder: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]]) + def bidRequest = BidRequest.defaultBidRequest.tap { + cur = [currency] + imp[0].ext.prebid.bidder.generic = null + imp[0].ext.prebid.bidder.amx = new Amx() + ext.prebid.tap { + bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule]) + alternateBidderCodes = new AlternateBidderCodes().tap { + enabled = true + bidders = [(AMX): new BidderConfig(enabled: true, allowedBidderCodes: [AMX])] + } + } + } + + and: "Default bid response with price and bidder code" + def originalPrice = PBSUtils.randomPrice + def bidResponse = BidResponse.getDefaultBidResponse(bidRequest, AMX).tap { + cur = currency + seatbid.first.bid.first.price = originalPrice + seatbid.first.bid.first.ext = new BidExt(bidderCode: ALIAS) + } + bidder.setResponse(bidRequest.id, bidResponse) + + when: "PBS processes auction request" + def response = pbsService.sendAuctionRequest(bidRequest) + + then: "Final bid price should be adjusted according to exact rule" + assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC) + assert response.cur == bidResponse.cur + + and: "Original bid price and currency should be presented in bid.ext" + verifyAll(response.seatbid.first.bid.first.ext) { + origbidcpm == originalPrice + origbidcur == bidResponse.cur + } + + and: "Bidder request should contain currency from request" + def bidderRequest = bidder.getBidderRequest(bidRequest.id) + assert bidderRequest.cur == [currency] + + and: "Response should contain adapter code" + assert response.seatbid.bid.ext.prebid.meta.adapterCode.flatten() == [AMX] + + and: "Response should contain seatbid.seat" + assert response.seatbid[0].seat == ALIAS + } + private static BigDecimal getAdjustedPrice(BigDecimal originalPrice, BigDecimal adjustedValue, AdjustmentType adjustmentType) { diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java index 1a1f33eb5f2..375ece90181 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java @@ -76,7 +76,7 @@ public class BidAdjustmentsProcessorTest extends VertxTest { public void before() { given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); target = new BidAdjustmentsProcessor( @@ -100,7 +100,7 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest); final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) .willReturn(adjustedPrice); final BigDecimal expectedPrice = new BigDecimal("123.5"); @@ -118,6 +118,7 @@ public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() { eq(Price.of("USD", BigDecimal.valueOf(2.0))), eq(bidRequest), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -137,7 +138,7 @@ public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() { given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocation -> invocation.getArgument(0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); // when @@ -191,7 +192,7 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving( given(currencyService.convertCurrency(any(), any(), any(), any())) .willAnswer(invocation -> invocation.getArgument(0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD")); // when @@ -233,7 +234,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "adapter", "seat")) .willReturn(BigDecimal.TEN); final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0)); - given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any())) + given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())) .willReturn(adjustedPrice); final BigDecimal expectedPrice = new BigDecimal("123.5"); given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice); @@ -252,6 +253,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAn eq(Price.of("USD", BigDecimal.valueOf(20.0))), eq(bidRequest), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -395,7 +397,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { .extracting(Bid::getPrice) .containsOnly(bidder3Price, updatedPrice, updatedPrice); - verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any()); + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any()); } @Test @@ -439,6 +441,7 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() { eq(Price.of("USD", BigDecimal.valueOf(4.936))), eq(bidRequest), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -489,6 +492,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), eq(ImpMediaType.video_instream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -539,6 +543,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoP eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), eq(ImpMediaType.video_instream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -591,6 +596,7 @@ public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideo eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), eq(ImpMediaType.video_outstream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -640,6 +646,7 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidIm eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), eq(ImpMediaType.video_outstream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -689,6 +696,7 @@ public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpId eq(Price.of("USD", BigDecimal.valueOf(2))), eq(bidRequest), eq(ImpMediaType.video_outstream), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -732,7 +740,7 @@ public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() .extracting(Bid::getPrice) .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1)); - verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any()); + verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any()); } @Test @@ -784,6 +792,7 @@ public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() { eq(Price.of("USD", BigDecimal.valueOf(6.912))), eq(bidRequest), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } @@ -831,6 +840,7 @@ public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresent eq(Price.of("USD", BigDecimal.ONE)), eq(bidRequest), eq(ImpMediaType.banner), + eq("seat"), eq("bidder"), eq("dealId")); } diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java index 8c81aa4adb7..3bc13250fe4 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java @@ -52,7 +52,7 @@ public void before() { public void resolveShouldApplyStaticRule() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) .willReturn(List.of(givenStatic("15", "EUR"))); // when @@ -60,6 +60,7 @@ public void resolveShouldApplyStaticRule() { Price.of("USD", BigDecimal.ONE), givenBidRequest, banner, + "seat", "bidderName", "dealId"); @@ -72,7 +73,7 @@ public void resolveShouldApplyStaticRule() { public void resolveShouldApplyCpmRule() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, video_outstream, "bidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, video_outstream, "seat", "bidderName", "dealId")) .willReturn(List.of(givenCpm("25", "UAH"))); // when @@ -80,6 +81,7 @@ public void resolveShouldApplyCpmRule() { Price.of("USD", BigDecimal.ONE), givenBidRequest, video_outstream, + "seat", "bidderName", "dealId"); @@ -92,7 +94,7 @@ public void resolveShouldApplyCpmRule() { public void resolveShouldApplyMultiplierRule() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) .willReturn(List.of(givenMultiplier("15"))); // when @@ -100,6 +102,7 @@ public void resolveShouldApplyMultiplierRule() { Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), banner, + "seat", "bidderName", "dealId"); @@ -112,7 +115,7 @@ public void resolveShouldApplyMultiplierRule() { public void resolveShouldApplyStaticAndCpmRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) .willReturn(List.of(givenStatic("25", "UAH"), givenMultiplier("25"))); // when @@ -120,6 +123,7 @@ public void resolveShouldApplyStaticAndCpmRules() { Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), banner, + "seat", "bidderName", "dealId"); @@ -132,7 +136,7 @@ public void resolveShouldApplyStaticAndCpmRules() { public void resolveShouldApplyCpmAndStaticRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) .willReturn(List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR"))); // when @@ -140,6 +144,7 @@ public void resolveShouldApplyCpmAndStaticRules() { Price.of("USD", BigDecimal.ONE), givenBidRequest, banner, + "seat", "bidderName", "dealId"); @@ -152,7 +157,7 @@ public void resolveShouldApplyCpmAndStaticRules() { public void resolveShouldApplyMultiplierAdnCpmRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", "dealId")) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", "dealId")) .willReturn(List.of(givenMultiplier("25"), givenCpm("25", "UAH"))); // when @@ -160,6 +165,7 @@ public void resolveShouldApplyMultiplierAdnCpmRules() { Price.of("USD", BigDecimal.ONE), givenBidRequest, banner, + "seat", "bidderName", "dealId"); @@ -172,7 +178,7 @@ public void resolveShouldApplyMultiplierAdnCpmRules() { public void resolveShouldApplyTwoCpmRules() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", null)) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", null)) .willReturn(List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY"))); // when @@ -180,6 +186,7 @@ public void resolveShouldApplyTwoCpmRules() { Price.of("USD", BigDecimal.ONE), givenBidRequest, banner, + "seat", "bidderName", null); @@ -193,7 +200,7 @@ public void resolveShouldApplyTwoCpmRules() { public void resolveShouldNotApplyAnyRulesWhenNoMatchFound() { // given final BidRequest givenBidRequest = BidRequest.builder().build(); - given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "bidderName", null)) + given(bidAdjustmentsRulesResolver.resolve(givenBidRequest, banner, "seat", "bidderName", null)) .willReturn(Collections.emptyList()); // when @@ -201,6 +208,7 @@ public void resolveShouldNotApplyAnyRulesWhenNoMatchFound() { Price.of("USD", BigDecimal.ONE), BidRequest.builder().build(), banner, + "seat", "bidderName", null); diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java index 1b35b34a273..2c85847fd63 100644 --- a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRulesResolverTest.java @@ -58,6 +58,7 @@ public void resolveShouldPickAndApplyRulesBySpecificMediaType() throws JsonProce final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "bidderName", "dealId"); @@ -101,6 +102,7 @@ public void resolveShouldPickAndApplyRulesByWildcardMediaType() throws JsonProce final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.video_outstream, + "seat", "bidderName", "dealId"); @@ -140,6 +142,7 @@ public void resolveShouldPickAndApplyRulesBySpecificBidder() throws JsonProcessi final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "bidderName", "dealId"); @@ -189,6 +192,7 @@ public void resolveShouldPickAndApplyRulesByWildcardBidder() throws JsonProcessi final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "anotherBidderName", "dealId"); @@ -238,6 +242,7 @@ public void resolveShouldPickAndApplyRulesBySpecificDealId() throws JsonProcessi final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "bidderName", "dealId"); @@ -285,6 +290,7 @@ public void resolveShouldPickAndApplyRulesByWildcardDealId() throws JsonProcessi final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "bidderName", "anotherDealId"); @@ -334,6 +340,7 @@ public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() thr final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "bidderName", null); @@ -366,6 +373,7 @@ public void resolveShouldReturnEmptyListWhenNoMatchFound() throws JsonProcessing final List actual = target.resolve( givenBidRequest(givenAdjustments), ImpMediaType.banner, + "seat", "bidderName", null); @@ -373,6 +381,287 @@ public void resolveShouldReturnEmptyListWhenNoMatchFound() throws JsonProcessing assertThat(actual).isEmpty(); } + @Test + public void resolveShouldPickAndApplyRulesBySpecificSeat() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "seat": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + null); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesByWildcardSeat() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "seat": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "anotherBidderName", + "anotherSeat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "UAH"), expectedMultiplier("25")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidderOverAbsentSeat() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "seat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificBidderCaseInsensitive() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "BIDDERname": { + "*": [ + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "*": { + "*": [ + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "seat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedMultiplier("15")); + } + + @Test + public void resolveShouldPickAndApplyRulesBySpecificSeatOverSpecificBidder() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "*": { + "bidderName": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ] + }, + "seat": { + "*": [ + { + "adjtype": "static", + "value": "25", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "seat", + "bidderName", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "UAH"), expectedMultiplier("25")); + } + + @Test + public void resolveShouldPickAndApplyMoreSpecificRuleOverLessSpecific() throws JsonProcessingException { + // given + final String givenAdjustments = """ + { + "mediatype": { + "banner": { + "bidderName": { + "*": [ + { + "adjtype": "static", + "value": "15", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "15" + } + ], + "dealId": [ + { + "adjtype": "static", + "value": "25", + "currency": "EUR" + }, + { + "adjtype": "multiplier", + "value": "25" + } + ] + }, + "seat": { + "*": [ + { + "adjtype": "static", + "value": "35", + "currency": "UAH" + }, + { + "adjtype": "multiplier", + "value": "35" + } + ] + } + } + } + } + """; + + // when + final List actual = target.resolve( + givenBidRequest(givenAdjustments), + ImpMediaType.banner, + "bidderName", + "seat", + "dealId"); + + // then + assertThat(actual).containsExactly(expectedStatic("25", "EUR"), expectedMultiplier("25")); + } + private static BidRequest givenBidRequest(String adjustmentsString) throws JsonProcessingException { final ObjectNode adjustmetsNode = (ObjectNode) mapper.readTree(adjustmentsString); return BidRequest.builder() diff --git a/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java index 14ea44e59dd..1cfb5dbe848 100644 --- a/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/FloorAdjustmentsResolverTest.java @@ -14,10 +14,10 @@ import org.prebid.server.proto.openrtb.ext.request.ImpMediaType; import java.math.BigDecimal; -import java.util.Collections; import java.util.List; import java.util.Set; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -63,10 +63,8 @@ public void resolveShouldReturnInitialPriceWhenNoRulesFoundForAnyMediaType() { final BidRequest bidRequest = givenBidRequest(USD); final Set mediaTypes = Set.of(banner, video_outstream); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) - .willReturn(Collections.emptyList()); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME, null)) - .willReturn(Collections.emptyList()); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)).willReturn(emptyList()); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME)).willReturn(emptyList()); given(currencyService.convertCurrency(BigDecimal.TEN, bidRequest, USD, USD)).willReturn(BigDecimal.TEN); // when @@ -87,7 +85,7 @@ public void resolveShouldApplyMultiplierRuleInReverse() { final BidAdjustmentsRule multiplierRule = givenMultiplier("2"); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)) .willReturn(singletonList(multiplierRule)); given(currencyService.convertCurrency(eq(new BigDecimal("10.0000")), eq(bidRequest), eq(USD), eq(USD))) .willReturn(BigDecimal.valueOf(20)); @@ -111,8 +109,7 @@ public void resolveShouldApplyCpmRuleInReverse() { final BidAdjustmentsRule cpmRule = givenCpm("5", EUR); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) - .willReturn(singletonList(cpmRule)); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)).willReturn(singletonList(cpmRule)); given(currencyService.convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD)) .willReturn(BigDecimal.valueOf(0.5)); given(currencyService.convertCurrency(new BigDecimal("50.5"), bidRequest, USD, USD)) @@ -138,7 +135,7 @@ public void resolveShouldThrowExceptionWhenStaticRuleIsEncountered() { final BidAdjustmentsRule staticRule = givenStatic("5", USD); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)) .willReturn(singletonList(staticRule)); // when and then @@ -159,8 +156,7 @@ public void resolveShouldApplyMultipleRulesInReverseOrder() { final BidAdjustmentsRule rule1 = givenMultiplier("2"); final BidAdjustmentsRule rule2 = givenCpm("5", EUR); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) - .willReturn(List.of(rule1, rule2)); + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)).willReturn(List.of(rule1, rule2)); given(currencyService.convertCurrency(new BigDecimal("5"), bidRequest, EUR, USD)) .willReturn(BigDecimal.valueOf(0.5)); given(currencyService.convertCurrency(new BigDecimal("50.2500"), bidRequest, USD, USD)) @@ -184,11 +180,11 @@ public void resolveShouldChooseMinimalFloorAcrossMediaTypesAfterConversion() { final Set mediaTypes = Set.of(banner, video_outstream); final BidAdjustmentsRule bannerRule = givenMultiplier("4"); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME, null)) + given(bidAdjustmentsRulesResolver.resolve(bidRequest, banner, BIDDER_NAME)) .willReturn(singletonList(bannerRule)); final BidAdjustmentsRule videoRule = givenCpm("500", UAH); - given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME, null)) + given(bidAdjustmentsRulesResolver.resolve(bidRequest, video_outstream, BIDDER_NAME)) .willReturn(singletonList(videoRule)); given(currencyService.convertCurrency(new BigDecimal("25.0000"), bidRequest, USD, EUR)) diff --git a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java index 45de1a0c64c..35afb5ca46d 100644 --- a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java +++ b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsRulesTest.java @@ -1,5 +1,6 @@ package org.prebid.server.bidadjustments.model; +import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -38,7 +39,7 @@ public void shouldBuildRulesSet() { final BidAdjustmentsRules actual = BidAdjustmentsRules.of(givenBidAdjustments); // then - final BidAdjustmentsRules expected = BidAdjustmentsRules.of(Map.of( + final BidAdjustmentsRules expected = BidAdjustmentsRules.of(new CaseInsensitiveMap<>(Map.of( "audio|bidderName|dealId", givenRules, "native|bidderName|dealId", givenRules, "video-instream|bidderName|dealId", givenRules, @@ -46,10 +47,9 @@ public void shouldBuildRulesSet() { "banner|bidderName|dealId", givenRules, "*|*|*", givenRules, "*|bidderName|*", givenRules, - "*|bidderName|dealId", givenRules)); + "*|bidderName|dealId", givenRules))); assertThat(actual).isEqualTo(expected); - } private static BidAdjustmentsRule givenRule(String value) {