github.com/prebid/prebid-server@v0.275.0/floors/floors.go (about) 1 package floors 2 3 import ( 4 "errors" 5 "math" 6 "math/rand" 7 8 "github.com/prebid/prebid-server/config" 9 "github.com/prebid/prebid-server/currency" 10 "github.com/prebid/prebid-server/openrtb_ext" 11 ) 12 13 type Price struct { 14 FloorMin float64 15 FloorMinCur string 16 } 17 18 const ( 19 defaultCurrency string = "USD" 20 defaultDelimiter string = "|" 21 catchAll string = "*" 22 skipRateMin int = 0 23 skipRateMax int = 100 24 modelWeightMax int = 100 25 modelWeightMin int = 1 26 enforceRateMin int = 0 27 enforceRateMax int = 100 28 floorPrecision float64 = 0.01 29 ) 30 31 // EnrichWithPriceFloors checks for floors enabled in account and request and selects floors data from dynamic fetched if present 32 // else selects floors data from req.ext.prebid.floors and update request with selected floors details 33 func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, account config.Account, conversions currency.Conversions) []error { 34 35 if bidRequestWrapper == nil || bidRequestWrapper.BidRequest == nil { 36 return []error{errors.New("Empty bidrequest")} 37 } 38 39 if !isPriceFloorsEnabled(account, bidRequestWrapper) { 40 return []error{errors.New("Floors feature is disabled at account or in the request")} 41 } 42 43 floors, err := resolveFloors(account, bidRequestWrapper, conversions) 44 45 updateReqErrs := updateBidRequestWithFloors(floors, bidRequestWrapper, conversions) 46 updateFloorsInRequest(bidRequestWrapper, floors) 47 return append(err, updateReqErrs...) 48 } 49 50 // updateBidRequestWithFloors will update imp.bidfloor and imp.bidfloorcur based on rules matching 51 func updateBidRequestWithFloors(extFloorRules *openrtb_ext.PriceFloorRules, request *openrtb_ext.RequestWrapper, conversions currency.Conversions) []error { 52 var ( 53 floorErrList []error 54 floorVal float64 55 ) 56 57 if extFloorRules == nil || extFloorRules.Data == nil || len(extFloorRules.Data.ModelGroups) == 0 { 58 return []error{} 59 } 60 61 modelGroup := extFloorRules.Data.ModelGroups[0] 62 if modelGroup.Schema.Delimiter == "" { 63 modelGroup.Schema.Delimiter = defaultDelimiter 64 } 65 66 extFloorRules.Skipped = new(bool) 67 if shouldSkipFloors(modelGroup.SkipRate, extFloorRules.Data.SkipRate, extFloorRules.SkipRate, rand.Intn) { 68 *extFloorRules.Skipped = true 69 return []error{} 70 } 71 72 floorErrList = validateFloorRulesAndLowerValidRuleKey(modelGroup.Schema, modelGroup.Schema.Delimiter, modelGroup.Values) 73 if len(modelGroup.Values) > 0 { 74 for _, imp := range request.GetImp() { 75 desiredRuleKey := createRuleKey(modelGroup.Schema, request, imp) 76 matchedRule, isRuleMatched := findRule(modelGroup.Values, modelGroup.Schema.Delimiter, desiredRuleKey) 77 floorVal = modelGroup.Default 78 if isRuleMatched { 79 floorVal = modelGroup.Values[matchedRule] 80 } 81 82 // No rule is matched or no default value provided or non-zero bidfloor not provided 83 if floorVal == 0.0 { 84 continue 85 } 86 87 floorMinVal, floorCur, err := getMinFloorValue(extFloorRules, imp, conversions) 88 if err == nil { 89 floorVal = roundToFourDecimals(floorVal) 90 bidFloor := floorVal 91 if floorMinVal > 0.0 && floorVal < floorMinVal { 92 bidFloor = floorMinVal 93 } 94 95 imp.BidFloor = bidFloor 96 imp.BidFloorCur = floorCur 97 98 if isRuleMatched { 99 err = updateImpExtWithFloorDetails(imp, matchedRule, floorVal, imp.BidFloor) 100 if err != nil { 101 floorErrList = append(floorErrList, err) 102 } 103 } 104 } else { 105 floorErrList = append(floorErrList, err) 106 } 107 } 108 } 109 return floorErrList 110 } 111 112 // roundToFourDecimals retuns given value to 4 decimal points 113 func roundToFourDecimals(in float64) float64 { 114 return math.Round(in*10000) / 10000 115 } 116 117 // isPriceFloorsEnabled check for floors are enabled at account and request level 118 func isPriceFloorsEnabled(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper) bool { 119 return isPriceFloorsEnabledForAccount(account) && isPriceFloorsEnabledForRequest(bidRequestWrapper) 120 } 121 122 // isPriceFloorsEnabledForAccount check for floors enabled flag in account config 123 func isPriceFloorsEnabledForAccount(account config.Account) bool { 124 return account.PriceFloors.Enabled 125 } 126 127 // isPriceFloorsEnabledForRequest check for floors are enabled flag in request 128 func isPriceFloorsEnabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) bool { 129 requestExt, err := bidRequestWrapper.GetRequestExt() 130 if err == nil { 131 if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil { 132 return prebidExt.Floors.GetEnabled() 133 } 134 } 135 return true 136 } 137 138 // resolveFloors does selection of floors fields from request data and dynamic fetched data if dynamic fetch is enabled 139 func resolveFloors(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper, conversions currency.Conversions) (*openrtb_ext.PriceFloorRules, []error) { 140 var errList []error 141 var floorRules *openrtb_ext.PriceFloorRules 142 143 reqFloor := extractFloorsFromRequest(bidRequestWrapper) 144 if reqFloor != nil { 145 floorRules, errList = createFloorsFrom(reqFloor, account, openrtb_ext.FetchNone, openrtb_ext.RequestLocation) 146 } else { 147 floorRules, errList = createFloorsFrom(nil, account, openrtb_ext.FetchNone, openrtb_ext.NoDataLocation) 148 } 149 return floorRules, errList 150 } 151 152 // createFloorsFrom does preparation of floors data which shall be used for further processing 153 func createFloorsFrom(floors *openrtb_ext.PriceFloorRules, account config.Account, fetchStatus, floorLocation string) (*openrtb_ext.PriceFloorRules, []error) { 154 var floorModelErrList []error 155 finalFloors := &openrtb_ext.PriceFloorRules{ 156 FetchStatus: fetchStatus, 157 PriceFloorLocation: floorLocation, 158 } 159 160 if floors != nil { 161 floorValidationErr := validateFloorParams(floors) 162 if floorValidationErr != nil { 163 return finalFloors, append(floorModelErrList, floorValidationErr) 164 } 165 166 finalFloors.Enforcement = floors.Enforcement 167 if floors.Data != nil { 168 validModelGroups, floorModelErrList := selectValidFloorModelGroups(floors.Data.ModelGroups, account) 169 if len(validModelGroups) == 0 { 170 return finalFloors, floorModelErrList 171 } else { 172 *finalFloors = *floors 173 finalFloors.Data = new(openrtb_ext.PriceFloorData) 174 *finalFloors.Data = *floors.Data 175 finalFloors.PriceFloorLocation = floorLocation 176 finalFloors.FetchStatus = fetchStatus 177 if len(validModelGroups) > 1 { 178 validModelGroups = selectFloorModelGroup(validModelGroups, rand.Intn) 179 } 180 finalFloors.Data.ModelGroups = []openrtb_ext.PriceFloorModelGroup{validModelGroups[0].Copy()} 181 } 182 } 183 } 184 185 return finalFloors, floorModelErrList 186 } 187 188 // extractFloorsFromRequest gets floors data from req.ext.prebid.floors 189 func extractFloorsFromRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) *openrtb_ext.PriceFloorRules { 190 requestExt, err := bidRequestWrapper.GetRequestExt() 191 if err == nil { 192 prebidExt := requestExt.GetPrebid() 193 if prebidExt != nil && prebidExt.Floors != nil { 194 return prebidExt.Floors 195 } 196 } 197 return nil 198 } 199 200 // updateFloorsInRequest updates req.ext.prebid.floors with floors data 201 func updateFloorsInRequest(bidRequestWrapper *openrtb_ext.RequestWrapper, priceFloors *openrtb_ext.PriceFloorRules) { 202 requestExt, err := bidRequestWrapper.GetRequestExt() 203 if err == nil { 204 prebidExt := requestExt.GetPrebid() 205 if prebidExt == nil { 206 prebidExt = &openrtb_ext.ExtRequestPrebid{} 207 } 208 prebidExt.Floors = priceFloors 209 requestExt.SetPrebid(prebidExt) 210 bidRequestWrapper.RebuildRequest() 211 } 212 }