Repository: prebid/prebid-server
Pull request #1857 (2021-05-21 06:17:53)

Bugfix for applyCategoryMapping


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fb9b6592..a1459626 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,7 +3,7 @@ name: Release
 on:
   push:
     tags:
-      - '[0-9]+.[0-9]+.[0-9]+'
+      - 'v[0-9]+.[0-9]+.[0-9]+'
 
 jobs:
   release:
@@ -13,13 +13,15 @@ jobs:
     steps:
       - name: Get Version
         id: get_version
-        run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//}
+        run: |
+          echo ::set-output name=tag::${GITHUB_REF/refs\/tags\/}
+          echo ::set-output name=version::${GITHUB_REF/refs\/tags\/v}
 
       - name: Create & Publish Release
         uses: release-drafter/release-drafter@v5.12.1
         with:
           name: ${{ steps.get_version.outputs.version }}
-          tag: ${{ steps.get_version.outputs.version }}
+          tag: ${{ steps.get_version.outputs.tag }}
           version: ${{ steps.get_version.outputs.version }}
           publish: true
         env:
diff --git a/README.md b/README.md
index 83ac89a9..f3338ce1 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,11 @@ go build .
 Load the landing page in your browser at `http://localhost:8000/`.
 For the full API reference, see [the endpoint documentation](https://docs.prebid.org/prebid-server/endpoints/pbs-endpoint-overview.html)
 
+## Go Modules
+
+The packages within this repository are intended to be used as part of the Prebid Server compiled binary. If you
+choose to import Prebid Server packages in other projects, please understand we make no promises on the stability
+of exported types.
 
 ## Contributing
 
@@ -59,7 +64,7 @@ Report bugs, request features, and suggest improvements [on Github](https://gith
 
 Or better yet, [open a pull request](https://github.com/prebid/prebid-server/compare) with the changes you'd like to see.
 
-## IDE Setup for PBS-Go development
+## IDE Recommendations
 
-The quickest way to start developing PBS-Go in a reproducible environment isolated from your host OS
-is by using this [VScode Remote Container Setup](devcontainer.md)
+The quickest way to start developing Prebid Server in a reproducible environment isolated from your host OS
+is by using Visual Studio Code with [Remote Container Setup](devcontainer.md).
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index 4d4324c4..b1004601 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"math/rand"
@@ -301,11 +302,20 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad
 			defaultDisplayManagerVer = fmt.Sprintf("%s-%s", source, version)
 		}
 	}
+	var adPodId *bool
+
 	for i := 0; i < len(request.Imp); i++ {
-		memberId, err := preprocess(&request.Imp[i], defaultDisplayManagerVer)
+		memberId, impAdPodId, err := preprocess(&request.Imp[i], defaultDisplayManagerVer)
 		if memberId != "" {
 			memberIds[memberId] = true
 		}
+		if adPodId == nil {
+			adPodId = &impAdPodId
+		} else if *adPodId != impAdPodId {
+			errs = append(errs, errors.New("generate ad pod option should be same for all pods in request"))
+			return nil, errs
+		}
+
 		// If the preprocessing failed, the server won't be able to bid on this Imp. Delete it, and note the error.
 		if err != nil {
 			errs = append(errs, err)
@@ -362,12 +372,13 @@ func (a *AppNexusAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ad
 
 	imps := request.Imp
 
-	// For long form requests adpod_id must be sent downstream.
+	// For long form requests if adpodId feature enabled, adpod_id must be sent downstream.
 	// Adpod id is a unique identifier for pod
 	// All impressions in the same pod must have the same pod id in request extension
 	// For this all impressions in  request should belong to the same pod
 	// If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same
-	if isVIDEO == 1 {
+	// If  adpodId feature disabled and impressions number per pod is more than maxImpsPerReq  - divide those imps to several requests but do not include ad pod id
+	if isVIDEO == 1 && *adPodId {
 		podImps := groupByPods(imps)
 
 		requests := make([]*adapters.RequestData, 0, len(podImps))
@@ -463,15 +474,15 @@ func keys(m map[string]bool) []string {
 // preprocess mutates the imp to get it ready to send to appnexus.
 //
 // It returns the member param, if it exists, and an error if anything went wrong during the preprocessing.
-func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, error) {
+func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, bool, error) {
 	var bidderExt adapters.ExtImpBidder
 	if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
-		return "", err
+		return "", false, err
 	}
 
 	var appnexusExt openrtb_ext.ExtImpAppnexus
 	if err := json.Unmarshal(bidderExt.Bidder, &appnexusExt); err != nil {
-		return "", err
+		return "", false, err
 	}
 
 	// Accept legacy Appnexus parameters if we don't have modern ones
@@ -487,7 +498,7 @@ func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, err
 	}
 
 	if appnexusExt.PlacementId == 0 && (appnexusExt.InvCode == "" || appnexusExt.Member == "") {
-		return "", &errortypes.BadInput{
+		return "", false, &errortypes.BadInput{
 			Message: "No placement or member+invcode provided",
 		}
 	}
@@ -529,10 +540,10 @@ func preprocess(imp *openrtb2.Imp, defaultDisplayManagerVer string) (string, err
 	}}
 	var err error
 	if imp.Ext, err = json.Marshal(&impExt); err != nil {
-		return appnexusExt.Member, err
+		return appnexusExt.Member, appnexusExt.AdPodId, err
 	}
 
-	return appnexusExt.Member, nil
+	return appnexusExt.Member, appnexusExt.AdPodId, nil
 }
 
 func makeKeywordStr(keywords []*openrtb_ext.ExtImpAppnexusKeyVal) string {
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index 2e361fa5..dc1ee9d0 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -66,6 +66,10 @@ const (
 	INVALID_HEIGHT    = "Invalid Height"
 	INVALID_MEDIATYPE = "Invalid MediaType"
 	INVALID_ADSLOT    = "Invalid AdSlot"
+
+	dctrKeyName        = "key_val"
+	pmZoneIDKeyName    = "pmZoneId"
+	pmZoneIDKeyNameOld = "pmZoneID"
 )
 
 func PrepareLogMessage(tID, pubId, adUnitId, bidID, details string, args ...interface{}) string {
@@ -485,30 +489,43 @@ func parseImpressionObject(imp *openrtb2.Imp, wrapExt *string, pubID *string) er
 		imp.Banner = bannerCopy
 	}
 
+	extMap := make(map[string]interface{}, 0)
 	if pubmaticExt.Keywords != nil && len(pubmaticExt.Keywords) != 0 {
-		kvstr := makeKeywordStr(pubmaticExt.Keywords)
-		imp.Ext = json.RawMessage([]byte(kvstr))
-	} else {
-		imp.Ext = nil
+		addKeywordsToExt(pubmaticExt.Keywords, extMap)
+	}
+	//Give preference to direct values of 'dctr' & 'pmZoneId' params in extension
+	if pubmaticExt.Dctr != "" {
+		extMap[dctrKeyName] = pubmaticExt.Dctr
+	}
+	if pubmaticExt.PmZoneID != "" {
+		extMap[pmZoneIDKeyName] = pubmaticExt.PmZoneID
+	}
+
+	imp.Ext = nil
+	if len(extMap) > 0 {
+		ext, err := json.Marshal(extMap)
+		if err == nil {
+			imp.Ext = ext
+		}
 	}
 
 	return nil
 
 }
 
-func makeKeywordStr(keywords []*openrtb_ext.ExtImpPubmaticKeyVal) string {
-	eachKv := make([]string, 0, len(keywords))
+func addKeywordsToExt(keywords []*openrtb_ext.ExtImpPubmaticKeyVal, extMap map[string]interface{}) {
 	for _, keyVal := range keywords {
 		if len(keyVal.Values) == 0 {
 			logf("No values present for key = %s", keyVal.Key)
 			continue
 		} else {
-			eachKv = append(eachKv, fmt.Sprintf("\"%s\":\"%s\"", keyVal.Key, strings.Join(keyVal.Values[:], ",")))
+			key := keyVal.Key
+			if keyVal.Key == pmZoneIDKeyNameOld {
+				key = pmZoneIDKeyName
+			}
+			extMap[key] = strings.Join(keyVal.Values[:], ",")
 		}
 	}
-
-	kvStr := "{" + strings.Join(eachKv, ",") + "}"
-	return kvStr
 }
 
 func prepareImpressionExt(keywords map[string]string) string {
diff --git a/config/config.go b/config/config.go
index afb4ac8d..27e917e4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -612,7 +612,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGrid, "https://x.bidswitch.net/check_uuid/"+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgrid%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BBSW_UUID%7D?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderGumGum, "https://rtb.gumgum.com/usync/prbds2s?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dgumgum%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderImprovedigital, "https://ad.360yield.com/server_match?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dimprovedigital%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%7BPUB_USER_ID%7D")
-	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=184674&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderIx, "https://ssum.casalemedia.com/usermatchredir?s=194962&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dix%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
 	// openrtb_ext.BidderInvibes doesn't have a good default.
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderJixie, "https://id.jixie.io/api/sync?pid=&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Djixie%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%25%25JXUID%25%25")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderKrushmedia, "https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dkrushmedia%26uid%3D%5BUID%5D")
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 0d3d93cf..ba70305f 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -99,6 +99,16 @@ func (big *bidIDGenerator) New() (string, error) {
 	return rawUuid.String(), err
 }
 
+type deduplicateChanceGenerator interface {
+	Generate() bool
+}
+
+type randomDeduplicateBidBooleanGenerator struct{}
+
+func (randomDeduplicateBidBooleanGenerator) Generate() bool {
+	return rand.Intn(100) < 50
+}
+
 func NewExchange(adapters map[openrtb_ext.BidderName]adaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gDPR gdpr.Permissions, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher) Exchange {
 	return &exchange{
 		adapterMap:          adapters,
@@ -205,7 +215,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r AuctionRequest, debugLog *
 		//If includebrandcategory is present in ext then CE feature is on.
 		if requestExt.Prebid.Targeting != nil && requestExt.Prebid.Targeting.IncludeBrandCategory != nil {
 			var rejections []string
-			bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, e.categoriesFetcher, targData)
+			bidCategory, adapterBids, rejections, err = applyCategoryMapping(ctx, requestExt, adapterBids, e.categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 			if err != nil {
 				return nil, fmt.Errorf("Error in category mapping : %s", err.Error())
 			}
@@ -612,7 +622,7 @@ func encodeBidResponseExt(bidResponseExt *openrtb_ext.ExtBidResponse) ([]byte, e
 	return buffer.Bytes(), err
 }
 
-func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) {
+func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtRequest, seatBids map[openrtb_ext.BidderName]*pbsOrtbSeatBid, categoriesFetcher stored_requests.CategoryFetcher, targData *targetData, booleanGenerator deduplicateChanceGenerator) (map[string]string, map[openrtb_ext.BidderName]*pbsOrtbSeatBid, []string, error) {
 	res := make(map[string]string)
 
 	type bidDedupe struct {
@@ -741,7 +751,7 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 					currBidPrice = 0
 				}
 				if dupeBidPrice == currBidPrice {
-					if rand.Intn(100) < 50 {
+					if booleanGenerator.Generate() {
 						dupeBidPrice = -1
 					} else {
 						currBidPrice = -1
@@ -756,11 +766,16 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 					} else {
 						// An older bid from a different seatBid we've already finished with
 						oldSeatBid := (seatBids)[dupe.bidderName]
+						rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated")
 						if len(oldSeatBid.bids) == 1 {
 							seatBidsToRemove = append(seatBidsToRemove, dupe.bidderName)
-							rejections = updateRejections(rejections, dupe.bidID, "Bid was deduplicated")
 						} else {
-							oldSeatBid.bids = append(oldSeatBid.bids[:dupe.bidIndex], oldSeatBid.bids[dupe.bidIndex+1:]...)
+							// This is a very rare, but still possible case where bid needs to be removed from already processed bidder
+							// This happens when current processing bidder has a bid that has same deduplication key as a bid from already processed bidder
+							// and already processed bid was selected to be removed
+							// See example of input data in unit test `TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice`
+							// Need to remove bid by name, not index in this case
+							removeBidById(oldSeatBid, dupe.bidID)
 						}
 					}
 					delete(res, dupe.bidID)
@@ -798,6 +813,24 @@ func applyCategoryMapping(ctx context.Context, requestExt *openrtb_ext.ExtReques
 	return res, seatBids, rejections, nil
 }
 
+func removeBidById(seatBid *pbsOrtbSeatBid, bidID string) {
+	//Find index of bid to remove
+	dupeBidIndex := -1
+	for i, bid := range seatBid.bids {
+		if bid.bid.ID == bidID {
+			dupeBidIndex = i
+			break
+		}
+	}
+	if dupeBidIndex != -1 {
+		if dupeBidIndex < len(seatBid.bids)-1 {
+			seatBid.bids = append(seatBid.bids[:dupeBidIndex], seatBid.bids[dupeBidIndex+1:]...)
+		} else if dupeBidIndex == len(seatBid.bids)-1 {
+			seatBid.bids = seatBid.bids[:len(seatBid.bids)-1]
+		}
+	}
+}
+
 func updateRejections(rejections []string, bidID string, reason string) []string {
 	message := fmt.Sprintf("bid rejected [bid ID: %s] reason: %s", bidID, reason)
 	return append(rejections, message)
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
old mode 100755
new mode 100644
diff --git a/openrtb_ext/imp_appnexus.go b/openrtb_ext/imp_appnexus.go
index 9ef69d06..83d762b3 100644
--- a/openrtb_ext/imp_appnexus.go
+++ b/openrtb_ext/imp_appnexus.go
@@ -17,6 +17,7 @@ type ExtImpAppnexus struct {
 	UsePmtRule              *bool                   `json:"use_pmt_rule"`
 	// At this time we do no processing on the private sizes, so just leaving it as a JSON blob.
 	PrivateSizes json.RawMessage `json:"private_sizes"`
+	AdPodId      bool            `json:"generate_ad_pod_id"`
 }
 
 // ExtImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.appnexus.keywords[i]
diff --git a/openrtb_ext/imp_pubmatic.go b/openrtb_ext/imp_pubmatic.go
index fd97836d..cf5093a2 100644
--- a/openrtb_ext/imp_pubmatic.go
+++ b/openrtb_ext/imp_pubmatic.go
@@ -11,6 +11,8 @@ import "encoding/json"
 type ExtImpPubmatic struct {
 	PublisherId string                  `json:"publisherId"`
 	AdSlot      string                  `json:"adSlot"`
+	Dctr        string                  `json:"dctr"`
+	PmZoneID    string                  `json:"pmzoneid"`
 	WrapExt     json.RawMessage         `json:"wrapper,omitempty"`
 	Keywords    []*ExtImpPubmaticKeyVal `json:"keywords,omitempty"`
 }
diff --git a/static/bidder-info/marsmedia.yaml b/static/bidder-info/marsmedia.yaml
index e0267205..143b893e 100644
--- a/static/bidder-info/marsmedia.yaml
+++ b/static/bidder-info/marsmedia.yaml
@@ -1,5 +1,6 @@
 maintainer:
   email: "prebid@mars.media"
+gvlVendorID: 776
 capabilities:
   app:
     mediaTypes:
diff --git a/static/bidder-params/appnexus.json b/static/bidder-params/appnexus.json
index 7da41a67..ce5ddad0 100644
--- a/static/bidder-params/appnexus.json
+++ b/static/bidder-params/appnexus.json
@@ -67,6 +67,10 @@
       "type": "boolean",
       "description": "Boolean to signal AppNexus to apply the relevant payment rule"
     },
+    "generate_ad_pod_id": {
+      "type": "boolean",
+      "description": "Boolean to signal AppNexus to add ad pod id to each request"
+    },
     "private_sizes" :{
       "type": "array",
       "items": {
diff --git a/static/bidder-params/pubmatic.json b/static/bidder-params/pubmatic.json
index 1b6a2f03..5d41b9fc 100644
--- a/static/bidder-params/pubmatic.json
+++ b/static/bidder-params/pubmatic.json
@@ -12,6 +12,14 @@
 			"type": "string",
 			"description": "An ID which identifies the ad slot"
 		},
+		"pmzoneid": {
+			"type": "string",
+			"description": "Comma separated zone id. Used im deal targeting & site section targeting. e.g drama,sport"
+		},
+		"dctr": {
+			"type": "string",
+			"description": "Deals Custom Targeting, pipe separated key-value pairs e.g key1=V1,V2,V3|key2=v1|key3=v3,v5"
+		},
 		"wrapper": {
 			"type": "object",
 			"description": "Specifies pubmatic openwrap configuration for a publisher",


Test patch:
diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go
index 35a5a570..1ec2fc08 100644
--- a/adapters/adapterstest/test_json.go
+++ b/adapters/adapterstest/test_json.go
@@ -57,6 +57,7 @@ func RunJSONBidderTest(t *testing.T, rootDir string, bidder adapters.Bidder) {
 	runTests(t, fmt.Sprintf("%s/supplemental", rootDir), bidder, true, false, false)
 	runTests(t, fmt.Sprintf("%s/amp", rootDir), bidder, true, true, false)
 	runTests(t, fmt.Sprintf("%s/video", rootDir), bidder, false, false, true)
+	runTests(t, fmt.Sprintf("%s/videosupplemental", rootDir), bidder, true, false, true)
 }
 
 // runTests runs all the *.json files in a directory. If allowErrors is false, and one of the test files
diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go
index ead45280..e67df953 100644
--- a/adapters/appnexus/appnexus_test.go
+++ b/adapters/appnexus/appnexus_test.go
@@ -69,7 +69,7 @@ func TestVideoSinglePod(t *testing.T) {
 	req.ID = "test_id"
 
 	reqExt := `{"prebid":{}}`
-	impExt := `{"bidder":{"placementId":123}}`
+	impExt := `{"bidder":{"placementId":123, "generate_ad_pod_id":true}}`
 	req.Ext = []byte(reqExt)
 
 	req.Imp = append(req.Imp, openrtb2.Imp{ID: "1_0", Ext: []byte(impExt)})
@@ -90,7 +90,7 @@ func TestVideoSinglePod(t *testing.T) {
 	error = json.Unmarshal(reqData.Ext, &reqDataExt)
 	assert.NoError(t, error, "Response ext unmarshalling error should be nil")
 
-	regMatch, matchErr := regexp.Match(`[0-9]19`, []byte(reqDataExt.Appnexus.AdPodId))
+	regMatch, matchErr := regexp.Match(`^[0-9]+$`, []byte(reqDataExt.Appnexus.AdPodId))
 	assert.NoError(t, matchErr, "Regex match error should be nil")
 	assert.True(t, regMatch, "AdPod id doesn't present in Appnexus extension or has incorrect format")
 }
@@ -140,7 +140,7 @@ func TestVideoSinglePodManyImps(t *testing.T) {
 	error = json.Unmarshal(reqData1.Ext, &reqDataExt1)
 	assert.NoError(t, error, "Response ext unmarshalling error should be nil")
 
-	adPodId1 := reqDataExt1.Appnexus.AdPodId
+	assert.Equal(t, "", reqDataExt1.Appnexus.AdPodId, "AdPod id should not be present in first request")
 
 	var reqData2 *openrtb2.BidRequest
 	error = json.Unmarshal(res[1].Body, &reqData2)
@@ -150,9 +150,7 @@ func TestVideoSinglePodManyImps(t *testing.T) {
 	error = json.Unmarshal(reqData2.Ext, &reqDataExt2)
 	assert.NoError(t, error, "Response ext unmarshalling error should be nil")
 
-	adPodId2 := reqDataExt2.Appnexus.AdPodId
-
-	assert.Equal(t, adPodId1, adPodId2, "AdPod id is not the same for the same pod")
+	assert.Equal(t, "", reqDataExt2.Appnexus.AdPodId, "AdPod id should not be present in second request")
 }
 
 func TestVideoTwoPods(t *testing.T) {
@@ -167,7 +165,7 @@ func TestVideoTwoPods(t *testing.T) {
 	req.ID = "test_id"
 
 	reqExt := `{"prebid":{}}`
-	impExt := `{"bidder":{"placementId":123}}`
+	impExt := `{"bidder":{"placementId":123, "generate_ad_pod_id": true}}`
 	req.Ext = []byte(reqExt)
 
 	req.Imp = append(req.Imp, openrtb2.Imp{ID: "1_0", Ext: []byte(impExt)})
@@ -219,7 +217,7 @@ func TestVideoTwoPodsManyImps(t *testing.T) {
 	req.ID = "test_id"
 
 	reqExt := `{"prebid":{}}`
-	impExt := `{"bidder":{"placementId":123}}`
+	impExt := `{"bidder":{"placementId":123, "generate_ad_pod_id":true}}`
 	req.Ext = []byte(reqExt)
 
 	req.Imp = append(req.Imp, openrtb2.Imp{ID: "1_0", Ext: []byte(impExt)})
diff --git a/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json b/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json
new file mode 100644
index 00000000..140cf0b0
--- /dev/null
+++ b/adapters/appnexus/appnexustest/video/video-no-adpodid-two-imps-different-pod.json
@@ -0,0 +1,94 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1
+			}
+		  }
+		},
+		{
+		  "id": "2_1",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"httpCalls": [
+	  {
+		"expectedRequest": {
+		  "uri": "http://ib.adnxs.com/openrtb2",
+		  "body": {
+			"id": "test-request-id",
+			"ext": {
+				"appnexus": {
+				  "hb_source": 6
+				},
+				"prebid": {}
+			},
+			"imp": [
+			  {
+					"id": "1_1",
+					"video": {
+						"mimes": ["video/mp4"],
+						"minduration": 15,
+						"maxduration": 30,
+						"protocols": [2, 3, 5, 6, 7, 8],
+						"w": 940,
+						"h": 560
+					},
+					"ext": {
+						"appnexus": {
+							"placement_id": 1
+						}
+					}
+				},
+			  {
+				"id": "2_1",
+				"video": {
+					"mimes": ["video/mp4"],
+					"minduration": 15,
+					"maxduration": 30,
+					"protocols": [2, 3, 5, 6, 7, 8],
+					"w": 940,
+					"h": 560
+				  },
+				"ext": {
+				  "appnexus": {
+					  "placement_id": 1
+				  }
+				}
+			  }
+			]
+		  }
+		},
+		"mockResponse": {
+		  "status": 200,
+		  "body": {}
+		}
+	  }
+	]
+  }
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json
new file mode 100644
index 00000000..5a453979
--- /dev/null
+++ b/adapters/appnexus/appnexustest/video/video-same-adpodid-two-imps-same-pod.json
@@ -0,0 +1,97 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1,
+			  "generate_ad_pod_id": true
+			}
+		  }
+		},
+		{
+		  "id": "1_2",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1,
+				  "generate_ad_pod_id": true
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"httpCalls": [
+	  {
+		"expectedRequest": {
+		  "uri": "http://ib.adnxs.com/openrtb2",
+		  "body": {
+			"id": "test-request-id",
+			"ext": {
+				"appnexus": {
+				 "adpod_id": "5577006791947779410",
+				  "hb_source": 6
+				},
+				"prebid": {}
+			},
+			"imp": [
+			  {
+				"id": "1_1",
+				"video": {
+					"mimes": ["video/mp4"],
+					"minduration": 15,
+					"maxduration": 30,
+					"protocols": [2, 3, 5, 6, 7, 8],
+					"w": 940,
+					"h": 560
+				  },
+				"ext": {
+				  "appnexus": {
+					  "placement_id": 1
+				  }
+				}
+			  },
+				{
+					"id": "1_2",
+					"video": {
+						"mimes": ["video/mp4"],
+						"minduration": 15,
+						"maxduration": 30,
+						"protocols": [2, 3, 5, 6, 7, 8],
+						"w": 940,
+						"h": 560
+					},
+					"ext": {
+						"appnexus": {
+							"placement_id": 1
+						}
+					}
+				}
+			]
+		  }
+		},
+		"mockResponse": {
+		  "status": 200,
+		  "body": {}
+		}
+	  }
+	]
+  }
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-different-pods.json b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-different-pods.json
new file mode 100644
index 00000000..3e2aabe4
--- /dev/null
+++ b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-different-pods.json
@@ -0,0 +1,48 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1,
+			  "generate_ad_pod_id": false
+			}
+		  }
+		},
+		{
+		  "id": "2_1",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1,
+				  "generate_ad_pod_id": true
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"expectedMakeRequestsErrors": [
+		{
+			"value": "generate ad pod option should be same for all pods in request",
+			"comparison": "literal"
+		}
+	]
+  }
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-same-pod.json b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-same-pod.json
new file mode 100644
index 00000000..a8efb807
--- /dev/null
+++ b/adapters/appnexus/appnexustest/videosupplemental/video-different-adpodid-two-imps-same-pod.json
@@ -0,0 +1,48 @@
+{
+	"mockBidRequest": {
+	  "id": "test-request-id",
+	  "imp": [
+		{
+		  "id": "1_1",
+		  "video": {
+			"mimes": ["video/mp4"],
+			"minduration": 15,
+			"maxduration": 30,
+			"protocols": [2, 3, 5, 6, 7, 8],
+			"w": 940,
+			"h": 560
+		  },
+		  "ext": {
+			"bidder": {
+			  "placement_id": 1,
+			  "generate_ad_pod_id": false
+			}
+		  }
+		},
+		{
+		  "id": "1_2",
+		  "video": {
+			  "mimes": ["video/mp4"],
+			  "minduration": 15,
+			  "maxduration": 30,
+			  "protocols": [2, 3, 5, 6, 7, 8],
+			  "w": 940,
+			  "h": 560
+		  },
+		  "ext": {
+			  "bidder": {
+				  "placement_id": 1,
+				  "generate_ad_pod_id": true
+			  }
+		  }
+		}
+	  ]
+	},
+
+	"expectedMakeRequestsErrors": [
+		{
+			"value": "generate ad pod option should be same for all pods in request",
+			"comparison": "literal"
+		}
+	]
+  }
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatictest/exemplary/banner.json b/adapters/pubmatic/pubmatictest/exemplary/banner.json
index 1eb8a212..f525673c 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/banner.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/banner.json
@@ -61,7 +61,7 @@
                 "w": 300
             },
                 "ext": {
-                    "pmZoneID": "Zone1,Zone2",
+                    "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
                 }
               }
diff --git a/adapters/pubmatic/pubmatictest/exemplary/video.json b/adapters/pubmatic/pubmatictest/exemplary/video.json
index 4c874535..1715e477 100644
--- a/adapters/pubmatic/pubmatictest/exemplary/video.json
+++ b/adapters/pubmatic/pubmatictest/exemplary/video.json
@@ -73,7 +73,7 @@
                     "maxbitrate": 10
                 },
                 "ext": {
-                    "pmZoneID": "Zone1,Zone2"
+                    "pmZoneId": "Zone1,Zone2"
                 }
               }
             ], 
diff --git a/adapters/pubmatic/pubmatictest/params/race/banner.json b/adapters/pubmatic/pubmatictest/params/race/banner.json
index 86ddc70b..ab88871e 100644
--- a/adapters/pubmatic/pubmatictest/params/race/banner.json
+++ b/adapters/pubmatic/pubmatictest/params/race/banner.json
@@ -1,6 +1,8 @@
 {
 	"publisherId": "156209",
 	"adSlot": "pubmatic_test2@300x250",
+	"pmzoneid": "drama,sport",
+	"dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
 	"keywords": {
 		"pmZoneID": "Zone1,Zone2",
 		"preference": "sports,movies"
diff --git a/adapters/pubmatic/pubmatictest/params/race/video.json b/adapters/pubmatic/pubmatictest/params/race/video.json
index 86ddc70b..ab88871e 100644
--- a/adapters/pubmatic/pubmatictest/params/race/video.json
+++ b/adapters/pubmatic/pubmatictest/params/race/video.json
@@ -1,6 +1,8 @@
 {
 	"publisherId": "156209",
 	"adSlot": "pubmatic_test2@300x250",
+	"pmzoneid": "drama,sport",
+	"dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
 	"keywords": {
 		"pmZoneID": "Zone1,Zone2",
 		"preference": "sports,movies"
diff --git a/adapters/pubmatic/pubmatictest/supplemental/app.json b/adapters/pubmatic/pubmatictest/supplemental/app.json
index 636433ca..e9f1048e 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/app.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/app.json
@@ -61,7 +61,7 @@
                 "w": 300
             },
                 "ext": {
-                    "pmZoneID": "Zone1,Zone2",
+                    "pmZoneId": "Zone1,Zone2",
                     "preference": "sports,movies"
                 }
               }
diff --git a/adapters/pubmatic/pubmatictest/supplemental/dctrAndPmZoneID.json b/adapters/pubmatic/pubmatictest/supplemental/dctrAndPmZoneID.json
new file mode 100644
index 00000000..d68517de
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/dctrAndPmZoneID.json
@@ -0,0 +1,162 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adSlot": "AdTag_Div1@300x250",
+            "publisherId": "  999  ",
+            "pmzoneid": "drama,sport",
+            "dctr": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
+            "keywords": [
+              {
+                "key": "pmZoneID",
+                "value": [
+                  "Zone1",
+                  "Zone2"
+                ]
+              },
+              {
+                "key": "preference",
+                "value": [
+                  "sports",
+                  "movies"
+                ]
+              }
+            ],
+            "wrapper": {
+              "version": 1,
+              "profile": 5123
+            }
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+    },
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "AdTag_Div1",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "h": 250,
+                "w": 300
+              },
+              "ext": {
+                "key_val": "abBucket=4|adType=page|entity=|paidByCategory=|sku=|userLevel=free|platform=android|majorVersion=3.54|version=3.54.0|mobileApplication=true|showId=20166|show=Kisah Untuk Geri|genre=Drama|contentUrl=https://www.iflix.com/title/show/20166|rating=TV-MA|contentLanguage=id",
+                "pmZoneId": "drama,sport",
+                "preference": "sports,movies"
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "999"
+            }
+          },
+          "ext": {
+            "wrapper": {
+              "profile": 5123,
+              "version": 1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "pubmatic.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "dealid": "test deal",
+                  "ext": {
+                    "dspid": 6,
+                    "deal_channel": 1
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "pubmatic.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "dealid": "test deal",
+            "ext": {
+              "dspid": 6,
+              "deal_channel": 1
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json b/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json
index c66a7189..a277ef53 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/noAdSlot.json
@@ -55,7 +55,7 @@
                         "h": 250
                     },
                     "ext": {
-                        "pmZoneID": "Zone1,Zone2",
+                        "pmZoneId": "Zone1,Zone2",
                         "preference": "sports,movies"
                     }
                 }],
diff --git a/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json b/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json
new file mode 100644
index 00000000..c9c335e8
--- /dev/null
+++ b/adapters/pubmatic/pubmatictest/supplemental/pmZoneIDInKeywords.json
@@ -0,0 +1,161 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "adSlot": "AdTag_Div1@300x250",
+            "publisherId": "  999  ",
+            "dctr": "key1=V1,V2,V3|key2=v1|key3=v3,v5",
+            "keywords": [
+              {
+                "key": "pmZoneID",
+                "value": [
+                  "Zone1",
+                  "Zone2"
+                ]
+              },
+              {
+                "key": "preference",
+                "value": [
+                  "sports",
+                  "movies"
+                ]
+              }
+            ],
+            "wrapper": {
+              "version": 1,
+              "profile": 5123
+            }
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+    },
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://hbopenbid.pubmatic.com/translator?source=prebid-server",
+        "body": {
+          "id": "test-request-id",
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "AdTag_Div1",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ],
+                "h": 250,
+                "w": 300
+              },
+              "ext": {
+                "key_val": "key1=V1,V2,V3|key2=v1|key3=v3,v5",
+                "pmZoneId": "Zone1,Zone2",
+                "preference": "sports,movies"
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "999"
+            }
+          },
+          "ext": {
+            "wrapper": {
+              "profile": 5123,
+              "version": 1
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-imp-id",
+                  "price": 0.500000,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "pubmatic.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300,
+                  "dealid": "test deal",
+                  "ext": {
+                    "dspid": 6,
+                    "deal_channel": 1
+                  }
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "pubmatic.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "dealid": "test deal",
+            "ext": {
+              "dspid": 6,
+              "deal_channel": 1
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json b/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json
index 6a344ee0..d207542a 100644
--- a/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json
+++ b/adapters/pubmatic/pubmatictest/supplemental/trimPublisherID.json
@@ -71,7 +71,7 @@
                 "w": 300
               },
               "ext": {
-                "pmZoneID": "Zone1,Zone2",
+                "pmZoneId": "Zone1,Zone2",
                 "preference": "sports,movies"
               }
             }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index c64b3935..a03c4786 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -1800,6 +1800,14 @@ func (big *mockBidIDGenerator) New() (string, error) {
 
 }
 
+type fakeRandomDeduplicateBidBooleanGenerator struct {
+	returnValue bool
+}
+
+func (m *fakeRandomDeduplicateBidBooleanGenerator) Generate() bool {
+	return m.returnValue
+}
+
 func newExtRequest() openrtb_ext.ExtRequest {
 	priceGran := openrtb_ext.PriceGranularity{
 		Precision: 2,
@@ -1899,7 +1907,7 @@ func TestCategoryMapping(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message")
@@ -1954,7 +1962,7 @@ func TestCategoryMappingNoIncludeBrandCategory(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be no bid rejection messages")
@@ -2006,7 +2014,7 @@ func TestCategoryMappingTranslateCategoriesNil(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Equal(t, 1, len(rejections), "There should be 1 bid rejection message")
@@ -2088,7 +2096,7 @@ func TestCategoryMappingTranslateCategoriesFalse(t *testing.T) {
 
 	adapterBids[bidderName1] = &seatBid
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.Equal(t, nil, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be no bid rejection messages")
@@ -2158,7 +2166,7 @@ func TestCategoryDedupe(t *testing.T) {
 
 		adapterBids[bidderName1] = &seatBid
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		assert.Equal(t, nil, err, "Category mapping error should be empty")
 		assert.Equal(t, 3, len(rejections), "There should be 2 bid rejection messages")
@@ -2238,7 +2246,7 @@ func TestNoCategoryDedupe(t *testing.T) {
 
 		adapterBids[bidderName1] = &seatBid
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		assert.Equal(t, nil, err, "Category mapping error should be empty")
 		assert.Equal(t, 2, len(rejections), "There should be 2 bid rejection messages")
@@ -2303,7 +2311,7 @@ func TestCategoryMappingBidderName(t *testing.T) {
 	adapterBids[bidderName1] = &seatBid1
 	adapterBids[bidderName2] = &seatBid2
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.NoError(t, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be 0 bid rejection messages")
@@ -2357,7 +2365,7 @@ func TestCategoryMappingBidderNameNoCategories(t *testing.T) {
 	adapterBids[bidderName1] = &seatBid1
 	adapterBids[bidderName2] = &seatBid2
 
-	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+	bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 	assert.NoError(t, err, "Category mapping error should be empty")
 	assert.Empty(t, rejections, "There should be 0 bid rejection messages")
@@ -2458,7 +2466,7 @@ func TestBidRejectionErrors(t *testing.T) {
 
 		adapterBids[bidderName] = &seatBid
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &test.reqExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &test.reqExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		if len(test.expectedCatDur) > 0 {
 			// Bid deduplication case
@@ -2521,7 +2529,7 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 		adapterBids[bidderNameApn1] = &seatBidApn1
 		adapterBids[bidderNameApn2] = &seatBidApn2
 
-		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData)
+		bidCategory, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &randomDeduplicateBidBooleanGenerator{})
 
 		assert.NoError(t, err, "Category mapping error should be empty")
 		assert.Len(t, rejections, 1, "There should be 1 bid rejection message")
@@ -2540,9 +2548,154 @@ func TestCategoryMappingTwoBiddersOneBidEachNoCategorySamePrice(t *testing.T) {
 		} else {
 			assert.Nil(t, seatBidApn1.bids, "Appnexus_1 seat bid should not have any bids back")
 			assert.Len(t, seatBidApn2.bids, 1, "Appnexus_2 seat bid should have only one back")
+		}
+	}
+}
+
+func TestCategoryMappingTwoBiddersManyBidsEachNoCategorySamePrice(t *testing.T) {
+	// This test covers a very rare de-duplication case where bid needs to be removed from already processed bidder
+	// This happens when current processing bidder has a bid that has same de-duplication key as a bid from already processed bidder
+	// and already processed bid was selected to be removed
+
+	//In this test case bids bid_idApn1_1 and bid_idApn1_2 will be removed due to hardcoded "fakeRandomDeduplicateBidBooleanGenerator{true}"
+
+	// Also there are should be more than one bids in bidder to test how we remove single element from bids array.
+	// In case there is just one bid to remove - we remove the entire bidder.
 
+	categoriesFetcher, error := newCategoryFetcher("./test/category-mapping")
+	if error != nil {
+		t.Errorf("Failed to create a category Fetcher: %v", error)
+	}
+
+	requestExt := newExtRequestTranslateCategories(nil)
+
+	targData := &targetData{
+		priceGranularity: requestExt.Prebid.Targeting.PriceGranularity,
+		includeWinners:   true,
+	}
+
+	requestExt.Prebid.Targeting.DurationRangeSec = []int{30}
+	requestExt.Prebid.Targeting.IncludeBrandCategory.WithCategory = false
+
+	cats1 := []string{"IAB1-3"}
+	cats2 := []string{"IAB1-4"}
+
+	bidApn1_1 := openrtb2.Bid{ID: "bid_idApn1_1", ImpID: "imp_idApn1_1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
+	bidApn1_2 := openrtb2.Bid{ID: "bid_idApn1_2", ImpID: "imp_idApn1_2", Price: 20.0000, Cat: cats1, W: 1, H: 1}
+
+	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 := pbsOrtbBid{&bidApn1_1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+
+	bid1_Apn2_1 := pbsOrtbBid{&bidApn2_1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn2_2 := pbsOrtbBid{&bidApn2_2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+
+	innerBidsApn1 := []*pbsOrtbBid{
+		&bid1_Apn1_1,
+		&bid1_Apn1_2,
+	}
+
+	innerBidsApn2 := []*pbsOrtbBid{
+		&bid1_Apn2_1,
+		&bid1_Apn2_2,
+	}
+
+	adapterBids := make(map[openrtb_ext.BidderName]*pbsOrtbSeatBid)
+
+	seatBidApn1 := pbsOrtbSeatBid{bids: innerBidsApn1, currency: "USD"}
+	bidderNameApn1 := openrtb_ext.BidderName("appnexus1")
+
+	seatBidApn2 := pbsOrtbSeatBid{bids: innerBidsApn2, currency: "USD"}
+	bidderNameApn2 := openrtb_ext.BidderName("appnexus2")
+
+	adapterBids[bidderNameApn1] = &seatBidApn1
+	adapterBids[bidderNameApn2] = &seatBidApn2
+
+	_, adapterBids, rejections, err := applyCategoryMapping(nil, &requestExt, adapterBids, categoriesFetcher, targData, &fakeRandomDeduplicateBidBooleanGenerator{true})
+
+	assert.NoError(t, err, "Category mapping error should be empty")
+
+	//Total number of bids from all bidders in this case should be 2
+	bidsFromFirstBidder := adapterBids[bidderNameApn1]
+	bidsFromSecondBidder := adapterBids[bidderNameApn2]
+
+	totalNumberOfbids := 0
+
+	if bidsFromFirstBidder.bids != nil {
+		totalNumberOfbids += len(bidsFromFirstBidder.bids)
+	}
+
+	if bidsFromSecondBidder.bids != nil {
+		totalNumberOfbids += len(bidsFromSecondBidder.bids)
+	}
+
+	assert.Equal(t, 2, totalNumberOfbids, "2 bids total should be returned")
+
+	assert.Len(t, adapterBids[bidderNameApn1].bids, 0)
+	assert.Len(t, adapterBids[bidderNameApn2].bids, 2)
+
+	assert.Equal(t, "bid_idApn2_1", adapterBids[bidderNameApn2].bids[0].bid.ID, "Incorrect expected bid 1 id")
+	assert.Equal(t, "bid_idApn2_2", adapterBids[bidderNameApn2].bids[1].bid.ID, "Incorrect expected bid 2 id")
+
+	assert.Len(t, rejections, 2, "2 bids should be de-duplicated")
+	assert.Equal(t, "bid rejected [bid ID: bid_idApn1_1] reason: Bid was deduplicated", rejections[0], "Incorrect rejected bid 1")
+	assert.Equal(t, "bid rejected [bid ID: bid_idApn1_2] reason: Bid was deduplicated", rejections[1], "Incorrect rejected bid 2")
+
+}
+
+func TestRemoveBidById(t *testing.T) {
+	cats1 := []string{"IAB1-3"}
+
+	bidApn1_1 := openrtb2.Bid{ID: "bid_idApn1_1", ImpID: "imp_idApn1_1", Price: 10.0000, Cat: cats1, W: 1, H: 1}
+	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 := pbsOrtbBid{&bidApn1_1, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_2 := pbsOrtbBid{&bidApn1_2, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+	bid1_Apn1_3 := pbsOrtbBid{&bidApn1_3, "video", nil, &openrtb_ext.ExtBidPrebidVideo{Duration: 30}, nil, 0, false, ""}
+
+	type aTest struct {
+		desc      string
+		inBidName string
+		outBids   []*pbsOrtbBid
+	}
+	testCases := []aTest{
+		{
+			desc:      "remove element from the middle",
+			inBidName: "bid_idApn1_2",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_3},
+		},
+		{
+			desc:      "remove element from the end",
+			inBidName: "bid_idApn1_3",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_2},
+		},
+		{
+			desc:      "remove element from the beginning",
+			inBidName: "bid_idApn1_1",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_2, &bid1_Apn1_3},
+		},
+		{
+			desc:      "remove element that doesn't exist",
+			inBidName: "bid_idApn",
+			outBids:   []*pbsOrtbBid{&bid1_Apn1_1, &bid1_Apn1_2, &bid1_Apn1_3},
+		},
+	}
+	for _, test := range testCases {
+
+		innerBidsApn1 := []*pbsOrtbBid{
+			&bid1_Apn1_1,
+			&bid1_Apn1_2,
+			&bid1_Apn1_3,
 		}
 
+		seatBidApn1 := &pbsOrtbSeatBid{bids: innerBidsApn1, currency: "USD"}
+
+		removeBidById(seatBidApn1, test.inBidName)
+		assert.Len(t, seatBidApn1.bids, len(test.outBids), test.desc)
+		assert.ElementsMatch(t, seatBidApn1.bids, test.outBids, "Incorrect bids in response")
 	}
 
 }