github.com/prebid/prebid-server/v2@v2.18.0/floors/enforce.go (about) 1 package floors 2 3 import ( 4 "errors" 5 "fmt" 6 "math/rand" 7 8 "github.com/prebid/openrtb/v20/openrtb2" 9 "github.com/prebid/prebid-server/v2/config" 10 "github.com/prebid/prebid-server/v2/currency" 11 "github.com/prebid/prebid-server/v2/exchange/entities" 12 "github.com/prebid/prebid-server/v2/openrtb_ext" 13 ) 14 15 // Enforce does floors enforcement for bids from all bidders based on floors provided in request, account level floors config 16 func Enforce(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, account config.Account, conversions currency.Conversions) (map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []error, []*entities.PbsOrtbSeatBid) { 17 rejectionErrs := []error{} 18 19 rejectedBids := []*entities.PbsOrtbSeatBid{} 20 21 requestExt, err := bidRequestWrapper.GetRequestExt() 22 if err != nil { 23 return seatBids, []error{errors.New("Error in getting request extension")}, rejectedBids 24 } 25 26 if !isPriceFloorsEnabled(account, bidRequestWrapper) { 27 return seatBids, nil, rejectedBids 28 } 29 30 if isSignalingSkipped(requestExt) || !isValidImpBidFloorPresent(bidRequestWrapper.BidRequest.Imp) { 31 return seatBids, nil, rejectedBids 32 } 33 34 enforceFloors := isSatisfiedByEnforceRate(requestExt, account.PriceFloors.EnforceFloorsRate, rand.Intn) 35 if updateEnforcePBS(enforceFloors, requestExt) { 36 err := bidRequestWrapper.RebuildRequest() 37 if err != nil { 38 return seatBids, []error{err}, rejectedBids 39 } 40 } 41 updateBidExt(bidRequestWrapper, seatBids) 42 if enforceFloors { 43 enforceDealFloors := account.PriceFloors.EnforceDealFloors && getEnforceDealsFlag(requestExt) 44 seatBids, rejectionErrs, rejectedBids = enforceFloorToBids(bidRequestWrapper, seatBids, conversions, enforceDealFloors) 45 } 46 return seatBids, rejectionErrs, rejectedBids 47 } 48 49 // updateEnforcePBS updates prebid extension in request if enforcePBS needs to be updated 50 func updateEnforcePBS(enforceFloors bool, requestExt *openrtb_ext.RequestExt) bool { 51 updateReqExt := false 52 53 prebidExt := requestExt.GetPrebid() 54 if prebidExt == nil { 55 prebidExt = new(openrtb_ext.ExtRequestPrebid) 56 } 57 58 if prebidExt.Floors == nil { 59 prebidExt.Floors = new(openrtb_ext.PriceFloorRules) 60 } 61 floorExt := prebidExt.Floors 62 63 if floorExt.Enforcement == nil { 64 floorExt.Enforcement = new(openrtb_ext.PriceFloorEnforcement) 65 } 66 67 if floorExt.Enforcement.EnforcePBS == nil { 68 updateReqExt = true 69 floorExt.Enforcement.EnforcePBS = new(bool) 70 *floorExt.Enforcement.EnforcePBS = enforceFloors 71 } else { 72 oldEnforcePBS := *floorExt.Enforcement.EnforcePBS 73 *floorExt.Enforcement.EnforcePBS = enforceFloors && *floorExt.Enforcement.EnforcePBS 74 updateReqExt = oldEnforcePBS != *floorExt.Enforcement.EnforcePBS 75 } 76 77 if updateReqExt { 78 requestExt.SetPrebid(prebidExt) 79 } 80 81 return updateReqExt 82 } 83 84 // updateBidExt updates bid extension for floors related details 85 func updateBidExt(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) { 86 impMap := make(map[string]*openrtb_ext.ImpWrapper, bidRequestWrapper.LenImp()) 87 for _, imp := range bidRequestWrapper.GetImp() { 88 impMap[imp.ID] = imp 89 } 90 91 for _, seatBid := range seatBids { 92 for _, bid := range seatBid.Bids { 93 reqImp, ok := impMap[bid.Bid.ImpID] 94 if ok { 95 updateBidExtWithFloors(reqImp, bid, reqImp.BidFloorCur) 96 } 97 } 98 } 99 } 100 101 // enforceFloorToBids function does floors enforcement for each bid, 102 // The bids returned by each partner below bid floor price are rejected and remaining eligible bids are considered for further processing 103 func enforceFloorToBids(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, conversions currency.Conversions, enforceDealFloors bool) (map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid, []error, []*entities.PbsOrtbSeatBid) { 104 errs := []error{} 105 rejectedBids := []*entities.PbsOrtbSeatBid{} 106 impMap := make(map[string]*openrtb_ext.ImpWrapper, bidRequestWrapper.LenImp()) 107 108 for _, v := range bidRequestWrapper.GetImp() { 109 impMap[v.ID] = v 110 } 111 112 for bidderName, seatBid := range seatBids { 113 eligibleBids := make([]*entities.PbsOrtbBid, 0, len(seatBid.Bids)) 114 for _, bid := range seatBid.Bids { 115 116 reqImp, ok := impMap[bid.Bid.ImpID] 117 if !ok { 118 continue 119 } 120 121 requestExt, err := bidRequestWrapper.GetRequestExt() 122 if err != nil { 123 errs = append(errs, fmt.Errorf("error in getting req extension = %v", err.Error())) 124 continue 125 } 126 127 if isEnforcementEnabled(requestExt) { 128 if hasDealID(bid) && !enforceDealFloors { 129 eligibleBids = append(eligibleBids, bid) 130 continue 131 } 132 133 rate, err := getCurrencyConversionRate(seatBid.Currency, reqImp.BidFloorCur, conversions) 134 if err != nil { 135 errs = append(errs, fmt.Errorf("error in rate conversion from = %s to %s with bidder %s for impression id %s and bid id %s error = %v", seatBid.Currency, reqImp.BidFloorCur, bidderName, bid.Bid.ImpID, bid.Bid.ID, err.Error())) 136 continue 137 } 138 139 bidPrice := rate * bid.Bid.Price 140 if (bidPrice + floorPrecision) < reqImp.BidFloor { 141 rejectedBid := &entities.PbsOrtbSeatBid{ 142 Currency: seatBid.Currency, 143 Seat: seatBid.Seat, 144 Bids: []*entities.PbsOrtbBid{bid}, 145 } 146 rejectedBids = append(rejectedBids, rejectedBid) 147 continue 148 } 149 } 150 eligibleBids = append(eligibleBids, bid) 151 } 152 seatBids[bidderName].Bids = eligibleBids 153 } 154 return seatBids, errs, rejectedBids 155 } 156 157 // isEnforcementEnabled check for floors enforcement enabled in request 158 func isEnforcementEnabled(requestExt *openrtb_ext.RequestExt) bool { 159 if floorsExt := getFloorsExt(requestExt); floorsExt != nil { 160 return floorsExt.GetEnforcePBS() 161 } 162 return true 163 } 164 165 // isSignalingSkipped check for floors signalling is skipped due to skip rate 166 func isSignalingSkipped(requestExt *openrtb_ext.RequestExt) bool { 167 if floorsExt := getFloorsExt(requestExt); floorsExt != nil { 168 return floorsExt.GetFloorsSkippedFlag() 169 } 170 return false 171 } 172 173 // getEnforceRateRequest returns enforceRate provided in request 174 func getEnforceRateRequest(requestExt *openrtb_ext.RequestExt) int { 175 if floorsExt := getFloorsExt(requestExt); floorsExt != nil { 176 return floorsExt.GetEnforceRate() 177 } 178 return 0 179 } 180 181 // getEnforceDealsFlag returns FloorDeals flag from req.ext.prebid.floors.enforcement 182 func getEnforceDealsFlag(requestExt *openrtb_ext.RequestExt) bool { 183 if floorsExt := getFloorsExt(requestExt); floorsExt != nil { 184 return floorsExt.GetEnforceDealsFlag() 185 } 186 return false 187 } 188 189 // getFloorsExt returns req.ext.prebid.floors 190 func getFloorsExt(requestExt *openrtb_ext.RequestExt) *openrtb_ext.PriceFloorRules { 191 if requestExt != nil { 192 if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil { 193 return prebidExt.Floors 194 } 195 } 196 return nil 197 } 198 199 // isValidImpBidFloorPresent checks if non zero imp.bidfloor is present in request 200 func isValidImpBidFloorPresent(imp []openrtb2.Imp) bool { 201 for i := range imp { 202 if imp[i].BidFloor > 0 { 203 return true 204 } 205 } 206 return false 207 } 208 209 // isSatisfiedByEnforceRate check enforcements should be done or not based on enforceRate in config and in request 210 func isSatisfiedByEnforceRate(requestExt *openrtb_ext.RequestExt, configEnforceRate int, f func(int) int) bool { 211 requestEnforceRate := getEnforceRateRequest(requestExt) 212 enforceRate := f(enforceRateMax) 213 satisfiedByRequest := requestEnforceRate == 0 || enforceRate < requestEnforceRate 214 satisfiedByAccount := configEnforceRate == 0 || enforceRate < configEnforceRate 215 shouldEnforce := satisfiedByRequest && satisfiedByAccount 216 217 return shouldEnforce 218 } 219 220 // hasDealID checks for dealID presence in bid 221 func hasDealID(bid *entities.PbsOrtbBid) bool { 222 if bid != nil && bid.Bid != nil && bid.Bid.DealID != "" { 223 return true 224 } 225 return false 226 } 227 228 // getCurrencyConversionRate gets conversion rate in case floor currency and seatBid currency are not same 229 func getCurrencyConversionRate(seatBidCur, reqImpCur string, conversions currency.Conversions) (float64, error) { 230 rate := 1.0 231 if seatBidCur != reqImpCur { 232 return conversions.GetRate(seatBidCur, reqImpCur) 233 } else { 234 return rate, nil 235 } 236 } 237 238 // updateBidExtWithFloors updates floors related details in bid extension 239 func updateBidExtWithFloors(reqImp *openrtb_ext.ImpWrapper, bid *entities.PbsOrtbBid, floorCurrency string) { 240 impExt, err := reqImp.GetImpExt() 241 if err != nil { 242 return 243 } 244 245 var bidExtFloors openrtb_ext.ExtBidPrebidFloors 246 prebidExt := impExt.GetPrebid() 247 if prebidExt == nil || prebidExt.Floors == nil { 248 if reqImp.BidFloor > 0 { 249 bidExtFloors.FloorValue = reqImp.BidFloor 250 bidExtFloors.FloorCurrency = reqImp.BidFloorCur 251 bid.BidFloors = &bidExtFloors 252 } 253 } else { 254 bidExtFloors.FloorRule = prebidExt.Floors.FloorRule 255 bidExtFloors.FloorRuleValue = prebidExt.Floors.FloorRuleValue 256 bidExtFloors.FloorValue = prebidExt.Floors.FloorValue 257 bidExtFloors.FloorCurrency = floorCurrency 258 bid.BidFloors = &bidExtFloors 259 } 260 }