Skip to content

Request Provided Currency Rates #1753

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

Merged
merged 32 commits into from
Jun 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2989689
First draft
guscarreon Feb 23, 2021
4d3196c
update with latest master
guscarreon Feb 23, 2021
f50db9f
Last test case working
guscarreon Mar 15, 2021
1e69876
Merge branch 'PBS-698' of https://github.com/guscarreon/prebid-server…
Mar 15, 2021
40afa09
Added Json test, corrected descriptions
Mar 16, 2021
5605a98
Mansi's review and resolve merge conflict
guscarreon Mar 23, 2021
57dde30
resolved openrtb2 merge conflicts
Mar 26, 2021
8b17152
Added inverse custom conversion rates
Apr 12, 2021
86b4350
Scott's eedback
Apr 13, 2021
95c82d3
Corrected error assertion
Apr 13, 2021
177e1e1
errortypes import in exchange.go
Apr 13, 2021
c1f136a
Merge branch 'master' into PBS-698
Apr 13, 2021
c0c81d7
Corrected test to not have a nil e.bidIDGenerator
Apr 13, 2021
5376694
update with latest master
Apr 16, 2021
348d83b
Created rateEngine Rates implementation and reingeneered getAuctionCu…
May 5, 2021
ed48b9c
remove .swp file
May 5, 2021
dc36e68
Corrected test cases
May 5, 2021
8f7df0a
Merge branch 'master' into PBS-698
May 5, 2021
3731241
Corrected native json test
May 5, 2021
00dda98
Scott's review. Renaming objects, getting rid of []Conversions
May 7, 2021
72aa90c
Merge branch 'master' into PBS-698
May 7, 2021
a7bba04
Corrected expected error message
May 7, 2021
9adf8b2
Renamed GroupedConversions to AggregateConversions and rate_engines.g…
May 11, 2021
11fa5d2
Removed logically dead code in getAuctionCurrencyRates
May 17, 2021
6ff91ed
Merge branch 'master' into PBS-698
May 21, 2021
e454459
Implemented NoConversionRate error type and Scott's review
May 24, 2021
b60b68f
Conversions disabled logic, warning and aggregateConversions GetRate …
May 25, 2021
561dff8
Mansi's review June 1st
Jun 2, 2021
b3db4a2
Mansi's review Jun 8th
Jun 8, 2021
663dde0
Scott's review June 8th
Jun 9, 2021
32a3a22
Scott's review June 9th
Jun 9, 2021
c9ee8e1
Mansi's feedback in regards of file and directory naming and addition…
Jun 9, 2021
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
41 changes: 41 additions & 0 deletions currency/aggregate_conversions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package currency

// AggregateConversions contains both the request-defined currency rate
// map found in request.ext.prebid.currency and the currencies conversion
// rates fetched with the RateConverter object defined in rate_converter.go
// It implements the Conversions interface.
type AggregateConversions struct {
customRates, serverRates Conversions
}

// NewAggregateConversions expects both customRates and pbsRates to not be nil
func NewAggregateConversions(customRates, pbsRates Conversions) *AggregateConversions {
return &AggregateConversions{
customRates: customRates,
serverRates: pbsRates,
}
}

// GetRate returns the conversion rate between two currencies prioritizing
// the customRates currency rate over that of the PBS currency rate service
// returns an error if both Conversions objects return error.
func (re *AggregateConversions) GetRate(from string, to string) (float64, error) {
rate, err := re.customRates.GetRate(from, to)
if err == nil {
return rate, nil
} else if _, isMissingRateErr := err.(ConversionRateNotFound); !isMissingRateErr {
// other error, return the error
return 0, err
}

// because the custom rates' GetRate() call returned an error other than "conversion
// rate not found", there's nothing wrong with the 3 letter currency code so let's
// try the PBS rates instead
return re.serverRates.GetRate(from, to)
}

// GetRates is not implemented for AggregateConversions . There is no need to call
// this function for this scenario.
func (r *AggregateConversions) GetRates() *map[string]map[string]float64 {
return nil
}
89 changes: 89 additions & 0 deletions currency/aggregate_conversions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package currency

import (
"errors"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestGroupedGetRate(t *testing.T) {

// Setup:
customRates := NewRates(time.Now(), map[string]map[string]float64{
"USD": {
"GBP": 3.00,
"EUR": 2.00,
},
})

pbsRates := NewRates(time.Now(), map[string]map[string]float64{
"USD": {
"GBP": 4.00,
"MXN": 10.00,
},
})
aggregateConversions := NewAggregateConversions(customRates, pbsRates)

// Test cases:
type aTest struct {
desc string
from string
to string
expectedRate float64
}

testGroups := []struct {
expectedError error
testCases []aTest
}{
{
expectedError: nil,
testCases: []aTest{
{"Found in both, return custom rate", "USD", "GBP", 3.00},
{"Found in both, return inverse custom rate", "GBP", "USD", 1 / 3.00},
{"Found in custom rates only", "USD", "EUR", 2.00},
{"Found in PBS rates only", "USD", "MXN", 10.00},
{"Found in PBS rates only, return inverse", "MXN", "USD", 1 / 10.00},
{"Same currency, return unitary rate", "USD", "USD", 1},
},
},
{
expectedError: errors.New("currency: tag is not well-formed"),
testCases: []aTest{
{"From-currency three-digit code malformed", "XX", "EUR", 0},
{"To-currency three-digit code malformed", "GBP", "", 0},
{"Both currencies malformed", "", "", 0},
},
},
{
expectedError: errors.New("currency: tag is not a recognized currency"),
testCases: []aTest{
{"From-currency three-digit code not found", "FOO", "EUR", 0},
{"To-currency three-digit code not found", "GBP", "BAR", 0},
},
},
{
expectedError: ConversionRateNotFound{"GBP", "EUR"},
testCases: []aTest{
{"Valid three-digit currency codes, but conversion rate not found", "GBP", "EUR", 0},
},
},
}

for _, group := range testGroups {
for _, tc := range group.testCases {
// Execute:
rate, err := aggregateConversions.GetRate(tc.from, tc.to)

// Verify:
assert.Equal(t, tc.expectedRate, rate, "conversion rate doesn't match the expected rate: %s\n", tc.desc)
if group.expectedError != nil {
assert.Error(t, err, "error doesn't match expected: %s\n", tc.desc)
} else {
assert.NoError(t, err, "err should be nil: %s\n", tc.desc)
}
}
}
}
4 changes: 1 addition & 3 deletions currency/constant_rates.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package currency

import (
"fmt"

"golang.org/x/text/currency"
)

Expand All @@ -29,7 +27,7 @@ func (r *ConstantRates) GetRate(from string, to string) (float64, error) {
}

if fromUnit.String() != toUnit.String() {
return 0, fmt.Errorf("Constant rates doesn't proceed to any conversions, cannot convert '%s' => '%s'", fromUnit.String(), toUnit.String())
return 0, ConversionRateNotFound{fromUnit.String(), toUnit.String()}
}

return 1, nil
Expand Down
13 changes: 13 additions & 0 deletions currency/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package currency

import "fmt"

// ConversionRateNotFound is thrown by the currency.Conversions GetRate(from string, to string) method
// when the conversion rate between the two currencies, nor its reciprocal, can be found.
type ConversionRateNotFound struct {
FromCur, ToCur string
}

func (err ConversionRateNotFound) Error() string {
return fmt.Sprintf("Currency conversion rate not found: '%s' => '%s'", err.FromCur, err.ToCur)
}
14 changes: 8 additions & 6 deletions currency/rates.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package currency
import (
"encoding/json"
"errors"
"fmt"
"time"

"golang.org/x/text/currency"
Expand Down Expand Up @@ -45,8 +44,11 @@ func (r *Rates) UnmarshalJSON(b []byte) error {
return nil
}

// GetRate returns the conversion rate between two currencies
// returns an error in case the conversion rate between the two given currencies is not in the currencies rates map
// GetRate returns the conversion rate between two currencies or:
// - An error if one of the currency strings is not well-formed
// - An error if any of the currency strings is not a recognized currency code.
// - A MissingConversionRate error in case the conversion rate between the two
// given currencies is not in the currencies rates map
func (r *Rates) GetRate(from string, to string) (float64, error) {
var err error
fromUnit, err := currency.ParseISO(from)
Expand All @@ -63,12 +65,12 @@ func (r *Rates) GetRate(from string, to string) (float64, error) {
if r.Conversions != nil {
if conversion, present := r.Conversions[fromUnit.String()][toUnit.String()]; present {
// In case we have an entry FROM -> TO
return conversion, err
return conversion, nil
} else if conversion, present := r.Conversions[toUnit.String()][fromUnit.String()]; present {
// In case we have an entry TO -> FROM
return 1 / conversion, err
return 1 / conversion, nil
}
return 0, fmt.Errorf("Currency conversion rate not found: '%s' => '%s'", fromUnit.String(), toUnit.String())
return 0, ConversionRateNotFound{fromUnit.String(), toUnit.String()}
}
return 0, errors.New("rates are nil")
}
Expand Down
29 changes: 29 additions & 0 deletions endpoints/openrtb2/auction.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/prebid/prebid-server/util/httputil"
"github.com/prebid/prebid-server/util/iputil"
"golang.org/x/net/publicsuffix"
"golang.org/x/text/currency"
)

const storedRequestTimeoutMillis = 50
Expand Down Expand Up @@ -343,6 +344,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error {
if err := deps.validateEidPermissions(bidExt, aliases); err != nil {
return []error{err}
}

if err := validateCustomRates(bidExt.Prebid.CurrencyConversions); err != nil {
return []error{err}
}
}

if (req.Site == nil && req.App == nil) || (req.Site != nil && req.App != nil) {
Expand Down Expand Up @@ -437,6 +442,30 @@ func validateSChains(req *openrtb_ext.ExtRequest) error {
return err
}

// validateCustomRates throws a bad input error if any of the 3-digit currency codes found in
// the bidRequest.ext.prebid.currency field is invalid, malfomed or does not represent any actual
// currency. No error is thrown if bidRequest.ext.prebid.currency is invalid or empty.
func validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) error {
if bidReqCurrencyRates == nil {
return nil
}

for fromCurrency, rates := range bidReqCurrencyRates.ConversionRates {
// Check if fromCurrency is a valid 3-letter currency code
if _, err := currency.ParseISO(fromCurrency); err != nil {
return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", fromCurrency)}
}

// Check if currencies mapped to fromCurrency are valid 3-letter currency codes
for toCurrency := range rates {
if _, err := currency.ParseISO(toCurrency); err != nil {
return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", toCurrency)}
}
}
}
return nil
}

func (deps *endpointDeps) validateEidPermissions(req *openrtb_ext.ExtRequest, aliases map[string]string) error {
if req == nil || req.Prebid.Data == nil {
return nil
Expand Down
Loading