Repository: prebid/prebid-server
Pull request #2757 (2023-05-08 21:39:59)

GPP in the `cookie_sync` endpoint
Requires #2756

This pull request is a continuation of GPP Phase 2, more info in [this document](https://docs.google.com/document/d/1dRxFUFmhh2jGanzGZvfkK_6jtHPpHXWD7Qsi6KEugeE/edit#) and issue #2380. Makes the `/cookie_sync` endpoint handler to prioritize the GPP string values over the other policy strings using the `go-gpp` library.



Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index aa8a0e99..f70914a1 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -65,8 +65,8 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 
 	var (
 		shouldGenerateAdPodId *bool
-		uniqueMemberIds       []string
-		memberIds             = make(map[string]struct{})
+		uniqueMemberIds       = make([]string, 0, len(request.Imp))
+		memberIds             = make(map[string]struct{}, len(request.Imp))
 		errs                  = make([]error, 0, len(request.Imp))
 	)
 
@@ -116,6 +116,7 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 		requestURI = appendMemberId(requestURI, uniqueMemberIds[0])
 		if len(uniqueMemberIds) > 1 {
 			errs = append(errs, fmt.Errorf("All request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained: %v", uniqueMemberIds))
+			return nil, errs
 		}
 	}
 
diff --git a/adapters/criteo/criteo.go b/adapters/criteo/criteo.go
index ac96574c..11d8d166 100644
--- a/adapters/criteo/criteo.go
+++ b/adapters/criteo/criteo.go
@@ -16,6 +16,15 @@ type adapter struct {
 	endpoint string
 }
 
+type BidExt struct {
+	Prebid ExtPrebid `json:"prebid"`
+}
+
+type ExtPrebid struct {
+	BidType     openrtb_ext.BidType `json:"type"`
+	NetworkName string              `json:"networkName"`
+}
+
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
 	bidder := &adapter{
 		endpoint: config.Endpoint,
@@ -67,7 +76,7 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 
 	for _, seatBid := range response.SeatBid {
 		for i := range seatBid.Bid {
-			var bidExt openrtb_ext.ExtBid
+			var bidExt BidExt
 			if err := json.Unmarshal(seatBid.Bid[i].Ext, &bidExt); err != nil {
 				return nil, []error{&errortypes.BadServerResponse{
 					Message: fmt.Sprintf("Missing ext.prebid.type in bid for impression : %s.", seatBid.Bid[i].ImpID),
@@ -76,7 +85,8 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 
 			b := &adapters.TypedBid{
 				Bid:     &seatBid.Bid[i],
-				BidType: bidExt.Prebid.Type,
+				BidType: bidExt.Prebid.BidType,
+				BidMeta: getBidMeta(bidExt),
 			}
 			bidResponse.Bids = append(bidResponse.Bids, b)
 		}
@@ -84,3 +94,13 @@ func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.R
 
 	return bidResponse, nil
 }
+
+func getBidMeta(ext BidExt) *openrtb_ext.ExtBidPrebidMeta {
+	var bidMeta *openrtb_ext.ExtBidPrebidMeta
+	if ext.Prebid.NetworkName != "" {
+		bidMeta = &openrtb_ext.ExtBidPrebidMeta{
+			NetworkName: ext.Prebid.NetworkName,
+		}
+	}
+	return bidMeta
+}
diff --git a/adapters/huaweiads/huaweiads.go b/adapters/huaweiads/huaweiads.go
index 28ae0d9a..4944fd5e 100644
--- a/adapters/huaweiads/huaweiads.go
+++ b/adapters/huaweiads/huaweiads.go
@@ -771,7 +771,7 @@ func convertCountryCode(country string) (out string) {
 		return mappedCountry
 	}
 
-	if len(country) >= 3 {
+	if len(country) >= 2 {
 		return country[0:2]
 	}
 
diff --git a/config/config.go b/config/config.go
index d70afc21..b537d128 100644
--- a/config/config.go
+++ b/config/config.go
@@ -39,6 +39,7 @@ type Configuration struct {
 	// If empty, it will return a 204 with no content.
 	StatusResponse    string          `mapstructure:"status_response"`
 	AuctionTimeouts   AuctionTimeouts `mapstructure:"auction_timeouts_ms"`
+	TmaxAdjustments   TmaxAdjustments `mapstructure:"tmax_adjustments"`
 	CacheURL          Cache           `mapstructure:"cache"`
 	ExtCacheURL       ExternalCache   `mapstructure:"external_cache"`
 	RecaptchaSecret   string          `mapstructure:"recaptcha_secret"`
@@ -148,6 +149,11 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 		errs = append(errs, errors.New("account_defaults.Events.VASTEvents has no effect as the feature is under development."))
 	}
 
+	if cfg.TmaxAdjustments.Enabled {
+		glog.Warning(`cfg.TmaxAdjustments.Enabled will currently not do anything as tmax adjustment feature is still under development.`)
+		cfg.TmaxAdjustments.Enabled = false
+	}
+
 	errs = cfg.Experiment.validate(errs)
 	errs = cfg.BidderInfos.validate(errs)
 	return errs
@@ -1029,6 +1035,11 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("debug.timeout_notification.fail_only", false)
 	v.SetDefault("debug.override_token", "")
 
+	v.SetDefault("tmax_adjustments.enabled", false)
+	v.SetDefault("tmax_adjustments.bidder_response_duration_min_ms", 0)
+	v.SetDefault("tmax_adjustments.bidder_network_latency_buffer_ms", 0)
+	v.SetDefault("tmax_adjustments.pbs_response_preparation_duration_ms", 0)
+
 	/* IPv4
 	/*  Site Local: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
 	/*  Link Local: 169.254.0.0/16
@@ -1506,3 +1517,22 @@ func isValidCookieSize(maxCookieSize int) error {
 	}
 	return nil
 }
+
+// Tmax Adjustments enables PBS to estimate the tmax value for bidders, indicating the allotted time for them to respond to a request.
+// It's important to note that the calculated tmax is just an estimate and will not be entirely precise.
+// PBS will calculate the bidder tmax as follows:
+// bidderTmax = request.tmax - reqProcessingTime - BidderNetworkLatencyBuffer - PBSResponsePreparationDuration
+// Note that reqProcessingTime is time taken by PBS to process a given request before it is sent to bid adapters and is computed at run time.
+type TmaxAdjustments struct {
+	// Enabled indicates whether bidder tmax should be calculated and passed on to bid adapters
+	Enabled bool `mapstructure:"enabled"`
+	// BidderNetworkLatencyBuffer accounts for network delays between PBS and bidder servers.
+	// A value of 0 indicates no network latency buffer should be accounted for when calculating the bidder tmax.
+	BidderNetworkLatencyBuffer uint `mapstructure:"bidder_network_latency_buffer_ms"`
+	// PBSResponsePreparationDuration accounts for amount of time required for PBS to process all bidder responses and generate final response for a request.
+	// A value of 0 indicates PBS response preparation time shouldn't be accounted for when calculating bidder tmax.
+	PBSResponsePreparationDuration uint `mapstructure:"pbs_response_preparation_duration_ms"`
+	// BidderResponseDurationMin is the minimum amount of time expected to get a response from a bidder request.
+	// PBS won't send a request to the bidder if the bidder tmax calculated is less than the BidderResponseDurationMin value
+	BidderResponseDurationMin uint `mapstructure:"bidder_response_duration_min_ms"`
+}
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 8b930962..fa154bbc 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -12,6 +12,8 @@ import (
 
 	"github.com/golang/glog"
 	"github.com/julienschmidt/httprouter"
+	gpplib "github.com/prebid/go-gpp"
+	gppConstants "github.com/prebid/go-gpp/constants"
 	accountService "github.com/prebid/prebid-server/account"
 	"github.com/prebid/prebid-server/analytics"
 	"github.com/prebid/prebid-server/config"
@@ -25,6 +27,7 @@ import (
 	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/stored_requests"
 	"github.com/prebid/prebid-server/usersync"
+	stringutil "github.com/prebid/prebid-server/util/stringutil"
 )
 
 var (
@@ -125,40 +128,12 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 		return usersync.Request{}, privacy.Policies{}, combineErrors(fetchErrs)
 	}
 
-	var gdprString string
-	if request.GDPR != nil {
-		gdprString = strconv.Itoa(*request.GDPR)
-	}
-	gdprSignal, err := gdpr.StrSignalParse(gdprString)
-	if err != nil {
-		return usersync.Request{}, privacy.Policies{}, err
-	}
-
-	if request.GDPRConsent == "" {
-		if gdprSignal == gdpr.SignalYes {
-			return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissing
-		}
-
-		if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, c.privacyConfig.gdprConfig.DefaultValue) == gdpr.SignalYes {
-			return usersync.Request{}, privacy.Policies{}, errCookieSyncGDPRConsentMissingSignalAmbiguous
-		}
-	}
-
 	request = c.setLimit(request, account.CookieSync)
 	request = c.setCooperativeSync(request, account.CookieSync)
 
-	privacyPolicies := privacy.Policies{
-		GDPR: gdprPrivacy.Policy{
-			Signal:  gdprString,
-			Consent: request.GDPRConsent,
-		},
-		CCPA: ccpa.Policy{
-			Consent: request.USPrivacy,
-		},
-		GPP: gppPrivacy.Policy{
-			Consent: request.GPP,
-			RawSID:  request.GPPSid,
-		},
+	privacyPolicies, gdprSignal, err := extractPrivacyPolicies(request, c.privacyConfig.gdprConfig.DefaultValue)
+	if err != nil {
+		return usersync.Request{}, privacy.Policies{}, err
 	}
 
 	ccpaParsedPolicy := ccpa.ParsedPolicy{}
@@ -178,7 +153,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 	}
 
 	gdprRequestInfo := gdpr.RequestInfo{
-		Consent:    request.GDPRConsent,
+		Consent:    privacyPolicies.GDPR.Consent,
 		GDPRSignal: gdprSignal,
 	}
 
@@ -201,6 +176,82 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 	return rx, privacyPolicies, nil
 }
 
+func extractPrivacyPolicies(request cookieSyncRequest, usersyncDefaultGDPRValue string) (privacy.Policies, gdpr.Signal, error) {
+	// GDPR
+	gppSID, err := stringutil.StrToInt8Slice(request.GPPSid)
+	if err != nil {
+		return privacy.Policies{}, gdpr.SignalNo, err
+	}
+
+	gdprSignal, gdprString, err := extractGDPRSignal(request.GDPR, gppSID)
+	if err != nil {
+		return privacy.Policies{}, gdpr.SignalNo, err
+	}
+
+	var gpp gpplib.GppContainer
+	if len(request.GPP) > 0 {
+		var err error
+		gpp, err = gpplib.Parse(request.GPP)
+		if err != nil {
+			return privacy.Policies{}, gdpr.SignalNo, err
+		}
+	}
+
+	gdprConsent := request.GDPRConsent
+	if i := gppPrivacy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
+		gdprConsent = gpp.Sections[i].GetValue()
+	}
+
+	if gdprConsent == "" {
+		if gdprSignal == gdpr.SignalYes {
+			return privacy.Policies{}, gdpr.SignalNo, errCookieSyncGDPRConsentMissing
+		}
+
+		if gdprSignal == gdpr.SignalAmbiguous && gdpr.SignalNormalize(gdprSignal, usersyncDefaultGDPRValue) == gdpr.SignalYes {
+			return privacy.Policies{}, gdpr.SignalNo, errCookieSyncGDPRConsentMissingSignalAmbiguous
+		}
+	}
+
+	// CCPA
+	ccpaString, err := ccpa.SelectCCPAConsent(request.USPrivacy, gpp, gppSID)
+	if err != nil {
+		return privacy.Policies{}, gdpr.SignalNo, err
+	}
+
+	return privacy.Policies{
+		GDPR: gdprPrivacy.Policy{
+			Signal:  gdprString,
+			Consent: gdprConsent,
+		},
+		CCPA: ccpa.Policy{
+			Consent: ccpaString,
+		},
+		GPP: gppPrivacy.Policy{
+			Consent: request.GPP,
+			RawSID:  request.GPPSid,
+		},
+	}, gdprSignal, nil
+}
+
+func extractGDPRSignal(requestGDPR *int, gppSID []int8) (gdpr.Signal, string, error) {
+	if len(gppSID) > 0 {
+		if gppPrivacy.IsSIDInList(gppSID, gppConstants.SectionTCFEU2) {
+			return gdpr.SignalYes, strconv.Itoa(int(gdpr.SignalYes)), nil
+		}
+		return gdpr.SignalNo, strconv.Itoa(int(gdpr.SignalNo)), nil
+	}
+
+	if requestGDPR == nil {
+		return gdpr.SignalAmbiguous, "", nil
+	}
+
+	gdprSignal, err := gdpr.IntSignalParse(*requestGDPR)
+	if err != nil {
+		return gdpr.SignalAmbiguous, strconv.Itoa(*requestGDPR), err
+	}
+	return gdprSignal, strconv.Itoa(*requestGDPR), nil
+}
+
 func (c *cookieSyncEndpoint) writeParseRequestErrorMetrics(err error) {
 	switch err {
 	case errCookieSyncAccountBlocked:
diff --git a/errortypes/code.go b/errortypes/code.go
index 99afb02a..c68eb196 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -27,6 +27,7 @@ const (
 	MultiBidWarningCode
 	AdServerTargetingWarningCode
 	BidAdjustmentWarningCode
+	FloorBidRejectionWarningCode
 )
 
 // Coder provides an error or warning code with severity.
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index ef45f71e..e2b3af82 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -241,6 +241,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderConnectAd:         connectad.Builder,
 		openrtb_ext.BidderConsumable:        consumable.Builder,
 		openrtb_ext.BidderConversant:        conversant.Builder,
+		openrtb_ext.BidderCopper6:           adtelligent.Builder,
 		openrtb_ext.BidderCpmstar:           cpmstar.Builder,
 		openrtb_ext.BidderCriteo:            criteo.Builder,
 		openrtb_ext.BidderCWire:             cwire.Builder,
diff --git a/exchange/entities/entities.go b/exchange/entities/entities.go
index 8a4121dc..1220da5c 100644
--- a/exchange/entities/entities.go
+++ b/exchange/entities/entities.go
@@ -32,6 +32,7 @@ type PbsOrtbSeatBid struct {
 // PbsOrtbBid.BidTargets does not need to be filled out by the Bidder. It will be set later by the exchange.
 // PbsOrtbBid.BidVideo is optional but should be filled out by the Bidder if BidType is video.
 // PbsOrtbBid.BidEvents is set by exchange when event tracking is enabled
+// PbsOrtbBid.BidFloors is set by exchange when floors is enabled
 // PbsOrtbBid.DealPriority is optionally provided by adapters and used internally by the exchange to support deal targeted campaigns.
 // PbsOrtbBid.DealTierSatisfied is set to true by exchange.updateHbPbCatDur if deal tier satisfied otherwise it will be set to false
 // PbsOrtbBid.GeneratedBidID is unique Bid id generated by prebid server if generate Bid id option is enabled in config
@@ -42,6 +43,7 @@ type PbsOrtbBid struct {
 	BidTargets        map[string]string
 	BidVideo          *openrtb_ext.ExtBidPrebidVideo
 	BidEvents         *openrtb_ext.ExtBidPrebidEvents
+	BidFloors         *openrtb_ext.ExtBidPrebidFloors
 	DealPriority      int
 	DealTierSatisfied bool
 	GeneratedBidID    string
diff --git a/exchange/exchange.go b/exchange/exchange.go
index c1b46d33..8361fa27 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -374,6 +374,19 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	)
 
 	if anyBidsReturned {
+		if e.floor.Enabled {
+			var rejectedBids []*entities.PbsOrtbSeatBid
+			var enforceErrs []error
+
+			adapterBids, enforceErrs, rejectedBids = floors.Enforce(r.BidRequestWrapper, adapterBids, r.Account, conversions)
+			errs = append(errs, enforceErrs...)
+			for _, rejectedBid := range rejectedBids {
+				errs = append(errs, &errortypes.Warning{
+					Message:     fmt.Sprintf("%s bid id %s rejected - bid price %.4f %s is less than bid floor %.4f %s for imp %s", rejectedBid.Seat, rejectedBid.Bids[0].Bid.ID, rejectedBid.Bids[0].Bid.Price, rejectedBid.Currency, rejectedBid.Bids[0].BidFloors.FloorValue, rejectedBid.Bids[0].BidFloors.FloorCurrency, rejectedBid.Bids[0].Bid.ImpID),
+					WarningCode: errortypes.FloorBidRejectionWarningCode})
+			}
+		}
+
 		var bidCategory map[string]string
 		//If includebrandcategory is present in ext then CE feature is on.
 		if requestExtPrebid.Targeting != nil && requestExtPrebid.Targeting.IncludeBrandCategory != nil {
@@ -1216,6 +1229,7 @@ func (e *exchange) makeBid(bids []*entities.PbsOrtbBid, auc *auction, returnCrea
 			DealTierSatisfied: bid.DealTierSatisfied,
 			Events:            bid.BidEvents,
 			Targeting:         bid.BidTargets,
+			Floors:            bid.BidFloors,
 			Type:              bid.BidType,
 			Meta:              bid.BidMeta,
 			Video:             bid.BidVideo,
diff --git a/floors/enforce.go b/floors/enforce.go
new file mode 100644
index 00000000..8bfc31c3
--- /dev/null
+++ b/floors/enforce.go
@@ -0,0 +1,260 @@
+package floors
+
+import (
+	"errors"
+	"fmt"
+	"math/rand"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/currency"
+	"github.com/prebid/prebid-server/exchange/entities"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// Enforce does floors enforcement for bids from all bidders based on floors provided in request, account level floors config
+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) {
+	rejectionErrs := []error{}
+
+	rejectedBids := []*entities.PbsOrtbSeatBid{}
+
+	requestExt, err := bidRequestWrapper.GetRequestExt()
+	if err != nil {
+		return seatBids, []error{errors.New("Error in getting request extension")}, rejectedBids
+	}
+
+	if !isPriceFloorsEnabled(account, bidRequestWrapper) {
+		return seatBids, []error{errors.New("Floors feature is disabled at account or in the request")}, rejectedBids
+	}
+
+	if isSignalingSkipped(requestExt) || !isValidImpBidFloorPresent(bidRequestWrapper.BidRequest.Imp) {
+		return seatBids, nil, rejectedBids
+	}
+
+	enforceFloors := isSatisfiedByEnforceRate(requestExt, account.PriceFloors.EnforceFloorsRate, rand.Intn)
+	if updateEnforcePBS(enforceFloors, requestExt) {
+		err := bidRequestWrapper.RebuildRequest()
+		if err != nil {
+			return seatBids, []error{err}, rejectedBids
+		}
+	}
+	updateBidExt(bidRequestWrapper, seatBids)
+	if enforceFloors {
+		enforceDealFloors := account.PriceFloors.EnforceDealFloors && getEnforceDealsFlag(requestExt)
+		seatBids, rejectionErrs, rejectedBids = enforceFloorToBids(bidRequestWrapper, seatBids, conversions, enforceDealFloors)
+	}
+	return seatBids, rejectionErrs, rejectedBids
+}
+
+// updateEnforcePBS updates prebid extension in request if enforcePBS needs to be updated
+func updateEnforcePBS(enforceFloors bool, requestExt *openrtb_ext.RequestExt) bool {
+	updateReqExt := false
+
+	prebidExt := requestExt.GetPrebid()
+	if prebidExt == nil {
+		prebidExt = new(openrtb_ext.ExtRequestPrebid)
+	}
+
+	if prebidExt.Floors == nil {
+		prebidExt.Floors = new(openrtb_ext.PriceFloorRules)
+	}
+	floorExt := prebidExt.Floors
+
+	if floorExt.Enforcement == nil {
+		floorExt.Enforcement = new(openrtb_ext.PriceFloorEnforcement)
+	}
+
+	if floorExt.Enforcement.EnforcePBS == nil {
+		updateReqExt = true
+		floorExt.Enforcement.EnforcePBS = new(bool)
+		*floorExt.Enforcement.EnforcePBS = enforceFloors
+	} else {
+		oldEnforcePBS := *floorExt.Enforcement.EnforcePBS
+		*floorExt.Enforcement.EnforcePBS = enforceFloors && *floorExt.Enforcement.EnforcePBS
+		updateReqExt = oldEnforcePBS != *floorExt.Enforcement.EnforcePBS
+	}
+
+	if updateReqExt {
+		requestExt.SetPrebid(prebidExt)
+	}
+
+	return updateReqExt
+}
+
+// updateBidExt updates bid extension for floors related details
+func updateBidExt(bidRequestWrapper *openrtb_ext.RequestWrapper, seatBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid) {
+	impMap := make(map[string]*openrtb_ext.ImpWrapper, bidRequestWrapper.LenImp())
+	for _, imp := range bidRequestWrapper.GetImp() {
+		impMap[imp.ID] = imp
+	}
+
+	for _, seatBid := range seatBids {
+		for _, bid := range seatBid.Bids {
+			reqImp, ok := impMap[bid.Bid.ImpID]
+			if ok {
+				updateBidExtWithFloors(reqImp, bid, reqImp.BidFloorCur)
+			}
+		}
+	}
+}
+
+// enforceFloorToBids function does floors enforcement for each bid,
+// The bids returned by each partner below bid floor price are rejected and remaining eligible bids are considered for further processing
+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) {
+	errs := []error{}
+	rejectedBids := []*entities.PbsOrtbSeatBid{}
+	impMap := make(map[string]*openrtb_ext.ImpWrapper, bidRequestWrapper.LenImp())
+
+	for _, v := range bidRequestWrapper.GetImp() {
+		impMap[v.ID] = v
+	}
+
+	for bidderName, seatBid := range seatBids {
+		eligibleBids := make([]*entities.PbsOrtbBid, 0, len(seatBid.Bids))
+		for _, bid := range seatBid.Bids {
+
+			reqImp, ok := impMap[bid.Bid.ImpID]
+			if !ok {
+				continue
+			}
+
+			requestExt, err := bidRequestWrapper.GetRequestExt()
+			if err != nil {
+				errs = append(errs, fmt.Errorf("error in getting req extension = %v", err.Error()))
+				continue
+			}
+
+			if isEnforcementEnabled(requestExt) {
+				if hasDealID(bid) && !enforceDealFloors {
+					eligibleBids = append(eligibleBids, bid)
+					continue
+				}
+
+				rate, err := getCurrencyConversionRate(seatBid.Currency, reqImp.BidFloorCur, conversions)
+				if err != nil {
+					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()))
+					continue
+				}
+
+				bidPrice := rate * bid.Bid.Price
+				if reqImp.BidFloor > bidPrice {
+					rejectedBid := &entities.PbsOrtbSeatBid{
+						Currency: seatBid.Currency,
+						Seat:     seatBid.Seat,
+						Bids:     []*entities.PbsOrtbBid{bid},
+					}
+					rejectedBids = append(rejectedBids, rejectedBid)
+					continue
+				}
+			}
+			eligibleBids = append(eligibleBids, bid)
+		}
+		seatBids[bidderName].Bids = eligibleBids
+	}
+	return seatBids, errs, rejectedBids
+}
+
+// isEnforcementEnabled check for floors enforcement enabled in request
+func isEnforcementEnabled(requestExt *openrtb_ext.RequestExt) bool {
+	if floorsExt := getFloorsExt(requestExt); floorsExt != nil {
+		return floorsExt.GetEnforcePBS()
+	}
+	return true
+}
+
+// isSignalingSkipped check for floors signalling is skipped due to skip rate
+func isSignalingSkipped(requestExt *openrtb_ext.RequestExt) bool {
+	if floorsExt := getFloorsExt(requestExt); floorsExt != nil {
+		return floorsExt.GetFloorsSkippedFlag()
+	}
+	return false
+}
+
+// getEnforceRateRequest returns enforceRate provided in request
+func getEnforceRateRequest(requestExt *openrtb_ext.RequestExt) int {
+	if floorsExt := getFloorsExt(requestExt); floorsExt != nil {
+		return floorsExt.GetEnforceRate()
+	}
+	return 0
+}
+
+// getEnforceDealsFlag returns FloorDeals flag from req.ext.prebid.floors.enforcement
+func getEnforceDealsFlag(requestExt *openrtb_ext.RequestExt) bool {
+	if floorsExt := getFloorsExt(requestExt); floorsExt != nil {
+		return floorsExt.GetEnforceDealsFlag()
+	}
+	return false
+}
+
+// getFloorsExt returns req.ext.prebid.floors
+func getFloorsExt(requestExt *openrtb_ext.RequestExt) *openrtb_ext.PriceFloorRules {
+	if requestExt != nil {
+		if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil {
+			return prebidExt.Floors
+		}
+	}
+	return nil
+}
+
+// isValidImpBidFloorPresent checks if non zero imp.bidfloor is present in request
+func isValidImpBidFloorPresent(imp []openrtb2.Imp) bool {
+	for i := range imp {
+		if imp[i].BidFloor > 0 {
+			return true
+		}
+	}
+	return false
+}
+
+// isSatisfiedByEnforceRate check enforcements should be done or not based on enforceRate in config and in request
+func isSatisfiedByEnforceRate(requestExt *openrtb_ext.RequestExt, configEnforceRate int, f func(int) int) bool {
+	requestEnforceRate := getEnforceRateRequest(requestExt)
+	enforceRate := f(enforceRateMax)
+	satisfiedByRequest := requestEnforceRate == 0 || enforceRate < requestEnforceRate
+	satisfiedByAccount := configEnforceRate == 0 || enforceRate < configEnforceRate
+	shouldEnforce := satisfiedByRequest && satisfiedByAccount
+
+	return shouldEnforce
+}
+
+// hasDealID checks for dealID presence in bid
+func hasDealID(bid *entities.PbsOrtbBid) bool {
+	if bid != nil && bid.Bid != nil && bid.Bid.DealID != "" {
+		return true
+	}
+	return false
+}
+
+// getCurrencyConversionRate gets conversion rate in case floor currency and seatBid currency are not same
+func getCurrencyConversionRate(seatBidCur, reqImpCur string, conversions currency.Conversions) (float64, error) {
+	rate := 1.0
+	if seatBidCur != reqImpCur {
+		return conversions.GetRate(seatBidCur, reqImpCur)
+	} else {
+		return rate, nil
+	}
+}
+
+// updateBidExtWithFloors updates floors related details in bid extension
+func updateBidExtWithFloors(reqImp *openrtb_ext.ImpWrapper, bid *entities.PbsOrtbBid, floorCurrency string) {
+	impExt, err := reqImp.GetImpExt()
+	if err != nil {
+		return
+	}
+
+	var bidExtFloors openrtb_ext.ExtBidPrebidFloors
+	prebidExt := impExt.GetPrebid()
+	if prebidExt == nil || prebidExt.Floors == nil {
+		if reqImp.BidFloor > 0 {
+			bidExtFloors.FloorValue = reqImp.BidFloor
+			bidExtFloors.FloorCurrency = reqImp.BidFloorCur
+			bid.BidFloors = &bidExtFloors
+		}
+	} else {
+		bidExtFloors.FloorRule = prebidExt.Floors.FloorRule
+		bidExtFloors.FloorRuleValue = prebidExt.Floors.FloorRuleValue
+		bidExtFloors.FloorValue = prebidExt.Floors.FloorValue
+		bidExtFloors.FloorCurrency = floorCurrency
+		bid.BidFloors = &bidExtFloors
+	}
+}
diff --git a/floors/floors.go b/floors/floors.go
index efed0d11..f9e13ec0 100644
--- a/floors/floors.go
+++ b/floors/floors.go
@@ -16,6 +16,7 @@ type Price struct {
 }
 
 const (
+	defaultCurrency  string = "USD"
 	defaultDelimiter string = "|"
 	catchAll         string = "*"
 	skipRateMin      int    = 0
@@ -34,7 +35,7 @@ func EnrichWithPriceFloors(bidRequestWrapper *openrtb_ext.RequestWrapper, accoun
 		return []error{errors.New("Empty bidrequest")}
 	}
 
-	if isPriceFloorsDisabled(account, bidRequestWrapper) {
+	if !isPriceFloorsEnabled(account, bidRequestWrapper) {
 		return []error{errors.New("Floors feature is disabled at account or in the request")}
 	}
 
@@ -112,25 +113,25 @@ func roundToFourDecimals(in float64) float64 {
 	return math.Round(in*10000) / 10000
 }
 
-// isPriceFloorsDisabled check for floors are disabled at account or request level
-func isPriceFloorsDisabled(account config.Account, bidRequestWrapper *openrtb_ext.RequestWrapper) bool {
-	return isPriceFloorsDisabledForAccount(account) || isPriceFloorsDisabledForRequest(bidRequestWrapper)
+// 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)
 }
 
-// isPriceFloorsDisabledForAccount check for floors are disabled at account
-func isPriceFloorsDisabledForAccount(account config.Account) bool {
-	return !account.PriceFloors.Enabled
+// isPriceFloorsEnabledForAccount check for floors enabled flag in account config
+func isPriceFloorsEnabledForAccount(account config.Account) bool {
+	return account.PriceFloors.Enabled
 }
 
-// isPriceFloorsDisabledForRequest check for floors are disabled at request
-func isPriceFloorsDisabledForRequest(bidRequestWrapper *openrtb_ext.RequestWrapper) bool {
+// 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 && !prebidExt.Floors.GetEnabled() {
-			return true
+		if prebidExt := requestExt.GetPrebid(); prebidExt != nil && prebidExt.Floors != nil {
+			return prebidExt.Floors.GetEnabled()
 		}
 	}
-	return false
+	return true
 }
 
 // resolveFloors does selection of floors fields from request data and dynamic fetched data if dynamic fetch is enabled
@@ -200,10 +201,11 @@ func updateFloorsInRequest(bidRequestWrapper *openrtb_ext.RequestWrapper, priceF
 	requestExt, err := bidRequestWrapper.GetRequestExt()
 	if err == nil {
 		prebidExt := requestExt.GetPrebid()
-		if prebidExt != nil {
-			prebidExt.Floors = priceFloors
-			requestExt.SetPrebid(prebidExt)
-			bidRequestWrapper.RebuildRequest()
+		if prebidExt == nil {
+			prebidExt = &openrtb_ext.ExtRequestPrebid{}
 		}
+		prebidExt.Floors = priceFloors
+		requestExt.SetPrebid(prebidExt)
+		bidRequestWrapper.RebuildRequest()
 	}
 }
diff --git a/floors/rule.go b/floors/rule.go
index 101239e9..f5f74cb6 100644
--- a/floors/rule.go
+++ b/floors/rule.go
@@ -51,7 +51,7 @@ func getFloorCurrency(floorExt *openrtb_ext.PriceFloorRules) string {
 	}
 
 	if len(floorCur) == 0 {
-		floorCur = "USD"
+		floorCur = defaultCurrency
 	}
 
 	return floorCur
diff --git a/openrtb_ext/bid.go b/openrtb_ext/bid.go
index b242258e..20bbf0b3 100644
--- a/openrtb_ext/bid.go
+++ b/openrtb_ext/bid.go
@@ -25,6 +25,15 @@ type ExtBidPrebid struct {
 	Events            *ExtBidPrebidEvents `json:"events,omitempty"`
 	BidId             string              `json:"bidid,omitempty"`
 	Passthrough       json.RawMessage     `json:"passthrough,omitempty"`
+	Floors            *ExtBidPrebidFloors `json:"floors,omitempty"`
+}
+
+// ExtBidPrebidFloors defines the contract for bidresponse.seatbid.bid[i].ext.prebid.floors
+type ExtBidPrebidFloors struct {
+	FloorRule      string  `json:"floorRule,omitempty"`
+	FloorRuleValue float64 `json:"floorRuleValue,omitempty"`
+	FloorValue     float64 `json:"floorValue,omitempty"`
+	FloorCurrency  string  `json:"floorCurrency,omitempty"`
 }
 
 // ExtBidPrebidCache defines the contract for  bidresponse.seatbid.bid[i].ext.prebid.cache
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 145e66f3..5a342a98 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -148,6 +148,7 @@ const (
 	BidderConnectAd         BidderName = "connectad"
 	BidderConsumable        BidderName = "consumable"
 	BidderConversant        BidderName = "conversant"
+	BidderCopper6           BidderName = "copper6"
 	BidderCpmstar           BidderName = "cpmstar"
 	BidderCriteo            BidderName = "criteo"
 	BidderCWire             BidderName = "cwire"
@@ -338,6 +339,7 @@ func CoreBidderNames() []BidderName {
 		BidderConnectAd,
 		BidderConsumable,
 		BidderConversant,
+		BidderCopper6,
 		BidderCpmstar,
 		BidderCriteo,
 		BidderCWire,
diff --git a/openrtb_ext/floors.go b/openrtb_ext/floors.go
index 409f9de9..3553946c 100644
--- a/openrtb_ext/floors.go
+++ b/openrtb_ext/floors.go
@@ -31,6 +31,46 @@ type PriceFloorRules struct {
 	PriceFloorLocation string                 `json:"location,omitempty"`
 }
 
+// GetEnforcePBS will check if floors enforcement is enabled in request
+func (Floors *PriceFloorRules) GetEnforcePBS() bool {
+	if Floors != nil && Floors.Enforcement != nil && Floors.Enforcement.EnforcePBS != nil {
+		return *Floors.Enforcement.EnforcePBS
+	}
+	return true
+}
+
+// GetFloorsSkippedFlag will return  floors skipped flag
+func (Floors *PriceFloorRules) GetFloorsSkippedFlag() bool {
+	if Floors != nil && Floors.Skipped != nil {
+		return *Floors.Skipped
+	}
+	return false
+}
+
+// GetEnforceRate will return enforcement rate in request
+func (Floors *PriceFloorRules) GetEnforceRate() int {
+	if Floors != nil && Floors.Enforcement != nil {
+		return Floors.Enforcement.EnforceRate
+	}
+	return 0
+}
+
+// GetEnforceDealsFlag will return FloorDeals flag in request
+func (Floors *PriceFloorRules) GetEnforceDealsFlag() bool {
+	if Floors != nil && Floors.Enforcement != nil && Floors.Enforcement.FloorDeals != nil {
+		return *Floors.Enforcement.FloorDeals
+	}
+	return false
+}
+
+// GetEnabled will check if floors is enabled in request
+func (Floors *PriceFloorRules) GetEnabled() bool {
+	if Floors != nil && Floors.Enabled != nil {
+		return *Floors.Enabled
+	}
+	return true
+}
+
 type PriceFloorEndpoint struct {
 	URL string `json:"url,omitempty"`
 }
@@ -53,6 +93,28 @@ type PriceFloorModelGroup struct {
 	Values       map[string]float64 `json:"values,omitempty"`
 	Default      float64            `json:"default,omitempty"`
 }
+
+func (mg PriceFloorModelGroup) Copy() PriceFloorModelGroup {
+	newMg := new(PriceFloorModelGroup)
+	newMg.Currency = mg.Currency
+	newMg.ModelVersion = mg.ModelVersion
+	newMg.SkipRate = mg.SkipRate
+	newMg.Default = mg.Default
+	if mg.ModelWeight != nil {
+		newMg.ModelWeight = new(int)
+		*newMg.ModelWeight = *mg.ModelWeight
+	}
+
+	newMg.Schema.Delimiter = mg.Schema.Delimiter
+	newMg.Schema.Fields = make([]string, len(mg.Schema.Fields))
+	copy(newMg.Schema.Fields, mg.Schema.Fields)
+	newMg.Values = make(map[string]float64, len(mg.Values))
+	for key, val := range mg.Values {
+		newMg.Values[key] = val
+	}
+	return *newMg
+}
+
 type PriceFloorSchema struct {
 	Fields    []string `json:"fields,omitempty"`
 	Delimiter string   `json:"delimiter,omitempty"`
@@ -83,32 +145,3 @@ type ExtImp struct {
 type ImpExtPrebid struct {
 	Floors Price `json:"floors,omitempty"`
 }
-
-// GetEnabled will check if floors is enabled in request
-func (Floors *PriceFloorRules) GetEnabled() bool {
-	if Floors != nil && Floors.Enabled != nil {
-		return *Floors.Enabled
-	}
-	return true
-}
-
-func (modelGroup PriceFloorModelGroup) Copy() PriceFloorModelGroup {
-	newModelGroup := new(PriceFloorModelGroup)
-	newModelGroup.Currency = modelGroup.Currency
-	newModelGroup.ModelVersion = modelGroup.ModelVersion
-	newModelGroup.SkipRate = modelGroup.SkipRate
-	newModelGroup.Default = modelGroup.Default
-	if modelGroup.ModelWeight != nil {
-		newModelGroup.ModelWeight = new(int)
-		*newModelGroup.ModelWeight = *modelGroup.ModelWeight
-	}
-
-	newModelGroup.Schema.Delimiter = modelGroup.Schema.Delimiter
-	newModelGroup.Schema.Fields = make([]string, len(modelGroup.Schema.Fields))
-	copy(newModelGroup.Schema.Fields, modelGroup.Schema.Fields)
-	newModelGroup.Values = make(map[string]float64, len(modelGroup.Values))
-	for key, val := range modelGroup.Values {
-		newModelGroup.Values[key] = val
-	}
-	return *newModelGroup
-}
diff --git a/openrtb_ext/imp_kargo.go b/openrtb_ext/imp_kargo.go
index 682561fb..f01e602a 100644
--- a/openrtb_ext/imp_kargo.go
+++ b/openrtb_ext/imp_kargo.go
@@ -1,5 +1,6 @@
 package openrtb_ext
 
 type ImpExtKargo struct {
-	AdSlotID string `json:"adSlotID"`
+	PlacementId string `json:"placementId"`
+	AdSlotID    string `json:"adSlotID"` // Deprecated - Use `placementId`
 }
diff --git a/static/bidder-info/copper6.yaml b/static/bidder-info/copper6.yaml
new file mode 100644
index 00000000..213cbe36
--- /dev/null
+++ b/static/bidder-info/copper6.yaml
@@ -0,0 +1,17 @@
+endpoint: "http://ghb.app.copper6.com/pbs/ortb"
+maintainer:
+  email: "info@copper6.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  # Copper6 ssp supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - iframe
diff --git a/static/bidder-params/copper6.json b/static/bidder-params/copper6.json
new file mode 100644
index 00000000..fa4050f6
--- /dev/null
+++ b/static/bidder-params/copper6.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Copper6 Adapter Params",
+  "description": "A schema which validates params accepted by the Copper6 ssp adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
diff --git a/static/bidder-params/kargo.json b/static/bidder-params/kargo.json
index 15c11526..314eb63f 100644
--- a/static/bidder-params/kargo.json
+++ b/static/bidder-params/kargo.json
@@ -3,13 +3,18 @@
     "title": "Kargo Adapter Params",
     "description": "A schema which validates params accepted by the Kargo adapter",
     "type": "object",
-  
     "properties": {
-        "adSlotID": {
-          "type": "string",
-          "description": "An ID which identifies the adslot placement. Equivalent to the id of target inventory, ad unit code, or placement id"
-        }
+      "placementId": {
+        "type": "string",
+        "description": "An ID which identifies the adslot placement. Equivalent to the id of target inventory, ad unit code, or placement id"
       },
-    
-    "required": ["adSlotID"]
+      "adSlotID": {
+        "type": "string",
+        "description": "[Deprecated: Use `placementId`] An ID which identifies the adslot placement. Equivalent to the id of target inventory, ad unit code, or placement id"
+      }
+    },
+    "oneOf": [
+      { "required": ["placementId"] },
+      { "required": ["adSlotID"] }
+    ]
   }
diff --git a/usersync/cookie.go b/usersync/cookie.go
index 5732e6c4..51474afd 100644
--- a/usersync/cookie.go
+++ b/usersync/cookie.go
@@ -226,10 +226,9 @@ func (cookie *Cookie) TrySync(key string, uid string) error {
 // This exists so that Cookie (which is public) can have private fields, and the rest of
 // the code doesn't have to worry about the cookie data storage format.
 type cookieJson struct {
-	LegacyUIDs map[string]string        `json:"uids,omitempty"`
-	UIDs       map[string]uidWithExpiry `json:"tempUIDs,omitempty"`
-	OptOut     bool                     `json:"optout,omitempty"`
-	Birthday   *time.Time               `json:"bday,omitempty"`
+	UIDs     map[string]uidWithExpiry `json:"tempUIDs,omitempty"`
+	OptOut   bool                     `json:"optout,omitempty"`
+	Birthday *time.Time               `json:"bday,omitempty"`
 }
 
 func (cookie *Cookie) MarshalJSON() ([]byte, error) {
@@ -240,53 +239,31 @@ func (cookie *Cookie) MarshalJSON() ([]byte, error) {
 	})
 }
 
-// UnmarshalJSON holds some transition code.
-//
-// "Legacy" cookies had UIDs *without* expiration dates, and recognized "0" as a legitimate UID for audienceNetwork.
-// "Current" cookies always include UIDs with expiration dates, and never allow "0" for audienceNetwork.
-//
-// This Unmarshal method interprets both data formats, and does some conversions on legacy data to make it current.
-// If you're seeing this message after March 2018, it's safe to assume that all the legacy cookies have been
-// updated and remove the legacy logic.
 func (cookie *Cookie) UnmarshalJSON(b []byte) error {
 	var cookieContract cookieJson
-	err := json.Unmarshal(b, &cookieContract)
-	if err == nil {
-		cookie.optOut = cookieContract.OptOut
-		cookie.birthday = cookieContract.Birthday
-
-		if cookie.optOut {
-			cookie.uids = make(map[string]uidWithExpiry)
-		} else {
-			cookie.uids = cookieContract.UIDs
-
-			if cookie.uids == nil {
-				cookie.uids = make(map[string]uidWithExpiry, len(cookieContract.LegacyUIDs))
-			}
+	if err := json.Unmarshal(b, &cookieContract); err != nil {
+		return err
+	}
 
-			// Interpret "legacy" UIDs as having been expired already.
-			// This should cause us to re-sync, since it would be time for a new one.
-			for bidder, uid := range cookieContract.LegacyUIDs {
-				if _, ok := cookie.uids[bidder]; !ok {
-					cookie.uids[bidder] = uidWithExpiry{
-						UID:     uid,
-						Expires: time.Now().Add(-5 * time.Minute),
-					}
-				}
-			}
+	cookie.optOut = cookieContract.OptOut
+	cookie.birthday = cookieContract.Birthday
 
-			// Any "0" values from audienceNetwork really meant "no ID available." This happens if they've never
-			// logged into Facebook. However... once we know a user's ID, we stop trying to re-sync them until the
-			// expiration date has passed.
-			//
-			// Since users may log into facebook later, this is a bad strategy.
-			// Since "0" is a fake ID for this bidder, we'll just treat it like it doesn't exist.
-			if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" {
-				delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork))
-			}
-		}
+	if cookie.optOut {
+		cookie.uids = nil
+	} else {
+		cookie.uids = cookieContract.UIDs
 	}
-	return err
+
+	if cookie.uids == nil {
+		cookie.uids = make(map[string]uidWithExpiry)
+	}
+
+	// Audience Network / Facebook Handling
+	if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" {
+		delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork))
+	}
+
+	return nil
 }
 
 func timestamp() *time.Time {


Test patch:
diff --git a/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json b/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json
index ce56b01e..03a72fd0 100644
--- a/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json
+++ b/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json
@@ -58,69 +58,6 @@
       }
     ]
   },
-  "httpCalls": [
-    {
-      "expectedRequest": {
-        "uri": "http://ib.adnxs.com/openrtb2?member_id=103",
-        "body": {
-          "id": "test-request-id",
-          "ext": {
-            "appnexus": {
-              "hb_source": 5,
-              "brand_category_uniqueness": true,
-              "include_brand_category": true
-            },
-            "prebid": {
-              "targeting": {"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includebidderkeys":true,"includewinners":true,"includebrandcategory":{"primaryadserver":1,"publisher":"","withcategory":true}}
-            }
-          },
-          "imp": [
-            {
-              "id":"test-imp-id",
-              "banner": {
-                "format": [
-                  {"w":300,"h":250},
-                  {"w":300,"h":600}
-                ],
-                "w": 300,
-                "h": 250,
-                "pos": 3
-              },
-              "tagid": "abc",
-              "bidfloor": 20,
-              "ext": {
-                "appnexus": {
-                  "use_pmt_rule": true
-                }
-              }
-            },
-            {
-              "id":"test-imp-id-2",
-              "banner": {
-                "format": [
-                  {"w":300,"h":250},
-                  {"w":300,"h":600}
-                ],
-                "w": 300,
-                "h": 250,
-                "pos": 3
-              },
-              "tagid": "abc",
-              "bidfloor": 20,
-              "ext": {
-                "appnexus": {
-                  "use_pmt_rule": true
-                }
-              }
-            }
-          ]
-        }
-      },
-      "mockResponse": {
-        "status": 204
-      }
-    }
-  ],
   "expectedBidResponses": [],
   "expectedMakeRequestsErrors": [
     {
diff --git a/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json b/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json
index 38ed5c45..af62c3d1 100755
--- a/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json
+++ b/adapters/criteo/criteotest/exemplary/simple-banner-inapp.json
@@ -84,7 +84,8 @@
                   "h": 250,
                   "ext": {
                     "prebid": {
-                      "type": "banner"
+                      "type": "banner",
+                      "networkName": "Criteo"
                     }
                   }
                 }
@@ -110,7 +111,8 @@
             "h": 250,
             "ext": {
               "prebid": {
-                "type": "banner"
+                "type": "banner",
+                "networkName": "Criteo"
               }
             }
           },
diff --git a/adapters/huaweiads/huaweiadstest/exemplary/banner2.json b/adapters/huaweiads/huaweiadstest/exemplary/banner2.json
index 4f1cc6b4..92afb558 100644
--- a/adapters/huaweiads/huaweiadstest/exemplary/banner2.json
+++ b/adapters/huaweiads/huaweiadstest/exemplary/banner2.json
@@ -51,7 +51,7 @@
       "carrier": "carrier",
       "connectiontype": 1,
       "geo": {
-        "country": "ZAF"
+        "country": "ZA"
       }
     },
     "app": {
@@ -269,4 +269,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/kargo/params_test.go b/adapters/kargo/params_test.go
index 44937637..cb9ffaf1 100644
--- a/adapters/kargo/params_test.go
+++ b/adapters/kargo/params_test.go
@@ -34,14 +34,20 @@ func TestInvalidParams(t *testing.T) {
 }
 
 var validParams = []string{
+	`{"placementId": ""}`,
+	`{"placementId": "11523"}`,
 	`{"adSlotID": ""}`,
 	`{"adSlotID": "11523"}`,
 }
 
 var invalidParams = []string{
+	`{"placementId": 42}`,
+	`{"placementId": }`,
+	`{"placementID": "32321"}`,
 	`{"adSlotID": 42}`,
 	`{"adSlotID": }`,
 	`{"adSlotId": "32321"}`,
 	`{"id": }`,
 	`{}`,
+	`{"placementId": "11523", "adSlotID": "12345"}`, // Can't include both
 }
diff --git a/config/config_test.go b/config/config_test.go
index fbd96d3f..64f4f8a0 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -196,6 +196,11 @@ func TestDefaults(t *testing.T) {
 	cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight))
 	cmpBools(t, "account_modules_metrics", false, cfg.Metrics.Disabled.AccountModulesMetrics)
 
+	cmpBools(t, "tmax_adjustments.enabled", false, cfg.TmaxAdjustments.Enabled)
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_response_duration_min_ms", 0, cfg.TmaxAdjustments.BidderResponseDurationMin)
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_network_latency_buffer_ms", 0, cfg.TmaxAdjustments.BidderNetworkLatencyBuffer)
+	cmpUnsignedInts(t, "tmax_adjustments.pbs_response_preparation_duration_ms", 0, cfg.TmaxAdjustments.PBSResponsePreparationDuration)
+
 	//Assert purpose VendorExceptionMap hash tables were built correctly
 	expectedTCF2 := TCF2{
 		Enabled: true,
@@ -461,6 +466,11 @@ account_defaults:
         use_dynamic_data: true
         max_rules: 120
         max_schema_dims: 5
+tmax_adjustments:
+  enabled: true
+  bidder_response_duration_min_ms: 700
+  bidder_network_latency_buffer_ms: 100
+  pbs_response_preparation_duration_ms: 100
 `)
 
 var oldStoredRequestsConfig = []byte(`
@@ -479,6 +489,11 @@ func cmpInts(t *testing.T, key string, expected, actual int) {
 	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
 }
 
+func cmpUnsignedInts(t *testing.T, key string, expected, actual uint) {
+	t.Helper()
+	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
+}
+
 func cmpInt8s(t *testing.T, key string, expected, actual *int8) {
 	t.Helper()
 	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
@@ -540,6 +555,10 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "validations.secure_markup", "skip", cfg.Validations.SecureMarkup)
 	cmpInts(t, "validations.max_creative_width", 0, int(cfg.Validations.MaxCreativeWidth))
 	cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight))
+	cmpBools(t, "tmax_adjustments.enabled", false, cfg.TmaxAdjustments.Enabled) // Tmax adjustment feature is still under development. Therefore enabled flag is set to false
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_response_duration_min_ms", 700, cfg.TmaxAdjustments.BidderResponseDurationMin)
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_network_latency_buffer_ms", 100, cfg.TmaxAdjustments.BidderNetworkLatencyBuffer)
+	cmpUnsignedInts(t, "tmax_adjustments.pbs_response_preparation_duration_ms", 100, cfg.TmaxAdjustments.PBSResponsePreparationDuration)
 
 	//Assert the price floor values
 	cmpBools(t, "price_floors.enabled", true, cfg.PriceFloors.Enabled)
diff --git a/endpoints/cookie_sync_test.go b/endpoints/cookie_sync_test.go
index 3947d1f0..37acf0c2 100644
--- a/endpoints/cookie_sync_test.go
+++ b/endpoints/cookie_sync_test.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 	"net/http/httptest"
+	"strconv"
 	"strings"
 	"testing"
 	"testing/iotest"
@@ -23,6 +24,7 @@ import (
 	gdprPrivacy "github.com/prebid/prebid-server/privacy/gdpr"
 	gppPrivacy "github.com/prebid/prebid-server/privacy/gpp"
 	"github.com/prebid/prebid-server/usersync"
+	"github.com/prebid/prebid-server/util/ptrutil"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -303,6 +305,185 @@ func TestCookieSyncHandle(t *testing.T) {
 	}
 }
 
+func TestExtractGDPRSignal(t *testing.T) {
+	type testInput struct {
+		requestGDPR *int
+		gppSID      []int8
+	}
+	type testOutput struct {
+		gdprSignal gdpr.Signal
+		gdprString string
+		err        error
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected testOutput
+	}{
+		{
+			desc: "SectionTCFEU2 is listed in GPP_SID array, expect SignalYes and nil error",
+			in: testInput{
+				requestGDPR: nil,
+				gppSID:      []int8{2},
+			},
+			expected: testOutput{
+				gdprSignal: gdpr.SignalYes,
+				gdprString: strconv.Itoa(int(gdpr.SignalYes)),
+				err:        nil,
+			},
+		},
+		{
+			desc: "SectionTCFEU2 is not listed in GPP_SID array, expect SignalNo and nil error",
+			in: testInput{
+				requestGDPR: nil,
+				gppSID:      []int8{6},
+			},
+			expected: testOutput{
+				gdprSignal: gdpr.SignalNo,
+				gdprString: strconv.Itoa(int(gdpr.SignalNo)),
+				err:        nil,
+			},
+		},
+		{
+			desc: "Empty GPP_SID array and nil requestGDPR value, expect SignalAmbiguous and nil error",
+			in: testInput{
+				requestGDPR: nil,
+				gppSID:      []int8{},
+			},
+			expected: testOutput{
+				gdprSignal: gdpr.SignalAmbiguous,
+				gdprString: "",
+				err:        nil,
+			},
+		},
+		{
+			desc: "Empty GPP_SID array and non-nil requestGDPR value that could not be successfully parsed, expect SignalAmbiguous and parse error",
+			in: testInput{
+				requestGDPR: ptrutil.ToPtr(2),
+				gppSID:      nil,
+			},
+			expected: testOutput{
+				gdprSignal: gdpr.SignalAmbiguous,
+				gdprString: "2",
+				err:        &errortypes.BadInput{"GDPR signal should be integer 0 or 1"},
+			},
+		},
+		{
+			desc: "Empty GPP_SID array and non-nil requestGDPR value that could be successfully parsed, expect SignalYes and nil error",
+			in: testInput{
+				requestGDPR: ptrutil.ToPtr(1),
+				gppSID:      nil,
+			},
+			expected: testOutput{
+				gdprSignal: gdpr.SignalYes,
+				gdprString: "1",
+				err:        nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		// run
+		outSignal, outGdprStr, outErr := extractGDPRSignal(tc.in.requestGDPR, tc.in.gppSID)
+		// assertions
+		assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc)
+		assert.Equal(t, tc.expected.gdprString, outGdprStr, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
+func TestExtractPrivacyPolicies(t *testing.T) {
+	type testInput struct {
+		request                  cookieSyncRequest
+		usersyncDefaultGDPRValue string
+	}
+	type testOutput struct {
+		policies   privacy.Policies
+		gdprSignal gdpr.Signal
+		err        error
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected testOutput
+	}{
+		{
+			desc: "request GPP string is malformed, expect empty policies, signal No and error",
+			in: testInput{
+				request: cookieSyncRequest{GPP: "malformedGPPString"},
+			},
+			expected: testOutput{
+				policies:   privacy.Policies{},
+				gdprSignal: gdpr.SignalNo,
+				err:        errors.New("error parsing GPP header, header must have type=3"),
+			},
+		},
+		{
+			desc: "Malformed GPPSid string",
+			in: testInput{
+				request: cookieSyncRequest{
+					GPP:       "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+					GPPSid:    "malformed",
+					USPrivacy: "1YYY",
+				},
+			},
+			expected: testOutput{
+				policies:   privacy.Policies{},
+				gdprSignal: gdpr.SignalNo,
+				err:        &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax},
+			},
+		},
+		{
+			desc: "request USPrivacy string is different from the one in the GPP string, expect empty policies, signalNo and error",
+			in: testInput{
+				request: cookieSyncRequest{
+					GPP:       "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+					GPPSid:    "6",
+					USPrivacy: "1YYY",
+				},
+			},
+			expected: testOutput{
+				policies:   privacy.Policies{},
+				gdprSignal: gdpr.SignalNo,
+				err:        errors.New("request.us_privacy consent does not match uspv1"),
+			},
+		},
+		{
+			desc: "no issues extracting privacy policies from request GPP and request GPPSid strings",
+			in: testInput{
+				request: cookieSyncRequest{
+					GPP:    "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+					GPPSid: "6",
+				},
+			},
+			expected: testOutput{
+				policies: privacy.Policies{
+					GDPR: gdprPrivacy.Policy{
+						Signal:  "0",
+						Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
+					},
+					CCPA: ccpa.Policy{
+						Consent: "1YNN",
+					},
+					GPP: gppPrivacy.Policy{
+						Consent: "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN",
+						RawSID:  "6",
+					},
+				},
+				gdprSignal: gdpr.SignalNo,
+				err:        nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		// run
+		outPolicies, outSignal, outErr := extractPrivacyPolicies(tc.in.request, tc.in.usersyncDefaultGDPRValue)
+		// assertions
+		assert.Equal(t, tc.expected.policies, outPolicies, tc.desc)
+		assert.Equal(t, tc.expected.gdprSignal, outSignal, tc.desc)
+		assert.Equal(t, tc.expected.err, outErr, tc.desc)
+	}
+}
+
 func TestCookieSyncParseRequest(t *testing.T) {
 	expectedCCPAParsedPolicy, _ := ccpa.Policy{Consent: "1NYN"}.Parse(map[string]struct{}{})
 
@@ -318,13 +499,13 @@ func TestCookieSyncParseRequest(t *testing.T) {
 		expectedRequest      usersync.Request
 	}{
 		{
-			description: "Complete Request",
+			description: "Complete Request - includes GPP string with EU TCF V2",
 			givenBody: strings.NewReader(`{` +
 				`"bidders":["a", "b"],` +
 				`"gdpr":1,` +
 				`"gdpr_consent":"anyGDPRConsent",` +
 				`"us_privacy":"1NYN",` +
-				`"gpp":"anyGPPString",` +
+				`"gpp":"DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",` +
 				`"gpp_sid":"2",` +
 				`"limit":42,` +
 				`"coopSync":true,` +
@@ -341,13 +522,13 @@ func TestCookieSyncParseRequest(t *testing.T) {
 			expectedPrivacy: privacy.Policies{
 				GDPR: gdprPrivacy.Policy{
 					Signal:  "1",
-					Consent: "anyGDPRConsent",
+					Consent: "CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
 				},
 				CCPA: ccpa.Policy{
 					Consent: "1NYN",
 				},
 				GPP: gppPrivacy.Policy{
-					Consent: "anyGPPString",
+					Consent: "DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA",
 					RawSID:  "2",
 				},
 			},
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 158bef50..367d3f5b 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -2327,7 +2327,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 	if spec.BidIDGenerator != nil {
 		*bidIdGenerator = *spec.BidIDGenerator
 	}
-	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.HostConfigBidValidation, spec.Server)
+	ex := newExchangeForTests(t, filename, spec.OutgoingRequests, aliases, privacyConfig, bidIdGenerator, spec.HostSChainFlag, spec.FloorsEnabled, spec.HostConfigBidValidation, spec.Server)
 	biddersInAuction := findBiddersInAuction(t, filename, &spec.IncomingRequest.OrtbRequest)
 	debugLog := &DebugLog{}
 	if spec.DebugLog != nil {
@@ -2362,6 +2362,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 				Enabled: &spec.EventsEnabled,
 			},
 			DebugAllow:  true,
+			PriceFloors: config.AccountPriceFloors{Enabled: spec.AccountFloorsEnabled},
 			Validations: spec.AccountConfigBidValidation,
 		},
 		UserSyncs:     mockIdFetcher(spec.IncomingRequest.Usersyncs),
@@ -2533,7 +2534,7 @@ func extractResponseTimes(t *testing.T, context string, bid *openrtb2.BidRespons
 	}
 }
 
-func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag bool, hostBidValidation config.Validations, server exchangeServer) Exchange {
+func newExchangeForTests(t *testing.T, filename string, expectations map[string]*bidderSpec, aliases map[string]string, privacyConfig config.Privacy, bidIDGenerator BidIDGenerator, hostSChainFlag, floorsFlag bool, hostBidValidation config.Validations, server exchangeServer) Exchange {
 	bidderAdapters := make(map[openrtb_ext.BidderName]AdaptedBidder, len(expectations))
 	bidderInfos := make(config.BidderInfos, len(expectations))
 	for _, bidderName := range openrtb_ext.CoreBidderNames() {
@@ -2622,6 +2623,7 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		server:                   config.Server{ExternalUrl: server.ExternalUrl, GvlID: server.GvlID, DataCenter: server.DataCenter},
 		bidValidationEnforcement: hostBidValidation,
 		requestSplitter:          requestSplitter,
+		floor:                    config.PriceFloors{Enabled: floorsFlag},
 	}
 }
 
@@ -2734,10 +2736,10 @@ func TestCategoryMapping(t *testing.T) {
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", ""}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 40.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 40.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2789,10 +2791,10 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) {
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 40.0000, Cat: cats4, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, 0, false, "", 30.0000, "USD", ""}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 40.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30, PrimaryCategory: "AdapterOverride"}, nil, nil, 0, false, "", 30.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 40.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2843,9 +2845,9 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2925,9 +2927,9 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 	bid3 := openrtb2.Bid{ID: "bid_id3", ImpID: "imp_id3", Price: 30.0000, Cat: cats3, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 30.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 40}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 30.0000, "USD", ""}
 
 	innerBids := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -2977,11 +2979,11 @@ func TestCategoryDedupe(t *testing.T) {
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1}
 	bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 20.0000, Cat: cats1, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, 0, false, "", 15.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 50}, nil, nil, 0, false, "", 15.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -3056,11 +3058,11 @@ func TestNoCategoryDedupe(t *testing.T) {
 	bid4 := openrtb2.Bid{ID: "bid_id4", ImpID: "imp_id4", Price: 20.0000, Cat: cats4, W: 1, H: 1}
 	bid5 := openrtb2.Bid{ID: "bid_id5", ImpID: "imp_id5", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 14.0000, "USD", ""}
-	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 14.0000, "USD", ""}
+	bid1_3 := entities.PbsOrtbBid{&bid3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_4 := entities.PbsOrtbBid{&bid4, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_5 := entities.PbsOrtbBid{&bid5, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
 
 	selectedBids := make(map[string]int)
 	expectedCategories := map[string]string{
@@ -3136,8 +3138,8 @@ func TestCategoryMappingBidderName(t *testing.T) {
 	bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 10.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
 
 	innerBids1 := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -3190,8 +3192,8 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) {
 	bid1 := openrtb2.Bid{ID: "bid_id1", ImpID: "imp_id1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bid2 := openrtb2.Bid{ID: "bid_id2", ImpID: "imp_id2", Price: 12.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 12.0000, "USD", ""}
+	bid1_1 := entities.PbsOrtbBid{&bid1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_2 := entities.PbsOrtbBid{&bid2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 12.0000, "USD", ""}
 
 	innerBids1 := []*entities.PbsOrtbBid{
 		&bid1_1,
@@ -3302,7 +3304,7 @@ func TestBidRejectionErrors(t *testing.T) {
 		innerBids := []*entities.PbsOrtbBid{}
 		for _, bid := range test.bids {
 			currentBid := entities.PbsOrtbBid{
-				bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, 0, false, "", 10.0000, "USD", ""}
+				bid, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: test.duration}, nil, nil, 0, false, "", 10.0000, "USD", ""}
 			innerBids = append(innerBids, &currentBid)
 		}
 
@@ -3350,8 +3352,8 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 	bidApn1 := openrtb2.Bid{ID: "bid_idApn1", ImpID: "imp_idApn1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 	bidApn2 := openrtb2.Bid{ID: "bid_idApn2", ImpID: "imp_idApn2", Price: 10.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn1 := entities.PbsOrtbBid{&bidApn1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn2 := entities.PbsOrtbBid{&bidApn2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
 
 	innerBidsApn1 := []*entities.PbsOrtbBid{
 		&bid1_Apn1,
@@ -3430,11 +3432,11 @@ func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T)
 	bidApn2_1 := openrtb2.Bid{ID: "bid_idApn2_1", ImpID: "imp_idApn2_1", Price: 10.0000, Cat: cats2, W: 1, H: 1}
 	bidApn2_2 := openrtb2.Bid{ID: "bid_idApn2_2", ImpID: "imp_idApn2_2", Price: 20.0000, Cat: cats2, W: 1, H: 1}
 
-	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
 
-	bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_Apn2_1 := entities.PbsOrtbBid{&bidApn2_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn2_2 := entities.PbsOrtbBid{&bidApn2_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
 
 	innerBidsApn1 := []*entities.PbsOrtbBid{
 		&bid1_Apn1_1,
@@ -3512,9 +3514,9 @@ func TestRemoveBidById(t *testing.T) {
 	bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1}
 	bidApn1_3 := openrtb2.Bid{ID: "bid_idApn1_3", ImpID: "imp_idApn1_3", Price: 10.0000, Cat: cats1, W: 1, H: 1}
 
-	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
-	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 20.0000, "USD", ""}
-	bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn1_1 := entities.PbsOrtbBid{&bidApn1_1, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
+	bid1_Apn1_2 := entities.PbsOrtbBid{&bidApn1_2, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 20.0000, "USD", ""}
+	bid1_Apn1_3 := entities.PbsOrtbBid{&bidApn1_3, nil, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, nil, 0, false, "", 10.0000, "USD", ""}
 
 	type aTest struct {
 		desc      string
@@ -3639,7 +3641,7 @@ func TestApplyDealSupport(t *testing.T) {
 			},
 		}
 
-		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", ""}
+		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""}
 		bidCategory := map[string]string{
 			bid.Bid.ID: test.targ["hb_pb_cat_dur"],
 		}
@@ -3699,8 +3701,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						"imp_id1": {
 							openrtb_ext.BidderName("appnexus"): {
-								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
-								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
 							},
 						},
 					},
@@ -3745,8 +3747,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						"imp_id1": {
 							openrtb_ext.BidderName("appnexus"): {
-								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
-								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
 							},
 						},
 					},
@@ -3796,8 +3798,8 @@ func TestApplyDealSupportMultiBid(t *testing.T) {
 					winningBidsByBidder: map[string]map[openrtb_ext.BidderName][]*entities.PbsOrtbBid{
 						"imp_id1": {
 							openrtb_ext.BidderName("appnexus"): {
-								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
-								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
+								&entities.PbsOrtbBid{&openrtb2.Bid{ID: "789101"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, 5, false, "", 0, "USD", ""},
 							},
 						},
 					},
@@ -3988,7 +3990,7 @@ func TestUpdateHbPbCatDur(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, test.dealPriority, false, "", 0, "USD", ""}
+		bid := entities.PbsOrtbBid{&openrtb2.Bid{ID: "123456"}, nil, "video", map[string]string{}, &openrtb_ext.ExtBidPrebidVideo{}, nil, nil, test.dealPriority, false, "", 0, "USD", ""}
 		bidCategory := map[string]string{
 			bid.Bid.ID: test.targ["hb_pb_cat_dur"],
 		}
@@ -5163,6 +5165,7 @@ func (ms *MockSigner) Sign(destinationURL string, body []byte) (string, error) {
 
 type exchangeSpec struct {
 	GDPREnabled                bool                   `json:"gdpr_enabled"`
+	FloorsEnabled              bool                   `json:"floors_enabled"`
 	IncomingRequest            exchangeRequest        `json:"incomingRequest"`
 	OutgoingRequests           map[string]*bidderSpec `json:"outgoingRequests"`
 	Response                   exchangeResponse       `json:"response,omitempty"`
@@ -5178,6 +5181,7 @@ type exchangeSpec struct {
 	HostSChainFlag             bool                   `json:"host_schain_flag,omitempty"`
 	HostConfigBidValidation    config.Validations     `json:"host_bid_validations"`
 	AccountConfigBidValidation config.Validations     `json:"account_bid_validations"`
+	AccountFloorsEnabled       bool                   `json:"account_floors_enabled"`
 	FledgeEnabled              bool                   `json:"fledge_enabled,omitempty"`
 	MultiBid                   *multiBidSpec          `json:"multiBid,omitempty"`
 	Server                     exchangeServer         `json:"server,omitempty"`
@@ -5228,6 +5232,7 @@ type bidderResponse struct {
 type bidderSeatBid struct {
 	Bids                 []bidderBid                        `json:"pbsBids,omitempty"`
 	Seat                 string                             `json:"seat"`
+	Currency             string                             `json:"currency"`
 	FledgeAuctionConfigs []*openrtb_ext.FledgeAuctionConfig `json:"fledgeAuctionConfigs,omitempty"`
 }
 
@@ -5293,6 +5298,7 @@ func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderR
 					Bids:                 bids,
 					HttpCalls:            mockResponse.HttpCalls,
 					Seat:                 mockSeatBid.Seat,
+					Currency:             mockSeatBid.Currency,
 					FledgeAuctionConfigs: mockSeatBid.FledgeAuctionConfigs,
 				})
 			}
diff --git a/exchange/exchangetest/floors_enforcement.json b/exchange/exchangetest/floors_enforcement.json
new file mode 100644
index 00000000..90d25301
--- /dev/null
+++ b/exchange/exchangetest/floors_enforcement.json
@@ -0,0 +1,153 @@
+{
+    "floors_enabled": true,
+    "account_floors_enabled": true,
+    "incomingRequest": {
+      "ortbRequest": {
+        "id": "some-request-id",
+        "site": {
+          "page": "test.somepage.com"
+        },
+        "imp": [
+          {
+            "id": "my-imp-id",
+            "video": {
+              "mimes": [
+                "video/mp4"
+              ]
+            },
+            "ext": {
+              "prebid": {
+                "bidder": {
+                  "appnexus": {
+                    "placementId": 1
+                  },
+                  "audienceNetwork": {
+                    "placementId": "some-placement"
+                  }
+                }
+              }
+            }
+          }
+        ],
+        "ext": {
+          "prebid": {
+            "floors": {
+              "data": {
+                "modelgroups": [
+                  {
+                    "currency": "USD",
+                    "modelversion": "version1",
+                    "default": 5,
+                    "values": {
+                      "banner|www.website.com": 3,
+                      "video|www.website.com": 7,
+                      "*|*": 11
+                    },
+                    "schema": {
+                      "fields": [
+                        "mediaType",
+                        "domain"
+                      ],
+                      "delimiter": "|"
+                    }
+                  }
+                ]
+              },
+              "enabled": true,
+              "enforcement": {
+                "enforcepbs": true,
+                "floordeals": true,
+                "enforcerate": 100
+              }
+            }
+          }
+        }
+      }
+    },
+    "outgoingRequests": {
+      "appnexus": {
+        "mockResponse": {
+          "pbsSeatBids": [
+            {
+              "pbsBids": [
+                {
+                  "ortbBid": {
+                    "id": "winning-bid1",
+                    "impid": "my-imp-id",
+                    "price": 12,
+                    "w": 200,
+                    "h": 250,
+                    "crid": "creative-1",
+                    "cat": [
+                      "IAB1-1"
+                    ]
+                  }
+                }
+              ],
+              "seat": "appnexus",
+              "currency": "USD"
+            }
+          ]
+        }
+      },
+      "audienceNetwork": {
+        "mockResponse": {
+          "pbsSeatBids": [
+            {
+              "pbsBids": [
+                {
+                  "ortbBid": {
+                    "id": "winning-bid2",
+                    "impid": "my-imp-id",
+                    "price": 7,
+                    "w": 200,
+                    "h": 250,
+                    "crid": "creative-1",
+                    "cat": [
+                      "IAB1-1"
+                    ]
+                  }
+                }
+              ],
+              "seat": "audienceNetwork",
+              "currency": "USD"
+            }
+          ]
+        }
+      }
+    },
+    "response": {
+      "bids": {
+        "id": "some-request-id",
+        "seatbid": [
+          {
+            "seat": "appnexus",
+            "bid": [
+              {
+                "id": "winning-bid1",
+                "impid": "my-imp-id",
+                "price": 12,
+                "w": 200,
+                "h": 250,
+                "crid": "creative-1",
+                "cat": [
+                  "IAB1-1"
+                ],
+                "ext": {
+                  "origbidcpm": 12,
+                  "prebid": {
+                    "floors": {
+                      "floorCurrency": "USD",
+                      "floorRule": "*|*",
+                      "floorRuleValue": 11,
+                      "floorValue": 11
+                    }
+                  }
+                }
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }
\ No newline at end of file
diff --git a/floors/enforce_test.go b/floors/enforce_test.go
new file mode 100644
index 00000000..826306a0
--- /dev/null
+++ b/floors/enforce_test.go
@@ -0,0 +1,1164 @@
+package floors
+
+import (
+	"encoding/json"
+	"errors"
+	"reflect"
+	"testing"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/currency"
+	"github.com/prebid/prebid-server/exchange/entities"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/ptrutil"
+	"github.com/stretchr/testify/assert"
+)
+
+type convert struct {
+}
+
+func (c convert) GetRate(from string, to string) (float64, error) {
+	if from == to {
+		return 1, nil
+	}
+
+	if from == "USD" && to == "INR" {
+		return 77.59, nil
+	} else if from == "INR" && to == "USD" {
+		return 0.013, nil
+	}
+	return 0, errors.New("currency conversion not supported")
+}
+
+func (c convert) GetRates() *map[string]map[string]float64 {
+	return &map[string]map[string]float64{}
+}
+
+func TestIsValidImpBidfloorPresentInRequest(t *testing.T) {
+
+	tests := []struct {
+		name string
+		imp  []openrtb2.Imp
+		want bool
+	}{
+		{
+			imp:  []openrtb2.Imp{{ID: "1234"}},
+			want: false,
+		},
+		{
+			imp:  []openrtb2.Imp{{ID: "1234", BidFloor: 10, BidFloorCur: "USD"}},
+			want: true,
+		},
+	}
+
+	for _, tt := range tests {
+		got := isValidImpBidFloorPresent(tt.imp)
+		assert.Equal(t, tt.want, got, tt.name)
+	}
+}
+
+func TestEnforceFloorToBids(t *testing.T) {
+	type args struct {
+		bidRequestWrapper *openrtb_ext.RequestWrapper
+		seatBids          map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		conversions       currency.Conversions
+		enforceDealFloors bool
+	}
+	tests := []struct {
+		name            string
+		args            args
+		expEligibleBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		expErrs         []error
+		expRejectedBids []*entities.PbsOrtbSeatBid
+	}{
+		{
+			name: "Floors enforcement disabled using enforcepbs = false",
+			args: args{
+				bidRequestWrapper: func() *openrtb_ext.RequestWrapper {
+					bw := openrtb_ext.RequestWrapper{
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{
+								{ID: "some-impression-id-1", BidFloor: 1.01, BidFloorCur: "USD"},
+								{ID: "some-impression-id-2", BidFloor: 2.01, BidFloorCur: "USD"},
+							},
+							Ext: json.RawMessage(`{"prebid":{"floors":{"enforcement":{"enforcepbs":false}}}}`),
+						},
+					}
+					bw.RebuildRequest()
+					return &bw
+				}(),
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+							{Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}},
+							{Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}},
+						},
+						Seat:     "appnexus",
+						Currency: "USD",
+					},
+				},
+				conversions:       currency.Conversions(convert{}),
+				enforceDealFloors: false,
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						{Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+				"appnexus": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}},
+						{Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}},
+					},
+					Seat:     "appnexus",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+			expErrs:         []error{},
+		},
+		{
+			name: "Bids with price less than bidfloor",
+			args: args{
+				bidRequestWrapper: func() *openrtb_ext.RequestWrapper {
+					bw := openrtb_ext.RequestWrapper{
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{
+								{ID: "some-impression-id-1", BidFloor: 1.01, BidFloorCur: "USD"},
+								{ID: "some-impression-id-2", BidFloor: 2.01, BidFloorCur: "USD"},
+							},
+						},
+					}
+					bw.RebuildRequest()
+					return &bw
+				}(),
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+							{Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}},
+							{Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}},
+						},
+						Seat:     "appnexus",
+						Currency: "USD",
+					},
+				},
+				conversions:       currency.Conversions(convert{}),
+				enforceDealFloors: false,
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						{Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+				"appnexus": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}},
+					},
+					Seat:     "appnexus",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{
+				{
+					Seat:     "appnexus",
+					Currency: "USD",
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}},
+					},
+				},
+			},
+			expErrs: []error{},
+		},
+		{
+			name: "Bids with different currency with enforceDealFloor true",
+			args: args{
+				bidRequestWrapper: func() *openrtb_ext.RequestWrapper {
+					bw := openrtb_ext.RequestWrapper{
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{
+								{ID: "some-impression-id-1", BidFloor: 60, BidFloorCur: "INR"},
+								{ID: "some-impression-id-2", BidFloor: 100, BidFloorCur: "INR"},
+							},
+						},
+					}
+					bw.RebuildRequest()
+					return &bw
+				}(),
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+							{Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}},
+							{Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}},
+						},
+						Seat:     "appnexus",
+						Currency: "USD",
+					},
+				},
+				conversions:       currency.Conversions(convert{}),
+				enforceDealFloors: true,
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						{Bid: &openrtb2.Bid{ID: "some-bid-2", Price: 1.5, DealID: "deal_Id", ImpID: "some-impression-id-2"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+				"appnexus": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-12", Price: 2.2, ImpID: "some-impression-id-2"}},
+					},
+					Seat:     "appnexus",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{
+				{
+					Seat:     "appnexus",
+					Currency: "USD",
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, ImpID: "some-impression-id-1"}},
+					},
+				},
+			},
+			expErrs: []error{},
+		},
+		{
+			name: "Error in currency conversion",
+			args: args{
+				bidRequestWrapper: func() *openrtb_ext.RequestWrapper {
+					bw := openrtb_ext.RequestWrapper{
+						BidRequest: &openrtb2.BidRequest{
+							ID:  "some-request-id",
+							Cur: []string{"USD"},
+							Imp: []openrtb2.Imp{{ID: "some-impression-id-1", BidFloor: 1.01}},
+						},
+					}
+					bw.RebuildRequest()
+					return &bw
+				}(),
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						},
+						Currency: "EUR",
+					},
+				},
+				conversions:       convert{},
+				enforceDealFloors: true,
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids:     []*entities.PbsOrtbBid{},
+					Currency: "EUR",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+			expErrs:         []error{errors.New("error in rate conversion from = EUR to  with bidder pubmatic for impression id some-impression-id-1 and bid id some-bid-1 error = currency conversion not supported")},
+		},
+		{
+			name: "Bids with invalid impression ID",
+			args: args{
+				bidRequestWrapper: func() *openrtb_ext.RequestWrapper {
+					bw := openrtb_ext.RequestWrapper{
+						BidRequest: &openrtb2.BidRequest{
+							ID: "some-request-id",
+							Imp: []openrtb2.Imp{
+								{ID: "some-impression-id-2", BidFloor: 2.01, BidFloorCur: "USD"},
+							},
+						},
+					}
+					bw.RebuildRequest()
+					return &bw
+				}(),
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-123"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+				},
+				conversions:       currency.Conversions(convert{}),
+				enforceDealFloors: false,
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids:     []*entities.PbsOrtbBid{},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+			expErrs:         []error{},
+		},
+	}
+	for _, tt := range tests {
+		seatbids, errs, rejBids := enforceFloorToBids(tt.args.bidRequestWrapper, tt.args.seatBids, tt.args.conversions, tt.args.enforceDealFloors)
+		assert.Equal(t, tt.expEligibleBids, seatbids, tt.name)
+		assert.Equal(t, tt.expErrs, errs, tt.name)
+		assert.Equal(t, tt.expRejectedBids, rejBids, tt.name)
+	}
+}
+
+func TestEnforce(t *testing.T) {
+	type args struct {
+		bidRequestWrapper *openrtb_ext.RequestWrapper
+		bidRequest        *openrtb2.BidRequest
+		seatBids          map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		priceFloorsCfg    config.AccountPriceFloors
+		conversions       currency.Conversions
+	}
+	tests := []struct {
+		name            string
+		args            args
+		expEligibleBids map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid
+		expErrs         []error
+		expRejectedBids []*entities.PbsOrtbSeatBid
+	}{
+		{
+			name: "Error in getting request extension",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID:          "some-impression-id-1",
+							Banner:      &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
+							BidFloor:    5.01,
+							BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":false,"skipped":false}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: false, EnforceFloorsRate: 100, EnforceDealFloors: true},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+			},
+			expErrs:         []error{errors.New("Error in getting request extension")},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+		},
+		{
+			name: "Should not enforce floors when req.ext.prebid.floors.enabled = false",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID:          "some-impression-id-1",
+							Banner:      &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}},
+							BidFloor:    5.01,
+							BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":false}}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+			},
+			expErrs:         []error{errors.New("Floors feature is disabled at account or in the request")},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+		},
+		{
+			name: "Should not enforce floors is disabled in account config",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID:          "some-impression-id-1",
+							Banner:      &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
+							BidFloor:    5.01,
+							BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":true}}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: false, EnforceFloorsRate: 100, EnforceDealFloors: true},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+			},
+			expErrs:         []error{errors.New("Floors feature is disabled at account or in the request")},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+		},
+		{
+			name: "Should not enforce floors when req.ext.prebid.floors.enforcement.enforcepbs = false",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID:          "some-impression-id-1",
+							Banner:      &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
+							BidFloor:    5.01,
+							BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":false,"floordeals":false},"enabled":true,"skipped":false}}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorValue: 5.01, FloorCurrency: "USD"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+			},
+			expErrs:         []error{},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+		},
+		{
+			name: "Should not enforce floors when req.ext.prebid.floors.skipped = true",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID:          "some-impression-id-1",
+							Banner:      &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}, {W: 300, H: 600}}},
+							BidFloor:    5.01,
+							BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":false},"enabled":true,"skipped":true}}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, ImpID: "some-impression-id-1"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{},
+		},
+		{
+			name: "Should enforce floors for deals, ext.prebid.floors.enforcement.floorDeals = true and floors enabled = true",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID: "some-impression-id-1", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, BidFloor: 20.01, BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1,"data":{"currency":"USD","skiprate":100,"modelgroups":[{"modelversion":"version1","skiprate":10,"schema":{"fields":["mediaType","size","domain"],"delimiter":"|"},"values":{"*|*|*":20.01,"*|*|www.website1.com":16.01},"default":21}]},"enforcement":{"enforcepbs":true,"floordeals":true},"enabled":true}}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids:     []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}}},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+					"appnexus": {
+						Bids:     []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, DealID: "deal_Id_3", ImpID: "some-impression-id-1"}}},
+						Seat:     "appnexus",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: true},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids:     []*entities.PbsOrtbBid{},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+				"appnexus": {
+					Bids:     []*entities.PbsOrtbBid{},
+					Seat:     "appnexus",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{
+				{
+					Seat:     "pubmatic",
+					Currency: "USD",
+					Bids:     []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorCurrency: "USD", FloorValue: 20.01}}},
+				},
+				{
+					Seat:     "appnexus",
+					Currency: "USD",
+					Bids:     []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 0.5, DealID: "deal_Id_3", ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorCurrency: "USD", FloorValue: 20.01}}},
+				},
+			},
+			expErrs: []error{},
+		},
+		{
+			name: "Should enforce floors when imp.bidfloor provided and enforcepbs not provided",
+			args: args{
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						ID: "some-request-id",
+						Imp: []openrtb2.Imp{{
+							ID: "some-impression-id-1", Banner: &openrtb2.Banner{Format: []openrtb2.Format{{W: 300, H: 250}}}, BidFloor: 5.01, BidFloorCur: "USD",
+						}},
+						Ext: json.RawMessage(`{"prebid":{"floors":{"floormin":1}}}`),
+					},
+				},
+				seatBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+					"pubmatic": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "pubmatic",
+						Currency: "USD",
+					},
+					"appnexus": {
+						Bids: []*entities.PbsOrtbBid{
+							{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}},
+						},
+						Seat:     "appnexus",
+						Currency: "USD",
+					},
+				},
+				conversions:    convert{},
+				priceFloorsCfg: config.AccountPriceFloors{Enabled: true, EnforceFloorsRate: 100, EnforceDealFloors: false},
+			},
+			expEligibleBids: map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid{
+				"pubmatic": {
+					Bids: []*entities.PbsOrtbBid{
+						{Bid: &openrtb2.Bid{ID: "some-bid-1", Price: 1.2, DealID: "deal_Id_1", ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorValue: 5.01, FloorCurrency: "USD"}},
+					},
+					Seat:     "pubmatic",
+					Currency: "USD",
+				},
+				"appnexus": {
+					Bids:     []*entities.PbsOrtbBid{},
+					Seat:     "appnexus",
+					Currency: "USD",
+				},
+			},
+			expRejectedBids: []*entities.PbsOrtbSeatBid{
+				{
+					Seat:     "appnexus",
+					Currency: "USD",
+					Bids:     []*entities.PbsOrtbBid{{Bid: &openrtb2.Bid{ID: "some-bid-11", Price: 4.5, ImpID: "some-impression-id-1"}, BidFloors: &openrtb_ext.ExtBidPrebidFloors{FloorValue: 5.01, FloorCurrency: "USD"}}},
+				},
+			},
+			expErrs: []error{},
+		},
+	}
+	for _, tt := range tests {
+		actEligibleBids, actErrs, actRejecteBids := Enforce(tt.args.bidRequestWrapper, tt.args.seatBids, config.Account{PriceFloors: tt.args.priceFloorsCfg}, tt.args.conversions)
+		assert.Equal(t, tt.expErrs, actErrs, tt.name)
+		if !reflect.DeepEqual(tt.expRejectedBids, actRejecteBids) {
+			assert.Fail(t, "rejected bids don't match")
+		}
+
+		if !reflect.DeepEqual(tt.expEligibleBids, actEligibleBids) {
+			assert.Fail(t, "eligible bids don't match")
+		}
+	}
+}
+
+func TestUpdateBidExtWithFloors(t *testing.T) {
+	type args struct {
+		reqImp        *openrtb_ext.ImpWrapper
+		bid           *entities.PbsOrtbBid
+		floorCurrency string
+	}
+	tests := []struct {
+		name        string
+		expBidFloor *openrtb_ext.ExtBidPrebidFloors
+		args        args
+	}{
+		{
+			name: "Empty prebid extension in imp.ext",
+			args: args{
+				reqImp: &openrtb_ext.ImpWrapper{
+					Imp: &openrtb2.Imp{BidFloor: 10, BidFloorCur: "USD"},
+				},
+				bid: &entities.PbsOrtbBid{
+					Bid: &openrtb2.Bid{
+						Price: 10.10,
+						AdM:   "Adm",
+					},
+				},
+				floorCurrency: "USD",
+			},
+			expBidFloor: &openrtb_ext.ExtBidPrebidFloors{
+				FloorValue:    10,
+				FloorCurrency: "USD",
+			},
+		},
+		{
+			name: "Valid prebid extension in imp.ext",
+			args: args{
+				reqImp: &openrtb_ext.ImpWrapper{Imp: &openrtb2.Imp{ID: "1234", Video: &openrtb2.Video{W: 300, H: 250}, Ext: []byte(`{"prebid":{"floors":{"floorrule":"test|123|xyz","floorrulevalue":5.5,"floorvalue":5.5}}}`)}},
+				bid: &entities.PbsOrtbBid{
+					Bid: &openrtb2.Bid{
+						Price: 10.10,
+						AdM:   "Adm",
+					},
+				},
+				floorCurrency: "USD",
+			},
+			expBidFloor: &openrtb_ext.ExtBidPrebidFloors{
+				FloorRule:      "test|123|xyz",
+				FloorRuleValue: 5.5,
+				FloorValue:     5.5,
+				FloorCurrency:  "USD",
+			},
+		},
+	}
+	for _, tt := range tests {
+		updateBidExtWithFloors(tt.args.reqImp, tt.args.bid, tt.args.floorCurrency)
+		assert.Equal(t, tt.expBidFloor, tt.args.bid.BidFloors, tt.name)
+	}
+}
+
+func TestIsEnforcementEnabledForRequest(t *testing.T) {
+	tests := []struct {
+		name   string
+		reqExt *openrtb_ext.RequestExt
+		want   bool
+	}{
+		{
+			name: "Req.ext not provided",
+			reqExt: func() *openrtb_ext.RequestExt {
+				return &openrtb_ext.RequestExt{}
+			}(),
+			want: true,
+		},
+		{
+			name: "Req.ext provided EnforcePBS = false",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							EnforcePBS: ptrutil.ToPtr(false),
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext provided EnforcePBS = true",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							EnforcePBS: ptrutil.ToPtr(true),
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		got := isEnforcementEnabled(tt.reqExt)
+		assert.Equal(t, tt.want, got, tt.name)
+	}
+}
+
+func TestIsSignalingSkipped(t *testing.T) {
+	tests := []struct {
+		name   string
+		reqExt *openrtb_ext.RequestExt
+		want   bool
+	}{
+		{
+			name: "Req.ext nil",
+			reqExt: func() *openrtb_ext.RequestExt {
+				return nil
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext provided without prebid ext",
+			reqExt: func() *openrtb_ext.RequestExt {
+				return &openrtb_ext.RequestExt{}
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext provided without Floors in prebid ext",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext provided Skipped = true",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Skipped: ptrutil.ToPtr(true),
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: true,
+		},
+		{
+			name: "Req.ext provided Skipped = false",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Skipped: ptrutil.ToPtr(false),
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		got := isSignalingSkipped(tt.reqExt)
+		assert.Equal(t, tt.want, got, tt.name)
+	}
+}
+
+func TestGetEnforceRateRequest(t *testing.T) {
+	tests := []struct {
+		name   string
+		reqExt *openrtb_ext.RequestExt
+		want   int
+	}{
+		{
+			name: "Req.ext not provided",
+			reqExt: func() *openrtb_ext.RequestExt {
+				return &openrtb_ext.RequestExt{}
+			}(),
+			want: 0,
+		},
+		{
+			name: "Req.ext.prebid.floors provided with EnforceRate = 0",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							EnforceRate: 0,
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: 0,
+		},
+		{
+			name: "Req.ext.prebid.floors provided with EnforceRate = 50",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							EnforceRate: 50,
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: 50,
+		},
+		{
+			name: "Req.ext.prebid.floors provided with EnforceRate = 100",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							EnforceRate: 100,
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: 100,
+		},
+	}
+	for _, tt := range tests {
+		got := getEnforceRateRequest(tt.reqExt)
+		assert.Equal(t, tt.want, got, tt.name)
+	}
+}
+
+func TestGetEnforceDealsFlag(t *testing.T) {
+	tests := []struct {
+		name   string
+		reqExt *openrtb_ext.RequestExt
+		want   bool
+	}{
+		{
+			name: "Req.ext not provided",
+			reqExt: func() *openrtb_ext.RequestExt {
+				return &openrtb_ext.RequestExt{}
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext.prebid.floors provided, enforceDeals not provided",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext.prebid.floors provided with enforceDeals = false",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							FloorDeals: ptrutil.ToPtr(false),
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: false,
+		},
+		{
+			name: "Req.ext.prebid.floors provided with enforceDeals = true",
+			reqExt: func() *openrtb_ext.RequestExt {
+				reqExt := openrtb_ext.RequestExt{}
+				prebidExt := openrtb_ext.ExtRequestPrebid{
+					Floors: &openrtb_ext.PriceFloorRules{
+						Enforcement: &openrtb_ext.PriceFloorEnforcement{
+							FloorDeals: ptrutil.ToPtr(true),
+						},
+					},
+				}
+				reqExt.SetPrebid(&prebidExt)
+				return &reqExt
+			}(),
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		got := getEnforceDealsFlag(tt.reqExt)
+		assert.Equal(t, tt.want, got, tt.name)
+	}
+}
+
+func TestIsSatisfiedByEnforceRate(t *testing.T) {
+	type args struct {
+		reqExt            *openrtb_ext.RequestExt
+		configEnforceRate int
+		f                 func(int) int
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "With EnforceRate = 50",
+			args: args{
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{
+						Floors: &openrtb_ext.PriceFloorRules{
+							Enforcement: &openrtb_ext.PriceFloorEnforcement{
+								EnforceRate: 50,
+								EnforcePBS:  ptrutil.ToPtr(true),
+							},
+						},
+					}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+				configEnforceRate: 100,
+				f: func(n int) int {
+					return n
+				},
+			},
+			want: false,
+		},
+		{
+			name: "With EnforceRate = 100",
+			args: args{
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{
+						Floors: &openrtb_ext.PriceFloorRules{
+							Enforcement: &openrtb_ext.PriceFloorEnforcement{
+								EnforceRate: 100,
+								EnforcePBS:  ptrutil.ToPtr(true),
+							},
+						},
+					}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+				configEnforceRate: 100,
+				f: func(n int) int {
+					return n - 1
+				},
+			},
+			want: true,
+		},
+		{
+			name: "With configEnforceRate = 0",
+			args: args{
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{
+						Floors: &openrtb_ext.PriceFloorRules{
+							Enforcement: &openrtb_ext.PriceFloorEnforcement{
+								EnforceRate: 0,
+								EnforcePBS:  ptrutil.ToPtr(true),
+							},
+						},
+					}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+				configEnforceRate: 0,
+				f: func(n int) int {
+					return n - 1
+				},
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		got := isSatisfiedByEnforceRate(tt.args.reqExt, tt.args.configEnforceRate, tt.args.f)
+		assert.Equal(t, tt.want, got, tt.name)
+	}
+}
+
+func TestUpdateEnforcePBS(t *testing.T) {
+	type args struct {
+		enforceFloors bool
+		reqExt        *openrtb_ext.RequestExt
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "Enforce PBS is true in request and to be updated = true",
+			args: args{
+				enforceFloors: true,
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{
+						Floors: &openrtb_ext.PriceFloorRules{
+							Enforcement: &openrtb_ext.PriceFloorEnforcement{
+								EnforcePBS: ptrutil.ToPtr(true),
+							},
+						},
+					}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+			},
+			want: false,
+		},
+		{
+			name: "Enforce PBS is false in request and to be updated = true",
+			args: args{
+				enforceFloors: true,
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{
+						Floors: &openrtb_ext.PriceFloorRules{
+							Enforcement: &openrtb_ext.PriceFloorEnforcement{
+								EnforcePBS: ptrutil.ToPtr(false),
+							},
+						},
+					}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+			},
+			want: false,
+		},
+		{
+			name: "Enforce PBS is true in request and to be updated = false",
+			args: args{
+				enforceFloors: false,
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{
+						Floors: &openrtb_ext.PriceFloorRules{
+							Enforcement: &openrtb_ext.PriceFloorEnforcement{
+								EnforcePBS: ptrutil.ToPtr(true),
+							},
+						},
+					}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+			},
+			want: true,
+		},
+		{
+			name: "empty prebid ext and to be updated = false",
+			args: args{enforceFloors: false,
+				reqExt: func() *openrtb_ext.RequestExt {
+					return &openrtb_ext.RequestExt{}
+				}(),
+			},
+			want: true,
+		},
+		{
+			name: "empty prebid ext and to be updated = true",
+			args: args{enforceFloors: true,
+				reqExt: func() *openrtb_ext.RequestExt {
+					reqExt := openrtb_ext.RequestExt{}
+					prebidExt := openrtb_ext.ExtRequestPrebid{}
+					reqExt.SetPrebid(&prebidExt)
+					return &reqExt
+				}(),
+			},
+			want: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := updateEnforcePBS(tt.args.enforceFloors, tt.args.reqExt)
+			assert.Equal(t, tt.want, got, tt.name)
+
+		})
+	}
+}
diff --git a/floors/floors_test.go b/floors/floors_test.go
index e5a4a329..7f27e1a2 100644
--- a/floors/floors_test.go
+++ b/floors/floors_test.go
@@ -671,3 +671,69 @@ func TestCreateFloorsFrom(t *testing.T) {
 		})
 	}
 }
+
+func TestIsPriceFloorsEnabled(t *testing.T) {
+	type args struct {
+		account           config.Account
+		bidRequestWrapper *openrtb_ext.RequestWrapper
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "Disabled in account and req",
+			args: args{
+				account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: false}},
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": false} }}`),
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "Enabled  in account and req",
+			args: args{
+				account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: true}},
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": true} }}`),
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "disabled  in account and enabled req",
+			args: args{
+				account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: false}},
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": true} }}`),
+					},
+				},
+			},
+			want: false,
+		},
+		{
+			name: "Enabled  in account and disabled in req",
+			args: args{
+				account: config.Account{PriceFloors: config.AccountPriceFloors{Enabled: true}},
+				bidRequestWrapper: &openrtb_ext.RequestWrapper{
+					BidRequest: &openrtb2.BidRequest{
+						Ext: json.RawMessage(`{"prebid":{"floors":{"enabled": false} }}`)},
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := isPriceFloorsEnabled(tt.args.account, tt.args.bidRequestWrapper)
+			assert.Equal(t, got, tt.want, tt.name)
+		})
+	}
+}
diff --git a/openrtb_ext/floors_test.go b/openrtb_ext/floors_test.go
new file mode 100644
index 00000000..20247736
--- /dev/null
+++ b/openrtb_ext/floors_test.go
@@ -0,0 +1,220 @@
+package openrtb_ext
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func getFlag(in bool) *bool {
+	return &in
+}
+
+func TestPriceFloorRulesGetEnforcePBS(t *testing.T) {
+	tests := []struct {
+		name   string
+		floors *PriceFloorRules
+		want   bool
+	}{
+		{
+			name: "EnforcePBS_Enabled",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Enforcement: &PriceFloorEnforcement{
+					EnforcePBS: getFlag(true),
+				},
+			},
+			want: true,
+		},
+		{
+			name: "EnforcePBS_NotProvided",
+			floors: &PriceFloorRules{
+				Enabled:     getFlag(true),
+				Enforcement: &PriceFloorEnforcement{},
+			},
+			want: true,
+		},
+		{
+			name: "EnforcePBS_Disabled",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Enforcement: &PriceFloorEnforcement{
+					EnforcePBS: getFlag(false),
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.floors.GetEnforcePBS()
+			assert.Equal(t, tt.want, got, tt.name)
+		})
+	}
+}
+
+func TestPriceFloorRulesGetFloorsSkippedFlag(t *testing.T) {
+	tests := []struct {
+		name   string
+		floors *PriceFloorRules
+		want   bool
+	}{
+		{
+			name: "Skipped_true",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Skipped: getFlag(true),
+			},
+			want: true,
+		},
+		{
+			name: "Skipped_false",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Skipped: getFlag(false),
+			},
+			want: false,
+		},
+		{
+			name: "Skipped_NotProvided",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.floors.GetFloorsSkippedFlag()
+			assert.Equal(t, tt.want, got, tt.name)
+		})
+	}
+}
+
+func TestPriceFloorRulesGetEnforceRate(t *testing.T) {
+	tests := []struct {
+		name   string
+		floors *PriceFloorRules
+		want   int
+	}{
+		{
+			name: "EnforceRate_100",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Enforcement: &PriceFloorEnforcement{
+					EnforcePBS:  getFlag(true),
+					EnforceRate: 100,
+				},
+			},
+			want: 100,
+		},
+		{
+			name: "EnforceRate_0",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Enforcement: &PriceFloorEnforcement{
+					EnforcePBS:  getFlag(true),
+					EnforceRate: 0,
+				},
+			},
+			want: 0,
+		},
+		{
+			name: "EnforceRate_NotProvided",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+			},
+			want: 0,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.floors.GetEnforceRate()
+			assert.Equal(t, tt.want, got, tt.name)
+		})
+	}
+}
+
+func TestPriceFloorRulesGetEnforceDealsFlag(t *testing.T) {
+	tests := []struct {
+		name   string
+		floors *PriceFloorRules
+		want   bool
+	}{
+		{
+			name: "FloorDeals_true",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Enforcement: &PriceFloorEnforcement{
+					EnforcePBS:  getFlag(true),
+					EnforceRate: 0,
+					FloorDeals:  getFlag(true),
+				},
+			},
+			want: true,
+		},
+		{
+			name: "FloorDeals_false",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+				Enforcement: &PriceFloorEnforcement{
+					EnforcePBS: getFlag(true),
+					FloorDeals: getFlag(false),
+				},
+				Skipped: getFlag(false),
+			},
+			want: false,
+		},
+		{
+			name: "FloorDeals_NotProvided",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+			},
+			want: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.floors.GetEnforceDealsFlag()
+			assert.Equal(t, tt.want, got, tt.name)
+		})
+	}
+}
+
+func TestPriceFloorRulesGetEnabled(t *testing.T) {
+	tests := []struct {
+		name   string
+		floors *PriceFloorRules
+		want   bool
+	}{
+		{
+			name: "Enabled_true",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(true),
+			},
+			want: true,
+		},
+		{
+			name: "Enabled_false",
+			floors: &PriceFloorRules{
+				Enabled: getFlag(false),
+			},
+			want: false,
+		},
+		{
+			name:   "Enabled_NotProvided",
+			floors: &PriceFloorRules{},
+			want:   true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got := tt.floors.GetEnabled()
+			assert.Equal(t, tt.want, got, tt.name)
+		})
+	}
+}
diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go
index 4e87db5d..8efa3b74 100644
--- a/usersync/cookie_test.go
+++ b/usersync/cookie_test.go
@@ -2,7 +2,6 @@ package usersync
 
 import (
 	"encoding/base64"
-	"encoding/json"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -15,7 +14,7 @@ import (
 
 func TestOptOutCookie(t *testing.T) {
 	cookie := &Cookie{
-		uids:     make(map[string]uidWithExpiry),
+		uids:     map[string]uidWithExpiry{"appnexus": {UID: "test"}},
 		optOut:   true,
 		birthday: timestamp(),
 	}
@@ -260,32 +259,6 @@ func TestCookieReadWrite(t *testing.T) {
 	assert.Len(t, received.uids, 2, "Sync Count")
 }
 
-func TestPopulatedLegacyCookieRead(t *testing.T) {
-	legacyJson := `{"uids":{"adnxs":"123","audienceNetwork":"456"},"bday":"2017-08-03T21:04:52.629198911Z"}`
-	var cookie Cookie
-	json.Unmarshal([]byte(legacyJson), &cookie)
-
-	if cookie.HasAnyLiveSyncs() {
-		t.Error("Expected 0 user syncs. Found at least 1.")
-	}
-	if cookie.HasLiveSync("adnxs") {
-		t.Errorf("Received cookie should act like it has no ID for adnxs.")
-	}
-	if cookie.HasLiveSync("audienceNetwork") {
-		t.Errorf("Received cookie should act like it has no ID for audienceNetwork.")
-	}
-}
-
-func TestEmptyLegacyCookieRead(t *testing.T) {
-	legacyJson := `{"bday":"2017-08-29T18:54:18.393925772Z"}`
-	var cookie Cookie
-	json.Unmarshal([]byte(legacyJson), &cookie)
-
-	if cookie.HasAnyLiveSyncs() {
-		t.Error("Expected 0 user syncs. Found at least 1.")
-	}
-}
-
 func TestNilCookie(t *testing.T) {
 	var nilCookie *Cookie
 
@@ -427,7 +400,11 @@ func ensureConsistency(t *testing.T, cookie *Cookie) {
 		t.Error("The PBSCookie interface shouldn't let modifications happen if the user has opted out")
 	}
 
-	assert.Equal(t, len(cookie.uids), len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.")
+	if cookie.optOut {
+		assert.Equal(t, 0, len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.")
+	} else {
+		assert.Equal(t, len(cookie.uids), len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.")
+	}
 
 	for family, uid := range copiedCookie.uids {
 		if !cookie.HasLiveSync(family) {