From c738ab502806fc675208a25f9cccc8c22edf16b8 Mon Sep 17 00:00:00 2001
From: Jaimin Panchal <jaiminpanchal27@gmail.com>
Date: Tue, 10 Apr 2018 13:36:07 -0400
Subject: [PATCH] Added bid pool and fixed getAllWinningBids function (#2328)

* Created bid pool, fixed getAllWinningBids and added new api getAllPrebidWinningBids

* Updated ttl buffer to 1000

* updated function names
---
 src/auction.js                        |   6 +-
 src/auctionManager.js                 |   4 +-
 src/prebid.js                         |  24 +++++--
 src/targeting.js                      |  21 +++++-
 test/fixtures/fixtures.js             |  38 ++++++++++
 test/spec/unit/core/targeting_spec.js | 100 +++++++++++++++++++++++++-
 test/spec/unit/pbjs_api_spec.js       |  28 +++++++-
 7 files changed, 205 insertions(+), 16 deletions(-)

diff --git a/src/auction.js b/src/auction.js
index 3c9e9bf86f1..9212b5afa2b 100644
--- a/src/auction.js
+++ b/src/auction.js
@@ -95,7 +95,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels})
   let _callback = callback;
   let _timer;
   let _timeout = cbTimeout;
-  let _winningBid;
+  let _winningBids = [];
 
   function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests) };
   function addBidReceived(bidsReceived) { _bidsReceived = _bidsReceived.concat(bidsReceived); }
@@ -202,8 +202,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels})
     executeCallback,
     callBids,
     bidsBackAll,
-    setWinningBid: (winningBid) => { _winningBid = winningBid },
-    getWinningBid: () => _winningBid,
+    addWinningBid: (winningBid) => { _winningBids = _winningBids.concat(winningBid) },
+    getWinningBids: () => _winningBids,
     getTimeout: () => _timeout,
     getAuctionId: () => _auctionId,
     getAuctionStatus: () => _auctionStatus,
diff --git a/src/auctionManager.js b/src/auctionManager.js
index 65723d6c199..f845db5f934 100644
--- a/src/auctionManager.js
+++ b/src/auctionManager.js
@@ -35,14 +35,14 @@ export function newAuctionManager() {
   auctionManager.addWinningBid = function(bid) {
     const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId);
     if (auction) {
-      auction.setWinningBid(bid);
+      auction.addWinningBid(bid);
     } else {
       utils.logWarn(`Auction not found when adding winning bid`);
     }
   }
 
   auctionManager.getAllWinningBids = function() {
-    return _auctions.map(auction => auction.getWinningBid())
+    return _auctions.map(auction => auction.getWinningBids())
       .reduce(flatten, []);
   }
 
diff --git a/src/prebid.js b/src/prebid.js
index 48a48d20707..b6f36a60086 100644
--- a/src/prebid.js
+++ b/src/prebid.js
@@ -7,7 +7,7 @@ import { userSync } from 'src/userSync.js';
 import { loadScript } from './adloader';
 import { config } from './config';
 import { auctionManager } from './auctionManager';
-import { targeting } from './targeting';
+import { targeting, getOldestBid, RENDERED, BID_TARGETING_SET } from './targeting';
 import { createHook } from 'src/hook';
 import includes from 'core-js/library/fn/array/includes';
 
@@ -20,8 +20,6 @@ const events = require('./events');
 const { triggerUserSyncs } = userSync;
 
 /* private variables */
-
-const RENDERED = 'rendered';
 const { ADD_AD_UNITS, BID_WON, REQUEST_BIDS, SET_TARGETING, AD_RENDER_FAILED } = CONSTANTS.EVENTS;
 const { PREVENT_WRITING_ON_MAIN_DOCUMENT, NO_AD, EXCEPTION, CANNOT_FIND_AD, MISSING_DOC_OR_ADID } = CONSTANTS.AD_RENDER_FAILED_REASON;
 
@@ -110,7 +108,8 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCode = function(adUnitCode) {
 
 $$PREBID_GLOBAL$$.getAdserverTargeting = function (adUnitCode) {
   utils.logInfo('Invoking $$PREBID_GLOBAL$$.getAdserverTargeting', arguments);
-  return targeting.getAllTargeting(adUnitCode, auctionManager.getBidsReceived());
+  let bidsReceived = auctionManager.getBidsReceived();
+  return targeting.getAllTargeting(adUnitCode, bidsReceived);
 };
 
 /**
@@ -556,14 +555,24 @@ $$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias) {
 */
 
 /**
- * Get all of the bids that have won their respective auctions.  Useful for [troubleshooting your integration](http://prebid.org/dev-docs/prebid-troubleshooting-guide.html).
- * @return {Array<AdapterBidResponse>} A list of bids that have won their respective auctions.
+ * Get all of the bids that have been rendered.  Useful for [troubleshooting your integration](http://prebid.org/dev-docs/prebid-troubleshooting-guide.html).
+ * @return {Array<AdapterBidResponse>} A list of bids that have been rendered.
 */
 $$PREBID_GLOBAL$$.getAllWinningBids = function () {
   return auctionManager.getAllWinningBids()
     .map(removeRequestId);
 };
 
+/**
+ * Get all of the bids that have won their respective auctions.
+ * @return {Array<AdapterBidResponse>} A list of bids that have won their respective auctions.
+ */
+$$PREBID_GLOBAL$$.getAllPrebidWinningBids = function () {
+  return auctionManager.getBidsReceived()
+    .filter(bid => bid.status === BID_TARGETING_SET)
+    .map(removeRequestId);
+};
+
 /**
  * Get array of highest cpm bids for all adUnits, or highest cpm bid
  * object for the given adUnit
@@ -572,7 +581,8 @@ $$PREBID_GLOBAL$$.getAllWinningBids = function () {
  * @return {Array} array containing highest cpm bid object(s)
  */
 $$PREBID_GLOBAL$$.getHighestCpmBids = function (adUnitCode) {
-  return targeting.getWinningBids(adUnitCode, auctionManager.getBidsReceived())
+  let bidsReceived = auctionManager.getBidsReceived().filter(getOldestBid);
+  return targeting.getWinningBids(adUnitCode, bidsReceived)
     .map(removeRequestId);
 };
 
diff --git a/src/targeting.js b/src/targeting.js
index 852a45b6bac..2abbc955aa5 100644
--- a/src/targeting.js
+++ b/src/targeting.js
@@ -10,14 +10,29 @@ var CONSTANTS = require('./constants.json');
 var pbTargetingKeys = [];
 
 export const BID_TARGETING_SET = 'targetingSet';
+export const RENDERED = 'rendered';
 
 const MAX_DFP_KEYLENGTH = 20;
+const TTL_BUFFER = 1000;
 
 // return unexpired bids
-export const isBidExpired = (bid) => (timestamp() - bid.responseTimestamp) < bid.ttl * 1000;
+export const isBidExpired = (bid) => (bid.responseTimestamp + bid.ttl * 1000 + TTL_BUFFER) > timestamp();
 
 // return bids whose status is not set. Winning bid can have status `targetingSet` or `rendered`.
-const isUnusedBid = (bid) => bid && ((bid.status && bid.status === BID_TARGETING_SET) || !bid.status);
+const isUnusedBid = (bid) => bid && ((bid.status && !includes([BID_TARGETING_SET, RENDERED], bid.status)) || !bid.status);
+
+// If two bids are found for same adUnitCode, we will use the latest one to take part in auction
+// This can happen in case of concurrent autions
+export const getOldestBid = function(bid, i, arr) {
+  let oldestBid = true;
+  arr.forEach((val, j) => {
+    if (i === j) return;
+    if (bid.bidder === val.bidder && bid.adUnitCode === val.adUnitCode && bid.responseTimestamp > val.responseTimestamp) {
+      oldestBid = false;
+    }
+  });
+  return oldestBid;
+}
 
 /**
  * @typedef {Object.<string,string>} targeting
@@ -162,6 +177,8 @@ export function newTargeting(auctionManager) {
     return auctionManager.getBidsReceived()
       .filter(isUnusedBid)
       .filter(exports.isBidExpired)
+      .filter(getOldestBid)
+    ;
   }
 
   /**
diff --git a/test/fixtures/fixtures.js b/test/fixtures/fixtures.js
index b1d94426db0..fc59d7eeab3 100644
--- a/test/fixtures/fixtures.js
+++ b/test/fixtures/fixtures.js
@@ -1404,3 +1404,41 @@ export function getCurrencyRates() {
     }
   };
 }
+
+export function createBidReceived({bidder, cpm, auctionId, responseTimestamp, adUnitCode, adId, status, ttl}) {
+  let bid = {
+    'bidderCode': bidder,
+    'width': '300',
+    'height': '250',
+    'statusMessage': 'Bid available',
+    'adId': adId,
+    'cpm': cpm,
+    'ad': 'markup',
+    'ad_id': adId,
+    'sizeId': '15',
+    'requestTimestamp': 1454535718610,
+    'responseTimestamp': responseTimestamp,
+    'auctionId': auctionId,
+    'timeToRespond': 123,
+    'pbLg': '0.50',
+    'pbMg': '0.50',
+    'pbHg': '0.53',
+    'adUnitCode': adUnitCode,
+    'bidder': bidder,
+    'size': '300x250',
+    'adserverTargeting': {
+      'hb_bidder': bidder,
+      'hb_adid': adId,
+      'hb_pb': cpm,
+      'foobar': '300x250'
+    },
+    'netRevenue': true,
+    'currency': 'USD',
+    'ttl': (!ttl) ? 300 : ttl
+  };
+
+  if (typeof status !== 'undefined') {
+    bid.status = status;
+  }
+  return bid;
+}
diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js
index c7eb3623e08..1fa648e9825 100644
--- a/test/spec/unit/core/targeting_spec.js
+++ b/test/spec/unit/core/targeting_spec.js
@@ -1,10 +1,11 @@
 import { expect } from 'chai';
 import { targeting as targetingInstance } from 'src/targeting';
 import { config } from 'src/config';
-import { getAdUnits } from 'test/fixtures/fixtures';
+import { getAdUnits, createBidReceived } from 'test/fixtures/fixtures';
 import CONSTANTS from 'src/constants.json';
 import { auctionManager } from 'src/auctionManager';
 import * as targetingModule from 'src/targeting';
+import * as utils from 'src/utils';
 
 const bid1 = {
   'bidderCode': 'rubicon',
@@ -134,4 +135,101 @@ describe('targeting tests', () => {
       expect(targeting['/123456/header-bid-tag-0']['hb_pb_rubicon']).to.deep.equal(targeting['/123456/header-bid-tag-0']['hb_pb']);
     });
   }); // end getAllTargeting tests
+
+  describe('Targeting in concurrent auctions', () => {
+    describe('check getOldestBid', () => {
+      let bidExpiryStub;
+      let auctionManagerStub;
+      beforeEach(() => {
+        bidExpiryStub = sinon.stub(targetingModule, 'isBidExpired').returns(true);
+        auctionManagerStub = sinon.stub(auctionManager, 'getBidsReceived');
+      });
+
+      afterEach(() => {
+        bidExpiryStub.restore();
+        auctionManagerStub.restore();
+      });
+
+      it('should use bids from pool to get Winning Bid', () => {
+        let bidsReceived = [
+          createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1'}),
+          createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-1', adId: 'adid-2'}),
+          createBidReceived({bidder: 'appnexus', cpm: 6, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-3'}),
+          createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-1', adId: 'adid-4'}),
+        ];
+        let adUnitCodes = ['code-0', 'code-1'];
+
+        let bids = targetingInstance.getWinningBids(adUnitCodes, bidsReceived);
+
+        expect(bids.length).to.equal(2);
+        expect(bids[0].adId).to.equal('adid-1');
+        expect(bids[1].adId).to.equal('adid-2');
+      });
+
+      it('should not use rendered bid to get winning bid', () => {
+        let bidsReceived = [
+          createBidReceived({bidder: 'appnexus', cpm: 8, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', status: 'rendered'}),
+          createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-1', adId: 'adid-2'}),
+          createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-3'}),
+          createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-1', adId: 'adid-4'}),
+        ];
+        auctionManagerStub.returns(bidsReceived);
+
+        let adUnitCodes = ['code-0', 'code-1'];
+        let bids = targetingInstance.getWinningBids(adUnitCodes);
+
+        expect(bids.length).to.equal(2);
+        expect(bids[0].adId).to.equal('adid-2');
+        expect(bids[1].adId).to.equal('adid-3');
+      });
+
+      it('should use oldest bids from bid pool to get winning bid', () => {
+        // Pool is having 4 bids from 2 auctions. There are 2 bids from rubicon, #2 which is first bid will be selected to take part in auction.
+        let bidsReceived = [
+          createBidReceived({bidder: 'appnexus', cpm: 8, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1'}),
+          createBidReceived({bidder: 'rubicon', cpm: 9, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-0', adId: 'adid-2'}),
+          createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-3'}),
+          createBidReceived({bidder: 'rubicon', cpm: 10, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-0', adId: 'adid-4'}),
+        ];
+        auctionManagerStub.returns(bidsReceived);
+
+        let adUnitCodes = ['code-0'];
+        let bids = targetingInstance.getWinningBids(adUnitCodes);
+
+        expect(bids.length).to.equal(1);
+        expect(bids[0].adId).to.equal('adid-2');
+      });
+    });
+
+    describe('check bidExpiry', () => {
+      let auctionManagerStub;
+      let timestampStub;
+      beforeEach(() => {
+        auctionManagerStub = sinon.stub(auctionManager, 'getBidsReceived');
+        timestampStub = sinon.stub(utils, 'timestamp');
+      });
+
+      afterEach(() => {
+        auctionManagerStub.restore();
+        timestampStub.restore();
+      });
+      it('should not include expired bids in the auction', () => {
+        timestampStub.returns(200000);
+        // Pool is having 4 bids from 2 auctions. All the bids are expired and only bid #3 is passing the bidExpiry check.
+        let bidsReceived = [
+          createBidReceived({bidder: 'appnexus', cpm: 18, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', ttl: 150}),
+          createBidReceived({bidder: 'sampleBidder', cpm: 16, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-0', adId: 'adid-2', ttl: 100}),
+          createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-3', ttl: 300}),
+          createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-0', adId: 'adid-4', ttl: 50}),
+        ];
+        auctionManagerStub.returns(bidsReceived);
+
+        let adUnitCodes = ['code-0', 'code-1'];
+        let bids = targetingInstance.getWinningBids(adUnitCodes);
+
+        expect(bids.length).to.equal(1);
+        expect(bids[0].adId).to.equal('adid-3');
+      });
+    });
+  });
 });
diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js
index 92b5d7b5a9f..5b78dfe78e2 100644
--- a/test/spec/unit/pbjs_api_spec.js
+++ b/test/spec/unit/pbjs_api_spec.js
@@ -5,7 +5,8 @@ import {
   getBidResponsesFromAPI,
   getTargetingKeys,
   getTargetingKeysBidLandscape,
-  getAdUnits
+  getAdUnits,
+  createBidReceived
 } from 'test/fixtures/fixtures';
 import { auctionManager, newAuctionManager } from 'src/auctionManager';
 import { targeting, newTargeting } from 'src/targeting';
@@ -1628,4 +1629,29 @@ describe('Unit: Prebid Module', function () {
       assert.equal($$PREBID_GLOBAL$$.que.push, $$PREBID_GLOBAL$$.cmd.push);
     });
   });
+
+  describe('getAllPrebidWinningBids', () => {
+    let auctionManagerStub;
+    beforeEach(() => {
+      auctionManagerStub = sinon.stub(auctionManager, 'getBidsReceived');
+    });
+
+    afterEach(() => {
+      auctionManagerStub.restore();
+    });
+
+    it('should return prebid auction winning bids', () => {
+      let bidsReceived = [
+        createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', status: 'targetingSet'}),
+        createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-1', adId: 'adid-2'}),
+        createBidReceived({bidder: 'appnexus', cpm: 6, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-3'}),
+        createBidReceived({bidder: 'rubicon', cpm: 6, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-1', adId: 'adid-4'}),
+      ];
+      auctionManagerStub.returns(bidsReceived)
+      let bids = $$PREBID_GLOBAL$$.getAllPrebidWinningBids();
+
+      expect(bids.length).to.equal(1);
+      expect(bids[0].adId).to.equal('adid-1');
+    });
+  });
 });