-
Notifications
You must be signed in to change notification settings - Fork 739
/
floors.go
320 lines (274 loc) · 11 KB
/
floors.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
package floors
import (
"errors"
"math"
"math/rand"
"strings"
"github.com/prebid/prebid-server/v2/config"
"github.com/prebid/prebid-server/v2/currency"
"github.com/prebid/prebid-server/v2/openrtb_ext"
"github.com/prebid/prebid-server/v2/util/ptrutil"
)
type Price struct {
FloorMin float64
FloorMinCur string
}
const (
defaultCurrency string = "USD"
defaultDelimiter string = "|"
catchAll string = "*"
skipRateMin int = 0
skipRateMax int = 100
modelWeightMax int = 100
modelWeightMin int = 1
enforceRateMin int = 0
enforceRateMax int = 100
floorPrecision float64 = 0.01
dataRateMin int = 0
dataRateMax int = 100
)
// EnrichWithPriceFloors checks for floors enabled in account and request and selects floors data from dynamic fetched if present
// else selects floors data from req.ext.prebid.floors and update request with selected floors details
func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, account config.Account, conversions currency.Conversions, priceFloorFetcher FloorFetcher) []error {
if bidRequestWrapper == nil || bidRequestWrapper.BidRequest == nil {
return []error{errors.New("Empty bidrequest")}
}
if !isPriceFloorsEnabled(account, bidRequestWrapper) {
return []error{errors.New("Floors feature is disabled at account or in the request")}
}
floors, err := resolveFloors(account, bidRequestWrapper, conversions, priceFloorFetcher)
updateReqErrs := updateBidRequestWithFloors(floors, bidRequestWrapper, conversions)
updateFloorsInRequest(bidRequestWrapper, floors)
return append(err, updateReqErrs...)
}
// updateBidRequestWithFloors will update imp.bidfloor and imp.bidfloorcur based on rules matching
func updateBidRequestWithFloors(extFloorRules *openrtb_ext.PriceFloorRules, request *openrtb_ext.RequestWrapper, conversions currency.Conversions) []error {
var (
floorErrList []error
floorVal float64
)
if extFloorRules == nil || extFloorRules.Data == nil || len(extFloorRules.Data.ModelGroups) == 0 {
return []error{}
}
modelGroup := extFloorRules.Data.ModelGroups[0]
if modelGroup.Schema.Delimiter == "" {
modelGroup.Schema.Delimiter = defaultDelimiter
}
extFloorRules.Skipped = new(bool)
if shouldSkipFloors(modelGroup.SkipRate, extFloorRules.Data.SkipRate, extFloorRules.SkipRate, rand.Intn) {
*extFloorRules.Skipped = true
return []error{}
}
floorErrList = validateFloorRulesAndLowerValidRuleKey(modelGroup.Schema, modelGroup.Schema.Delimiter, modelGroup.Values)
if len(modelGroup.Values) > 0 {
for _, imp := range request.GetImp() {
desiredRuleKey := createRuleKey(modelGroup.Schema, request, imp)
matchedRule, isRuleMatched := findRule(modelGroup.Values, modelGroup.Schema.Delimiter, desiredRuleKey)
floorVal = modelGroup.Default
if isRuleMatched {
floorVal = modelGroup.Values[matchedRule]
}
// No rule is matched or no default value provided or non-zero bidfloor not provided
if floorVal == 0.0 {
continue
}
floorMinVal, floorCur, err := getMinFloorValue(extFloorRules, imp, conversions)
if err == nil {
floorVal = roundToFourDecimals(floorVal)
bidFloor := floorVal
if floorMinVal > 0.0 && floorVal < floorMinVal {
bidFloor = floorMinVal
}
imp.BidFloor = bidFloor
imp.BidFloorCur = floorCur
if isRuleMatched {
err = updateImpExtWithFloorDetails(imp, matchedRule, floorVal, imp.BidFloor)
if err != nil {
floorErrList = append(floorErrList, err)
}
}
} else {
floorErrList = append(floorErrList, err)
}
}
}
return floorErrList
}
// roundToFourDecimals retuns given value to 4 decimal points
func roundToFourDecimals(in float64) float64 {
return math.Round(in*10000) / 10000
}
// isPriceFloorsEnabled check for floors are enabled at account and request level
func isPriceFloorsEnabled(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper) bool {
return isPriceFloorsEnabledForAccount(account) && isPriceFloorsEnabledForRequest(bidRequestWrapper)
}
// isPriceFloorsEnabledForAccount check for floors enabled flag in account config
func isPriceFloorsEnabledForAccount(account config.Account) bool {
return account.PriceFloors.Enabled
}
// isPriceFloorsEnabledForRequest check for floors are enabled flag in request
func isPriceFloorsEnabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) bool {
requestExt, err := bidRequestWrapper.GetRequestExt()
if err == nil {
if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil {
return prebidExt.Floors.GetEnabled()
}
}
return true
}
// useFetchedData will check if to use fetched data or request data
func useFetchedData(rate *int) bool {
if rate == nil {
return true
}
randomNumber := rand.Intn(dataRateMax)
return randomNumber < *rate
}
// resolveFloors does selection of floors fields from request data and dynamic fetched data if dynamic fetch is enabled
func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper, conversions currency.Conversions, priceFloorFetcher FloorFetcher) (*openrtb_ext.PriceFloorRules, []error) {
var (
errList []error
floorRules *openrtb_ext.PriceFloorRules
fetchResult *openrtb_ext.PriceFloorRules
fetchStatus string
)
reqFloor := extractFloorsFromRequest(bidRequestWrapper)
if reqFloor != nil && reqFloor.Location != nil && len(reqFloor.Location.URL) > 0 {
account.PriceFloors.Fetcher.URL = reqFloor.Location.URL
}
account.PriceFloors.Fetcher.AccountID = account.ID
if priceFloorFetcher != nil && account.PriceFloors.UseDynamicData {
fetchResult, fetchStatus = priceFloorFetcher.Fetch(account.PriceFloors)
}
if fetchResult != nil && fetchStatus == openrtb_ext.FetchSuccess && useFetchedData(fetchResult.Data.FetchRate) {
mergedFloor := mergeFloors(reqFloor, fetchResult, conversions)
floorRules, errList = createFloorsFrom(mergedFloor, account, fetchStatus, openrtb_ext.FetchLocation)
} else if reqFloor != nil {
floorRules, errList = createFloorsFrom(reqFloor, account, openrtb_ext.FetchNone, openrtb_ext.RequestLocation)
} else {
floorRules, errList = createFloorsFrom(nil, account, openrtb_ext.FetchNone, openrtb_ext.NoDataLocation)
}
return floorRules, errList
}
// createFloorsFrom does preparation of floors data which shall be used for further processing
func createFloorsFrom(floors *openrtb_ext.PriceFloorRules, account config.Account, fetchStatus, floorLocation string) (*openrtb_ext.PriceFloorRules, []error) {
var floorModelErrList []error
finalFloors := &openrtb_ext.PriceFloorRules{
FetchStatus: fetchStatus,
PriceFloorLocation: floorLocation,
}
if floors != nil {
floorValidationErr := validateFloorParams(floors)
if floorValidationErr != nil {
return finalFloors, append(floorModelErrList, floorValidationErr)
}
finalFloors.Enforcement = floors.Enforcement
if floors.Data != nil {
validModelGroups, floorModelErrList := selectValidFloorModelGroups(floors.Data.ModelGroups, account)
if len(validModelGroups) == 0 {
return finalFloors, floorModelErrList
} else {
*finalFloors = *floors
finalFloors.Data = new(openrtb_ext.PriceFloorData)
*finalFloors.Data = *floors.Data
finalFloors.PriceFloorLocation = floorLocation
finalFloors.FetchStatus = fetchStatus
if len(validModelGroups) > 1 {
validModelGroups = selectFloorModelGroup(validModelGroups, rand.Intn)
}
finalFloors.Data.ModelGroups = []openrtb_ext.PriceFloorModelGroup{validModelGroups[0].Copy()}
}
}
}
return finalFloors, floorModelErrList
}
// extractFloorsFromRequest gets floors data from req.ext.prebid.floors
func extractFloorsFromRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) *openrtb_ext.PriceFloorRules {
requestExt, err := bidRequestWrapper.GetRequestExt()
if err == nil {
prebidExt := requestExt.GetPrebid()
if prebidExt != nil && prebidExt.Floors != nil {
return prebidExt.Floors
}
}
return nil
}
// updateFloorsInRequest updates req.ext.prebid.floors with floors data
func updateFloorsInRequest(bidRequestWrapper *openrtb_ext.RequestWrapper, priceFloors *openrtb_ext.PriceFloorRules) {
requestExt, err := bidRequestWrapper.GetRequestExt()
if err == nil {
prebidExt := requestExt.GetPrebid()
if prebidExt == nil {
prebidExt = &openrtb_ext.ExtRequestPrebid{}
}
prebidExt.Floors = priceFloors
requestExt.SetPrebid(prebidExt)
bidRequestWrapper.RebuildRequest()
}
}
// resolveFloorMin gets floorMin value from request and dynamic fetched data
func resolveFloorMin(reqFloors *openrtb_ext.PriceFloorRules, fetchFloors *openrtb_ext.PriceFloorRules, conversions currency.Conversions) Price {
var requestFloorMinCur, providerFloorMinCur string
var requestFloorMin, providerFloorMin float64
if reqFloors != nil {
requestFloorMin = reqFloors.FloorMin
requestFloorMinCur = reqFloors.FloorMinCur
if len(requestFloorMinCur) == 0 && reqFloors.Data != nil {
requestFloorMinCur = reqFloors.Data.Currency
}
}
if fetchFloors != nil {
providerFloorMin = fetchFloors.FloorMin
providerFloorMinCur = fetchFloors.FloorMinCur
if len(providerFloorMinCur) == 0 && fetchFloors.Data != nil {
providerFloorMinCur = fetchFloors.Data.Currency
}
}
if len(requestFloorMinCur) > 0 {
if requestFloorMin > 0 {
return Price{FloorMin: requestFloorMin, FloorMinCur: requestFloorMinCur}
}
if providerFloorMin > 0 {
if strings.Compare(providerFloorMinCur, requestFloorMinCur) == 0 || len(providerFloorMinCur) == 0 {
return Price{FloorMin: providerFloorMin, FloorMinCur: requestFloorMinCur}
}
rate, err := conversions.GetRate(providerFloorMinCur, requestFloorMinCur)
if err != nil {
return Price{FloorMin: 0, FloorMinCur: requestFloorMinCur}
}
return Price{FloorMin: roundToFourDecimals(rate * providerFloorMin), FloorMinCur: requestFloorMinCur}
}
}
if len(providerFloorMinCur) > 0 {
if providerFloorMin > 0 {
return Price{FloorMin: providerFloorMin, FloorMinCur: providerFloorMinCur}
}
if requestFloorMin > 0 {
return Price{FloorMin: requestFloorMin, FloorMinCur: providerFloorMinCur}
}
}
return Price{FloorMin: requestFloorMin, FloorMinCur: requestFloorMinCur}
}
// mergeFloors does merging for floors data from request and dynamic fetch
func mergeFloors(reqFloors *openrtb_ext.PriceFloorRules, fetchFloors *openrtb_ext.PriceFloorRules, conversions currency.Conversions) *openrtb_ext.PriceFloorRules {
mergedFloors := fetchFloors.DeepCopy()
if mergedFloors.Enabled == nil {
mergedFloors.Enabled = new(bool)
}
*mergedFloors.Enabled = fetchFloors.GetEnabled() && reqFloors.GetEnabled()
if reqFloors == nil {
return mergedFloors
}
if reqFloors.Enforcement != nil {
mergedFloors.Enforcement = reqFloors.Enforcement.DeepCopy()
}
floorMinPrice := resolveFloorMin(reqFloors, fetchFloors, conversions)
if floorMinPrice.FloorMin > 0 {
mergedFloors.FloorMin = floorMinPrice.FloorMin
mergedFloors.FloorMinCur = floorMinPrice.FloorMinCur
}
if reqFloors != nil && reqFloors.Location != nil && reqFloors.Location.URL != "" {
mergedFloors.Location = ptrutil.Clone(reqFloors.Location)
}
return mergedFloors
}