Skip to content

Commit e096d94

Browse files
guscarreonjizeyopera
authored andcommitted
Request Provided Currency Rates (prebid#1753)
1 parent e546edb commit e096d94

File tree

50 files changed

+1761
-106
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1761
-106
lines changed

currency/aggregate_conversions.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package currency
2+
3+
// AggregateConversions contains both the request-defined currency rate
4+
// map found in request.ext.prebid.currency and the currencies conversion
5+
// rates fetched with the RateConverter object defined in rate_converter.go
6+
// It implements the Conversions interface.
7+
type AggregateConversions struct {
8+
customRates, serverRates Conversions
9+
}
10+
11+
// NewAggregateConversions expects both customRates and pbsRates to not be nil
12+
func NewAggregateConversions(customRates, pbsRates Conversions) *AggregateConversions {
13+
return &AggregateConversions{
14+
customRates: customRates,
15+
serverRates: pbsRates,
16+
}
17+
}
18+
19+
// GetRate returns the conversion rate between two currencies prioritizing
20+
// the customRates currency rate over that of the PBS currency rate service
21+
// returns an error if both Conversions objects return error.
22+
func (re *AggregateConversions) GetRate(from string, to string) (float64, error) {
23+
rate, err := re.customRates.GetRate(from, to)
24+
if err == nil {
25+
return rate, nil
26+
} else if _, isMissingRateErr := err.(ConversionRateNotFound); !isMissingRateErr {
27+
// other error, return the error
28+
return 0, err
29+
}
30+
31+
// because the custom rates' GetRate() call returned an error other than "conversion
32+
// rate not found", there's nothing wrong with the 3 letter currency code so let's
33+
// try the PBS rates instead
34+
return re.serverRates.GetRate(from, to)
35+
}
36+
37+
// GetRates is not implemented for AggregateConversions . There is no need to call
38+
// this function for this scenario.
39+
func (r *AggregateConversions) GetRates() *map[string]map[string]float64 {
40+
return nil
41+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package currency
2+
3+
import (
4+
"errors"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestGroupedGetRate(t *testing.T) {
12+
13+
// Setup:
14+
customRates := NewRates(time.Now(), map[string]map[string]float64{
15+
"USD": {
16+
"GBP": 3.00,
17+
"EUR": 2.00,
18+
},
19+
})
20+
21+
pbsRates := NewRates(time.Now(), map[string]map[string]float64{
22+
"USD": {
23+
"GBP": 4.00,
24+
"MXN": 10.00,
25+
},
26+
})
27+
aggregateConversions := NewAggregateConversions(customRates, pbsRates)
28+
29+
// Test cases:
30+
type aTest struct {
31+
desc string
32+
from string
33+
to string
34+
expectedRate float64
35+
}
36+
37+
testGroups := []struct {
38+
expectedError error
39+
testCases []aTest
40+
}{
41+
{
42+
expectedError: nil,
43+
testCases: []aTest{
44+
{"Found in both, return custom rate", "USD", "GBP", 3.00},
45+
{"Found in both, return inverse custom rate", "GBP", "USD", 1 / 3.00},
46+
{"Found in custom rates only", "USD", "EUR", 2.00},
47+
{"Found in PBS rates only", "USD", "MXN", 10.00},
48+
{"Found in PBS rates only, return inverse", "MXN", "USD", 1 / 10.00},
49+
{"Same currency, return unitary rate", "USD", "USD", 1},
50+
},
51+
},
52+
{
53+
expectedError: errors.New("currency: tag is not well-formed"),
54+
testCases: []aTest{
55+
{"From-currency three-digit code malformed", "XX", "EUR", 0},
56+
{"To-currency three-digit code malformed", "GBP", "", 0},
57+
{"Both currencies malformed", "", "", 0},
58+
},
59+
},
60+
{
61+
expectedError: errors.New("currency: tag is not a recognized currency"),
62+
testCases: []aTest{
63+
{"From-currency three-digit code not found", "FOO", "EUR", 0},
64+
{"To-currency three-digit code not found", "GBP", "BAR", 0},
65+
},
66+
},
67+
{
68+
expectedError: ConversionRateNotFound{"GBP", "EUR"},
69+
testCases: []aTest{
70+
{"Valid three-digit currency codes, but conversion rate not found", "GBP", "EUR", 0},
71+
},
72+
},
73+
}
74+
75+
for _, group := range testGroups {
76+
for _, tc := range group.testCases {
77+
// Execute:
78+
rate, err := aggregateConversions.GetRate(tc.from, tc.to)
79+
80+
// Verify:
81+
assert.Equal(t, tc.expectedRate, rate, "conversion rate doesn't match the expected rate: %s\n", tc.desc)
82+
if group.expectedError != nil {
83+
assert.Error(t, err, "error doesn't match expected: %s\n", tc.desc)
84+
} else {
85+
assert.NoError(t, err, "err should be nil: %s\n", tc.desc)
86+
}
87+
}
88+
}
89+
}

currency/constant_rates.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package currency
22

33
import (
4-
"fmt"
5-
64
"golang.org/x/text/currency"
75
)
86

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

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

3533
return 1, nil

currency/errors.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package currency
2+
3+
import "fmt"
4+
5+
// ConversionRateNotFound is thrown by the currency.Conversions GetRate(from string, to string) method
6+
// when the conversion rate between the two currencies, nor its reciprocal, can be found.
7+
type ConversionRateNotFound struct {
8+
FromCur, ToCur string
9+
}
10+
11+
func (err ConversionRateNotFound) Error() string {
12+
return fmt.Sprintf("Currency conversion rate not found: '%s' => '%s'", err.FromCur, err.ToCur)
13+
}

currency/rates.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package currency
33
import (
44
"encoding/json"
55
"errors"
6-
"fmt"
76
"time"
87

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

48-
// GetRate returns the conversion rate between two currencies
49-
// returns an error in case the conversion rate between the two given currencies is not in the currencies rates map
47+
// GetRate returns the conversion rate between two currencies or:
48+
// - An error if one of the currency strings is not well-formed
49+
// - An error if any of the currency strings is not a recognized currency code.
50+
// - A MissingConversionRate error in case the conversion rate between the two
51+
// given currencies is not in the currencies rates map
5052
func (r *Rates) GetRate(from string, to string) (float64, error) {
5153
var err error
5254
fromUnit, err := currency.ParseISO(from)
@@ -63,12 +65,12 @@ func (r *Rates) GetRate(from string, to string) (float64, error) {
6365
if r.Conversions != nil {
6466
if conversion, present := r.Conversions[fromUnit.String()][toUnit.String()]; present {
6567
// In case we have an entry FROM -> TO
66-
return conversion, err
68+
return conversion, nil
6769
} else if conversion, present := r.Conversions[toUnit.String()][fromUnit.String()]; present {
6870
// In case we have an entry TO -> FROM
69-
return 1 / conversion, err
71+
return 1 / conversion, nil
7072
}
71-
return 0, fmt.Errorf("Currency conversion rate not found: '%s' => '%s'", fromUnit.String(), toUnit.String())
73+
return 0, ConversionRateNotFound{fromUnit.String(), toUnit.String()}
7274
}
7375
return 0, errors.New("rates are nil")
7476
}

endpoints/openrtb2/auction.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/prebid/prebid-server/util/httputil"
3838
"github.com/prebid/prebid-server/util/iputil"
3939
"golang.org/x/net/publicsuffix"
40+
"golang.org/x/text/currency"
4041
)
4142

4243
const storedRequestTimeoutMillis = 50
@@ -343,6 +344,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error {
343344
if err := deps.validateEidPermissions(bidExt, aliases); err != nil {
344345
return []error{err}
345346
}
347+
348+
if err := validateCustomRates(bidExt.Prebid.CurrencyConversions); err != nil {
349+
return []error{err}
350+
}
346351
}
347352

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

445+
// validateCustomRates throws a bad input error if any of the 3-digit currency codes found in
446+
// the bidRequest.ext.prebid.currency field is invalid, malfomed or does not represent any actual
447+
// currency. No error is thrown if bidRequest.ext.prebid.currency is invalid or empty.
448+
func validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) error {
449+
if bidReqCurrencyRates == nil {
450+
return nil
451+
}
452+
453+
for fromCurrency, rates := range bidReqCurrencyRates.ConversionRates {
454+
// Check if fromCurrency is a valid 3-letter currency code
455+
if _, err := currency.ParseISO(fromCurrency); err != nil {
456+
return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", fromCurrency)}
457+
}
458+
459+
// Check if currencies mapped to fromCurrency are valid 3-letter currency codes
460+
for toCurrency := range rates {
461+
if _, err := currency.ParseISO(toCurrency); err != nil {
462+
return &errortypes.BadInput{Message: fmt.Sprintf("currency code %s is not recognized or malformed", toCurrency)}
463+
}
464+
}
465+
}
466+
return nil
467+
}
468+
440469
func (deps *endpointDeps) validateEidPermissions(req *openrtb_ext.ExtRequest, aliases map[string]string) error {
441470
if req == nil || req.Prebid.Data == nil {
442471
return nil

0 commit comments

Comments
 (0)