Skip to content

Commit

Permalink
Feature: Cross-currency conversion capability (#3084)
Browse files Browse the repository at this point in the history
* Added cross-currency conversion capability and updated precision determination

* Small update to test
  • Loading branch information
Net-burst authored Apr 2, 2024
1 parent f5a365c commit 5470ffa
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -272,7 +273,13 @@ private static BigDecimal getConversionRate(Map<String, Map<String, BigDecimal>>
return conversionRate;
}

return findIntermediateConversionRate(directCurrencyRates, reverseCurrencyRates);
final BigDecimal intermediateConversionRate = findIntermediateConversionRate(directCurrencyRates,
reverseCurrencyRates);
if (intermediateConversionRate != null) {
return intermediateConversionRate;
}

return findCrossConversionRate(currencyConversionRates, fromCurrency, toCurrency);
}

/**
Expand All @@ -286,7 +293,8 @@ private static BigDecimal findReverseConversionRate(Map<String, BigDecimal> curr
: null;

return reverseConversionRate != null
? BigDecimal.ONE.divide(reverseConversionRate, reverseConversionRate.precision(),
? BigDecimal.ONE.divide(reverseConversionRate,
getRatePrecision(reverseConversionRate),
RoundingMode.HALF_EVEN)
: null;
}
Expand All @@ -310,15 +318,38 @@ private static BigDecimal findIntermediateConversionRate(Map<String, BigDecimal>
final BigDecimal reverseCurrencyRateIntermediate = reverseCurrencyRates.get(sharedCurrency);
conversionRate = directCurrencyRateIntermediate.divide(reverseCurrencyRateIntermediate,
// chose largest precision among intermediate rates
reverseCurrencyRateIntermediate.compareTo(directCurrencyRateIntermediate) > 0
? reverseCurrencyRateIntermediate.precision()
: directCurrencyRateIntermediate.precision(),
getRatePrecision(directCurrencyRateIntermediate, reverseCurrencyRateIntermediate),
RoundingMode.HALF_EVEN);
}
}
return conversionRate;
}

private static BigDecimal findCrossConversionRate(Map<String, Map<String, BigDecimal>> currencyConversionRates,
String fromCurrency,
String toCurrency) {
for (Map<String, BigDecimal> rates : currencyConversionRates.values()) {
final BigDecimal fromRate = rates.get(fromCurrency);
final BigDecimal toRate = rates.get(toCurrency);
if (fromRate != null && toRate != null) {
return toRate.divide(fromRate,
getRatePrecision(fromRate, toRate),
RoundingMode.HALF_EVEN);
}
}

return null;
}

private static int getRatePrecision(BigDecimal... rates) {
final int precision = Arrays.stream(rates)
.map(BigDecimal::precision)
.max(Integer::compareTo)
.orElse(DEFAULT_PRICE_PRECISION);

return Math.max(precision, DEFAULT_PRICE_PRECISION);
}

private boolean isRatesStale() {
if (lastUpdated == null) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue

enum Currency {

USD, EUR, GBP, JPY, BOGUS
USD, EUR, GBP, JPY, CHF, CAD, BOGUS

@JsonValue
String getValue() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversio

import java.math.RoundingMode

import static org.prebid.server.functional.model.Currency.CAD
import static org.prebid.server.functional.model.Currency.CHF
import static org.prebid.server.functional.model.Currency.EUR
import static org.prebid.server.functional.model.Currency.JPY
import static org.prebid.server.functional.model.Currency.USD
Expand All @@ -18,8 +20,11 @@ class CurrencySpec extends BaseSpec {

private static final Currency DEFAULT_CURRENCY = USD
private static final int PRICE_PRECISION = 3
private static final Map<Currency, Map<Currency, BigDecimal>> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.8872327211427558,
(JPY): 114.12],
private static final Map<Currency, Map<Currency, BigDecimal>> DEFAULT_CURRENCY_RATES = [(USD): [(USD): 1,
(EUR): 0.9249838127832763,
(CHF): 0.9033391915641477,
(JPY): 151.1886041994265,
(CAD): 1.357136250115623],
(EUR): [(USD): 1.3429368029739777]]
private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap {
setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES))
Expand Down Expand Up @@ -113,6 +118,34 @@ class CurrencySpec extends BaseSpec {
JPY || USD
}

def "PBS should use cross currency conversion when direct, reverse and intermediate conversion is not available"() {
given: "Default BidRequest with #requestCurrency currency"
def bidRequest = BidRequest.defaultBidRequest.tap { cur = [requestCurrency] }

and: "Default Bid with a #bidCurrency currency"
def bidderResponse = BidResponse.getDefaultBidResponse(bidRequest).tap { cur = bidCurrency }
bidder.setResponse(bidRequest.id, bidderResponse)

when: "PBS processes auction request"
def bidResponse = pbsService.sendAuctionRequest(bidRequest)

then: "Auction response should contain bid in #requestCurrency currency"
assert bidResponse.cur == requestCurrency
def bidPrice = bidResponse.seatbid[0].bid[0].price
assert bidPrice == convertCurrency(bidderResponse.seatbid[0].bid[0].price, bidCurrency, requestCurrency)
assert bidResponse.seatbid[0].bid[0].ext.origbidcpm == bidderResponse.seatbid[0].bid[0].price
assert bidResponse.seatbid[0].bid[0].ext.origbidcur == bidCurrency

where:
requestCurrency || bidCurrency
CHF || JPY
JPY || CHF
CAD || JPY
JPY || CAD
EUR || CHF
CHF || EUR
}

private static Map<String, String> getExternalCurrencyConverterConfig() {
["auction.ad-server-currency" : DEFAULT_CURRENCY as String,
"currency-converter.external-rates.enabled" : "true",
Expand All @@ -129,11 +162,26 @@ class CurrencySpec extends BaseSpec {
def conversionRate
if (fromCurrency == toCurrency) {
conversionRate = 1
} else if (fromCurrency in DEFAULT_CURRENCY_RATES) {
} else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) {
conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency]
} else {
} else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) {
conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency]
} else {
conversionRate = getCrossConversionRate(fromCurrency, toCurrency)
}
conversionRate
}

private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) {
for (Map<Currency, BigDecimal> rates : DEFAULT_CURRENCY_RATES.values()) {
def fromRate = rates?[fromCurrency]
def toRate = rates?[toCurrency]

if (fromRate && toRate) {
return toRate / fromRate
}
}

null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public void convertCurrencyShouldUseLatestRatesIfRequestRatesIsNull() {
BigDecimal.ONE, givenBidRequestWithCurrencies(null, false), EUR, GBP);

// then
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(0.770));
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(0.769));
}

@Test
Expand All @@ -220,7 +220,7 @@ public void convertCurrencyShouldUseConversionRateFromServerIfusepbsratesIsTrue(
givenBidRequestWithCurrencies(requestConversionRates, true), EUR, GBP);

// then
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(0.770));
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(0.769));
}

@Test
Expand Down Expand Up @@ -251,6 +251,36 @@ public void convertCurrencyShouldUseLatestRatesIfMultiplierWasNotFoundInRequestR
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(5));
}

@Test
public void convertCurrencyShouldUseCrossRateIfOtherRatesAreNotAvailable() {
// given
final Map<String, Map<String, BigDecimal>> requestConversionRates = new HashMap<>();
requestConversionRates.put(USD, Map.of(GBP, BigDecimal.valueOf(2),
EUR, BigDecimal.valueOf(0.5)));

// when
final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
givenBidRequestWithCurrencies(requestConversionRates, false), GBP, EUR);

// then
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(0.25));
}

@Test
public void convertCurrencyShouldUseCrossRateIfOtherRatesAreNotAvailableReversed() {
// given
final Map<String, Map<String, BigDecimal>> requestConversionRates = new HashMap<>();
requestConversionRates.put(USD, Map.of(GBP, BigDecimal.valueOf(2),
EUR, BigDecimal.valueOf(0.5)));

// when
final BigDecimal price = currencyService.convertCurrency(BigDecimal.ONE,
givenBidRequestWithCurrencies(requestConversionRates, false), EUR, GBP);

// then
assertThat(price).isEqualByComparingTo(BigDecimal.valueOf(4));
}

@Test
public void convertCurrencyShouldReturnSamePriceIfBidCurrencyIsNullAndServerCurrencyUSD() {
// when
Expand Down

0 comments on commit 5470ffa

Please sign in to comment.