Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bid Adjustments Feature #3542

Merged
merged 8 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
141 changes: 12 additions & 129 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,19 @@

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) {
PriceFloorEnforcer priceFloorEnforcer, BidAdjustmentsProcessor bidAdjustmentsProcessor,
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
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 +43,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 +120,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
Loading