Skip to content

Commit af13ec8

Browse files
Core: Bid Adjustments Feature (#3542)
1 parent 0970699 commit af13ec8

33 files changed

Lines changed: 3560 additions & 862 deletions

docs/application-settings.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ There are two ways to configure application settings: database and file. This do
1919
operational warning.
2020
- "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject
2121
the bid and log an operational warning.
22+
- `auction.bidadjustments` - configuration JSON for default bid adjustments
23+
- `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)
24+
- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static)
25+
- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment
26+
- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment
2227
- `auction.events.enabled` - enables events for account if true
23-
- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true.
28+
- `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true.
2429
- `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false.
2530
- `auction.price-floors.fetch.url` - url to fetch price floors data from.
2631
- `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000.
Lines changed: 12 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,19 @@
11
package org.prebid.server.auction;
22

3-
import com.fasterxml.jackson.databind.JsonNode;
4-
import com.fasterxml.jackson.databind.node.DecimalNode;
5-
import com.fasterxml.jackson.databind.node.ObjectNode;
6-
import com.fasterxml.jackson.databind.node.TextNode;
73
import com.iab.openrtb.request.BidRequest;
84
import com.iab.openrtb.response.Bid;
9-
import org.apache.commons.lang3.StringUtils;
10-
import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
115
import org.prebid.server.auction.model.AuctionContext;
126
import org.prebid.server.auction.model.AuctionParticipation;
137
import org.prebid.server.auction.model.BidderResponse;
8+
import org.prebid.server.bidadjustments.BidAdjustmentsProcessor;
149
import org.prebid.server.bidder.model.BidderBid;
1510
import org.prebid.server.bidder.model.BidderError;
1611
import org.prebid.server.bidder.model.BidderSeatBid;
17-
import org.prebid.server.currency.CurrencyConversionService;
18-
import org.prebid.server.exception.PreBidException;
1912
import org.prebid.server.floors.PriceFloorEnforcer;
20-
import org.prebid.server.json.JacksonMapper;
21-
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
22-
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
23-
import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
2413
import org.prebid.server.util.ObjectUtil;
25-
import org.prebid.server.util.PbsUtil;
2614
import org.prebid.server.validation.ResponseBidValidator;
2715
import org.prebid.server.validation.model.ValidationResult;
2816

29-
import java.math.BigDecimal;
3017
import java.util.ArrayList;
3118
import java.util.List;
3219
import java.util.Objects;
@@ -35,29 +22,20 @@
3522

3623
public class BidsAdjuster {
3724

38-
private static final String ORIGINAL_BID_CPM = "origbidcpm";
39-
private static final String ORIGINAL_BID_CURRENCY = "origbidcur";
40-
4125
private final ResponseBidValidator responseBidValidator;
42-
private final CurrencyConversionService currencyService;
43-
private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
4426
private final PriceFloorEnforcer priceFloorEnforcer;
27+
private final BidAdjustmentsProcessor bidAdjustmentsProcessor;
4528
private final DsaEnforcer dsaEnforcer;
46-
private final JacksonMapper mapper;
4729

4830
public BidsAdjuster(ResponseBidValidator responseBidValidator,
49-
CurrencyConversionService currencyService,
50-
BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
5131
PriceFloorEnforcer priceFloorEnforcer,
52-
DsaEnforcer dsaEnforcer,
53-
JacksonMapper mapper) {
32+
BidAdjustmentsProcessor bidAdjustmentsProcessor,
33+
DsaEnforcer dsaEnforcer) {
5434

5535
this.responseBidValidator = Objects.requireNonNull(responseBidValidator);
56-
this.currencyService = Objects.requireNonNull(currencyService);
57-
this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver);
5836
this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer);
37+
this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor);
5938
this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer);
60-
this.mapper = Objects.requireNonNull(mapper);
6139
}
6240

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

6745
return auctionParticipations.stream()
6846
.map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases))
69-
.map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest()))
47+
48+
.map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids(
49+
auctionParticipation,
50+
auctionContext.getBidRequest(),
51+
auctionContext.getBidAdjustments()))
52+
7053
.map(auctionParticipation -> priceFloorEnforcer.enforce(
7154
auctionContext.getBidRequest(),
7255
auctionParticipation,
7356
auctionContext.getAccount(),
7457
auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder())))
58+
7559
.map(auctionParticipation -> dsaEnforcer.enforce(
7660
auctionContext.getBidRequest(),
7761
auctionParticipation,
@@ -137,104 +121,4 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati
137121
final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown");
138122
return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors);
139123
}
140-
141-
private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation,
142-
BidRequest bidRequest) {
143-
if (auctionParticipation.isRequestBlocked()) {
144-
return auctionParticipation;
145-
}
146-
147-
final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
148-
final BidderSeatBid seatBid = bidderResponse.getSeatBid();
149-
150-
final List<BidderBid> bidderBids = seatBid.getBids();
151-
if (bidderBids.isEmpty()) {
152-
return auctionParticipation;
153-
}
154-
155-
final List<BidderBid> updatedBidderBids = new ArrayList<>(bidderBids.size());
156-
final List<BidderError> errors = new ArrayList<>(seatBid.getErrors());
157-
final String adServerCurrency = bidRequest.getCur().getFirst();
158-
159-
for (final BidderBid bidderBid : bidderBids) {
160-
try {
161-
final BidderBid updatedBidderBid =
162-
updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency);
163-
updatedBidderBids.add(updatedBidderBid);
164-
} catch (PreBidException e) {
165-
errors.add(BidderError.generic(e.getMessage()));
166-
}
167-
}
168-
169-
final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder()
170-
.bids(updatedBidderBids)
171-
.errors(errors)
172-
.build());
173-
return auctionParticipation.with(resultBidderResponse);
174-
}
175-
176-
private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid,
177-
BidderResponse bidderResponse,
178-
BidRequest bidRequest,
179-
String adServerCurrency) {
180-
final Bid bid = bidderBid.getBid();
181-
final String bidCurrency = bidderBid.getBidCurrency();
182-
final BigDecimal price = bid.getPrice();
183-
184-
final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency(
185-
price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency);
186-
187-
final BigDecimal priceAdjustmentFactor =
188-
bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid);
189-
final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency);
190-
191-
final ObjectNode bidExt = bid.getExt();
192-
final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode();
193-
194-
updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency);
195-
196-
final Bid.BidBuilder bidBuilder = bid.toBuilder();
197-
if (adjustedPrice.compareTo(price) != 0) {
198-
bidBuilder.price(adjustedPrice);
199-
}
200-
201-
if (!updatedBidExt.isEmpty()) {
202-
bidBuilder.ext(updatedBidExt);
203-
}
204-
205-
return bidderBid.toBuilder().bid(bidBuilder.build()).build();
206-
}
207-
208-
private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) {
209-
final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest);
210-
if (adjustmentFactors == null) {
211-
return null;
212-
}
213-
final ImpMediaType mediaType = ImpMediaTypeResolver.resolve(
214-
bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType());
215-
216-
return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder);
217-
}
218-
219-
private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) {
220-
final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest);
221-
return prebid != null ? prebid.getBidadjustmentfactors() : null;
222-
}
223-
224-
private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) {
225-
return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0
226-
? price.multiply(priceAdjustmentFactor)
227-
: price;
228-
}
229-
230-
private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) {
231-
addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price));
232-
if (StringUtils.isNotBlank(bidCurrency)) {
233-
addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency));
234-
}
235-
}
236-
237-
private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) {
238-
node.set(propertyName, propertyValue);
239-
}
240124
}

src/main/java/org/prebid/server/auction/model/AuctionContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
99
import org.prebid.server.auction.gpp.model.GppContext;
1010
import org.prebid.server.auction.model.debug.DebugContext;
11+
import org.prebid.server.bidadjustments.model.BidAdjustments;
1112
import org.prebid.server.cache.model.DebugHttpCall;
1213
import org.prebid.server.cookie.UidsCookie;
1314
import org.prebid.server.geolocation.model.GeoInfo;
@@ -17,6 +18,7 @@
1718
import org.prebid.server.privacy.model.PrivacyContext;
1819
import org.prebid.server.settings.model.Account;
1920

21+
import java.util.Collections;
2022
import java.util.List;
2123
import java.util.Map;
2224

@@ -71,6 +73,10 @@ public class AuctionContext {
7173

7274
CachedDebugLog cachedDebugLog;
7375

76+
@JsonIgnore
77+
@Builder.Default
78+
BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap());
79+
7480
public AuctionContext with(Account account) {
7581
return this.toBuilder().account(account).build();
7682
}
@@ -124,6 +130,12 @@ public AuctionContext with(GeoInfo geoInfo) {
124130
.build();
125131
}
126132

133+
public AuctionContext with(BidAdjustments bidAdjustments) {
134+
return this.toBuilder()
135+
.bidAdjustments(bidAdjustments)
136+
.build();
137+
}
138+
127139
public AuctionContext withRequestRejected() {
128140
return this.toBuilder()
129141
.requestRejected(true)

src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.prebid.server.auction.model.AuctionStoredResult;
1818
import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory;
1919
import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager;
20+
import org.prebid.server.bidadjustments.BidAdjustmentsRetriever;
2021
import org.prebid.server.cookie.CookieDeprecationService;
2122
import org.prebid.server.exception.InvalidRequestException;
2223
import org.prebid.server.json.JacksonMapper;
@@ -50,6 +51,7 @@ public class AuctionRequestFactory {
5051
private final JacksonMapper mapper;
5152
private final OrtbTypesResolver ortbTypesResolver;
5253
private final GeoLocationServiceWrapper geoLocationServiceWrapper;
54+
private final BidAdjustmentsRetriever bidAdjustmentsRetriever;
5355

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

@@ -66,7 +68,8 @@ public AuctionRequestFactory(long maxRequestSize,
6668
AuctionPrivacyContextFactory auctionPrivacyContextFactory,
6769
DebugResolver debugResolver,
6870
JacksonMapper mapper,
69-
GeoLocationServiceWrapper geoLocationServiceWrapper) {
71+
GeoLocationServiceWrapper geoLocationServiceWrapper,
72+
BidAdjustmentsRetriever bidAdjustmentsRetriever) {
7073

7174
this.maxRequestSize = maxRequestSize;
7275
this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory);
@@ -82,6 +85,7 @@ public AuctionRequestFactory(long maxRequestSize,
8285
this.debugResolver = Objects.requireNonNull(debugResolver);
8386
this.mapper = Objects.requireNonNull(mapper);
8487
this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper);
88+
this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever);
8589
}
8690

8791
/**
@@ -142,6 +146,8 @@ public Future<AuctionContext> enrichAuctionContext(AuctionContext initialContext
142146
.compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext)
143147
.map(auctionContext::with))
144148

149+
.map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext)))
150+
145151
.compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext)
146152
.map(auctionContext::with))
147153

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package org.prebid.server.bidadjustments;
2+
3+
import org.apache.commons.collections4.MapUtils;
4+
import org.apache.commons.lang3.StringUtils;
5+
import org.prebid.server.bidadjustments.model.BidAdjustmentType;
6+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
7+
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
8+
import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
9+
import org.prebid.server.validation.ValidationException;
10+
11+
import java.math.BigDecimal;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Set;
15+
16+
public class BidAdjustmentRulesValidator {
17+
18+
public static final Set<String> SUPPORTED_MEDIA_TYPES = Set.of(
19+
BidAdjustmentsResolver.WILDCARD,
20+
ImpMediaType.banner.toString(),
21+
ImpMediaType.audio.toString(),
22+
ImpMediaType.video_instream.toString(),
23+
ImpMediaType.video_outstream.toString(),
24+
ImpMediaType.xNative.toString());
25+
26+
private BidAdjustmentRulesValidator() {
27+
28+
}
29+
30+
public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException {
31+
if (bidAdjustments == null) {
32+
return;
33+
}
34+
35+
final Map<String, Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>>> mediatypes =
36+
bidAdjustments.getMediatype();
37+
38+
if (MapUtils.isEmpty(mediatypes)) {
39+
return;
40+
}
41+
42+
for (String mediatype : mediatypes.keySet()) {
43+
if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) {
44+
final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> bidders = mediatypes.get(mediatype);
45+
if (MapUtils.isEmpty(bidders)) {
46+
throw new ValidationException("no bidders found in %s".formatted(mediatype));
47+
}
48+
for (String bidder : bidders.keySet()) {
49+
final Map<String, List<ExtRequestBidAdjustmentsRule>> deals = bidders.get(bidder);
50+
51+
if (MapUtils.isEmpty(deals)) {
52+
throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder));
53+
}
54+
55+
for (String dealId : deals.keySet()) {
56+
final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId);
57+
validateRules(deals.get(dealId), path);
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
private static void validateRules(List<ExtRequestBidAdjustmentsRule> rules,
65+
String path) throws ValidationException {
66+
67+
if (rules == null) {
68+
throw new ValidationException("no bid adjustment rules found in %s".formatted(path));
69+
}
70+
71+
for (ExtRequestBidAdjustmentsRule rule : rules) {
72+
final BidAdjustmentType type = rule.getAdjType();
73+
final String currency = rule.getCurrency();
74+
final BigDecimal value = rule.getValue();
75+
76+
final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency);
77+
78+
final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN;
79+
80+
final boolean invalidCpm = type == BidAdjustmentType.CPM
81+
&& (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));
82+
83+
final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER
84+
&& isValueNotInRange(value, 0, 100);
85+
86+
final boolean invalidStatic = type == BidAdjustmentType.STATIC
87+
&& (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));
88+
89+
if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) {
90+
throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path));
91+
}
92+
}
93+
}
94+
95+
private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) {
96+
return value == null
97+
|| value.compareTo(BigDecimal.valueOf(minValue)) < 0
98+
|| value.compareTo(BigDecimal.valueOf(maxValue)) >= 0;
99+
}
100+
}

0 commit comments

Comments
 (0)