From 879e5c49628f5a4b3ad11f75a0359414742d9db1 Mon Sep 17 00:00:00 2001 From: Jason Quaccia Date: Fri, 5 May 2023 08:33:34 -0700 Subject: [PATCH] Prebid Core: Functionality to Optionally Defer Billing for an Ad (#9640) * logic for billing deferrals * refactored and addressed feedback * refactored triggerBilling func * addressed feedback * rebased on top of master * reverted some unneeded changes * refactored triggerBilling and addWinningBid funcs * reverted changes to example html file * addressed changes from feedback --- src/adapterManager.js | 4 +++ src/auction.js | 2 ++ src/prebid.js | 48 +++++++++++++++++++------- test/spec/unit/pbjs_api_spec.js | 60 ++++++++++++++++++++++++++++++++- 4 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/adapterManager.js b/src/adapterManager.js index 45438f59b55..8fcf04c7b41 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -612,6 +612,10 @@ adapterManager.callBidWonBidder = function(bidder, bid, adUnits) { tryCallBidderMethod(bidder, 'onBidWon', bid); }; +adapterManager.callBidBillableBidder = function(bid) { + tryCallBidderMethod(bid.bidder, 'onBidBillable', bid); +}; + adapterManager.callSetTargetingBidder = function(bidder, bid) { tryCallBidderMethod(bidder, 'onSetTargeting', bid); }; diff --git a/src/auction.js b/src/auction.js index 60892e5d7a2..736bc804ac5 100644 --- a/src/auction.js +++ b/src/auction.js @@ -367,8 +367,10 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function addWinningBid(winningBid) { + const winningAd = adUnits.find(adUnit => adUnit.transactionId === winningBid.transactionId); _winningBids = _winningBids.concat(winningBid); adapterManager.callBidWonBidder(winningBid.adapterCode || winningBid.bidder, winningBid, adUnits); + if (winningAd && !winningAd.deferBilling) adapterManager.callBidBillableBidder(winningBid); } function setBidTargeting(bid) { diff --git a/src/prebid.js b/src/prebid.js index 5070b6b42a4..c9848b5800c 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -1001,18 +1001,7 @@ if (FEATURES.VIDEO) { * @alias module:pbjs.markWinningBidAsUsed */ pbjsInstance.markWinningBidAsUsed = function (markBidRequest) { - let bids = []; - - if (markBidRequest.adUnitCode && markBidRequest.adId) { - bids = auctionManager.getBidsReceived() - .filter(bid => bid.adId === markBidRequest.adId && bid.adUnitCode === markBidRequest.adUnitCode); - } else if (markBidRequest.adUnitCode) { - bids = targeting.getWinningBids(markBidRequest.adUnitCode); - } else if (markBidRequest.adId) { - bids = auctionManager.getBidsReceived().filter(bid => bid.adId === markBidRequest.adId); - } else { - logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); - } + const bids = fetchReceivedBids(markBidRequest, 'Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); if (bids.length > 0) { bids[0].status = CONSTANTS.BID_STATUS.RENDERED; @@ -1020,6 +1009,23 @@ if (FEATURES.VIDEO) { } } +const fetchReceivedBids = (bidRequest, warningMessage) => { + let bids = []; + + if (bidRequest.adUnitCode && bidRequest.adId) { + bids = auctionManager.getBidsReceived() + .filter(bid => bid.adId === bidRequest.adId && bid.adUnitCode === bidRequest.adUnitCode); + } else if (bidRequest.adUnitCode) { + bids = targeting.getWinningBids(bidRequest.adUnitCode); + } else if (bidRequest.adId) { + bids = auctionManager.getBidsReceived().filter(bid => bid.adId === bidRequest.adId); + } else { + logWarn(warningMessage); + } + + return bids; +}; + /** * Get Prebid config options * @param {Object} options @@ -1097,4 +1103,22 @@ pbjsInstance.processQueue = function () { processQueue(pbjsInstance.cmd); }; +/** + * @alias module:pbjs.triggerBilling + */ +pbjsInstance.triggerBilling = (winningBid) => { + const bids = fetchReceivedBids(winningBid, 'Improper use of triggerBilling. It requires a bid with at least an adUnitCode or an adId to function.'); + const triggerBillingBid = bids.find(bid => bid.requestId === winningBid.requestId) || bids[0]; + + if (bids.length > 0 && triggerBillingBid) { + try { + adapterManager.callBidBillableBidder(triggerBillingBid); + } catch (e) { + logError('Error when triggering billing :', e); + } + } else { + logWarn('The bid provided to triggerBilling did not match any bids received.'); + } +}; + export default pbjsInstance; diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 820d87ef49c..58b90d38ddb 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -2501,7 +2501,6 @@ describe('Unit: Prebid Module', function () { }]; let adUnitCodes = ['adUnit-code']; let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); - adUnits[0]['mediaTypes'] = { native: {} }; adUnitCodes = ['adUnit-code']; let auction1 = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: timeout}); @@ -3546,4 +3545,63 @@ describe('Unit: Prebid Module', function () { expect(bids[0].adId).to.equal('adid-1'); }); }); + + describe('deferred billing', function () { + const sandbox = sinon.createSandbox(); + + let adUnits = [ + { + code: 'adUnit-code-1', + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + transactionId: '1234567890', + bids: [ + { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1' } + ] + }, + { + code: 'adUnit-code-2', + deferBilling: true, + mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, + transactionId: '0987654321', + bids: [ + { bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2' } + ] + } + ]; + + let winningBid1 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-1', transactionId: '1234567890', adId: 'abcdefg' } + let winningBid2 = { adapterCode: 'pubmatic', bidder: 'pubmatic', params: {placementId: '10433394'}, adUnitCode: 'adUnit-code-2', transactionId: '0987654321' } + let adUnitCodes = ['adUnit-code-1', 'adUnit-code-2']; + let auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: function() {}, cbTimeout: 2000}); + + beforeEach(function () { + sandbox.spy(adapterManager, 'callBidWonBidder'); + sandbox.spy(adapterManager, 'callBidBillableBidder'); + sandbox.stub(auctionManager, 'getBidsReceived').returns([winningBid1]); + }); + + afterEach(function () { + sandbox.resetHistory(); + sandbox.restore(); + }); + + it('should by default invoke callBidWonBidder and callBidBillableBidder', function () { + auction.addWinningBid(winningBid1); + sinon.assert.calledOnce(adapterManager.callBidWonBidder); + sinon.assert.calledOnce(adapterManager.callBidBillableBidder); + }); + + it('should only invoke callBidWonBidder and NOT callBidBillableBidder if deferBilling is present and true within the winning adUnit object', function () { + auction.addWinningBid(winningBid2); + sinon.assert.calledOnce(adapterManager.callBidWonBidder); + sinon.assert.notCalled(adapterManager.callBidBillableBidder); + }); + + it('should invoke callBidBillableBidder when pbjs.triggerBilling is invoked', function () { + $$PREBID_GLOBAL$$.triggerBilling(winningBid1); + sinon.assert.calledOnce(auctionManager.getBidsReceived); + sinon.assert.notCalled(adapterManager.callBidWonBidder); + sinon.assert.calledOnce(adapterManager.callBidBillableBidder); + }); + }); });