Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/application-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ There are two ways to configure application settings: database and file. This do
operational warning.
- "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject
the bid and log an operational warning.
- `auction.bidadjustments` - configuration JSON for default bid adjustments
- `auction.bidadjustments.mediatype.{banner, video-instream, video-outstream, audio, native, *}.{<BIDDER>, *}.{<DEAL_ID>, *}[]` - array of bid adjustment to be applied to any bid of the provided mediatype, <BIDDER> and <DEAL_ID> (`*` means ANY)
- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static)
- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment
- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment
- `auction.events.enabled` - enables events for account if true
- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true.
- `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true.
- `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false.
- `auction.price-floors.fetch.url` - url to fetch price floors data from.
- `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000.
Expand Down
140 changes: 12 additions & 128 deletions src/main/java/org/prebid/server/auction/BidsAdjuster.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
package org.prebid.server.auction;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.response.Bid;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.auction.model.AuctionParticipation;
import org.prebid.server.auction.model.BidderResponse;
import org.prebid.server.bidadjustments.BidAdjustmentsProcessor;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.BidderSeatBid;
import org.prebid.server.currency.CurrencyConversionService;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.floors.PriceFloorEnforcer;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
import org.prebid.server.util.ObjectUtil;
import org.prebid.server.util.PbsUtil;
import org.prebid.server.validation.ResponseBidValidator;
import org.prebid.server.validation.model.ValidationResult;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand All @@ -35,29 +22,20 @@

public class BidsAdjuster {

private static final String ORIGINAL_BID_CPM = "origbidcpm";
private static final String ORIGINAL_BID_CURRENCY = "origbidcur";

private final ResponseBidValidator responseBidValidator;
private final CurrencyConversionService currencyService;
private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
private final PriceFloorEnforcer priceFloorEnforcer;
private final BidAdjustmentsProcessor bidAdjustmentsProcessor;
private final DsaEnforcer dsaEnforcer;
private final JacksonMapper mapper;

public BidsAdjuster(ResponseBidValidator responseBidValidator,
CurrencyConversionService currencyService,
BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
PriceFloorEnforcer priceFloorEnforcer,
DsaEnforcer dsaEnforcer,
JacksonMapper mapper) {
BidAdjustmentsProcessor bidAdjustmentsProcessor,
DsaEnforcer dsaEnforcer) {

this.responseBidValidator = Objects.requireNonNull(responseBidValidator);
this.currencyService = Objects.requireNonNull(currencyService);
this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver);
this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer);
this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor);
this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer);
this.mapper = Objects.requireNonNull(mapper);
}

public List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipation> auctionParticipations,
Expand All @@ -66,12 +44,18 @@ public List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipatio

return auctionParticipations.stream()
.map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases))
.map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest()))

.map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids(
auctionParticipation,
auctionContext.getBidRequest(),
auctionContext.getBidAdjustments()))

.map(auctionParticipation -> priceFloorEnforcer.enforce(
auctionContext.getBidRequest(),
auctionParticipation,
auctionContext.getAccount(),
auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder())))

.map(auctionParticipation -> dsaEnforcer.enforce(
auctionContext.getBidRequest(),
auctionParticipation,
Expand Down Expand Up @@ -137,104 +121,4 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati
final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown");
return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors);
}

private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation,
BidRequest bidRequest) {
if (auctionParticipation.isRequestBlocked()) {
return auctionParticipation;
}

final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
final BidderSeatBid seatBid = bidderResponse.getSeatBid();

final List<BidderBid> bidderBids = seatBid.getBids();
if (bidderBids.isEmpty()) {
return auctionParticipation;
}

final List<BidderBid> updatedBidderBids = new ArrayList<>(bidderBids.size());
final List<BidderError> errors = new ArrayList<>(seatBid.getErrors());
final String adServerCurrency = bidRequest.getCur().getFirst();

for (final BidderBid bidderBid : bidderBids) {
try {
final BidderBid updatedBidderBid =
updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency);
updatedBidderBids.add(updatedBidderBid);
} catch (PreBidException e) {
errors.add(BidderError.generic(e.getMessage()));
}
}

final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder()
.bids(updatedBidderBids)
.errors(errors)
.build());
return auctionParticipation.with(resultBidderResponse);
}

private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid,
BidderResponse bidderResponse,
BidRequest bidRequest,
String adServerCurrency) {
final Bid bid = bidderBid.getBid();
final String bidCurrency = bidderBid.getBidCurrency();
final BigDecimal price = bid.getPrice();

final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency(
price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency);

final BigDecimal priceAdjustmentFactor =
bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid);
final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency);

final ObjectNode bidExt = bid.getExt();
final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode();

updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency);

final Bid.BidBuilder bidBuilder = bid.toBuilder();
if (adjustedPrice.compareTo(price) != 0) {
bidBuilder.price(adjustedPrice);
}

if (!updatedBidExt.isEmpty()) {
bidBuilder.ext(updatedBidExt);
}

return bidderBid.toBuilder().bid(bidBuilder.build()).build();
}

private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) {
final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest);
if (adjustmentFactors == null) {
return null;
}
final ImpMediaType mediaType = ImpMediaTypeResolver.resolve(
bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType());

return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder);
}

private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) {
final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest);
return prebid != null ? prebid.getBidadjustmentfactors() : null;
}

private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) {
return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0
? price.multiply(priceAdjustmentFactor)
: price;
}

private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) {
addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price));
if (StringUtils.isNotBlank(bidCurrency)) {
addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency));
}
}

private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) {
node.set(propertyName, propertyValue);
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/prebid/server/auction/model/AuctionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand All @@ -17,6 +18,7 @@
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;

Expand Down Expand Up @@ -71,6 +73,10 @@ 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();
}
Expand Down Expand Up @@ -124,6 +130,12 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +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.cookie.CookieDeprecationService;
import org.prebid.server.exception.InvalidRequestException;
import org.prebid.server.json.JacksonMapper;
Expand Down Expand Up @@ -50,6 +51,7 @@ public class AuctionRequestFactory {
private final JacksonMapper mapper;
private final OrtbTypesResolver ortbTypesResolver;
private final GeoLocationServiceWrapper geoLocationServiceWrapper;
private final BidAdjustmentsRetriever bidAdjustmentsRetriever;

private static final String ENDPOINT = Endpoint.openrtb2_auction.value();

Expand All @@ -66,7 +68,8 @@ public AuctionRequestFactory(long maxRequestSize,
AuctionPrivacyContextFactory auctionPrivacyContextFactory,
DebugResolver debugResolver,
JacksonMapper mapper,
GeoLocationServiceWrapper geoLocationServiceWrapper) {
GeoLocationServiceWrapper geoLocationServiceWrapper,
BidAdjustmentsRetriever bidAdjustmentsRetriever) {

this.maxRequestSize = maxRequestSize;
this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory);
Expand All @@ -82,6 +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);
}

/**
Expand Down Expand Up @@ -142,6 +146,8 @@ public Future<AuctionContext> enrichAuctionContext(AuctionContext initialContext
.compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext)
.map(auctionContext::with))

.map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext)))

.compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext)
.map(auctionContext::with))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.prebid.server.bidadjustments;

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.proto.openrtb.ext.request.ImpMediaType;
import org.prebid.server.validation.ValidationException;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class BidAdjustmentRulesValidator {

public static final Set<String> SUPPORTED_MEDIA_TYPES = Set.of(
BidAdjustmentsResolver.WILDCARD,
ImpMediaType.banner.toString(),
ImpMediaType.audio.toString(),
ImpMediaType.video_instream.toString(),
ImpMediaType.video_outstream.toString(),
ImpMediaType.xNative.toString());

private BidAdjustmentRulesValidator() {

}

public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException {
if (bidAdjustments == null) {
return;
}

final Map<String, Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>>> mediatypes =
bidAdjustments.getMediatype();

if (MapUtils.isEmpty(mediatypes)) {
return;
}

for (String mediatype : mediatypes.keySet()) {
if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) {
final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> 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<String, List<ExtRequestBidAdjustmentsRule>> deals = bidders.get(bidder);

if (MapUtils.isEmpty(deals)) {
throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder));
}

for (String dealId : deals.keySet()) {
final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId);
validateRules(deals.get(dealId), path);
}
}
}
}
}

private static void validateRules(List<ExtRequestBidAdjustmentsRule> rules,
String path) throws ValidationException {

if (rules == null) {
throw new ValidationException("no bid adjustment rules found in %s".formatted(path));
}

for (ExtRequestBidAdjustmentsRule rule : rules) {
final BidAdjustmentType type = rule.getAdjType();
final String currency = rule.getCurrency();
final BigDecimal value = rule.getValue();

final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency);

final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN;

final boolean invalidCpm = type == BidAdjustmentType.CPM
&& (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));

final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER
&& isValueNotInRange(value, 0, 100);

final boolean invalidStatic = type == BidAdjustmentType.STATIC
&& (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));

if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) {
throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path));
}
}
}

private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) {
return value == null
|| value.compareTo(BigDecimal.valueOf(minValue)) < 0
|| value.compareTo(BigDecimal.valueOf(maxValue)) >= 0;
}
}
Loading