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

Tests: Rework of calculation custom price granularity #2842

Merged
merged 6 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
57 changes: 32 additions & 25 deletions src/main/java/org/prebid/server/auction/CpmRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.text.NumberFormat;
import java.util.Locale;

/**
Expand All @@ -15,6 +15,7 @@
public class CpmRange {

private static final Locale LOCALE = Locale.US;
private static final int DEFAULT_PRECISION = 2;

private CpmRange() {
}
Expand All @@ -24,52 +25,58 @@ private CpmRange() {
*/
public static String fromCpm(BigDecimal cpm, PriceGranularity priceGranularity) {
final BigDecimal value = fromCpmAsNumber(cpm, priceGranularity);
return value != null
? format(value, ObjectUtils.defaultIfNull(priceGranularity.getPrecision(), 2))
: StringUtils.EMPTY;
return value != null ? format(value, priceGranularity.getPrecision()) : StringUtils.EMPTY;
}

/**
* Formats {@link BigDecimal} value with a given precision and return it's string representation.
*/
public static String format(BigDecimal value, Integer precision) {
final String format = "%%.%sf".formatted(precision);
return String.format(LOCALE, format, value);
return numberFormat(ObjectUtils.defaultIfNull(precision, DEFAULT_PRECISION)).format(value);
}

private static NumberFormat numberFormat(int precision) {
final NumberFormat numberFormat = NumberFormat.getInstance(LOCALE);
numberFormat.setRoundingMode(RoundingMode.FLOOR);
numberFormat.setMaximumFractionDigits(precision);
numberFormat.setMinimumFractionDigits(precision);
return numberFormat;
}

/**
* Rounding price by specified rules defined in {@link PriceGranularity} object and returns it in {@link BigDecimal}
* format
*/
public static BigDecimal fromCpmAsNumber(BigDecimal cpm, PriceGranularity priceGranularity) {
if (cpm.compareTo(BigDecimal.ZERO) <= 0) {
return null;
}

final BigDecimal rangeMax = priceGranularity.getRangesMax();
if (cpm.compareTo(rangeMax) > 0) {
return rangeMax;
}
final ExtGranularityRange range = findRangeFor(cpm, priceGranularity.getRanges());
final BigDecimal increment = range != null ? range.getIncrement() : null;

return increment != null ? cpm.divide(increment, 0, RoundingMode.FLOOR).multiply(increment) : null;
}

/**
* Returns range cpm fits in.
*/
private static ExtGranularityRange findRangeFor(BigDecimal cpm, List<ExtGranularityRange> ranges) {
BigDecimal min = BigDecimal.ZERO;
for (ExtGranularityRange range : ranges) {
if (includes(cpm, min, range.getMax())) {
return range;
BigDecimal increment = null;
for (ExtGranularityRange range : priceGranularity.getRanges()) {
final BigDecimal max = range.getMax();
if (cpm.compareTo(max) <= 0) {
increment = range.getIncrement();
break;
}
min = range.getMax();

min = max;
}
return null;

return increment != null ? calculate(cpm, min, increment) : null;
}

/**
* Checks if cpm fits into the borders.
*/
private static boolean includes(BigDecimal cpm, BigDecimal min, BigDecimal max) {
return cpm.compareTo(min) >= 0 && cpm.compareTo(max) <= 0;
private static BigDecimal calculate(BigDecimal cpm, BigDecimal min, BigDecimal increment) {
return cpm
.subtract(min)
.divide(increment, 0, RoundingMode.FLOOR)
.multiply(increment)
.add(min);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package org.prebid.server.proto.openrtb.ext.request;

import lombok.AllArgsConstructor;
import lombok.Value;

import java.math.BigDecimal;

/**
* Defines the contract for bidrequest.ext.prebid.targeting.pricegranularity.ranges[i]
*/
@AllArgsConstructor(staticName = "of")
@Value
@Value(staticConstructor = "of")
public class ExtGranularityRange {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import groovy.transform.ToString
@ToString(includeNames = true, ignoreNulls = true)
class Range {

Integer max
BigDecimal max
BigDecimal increment
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import org.prebid.server.functional.model.request.amp.AmpRequest
import org.prebid.server.functional.model.request.auction.AdServerTargeting
import org.prebid.server.functional.model.request.auction.BidRequest
import org.prebid.server.functional.model.request.auction.PrebidCache
import org.prebid.server.functional.model.request.auction.PriceGranularity
import org.prebid.server.functional.model.request.auction.Range
import org.prebid.server.functional.model.request.auction.StoredBidResponse
import org.prebid.server.functional.model.request.auction.Targeting
import org.prebid.server.functional.model.response.auction.Bid
Expand All @@ -19,6 +21,8 @@ import org.prebid.server.functional.service.PrebidServerException
import org.prebid.server.functional.service.PrebidServerService
import org.prebid.server.functional.util.PBSUtils

import java.math.RoundingMode

import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400
import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
Expand Down Expand Up @@ -416,6 +420,73 @@ class TargetingSpec extends BaseSpec {
assert response.targeting.isEmpty()
}

def "PBS amp should use ranges.max value for hb_pb targeting when bid.price biggest that ranges.max"() {
osulzhenko marked this conversation as resolved.
Show resolved Hide resolved
given: "Default amp request"
def ampRequest = AmpRequest.defaultAmpRequest

and: "Default bid request with targeting and stored bid response"
def storedBidResponseId = PBSUtils.randomString
def max = PBSUtils.getRandomDecimal(0)
osulzhenko marked this conversation as resolved.
Show resolved Hide resolved
def ampStoredRequest = BidRequest.defaultBidRequest.tap {
imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedBidResponseId, bidder: GENERIC)]
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap {
priceGranularity = new PriceGranularity().tap {
precision = 2
ranges = [new Range(max: max,increment: 1.0)]}
}
}

and: "Create and save stored request into DB"
def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
storedRequestDao.save(storedRequest)

and: "Create and save stored response into DB"
def storedBidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest).tap {
seatbid[0].bid[0].price = max.plus(1)
}
def storedResponse = new StoredResponse(responseId: storedBidResponseId, storedBidResponse: storedBidResponse)
storedResponseDao.save(storedResponse)

and: "Create and save account in the DB"
def account = new Account(uuid: ampRequest.account)
accountDao.save(account)

when: "PBS processes amp request"
def response = defaultPbsService.sendAmpRequest(ampRequest)

then: "Response should contain targeting hb_pb"
assert response.targeting["hb_pb"] == String.format("%,.2f", max.setScale(2, RoundingMode.DOWN))
}

def "PBS auction should use ranges.max value for hb_pb targeting when bid.price biggest that ranges.max"() {
osulzhenko marked this conversation as resolved.
Show resolved Hide resolved
given: "Default bid request"
def storedBidResponseId = PBSUtils.randomString
def max = PBSUtils.getRandomDecimal(0)
def bidRequest = BidRequest.defaultBidRequest.tap {
imp[0].ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedBidResponseId, bidder: GENERIC)]
ext.prebid.targeting = Targeting.createWithAllValuesSetTo(true).tap {
priceGranularity = new PriceGranularity().tap {
precision = 2
ranges = [new Range(max: max,increment: 1.0)]
}
}
}

and: "Create and save stored response into DB"
def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
seatbid[0].bid[0].price = max.plus(1)
}
def storedResponse = new StoredResponse(responseId: storedBidResponseId, storedBidResponse: storedBidResponse)
storedResponseDao.save(storedResponse)

when: "PBS processes auction request"
def response = defaultPbsService.sendAuctionRequest(bidRequest)

then: "Response should contain targeting hb_pb"
def targetingKeyMap = response.seatbid?.first()?.bid?.first()?.ext?.prebid?.targeting
assert targetingKeyMap["hb_pb"] == String.format("%,.2f", max.setScale(2, RoundingMode.DOWN))
}

def "PBS auction should use default targeting prefix when ext.prebid.targeting.prefix is biggest that twenty"() {
given: "Bid request with long targeting prefix"
def prefix = PBSUtils.getRandomString(30)
Expand Down
16 changes: 16 additions & 0 deletions src/test/java/org/prebid/server/auction/CpmRangeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.math.BigDecimal;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -121,6 +122,21 @@ public void fromCpmAsNumberShouldReturnExpectedResult() {
assertThat(result.compareTo(BigDecimal.valueOf(2.33))).isEqualTo(0);
}

@Test
public void fromCpmAsNumberShouldReturnExpectedResultForMultipleRanges() {
// given
final PriceGranularity priceGranularity = PriceGranularity.createFromExtPriceGranularity(
ExtPriceGranularity.of(2, asList(
ExtGranularityRange.of(BigDecimal.valueOf(1.5), BigDecimal.ONE),
ExtGranularityRange.of(BigDecimal.valueOf(2.5), BigDecimal.valueOf(1.2)))));

// when
final BigDecimal result = CpmRange.fromCpmAsNumber(BigDecimal.valueOf(2), priceGranularity);

// then
assertThat(result.compareTo(BigDecimal.valueOf(1.5))).isEqualTo(0);
}

@Test
public void fromCpmAsNumberShouldRetunNullIfPriceDoesNotFitToRange() {
// given
Expand Down