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