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

Price Floors: Provider contract change #1837

Merged
merged 3 commits into from
Apr 26, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.bidder.model.Price;
import org.prebid.server.currency.CurrencyConversionService;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.floors.model.PriceFloorData;
import org.prebid.server.floors.model.PriceFloorEnforcement;
import org.prebid.server.floors.model.PriceFloorLocation;
import org.prebid.server.floors.model.PriceFloorModelGroup;
import org.prebid.server.floors.model.PriceFloorResult;
Expand Down Expand Up @@ -53,17 +51,14 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {

private final PriceFloorFetcher floorFetcher;
private final PriceFloorResolver floorResolver;
private final CurrencyConversionService conversionService;
private final JacksonMapper mapper;

public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
PriceFloorResolver floorResolver,
CurrencyConversionService conversionService,
JacksonMapper mapper) {

this.floorFetcher = Objects.requireNonNull(floorFetcher);
this.floorResolver = Objects.requireNonNull(floorResolver);
this.conversionService = Objects.requireNonNull(conversionService);
this.mapper = Objects.requireNonNull(mapper);
}

Expand Down Expand Up @@ -127,13 +122,13 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li
final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus);

if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) {
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRules());
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
}

if (requestFloors != null) {
try {
PriceFloorRulesValidator.validate(requestFloors, Integer.MAX_VALUE);
PriceFloorRulesValidator.validateRules(requestFloors, Integer.MAX_VALUE);
return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request);
} catch (PreBidException e) {
errors.add(String.format("Failed to parse price floors from request,"
Expand All @@ -159,30 +154,18 @@ private static boolean shouldUseDynamicData(Account account) {
}

private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
PriceFloorRules providerFloors) {
PriceFloorData providerRulesData) {

final Boolean floorsEnabledByRequest = ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getEnabled);
final PriceFloorEnforcement floorsRequestEnforcement =
ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getEnforcement);
final Integer enforceRate =
ObjectUtil.getIfNotNull(floorsRequestEnforcement, PriceFloorEnforcement::getEnforceRate);
final Price floorMinPrice = resolveFloorMinPrice(requestFloors, providerFloors);
final Price floorMinPrice = resolveFloorMinPrice(requestFloors);

final Boolean floorsEnabledByProvider =
ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnabled);
final PriceFloorEnforcement floorsProviderEnforcement =
ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getEnforcement);

return (providerFloors != null ? providerFloors.toBuilder() : PriceFloorRules.builder())
return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder())
.floorMinCur(ObjectUtil.getIfNotNull(floorMinPrice, Price::getCurrency))
.floorMin(ObjectUtil.getIfNotNull(floorMinPrice, Price::getValue))
.enabled(resolveFloorsEnabled(floorsEnabledByRequest, floorsEnabledByProvider))
.enforcement(resolveFloorsEnforcement(floorsProviderEnforcement, enforceRate))
.data(providerRulesData)
.build();
}

private Price resolveFloorMinPrice(PriceFloorRules requestFloors,
PriceFloorRules providerFloors) {
private Price resolveFloorMinPrice(PriceFloorRules requestFloors) {
final String requestDataCurrency =
ObjectUtil.getIfNotNull(
ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getData), PriceFloorData::getCurrency);
Expand All @@ -191,66 +174,11 @@ private Price resolveFloorMinPrice(PriceFloorRules requestFloors,
requestDataCurrency);
final BigDecimal requestFloorMin = ObjectUtil.getIfNotNull(requestFloors, PriceFloorRules::getFloorMin);

final String providerFloorMinCur =
ObjectUtil.getIfNotNull(
ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getData), PriceFloorData::getCurrency);
final BigDecimal providerFloorMin = ObjectUtil.getIfNotNull(providerFloors, PriceFloorRules::getFloorMin);

if (StringUtils.isNotBlank(requestFloorMinCur)) {
if (BidderUtil.isValidPrice(requestFloorMin)) {
return Price.of(requestFloorMinCur, requestFloorMin);
} else if (BidderUtil.isValidPrice(providerFloorMin)) {
if (StringUtils.equals(providerFloorMinCur, requestFloorMinCur)) {
return Price.of(requestFloorMinCur, providerFloorMin);
}

return Price.of(
requestFloorMinCur,
conversionService.convertCurrency(
providerFloorMin,
Collections.emptyMap(),
providerFloorMinCur,
requestFloorMinCur,
false));
}
}

if (StringUtils.isNotBlank(providerFloorMinCur)) {
if (BidderUtil.isValidPrice(requestFloorMin)) {
return Price.of(
requestFloorMinCur,
conversionService.convertCurrency(
requestFloorMin,
Collections.emptyMap(),
requestFloorMinCur,
providerFloorMinCur,
false));
}

return Price.of(requestFloorMinCur, providerFloorMin);
}

return Price.of(null, ObjectUtils.firstNonNull(requestFloorMin, providerFloorMin));
}

private static Boolean resolveFloorsEnabled(Boolean enabledByRequest, Boolean enabledByProvider) {
if (BooleanUtils.isFalse(enabledByRequest) || BooleanUtils.isFalse(enabledByProvider)) {
return false;
if (StringUtils.isNotBlank(requestFloorMinCur) && BidderUtil.isValidPrice(requestFloorMin)) {
return Price.of(requestFloorMinCur, requestFloorMin);
}

return ObjectUtils.defaultIfNull(enabledByRequest, enabledByProvider);
}

private static PriceFloorEnforcement resolveFloorsEnforcement(PriceFloorEnforcement providerEnforcement,
Integer enforceRate) {

if (enforceRate == null) {
return providerEnforcement;
}

return (providerEnforcement != null ? providerEnforcement.toBuilder() : PriceFloorEnforcement.builder())
.enforceRate(enforceRate)
.build();
return Price.of(null, requestFloorMin);
}

private static PriceFloorRules createFloorsFrom(PriceFloorRules floors,
Expand Down
54 changes: 27 additions & 27 deletions src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import org.apache.http.HttpStatus;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.execution.TimeoutFactory;
import org.prebid.server.floors.model.PriceFloorData;
import org.prebid.server.floors.model.PriceFloorDebugProperties;
import org.prebid.server.floors.model.PriceFloorRules;
import org.prebid.server.floors.proto.FetchResult;
import org.prebid.server.floors.proto.FetchStatus;
import org.prebid.server.json.DecodeException;
Expand Down Expand Up @@ -85,11 +85,11 @@ public FetchResult fetch(Account account) {
final AccountFetchContext accountFetchContext = fetchedData.get(account.getId());

return accountFetchContext != null
? FetchResult.of(accountFetchContext.getRules(), accountFetchContext.getFetchStatus())
: fetchPriceFloorRules(account);
? FetchResult.of(accountFetchContext.getRulesData(), accountFetchContext.getFetchStatus())
: fetchPriceFloorData(account);
}

private FetchResult fetchPriceFloorRules(Account account) {
private FetchResult fetchPriceFloorData(Account account) {
final AccountPriceFloorsFetchConfig fetchConfig = getFetchConfig(account);
final Boolean fetchEnabled = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getEnabled);

Expand All @@ -104,7 +104,7 @@ private FetchResult fetchPriceFloorRules(Account account) {
return FetchResult.of(null, FetchStatus.error);
}
if (!fetchInProgress.contains(accountId)) {
fetchPriceFloorRulesAsynchronous(fetchConfig, accountId);
fetchPriceFloorDataAsynchronous(fetchConfig, accountId);
}

return FetchResult.of(null, FetchStatus.inprogress);
Expand All @@ -131,7 +131,7 @@ private static AccountPriceFloorsFetchConfig getFetchConfig(Account account) {
return ObjectUtil.getIfNotNull(priceFloorsConfig, AccountPriceFloorsConfig::getFetch);
}

private void fetchPriceFloorRulesAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) {
private void fetchPriceFloorDataAsynchronous(AccountPriceFloorsFetchConfig fetchConfig, String accountId) {
final Long accountTimeout = ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getTimeout);
final Long timeout = ObjectUtils.firstNonNull(
ObjectUtil.getIfNotNull(debugProperties, PriceFloorDebugProperties::getMinTimeoutMs),
Expand All @@ -146,7 +146,7 @@ private void fetchPriceFloorRulesAsynchronous(AccountPriceFloorsFetchConfig fetc
.map(httpClientResponse -> parseFloorResponse(httpClientResponse, fetchConfig, accountId))
.recover(throwable -> recoverFromFailedFetching(throwable, fetchUrl, accountId))
.map(cacheInfo -> updateCache(cacheInfo, fetchConfig, accountId))
.map(priceFloorRules -> createPeriodicTimerForRulesFetch(priceFloorRules, fetchConfig, accountId));
.map(priceFloorData -> createPeriodicTimerForRulesFetch(priceFloorData, fetchConfig, accountId));
}

private static long resolveMaxFileSize(Long maxSizeInKBytes) {
Expand All @@ -169,24 +169,24 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon
+ "response body can not be empty", accountId));
}

final PriceFloorRules priceFloorRules = parsePriceFloorRules(body, accountId);
PriceFloorRulesValidator.validate(priceFloorRules, resolveMaxRules(fetchConfig.getMaxRules()));
final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId);
PriceFloorRulesValidator.validateRulesData(priceFloorData, resolveMaxRules(fetchConfig.getMaxRules()));

return ResponseCacheInfo.of(priceFloorRules,
return ResponseCacheInfo.of(priceFloorData,
FetchStatus.success,
cacheTtlFromResponse(httpClientResponse, fetchConfig.getUrl()));
}

private PriceFloorRules parsePriceFloorRules(String body, String accountId) {
final PriceFloorRules priceFloorRules;
private PriceFloorData parsePriceFloorData(String body, String accountId) {
final PriceFloorData priceFloorData;
try {
priceFloorRules = mapper.decodeValue(body, PriceFloorRules.class);
priceFloorData = mapper.decodeValue(body, PriceFloorData.class);
} catch (DecodeException e) {
throw new PreBidException(
String.format("Failed to parse price floor response for account %s, cause: %s",
accountId, ExceptionUtils.getMessage(e)));
}
return priceFloorRules;
return priceFloorData;
}

private static int resolveMaxRules(Long accountMaxRules) {
Expand Down Expand Up @@ -214,20 +214,20 @@ private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String
return null;
}

private PriceFloorRules updateCache(ResponseCacheInfo cacheInfo,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {
private PriceFloorData updateCache(ResponseCacheInfo cacheInfo,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {

long maxAgeTimerId = createMaxAgeTimer(accountId, resolveCacheTtl(cacheInfo, fetchConfig));
final AccountFetchContext fetchContext =
AccountFetchContext.of(cacheInfo.getRules(), cacheInfo.getFetchStatus(), maxAgeTimerId);
AccountFetchContext.of(cacheInfo.getRulesData(), cacheInfo.getFetchStatus(), maxAgeTimerId);

if (cacheInfo.getRules() != null || !fetchedData.containsKey(accountId)) {
if (cacheInfo.getRulesData() != null || !fetchedData.containsKey(accountId)) {
fetchedData.put(accountId, fetchContext);
fetchInProgress.remove(accountId);
}

return fetchContext.getRules();
return fetchContext.getRulesData();
}

private long resolveCacheTtl(ResponseCacheInfo cacheInfo, AccountPriceFloorsFetchConfig fetchConfig) {
Expand Down Expand Up @@ -277,9 +277,9 @@ private Future<ResponseCacheInfo> recoverFromFailedFetching(Throwable throwable,
return Future.succeededFuture(ResponseCacheInfo.withStatus(fetchStatus));
}

private PriceFloorRules createPeriodicTimerForRulesFetch(PriceFloorRules priceFloorRules,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {
private PriceFloorData createPeriodicTimerForRulesFetch(PriceFloorData priceFloorData,
AccountPriceFloorsFetchConfig fetchConfig,
String accountId) {
final long accountPeriodicTimeSec =
ObjectUtil.getIfNotNull(fetchConfig, AccountPriceFloorsFetchConfig::getPeriodSec);
final long periodicTimeSec =
Expand All @@ -288,11 +288,11 @@ private PriceFloorRules createPeriodicTimerForRulesFetch(PriceFloorRules priceFl
accountPeriodicTimeSec);
vertx.setTimer(TimeUnit.SECONDS.toMillis(periodicTimeSec), ignored -> periodicFetch(accountId));

return priceFloorRules;
return priceFloorData;
}

private void periodicFetch(String accountId) {
accountById(accountId).map(this::fetchPriceFloorRules);
accountById(accountId).map(this::fetchPriceFloorData);
}

private Future<Account> accountById(String accountId) {
Expand All @@ -306,7 +306,7 @@ private Future<Account> accountById(String accountId) {
@Value(staticConstructor = "of")
private static class AccountFetchContext {

PriceFloorRules rules;
PriceFloorData rulesData;

FetchStatus fetchStatus;

Expand All @@ -316,7 +316,7 @@ private static class AccountFetchContext {
@Value(staticConstructor = "of")
private static class ResponseCacheInfo {

PriceFloorRules rules;
PriceFloorData rulesData;

FetchStatus fetchStatus;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class PriceFloorRulesValidator {
private PriceFloorRulesValidator() {
}

public static void validate(PriceFloorRules priceFloorRules, Integer maxRules) {
public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules) {

final Integer rootSkipRate = priceFloorRules.getSkipRate();
if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) {
Expand All @@ -35,22 +35,25 @@ public static void validate(PriceFloorRules priceFloorRules, Integer maxRules) {
+ "must be positive float, but was %s", floorMin));
}

final PriceFloorData data = priceFloorRules.getData();
if (data == null) {
validateRulesData(priceFloorRules.getData(), maxRules);
}

public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules) {
if (priceFloorData == null) {
throw new PreBidException("Price floor rules data must be present");
}

final Integer dataSkipRate = data.getSkipRate();
final Integer dataSkipRate = priceFloorData.getSkipRate();
if (dataSkipRate != null && (dataSkipRate < SKIP_RATE_MIN || dataSkipRate > SKIP_RATE_MAX)) {
throw new PreBidException(String.format("Price floor data skipRate "
+ "must be in range(0-100), but was %s", dataSkipRate));
}

if (CollectionUtils.isEmpty(data.getModelGroups())) {
if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) {
throw new PreBidException("Price floor rules should contain at least one model group");
}

data.getModelGroups().stream()
priceFloorData.getModelGroups().stream()
.filter(Objects::nonNull)
.forEach(modelGroup -> validateModelGroup(modelGroup, maxRules));
}
Expand Down
14 changes: 0 additions & 14 deletions src/main/java/org/prebid/server/floors/model/PriceFloorRules.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,10 @@

import java.math.BigDecimal;

/**
* This model is a trade-off.
* <p>
* It defines both:
* 1. The contract for prebid server bidrequest.ext.prebid.floors field.
* 2. The contract for floors provider (assuming prebid server specific fields will not be overridden).
* <p>
* To make things better, it should be divided in two separate models:
* for prebid request and floors provider.
*/
@Value
@Builder(toBuilder = true)
public class PriceFloorRules {

// prebid server and floors provider fields

@JsonProperty("floorMin")
BigDecimal floorMin;

Expand All @@ -38,8 +26,6 @@ public class PriceFloorRules {

PriceFloorData data;

// prebid server specific fields

Boolean enabled;

@JsonProperty("fetchStatus")
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/prebid/server/floors/proto/FetchResult.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package org.prebid.server.floors.proto;

import lombok.Value;
import org.prebid.server.floors.model.PriceFloorRules;
import org.prebid.server.floors.model.PriceFloorData;

@Value(staticConstructor = "of")
public class FetchResult {

PriceFloorRules rules;
PriceFloorData rulesData;

FetchStatus fetchStatus;
}
Loading