Skip to content

Commit

Permalink
Pass currency converter to modules (#3278)
Browse files Browse the repository at this point in the history
  • Loading branch information
pm-nilesh-chate authored Nov 20, 2023
1 parent 06e1145 commit e5c920a
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 258 deletions.
42 changes: 42 additions & 0 deletions currency/currency.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package currency

import (
"github.com/prebid/prebid-server/v2/openrtb_ext"
)

func GetAuctionCurrencyRates(currencyConverter *RateConverter, requestRates *openrtb_ext.ExtRequestCurrency) Conversions {
if currencyConverter == nil && requestRates == nil {
return nil
}

if requestRates == nil {
// No bidRequest.ext.currency field was found, use PBS rates as usual
return currencyConverter.Rates()
}

// currencyConverter will never be nil, refer main.serve(), adding this check for future usecases
if currencyConverter == nil {
return NewRates(requestRates.ConversionRates)
}

// If bidRequest.ext.currency.usepbsrates is nil, we understand its value as true. It will be false
// only if it's explicitly set to false
usePbsRates := requestRates.UsePBSRates == nil || *requestRates.UsePBSRates

if !usePbsRates {
// At this point, we can safely assume the ConversionRates map is not empty because
// validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) would have
// thrown an error under such conditions.
return NewRates(requestRates.ConversionRates)
}

// Both PBS and custom rates can be used, check if ConversionRates is not empty
if len(requestRates.ConversionRates) == 0 {
// Custom rates map is empty, use PBS rates only
return currencyConverter.Rates()
}

// Return an AggregateConversions object that includes both custom and PBS currency rates but will
// prioritize custom rates over PBS rates whenever a currency rate is found in both
return NewAggregateConversions(NewRates(requestRates.ConversionRates), currencyConverter.Rates())
}
20 changes: 20 additions & 0 deletions currency/currency_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package currency

import (
"io"
"net/http"
"strings"
)

// MockCurrencyRatesHttpClient is a simple http client mock returning a constant response body
type MockCurrencyRatesHttpClient struct {
ResponseBody string
}

func (m *MockCurrencyRatesHttpClient) Do(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(m.ResponseBody)),
}, nil
}
199 changes: 199 additions & 0 deletions currency/currency_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package currency

import (
"testing"
"time"

"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/prebid/prebid-server/v2/util/jsonutil"
"github.com/prebid/prebid-server/v2/util/ptrutil"
"github.com/stretchr/testify/assert"
)

func TestGetAuctionCurrencyRates(t *testing.T) {
pbsRates := map[string]map[string]float64{
"MXN": {
"USD": 20.13,
"EUR": 27.82,
"JPY": 5.09, // "MXN" to "JPY" rate not found in customRates
},
}

customRates := map[string]map[string]float64{
"MXN": {
"USD": 25.00, // different rate than in pbsRates
"EUR": 27.82, // same as in pbsRates
"GBP": 31.12, // not found in pbsRates at all
},
}

expectedRateEngineRates := map[string]map[string]float64{
"MXN": {
"USD": 25.00, // rates engine will prioritize the value found in custom rates
"EUR": 27.82, // same value in both the engine reads the custom entry first
"JPY": 5.09, // the engine will find it in the pbsRates conversions
"GBP": 31.12, // the engine will find it in the custom conversions
},
}

setupMockRateConverter := func(pbsRates map[string]map[string]float64) *RateConverter {
if pbsRates == nil {
return nil
}

jsonPbsRates, err := jsonutil.Marshal(pbsRates)
if err != nil {
t.Fatalf("Failed to marshal PBS rates: %v", err)
}

// Init mock currency conversion service
mockCurrencyClient := &MockCurrencyRatesHttpClient{
ResponseBody: `{"dataAsOf":"2018-09-12","conversions":` + string(jsonPbsRates) + `}`,
}

return NewRateConverter(
mockCurrencyClient,
"currency.fake.com",
24*time.Hour,
)
}

type args struct {
currencyConverter *RateConverter
requestRates *openrtb_ext.ExtRequestCurrency
}
tests := []struct {
name string
args args
assertRates map[string]map[string]float64
}{
{
name: "valid ConversionRates, valid pbsRates, false UsePBSRates. Resulting rates identical to customRates",
args: args{
currencyConverter: setupMockRateConverter(pbsRates),
requestRates: &openrtb_ext.ExtRequestCurrency{
ConversionRates: customRates,
UsePBSRates: ptrutil.ToPtr(false),
},
},
assertRates: customRates,
},
{
name: "valid ConversionRates, valid pbsRates, true UsePBSRates. Resulting rates are a mix but customRates gets priority",
args: args{
currencyConverter: setupMockRateConverter(pbsRates),
requestRates: &openrtb_ext.ExtRequestCurrency{
ConversionRates: customRates,
UsePBSRates: ptrutil.ToPtr(true),
},
},
assertRates: expectedRateEngineRates,
},
{
name: "valid ConversionRates, nil pbsRates, false UsePBSRates. Resulting rates identical to customRates",
args: args{
currencyConverter: nil,
requestRates: &openrtb_ext.ExtRequestCurrency{
ConversionRates: customRates,
UsePBSRates: ptrutil.ToPtr(false),
},
},
assertRates: customRates,
},
{
name: "valid ConversionRates, nil pbsRates, true UsePBSRates. Resulting rates identical to customRates",
args: args{
currencyConverter: nil,
requestRates: &openrtb_ext.ExtRequestCurrency{
ConversionRates: customRates,
UsePBSRates: ptrutil.ToPtr(true),
},
},
assertRates: customRates,
},
{
name: "empty ConversionRates, valid pbsRates, false UsePBSRates. Because pbsRates cannot be used, disable currency conversion",
args: args{
currencyConverter: setupMockRateConverter(pbsRates),
requestRates: &openrtb_ext.ExtRequestCurrency{
// ConversionRates inCustomRates not initialized makes for a zero-length map
UsePBSRates: ptrutil.ToPtr(false),
},
},
assertRates: nil,
},
{
name: "nil ConversionRates, valid pbsRates, true UsePBSRates. Resulting rates will be identical to pbsRates",
args: args{
currencyConverter: setupMockRateConverter(pbsRates),
requestRates: nil,
},
assertRates: pbsRates,
},
{
name: "empty ConversionRates, nil pbsRates, false UsePBSRates. No conversion rates available, disable currency conversion",
args: args{
currencyConverter: setupMockRateConverter(pbsRates),
requestRates: &openrtb_ext.ExtRequestCurrency{
// ConversionRates inCustomRates not initialized makes for a zero-length map
UsePBSRates: ptrutil.ToPtr(false),
},
},
assertRates: nil,
},

{
name: "empty ConversionRates, nil pbsRates, true UsePBSRates. No conversion rates available, disable currency conversion",
args: args{
currencyConverter: nil,
requestRates: &openrtb_ext.ExtRequestCurrency{
// ConversionRates inCustomRates not initialized makes for a zero-length map
UsePBSRates: ptrutil.ToPtr(true),
},
},
assertRates: nil,
},
{
name: "nil customRates, nil pbsRates. No conversion rates available, disable currency conversion",
args: args{
currencyConverter: nil,
requestRates: nil,
},
assertRates: nil,
},
{
name: "empty ConversionRates, valid pbsRates, true UsePBSRates. Resulting rates will be identical to pbsRates",
args: args{
currencyConverter: setupMockRateConverter(pbsRates),
requestRates: &openrtb_ext.ExtRequestCurrency{
// ConversionRates inCustomRates not initialized makes for a zero-length map
UsePBSRates: ptrutil.ToPtr(true),
},
},
assertRates: pbsRates,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.args.currencyConverter != nil {
tt.args.currencyConverter.Run()
}
auctionRates := GetAuctionCurrencyRates(tt.args.currencyConverter, tt.args.requestRates)
if tt.args.currencyConverter == nil && tt.args.requestRates == nil && tt.assertRates == nil {
assert.Nil(t, auctionRates)
} else if tt.assertRates == nil {
rate, err := auctionRates.GetRate("USD", "MXN")
assert.Error(t, err, tt.name)
assert.Equal(t, float64(0), rate, tt.name)
} else {
for fromCurrency, rates := range tt.assertRates {
for toCurrency, expectedRate := range rates {
actualRate, err := auctionRates.GetRate(fromCurrency, toCurrency)
assert.NoError(t, err, tt.name)
assert.Equal(t, expectedRate, actualRate, tt.name)
}
}
}
})
}
}
30 changes: 1 addition & 29 deletions exchange/exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
}

// Get currency rates conversions for the auction
conversions := e.getAuctionCurrencyRates(requestExtPrebid.CurrencyConversions)
conversions := currency.GetAuctionCurrencyRates(e.currencyConverter, requestExtPrebid.CurrencyConversions)

var floorErrs []error
if e.priceFloorEnabled {
Expand Down Expand Up @@ -1362,34 +1362,6 @@ func (e *exchange) getBidCacheInfo(bid *entities.PbsOrtbBid, auction *auction) (
return
}

func (e *exchange) getAuctionCurrencyRates(requestRates *openrtb_ext.ExtRequestCurrency) currency.Conversions {
if requestRates == nil {
// No bidRequest.ext.currency field was found, use PBS rates as usual
return e.currencyConverter.Rates()
}

// If bidRequest.ext.currency.usepbsrates is nil, we understand its value as true. It will be false
// only if it's explicitly set to false
usePbsRates := requestRates.UsePBSRates == nil || *requestRates.UsePBSRates

if !usePbsRates {
// At this point, we can safely assume the ConversionRates map is not empty because
// validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) would have
// thrown an error under such conditions.
return currency.NewRates(requestRates.ConversionRates)
}

// Both PBS and custom rates can be used, check if ConversionRates is not empty
if len(requestRates.ConversionRates) == 0 {
// Custom rates map is empty, use PBS rates only
return e.currencyConverter.Rates()
}

// Return an AggregateConversions object that includes both custom and PBS currency rates but will
// prioritize custom rates over PBS rates whenever a currency rate is found in both
return currency.NewAggregateConversions(currency.NewRates(requestRates.ConversionRates), e.currencyConverter.Rates())
}

func findCacheID(bid *entities.PbsOrtbBid, auction *auction) (string, bool) {
if bid != nil && bid.Bid != nil && auction != nil {
if id, found := auction.cacheIds[bid.Bid]; found {
Expand Down
Loading

0 comments on commit e5c920a

Please sign in to comment.