diff --git a/config/account.go b/config/account.go index 038a8daaf9e..e5265072164 100644 --- a/config/account.go +++ b/config/account.go @@ -85,6 +85,10 @@ func (pf *AccountPriceFloors) validate(errs []error) []error { return errs } +func (pf *AccountPriceFloors) IsAdjustForBidAdjustmentEnabled() bool { + return pf.AdjustForBidAdjustment +} + // EnabledForChannelType indicates whether CCPA is turned on at the account level for the specified channel type // by using the channel type setting if defined or the general CCPA setting if defined; otherwise it returns nil func (a *AccountCCPA) EnabledForChannelType(channelType ChannelType) *bool { diff --git a/exchange/exchange.go b/exchange/exchange.go index 70d92c08fba..136f1040740 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -319,7 +319,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog Prebid: *requestExtPrebid, SChain: requestExt.GetSChain(), } - bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprDefaultValue) + bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprDefaultValue, bidAdjustmentFactors) errs = append(errs, floorErrs...) mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments) diff --git a/exchange/utils.go b/exchange/utils.go index 267d928ca72..5731c9e4399 100644 --- a/exchange/utils.go +++ b/exchange/utils.go @@ -54,7 +54,7 @@ type requestSplitter struct { func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, auctionReq AuctionRequest, requestExt *openrtb_ext.ExtRequest, - gdprDefaultValue gdpr.Signal, + gdprDefaultValue gdpr.Signal, bidAdjustmentFactors map[string]float64, ) (allowedBidderRequests []BidderRequest, privacyLabels metrics.PrivacyLabels, errs []error) { req := auctionReq.BidRequestWrapper @@ -93,6 +93,11 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context, } } + if auctionReq.Account.PriceFloors.IsAdjustForBidAdjustmentEnabled() { + //Apply BidAdjustmentFactor to imp.BidFloor + applyBidAdjustmentToFloor(allBidderRequests, bidAdjustmentFactors) + } + gdprSignal, err := getGDPR(req) if err != nil { errs = append(errs, err) @@ -1071,3 +1076,25 @@ func getPrebidMediaTypeForBid(bid openrtb2.Bid) (openrtb_ext.BidType, error) { Message: errMsg, } } + +func applyBidAdjustmentToFloor(allBidderRequests []BidderRequest, bidAdjustmentFactors map[string]float64) { + + if len(bidAdjustmentFactors) == 0 { + return + } + + for _, bidderRequest := range allBidderRequests { + bidAdjustment := 1.0 + + if bidAdjustemntValue, ok := bidAdjustmentFactors[string(bidderRequest.BidderName)]; ok { + bidAdjustment = bidAdjustemntValue + } + + if bidAdjustment != 1.0 { + for index, imp := range bidderRequest.BidRequest.Imp { + imp.BidFloor = imp.BidFloor / bidAdjustment + bidderRequest.BidRequest.Imp[index] = imp + } + } + } +} diff --git a/exchange/utils_test.go b/exchange/utils_test.go index a1ca0a607ef..ee6dad1ad96 100644 --- a/exchange/utils_test.go +++ b/exchange/utils_test.go @@ -486,7 +486,7 @@ func TestCleanOpenRTBRequests(t *testing.T) { hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } - bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo) + bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, map[string]float64{}) if test.hasError { assert.NotNil(t, err, "Error shouldn't be nil") } else { @@ -552,7 +552,7 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo) + bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, map[string]float64{}) assert.Empty(t, err, "No errors should be returned") for _, bidderRequest := range bidderRequests { bidderName := bidderRequest.BidderName @@ -867,7 +867,7 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) { bidderInfo: config.BidderInfos{}, } - actualBidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + actualBidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) assert.Empty(t, err, "No errors should be returned") assert.Len(t, actualBidderRequests, len(test.expectedBidderRequests), "result len doesn't match for testCase %s", test.description) for _, actualBidderRequest := range actualBidderRequests { @@ -1036,7 +1036,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) result := bidderRequests[0] assert.Nil(t, errs) @@ -1113,7 +1113,7 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) { bidderInfo: config.BidderInfos{}, } - _, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, gdpr.SignalNo) + _, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, &reqExtStruct, gdpr.SignalNo, map[string]float64{}) assert.ElementsMatch(t, []error{test.expectError}, errs, test.description) } @@ -1172,7 +1172,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + bidderRequests, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) result := bidderRequests[0] assert.Nil(t, errs) @@ -1265,7 +1265,7 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) if test.hasError == true { assert.NotNil(t, errs) assert.Len(t, bidderRequests, 0) @@ -1336,7 +1336,7 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) if test.hasError == true { assert.NotNil(t, errs) assert.Len(t, bidderRequests, 0) @@ -1923,7 +1923,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) { bidderInfo: config.BidderInfos{}, } - results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) result := results[0] assert.Nil(t, errs) @@ -2156,7 +2156,7 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) { bidderInfo: config.BidderInfos{}, } - results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdprDefaultValue) + results, privacyLabels, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdprDefaultValue, map[string]float64{}) result := results[0] if test.expectError { @@ -2257,7 +2257,7 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) { bidderInfo: config.BidderInfos{}, } - results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) // extract bidder name from each request in the results bidders := []openrtb_ext.BidderName{} @@ -2344,7 +2344,7 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) { hostSChainNode: nil, bidderInfo: test.bidderInfos, } - bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo) + bidderRequests, _, err := reqSplitter.cleanOpenRTBRequests(context.Background(), test.req, nil, gdpr.SignalNo, map[string]float64{}) assert.Nil(t, err, "Err should be nil") bidRequest := bidderRequests[0] assert.Equal(t, test.expectRegs, bidRequest.BidRequest.Regs) @@ -2605,7 +2605,8 @@ func newBidRequest(t *testing.T) *openrtb2.BidRequest { Geo: &openrtb2.Geo{Lat: 123.456}, }, Imp: []openrtb2.Imp{{ - ID: "some-imp-id", + BidFloor: 100, + ID: "some-imp-id", Banner: &openrtb2.Banner{ Format: []openrtb2.Format{{ W: 300, @@ -3100,7 +3101,7 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { hostSChainNode: nil, bidderInfo: config.BidderInfos{}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) assert.Nil(t, errs) assert.Len(t, bidderRequests, 2, "Bid request count is not 2") @@ -3116,6 +3117,117 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) { assert.Equal(t, axonixPrebidSchainsSchain, bidRequestSourceExts["axonix"], "Incorrect axonix bid request schain in source.ext") } +func TestCleanOpenRTBRequestsBidAdjustment(t *testing.T) { + tcf2Consent := "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA" + falseValue := false + testCases := []struct { + description string + gdprAccountEnabled *bool + gdprHostEnabled bool + gdpr string + gdprConsent string + gdprScrub bool + permissionsError error + gdprDefaultValue string + expectPrivacyLabels metrics.PrivacyLabels + expectError bool + bidAdjustmentFactor map[string]float64 + expectedImp []openrtb2.Imp + }{ + { + description: "BidFloor Adjustment Done for Appnexus", + gdprAccountEnabled: &falseValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: tcf2Consent, + gdprScrub: false, + gdprDefaultValue: "1", + expectPrivacyLabels: metrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + bidAdjustmentFactor: map[string]float64{"appnexus": 0.50}, + expectedImp: []openrtb2.Imp{{ + BidFloor: 200, + ID: "some-imp-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{{ + W: 300, + H: 250, + }, { + W: 300, + H: 600, + }}, + }, + Ext: json.RawMessage(`{"bidder":{"placementId":1}}`), + }}, + }, + { + description: "bidAjustement Not provided", + gdprAccountEnabled: &falseValue, + gdprHostEnabled: true, + gdpr: "1", + gdprConsent: tcf2Consent, + gdprScrub: false, + gdprDefaultValue: "1", + expectPrivacyLabels: metrics.PrivacyLabels{ + GDPREnforced: false, + GDPRTCFVersion: "", + }, + bidAdjustmentFactor: map[string]float64{}, + expectedImp: []openrtb2.Imp{{ + BidFloor: 100, + ID: "some-imp-id", + Banner: &openrtb2.Banner{ + Format: []openrtb2.Format{{ + W: 300, + H: 250, + }, { + W: 300, + H: 600, + }}, + }, + Ext: json.RawMessage(`{"bidder":{"placementId":1}}`), + }}, + }, + } + for _, test := range testCases { + req := newBidRequest(t) + accountConfig := config.Account{ + GDPR: config.AccountGDPR{ + Enabled: &falseValue, + }, + PriceFloors: config.AccountPriceFloors{ + AdjustForBidAdjustment: true, + }, + } + auctionReq := AuctionRequest{ + BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req}, + UserSyncs: &emptyUsersync{}, + Account: accountConfig, + } + gdprPermissionsBuilder := fakePermissionsBuilder{ + permissions: &permissionsMock{ + allowAllBidders: true, + passGeo: !test.gdprScrub, + passID: !test.gdprScrub, + activitiesError: test.permissionsError, + }, + }.Builder + reqSplitter := &requestSplitter{ + bidderToSyncerKey: map[string]string{}, + me: &metrics.MetricsEngineMock{}, + gdprPermsBuilder: gdprPermissionsBuilder, + hostSChainNode: nil, + bidderInfo: config.BidderInfos{}, + } + results, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, test.bidAdjustmentFactor) + result := results[0] + assert.Nil(t, errs) + assert.Equal(t, test.expectedImp, result.BidRequest.Imp, test.description) + } +} + func TestApplyFPD(t *testing.T) { testCases := []struct { @@ -3450,7 +3562,7 @@ func TestCleanOpenRTBRequestsFilterBidderRequestExt(t *testing.T) { bidderInfo: config.BidderInfos{}, } - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, extRequest, gdpr.SignalNo, map[string]float64{}) assert.Equal(t, test.wantError, len(errs) != 0, test.desc) sort.Slice(bidderRequests, func(i, j int) bool { return bidderRequests[i].BidderCoreName < bidderRequests[j].BidderCoreName @@ -4380,7 +4492,7 @@ func TestCleanOpenRTBRequestsActivities(t *testing.T) { } t.Run(test.name, func(t *testing.T) { - bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo) + bidderRequests, _, errs := reqSplitter.cleanOpenRTBRequests(context.Background(), auctionReq, nil, gdpr.SignalNo, map[string]float64{}) assert.Empty(t, errs) assert.Len(t, bidderRequests, test.expectedReqNumber) @@ -4440,3 +4552,122 @@ func getTransmitTIDActivityConfig(componentName string, allow bool) config.Accou }, } } + +func TestApplyBidAdjustmentToFloor(t *testing.T) { + type args struct { + allBidderRequests []BidderRequest + bidAdjustmentFactors map[string]float64 + } + tests := []struct { + name string + args args + expectedAllBidderRequests []BidderRequest + }{ + { + name: " bidAdjustmentFactor is empty", + args: args{ + allBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + }, + bidAdjustmentFactors: map[string]float64{}, + }, + expectedAllBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + }, + }, + { + name: "bidAdjustmentFactor not present for request bidder", + args: args{ + allBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + }, + bidAdjustmentFactors: map[string]float64{"pubmatic": 1.0}, + }, + expectedAllBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + }, + }, + { + name: "bidAdjustmentFactor present for request bidder", + args: args{ + allBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + }, + bidAdjustmentFactors: map[string]float64{"pubmatic": 1.0, "appnexus": 0.75}, + }, + expectedAllBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 133.33333333333334}, {BidFloor: 200}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + }, + }, + { + name: "bidAdjustmentFactor present only for appnexus request bidder", + args: args{ + allBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("pubmatic"), + }, + }, + bidAdjustmentFactors: map[string]float64{"appnexus": 0.75}, + }, + expectedAllBidderRequests: []BidderRequest{ + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 133.33333333333334}, {BidFloor: 200}}, + }, + BidderName: openrtb_ext.BidderName("appnexus"), + }, + { + BidRequest: &openrtb2.BidRequest{ + Imp: []openrtb2.Imp{{BidFloor: 100}, {BidFloor: 150}}, + }, + BidderName: openrtb_ext.BidderName("pubmatic"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + applyBidAdjustmentToFloor(tt.args.allBidderRequests, tt.args.bidAdjustmentFactors) + assert.Equal(t, tt.expectedAllBidderRequests, tt.args.allBidderRequests, tt.name) + }) + } +}