diff --git a/modules/equativBidAdapter.js b/modules/equativBidAdapter.js new file mode 100644 index 00000000000..c5c96039f67 --- /dev/null +++ b/modules/equativBidAdapter.js @@ -0,0 +1,153 @@ +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepAccess, deepSetValue, isFn, mergeDeep } from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + +export const spec = { + code: 'equativ', + gvlid: 45, + supportedMediaTypes: [BANNER], + + /** + * @param bidRequests + * @param bidderRequest + * @returns {ServerRequest[]} + */ + buildRequests: (bidRequests, bidderRequest) => { + return { + data: converter.toORTB({ bidderRequest, bidRequests }), + method: 'POST', + url: 'https://ssb-global.smartadserver.com/api/bid?callerId=169' + }; + }, + + /** + * @param bidRequest + * @returns {number} + */ + getMinFloor: (bidRequest) => { + const floors = []; + + if (isFn(bidRequest.getFloor)) { + (deepAccess(bidRequest, 'mediaTypes.banner.sizes') || []).forEach(size => { + const floor = bidRequest.getFloor({ size }).floor; + if (!isNaN(floor)) { + floors.push(floor); + } else { + floors.push(0.0); + } + }); + } + + return floors.length ? Math.min(...floors) : 0.0; + }, + + /** + * @param serverResponse + * @param bidRequest + * @returns {Bid[]} + */ + interpretResponse: (serverResponse, bidRequest) => + converter.fromORTB({ + request: bidRequest.data, + response: serverResponse.body, + }), + + /** + * @param bidRequest + * @returns {boolean} + */ + isBidRequestValid: (bidRequest) => { + return !!( + deepAccess(bidRequest, 'params.networkId') || + deepAccess(bidRequest, 'ortb2.site.publisher.id') || + deepAccess(bidRequest, 'ortb2.app.publisher.id') || + deepAccess(bidRequest, 'ortb2.dooh.publisher.id') + ); + }, + + /** + * @param syncOptions + * @param serverResponse + * @returns {{type: string, url: string}[]} + */ + // getUserSyncs: (syncOptions, serverResponse) => { + // if (syncOptions.iframeEnabled && serverResponses[0]?.body.cSyncUrl) { + // return [ + // { + // type: 'iframe', + // url: serverResponses[0].body.cSyncUrl, + // }, + // ]; + // } + // return (syncOptions.pixelEnabled && serverResponse.body?.dspPixels) + // ? serverResponse.body.dspPixels.map((pixel) => ({ + // type: 'image', + // url: pixel, + // })) : []; + // }, +}; + +export const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const { siteId, pageId, formatId } = bidRequest.params; + + delete imp.dt; + + imp.bidfloor = imp.bidfloor || spec.getMinFloor(bidRequest); + imp.secure = Number(window.location.protocol === 'https:'); + imp.tagid = bidRequest.adUnitCode; + + if (siteId || pageId || formatId) { + const bidder = {}; + + if (siteId) { + bidder.siteId = siteId; + } + + if (pageId) { + bidder.pageId = pageId; + } + + if (formatId) { + bidder.formatId = formatId; + } + + mergeDeep(imp, { + ext: { bidder }, + }); + } + + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const bid = context.bidRequests[0]; + const req = buildRequest(imps, bidderRequest, context); + + if (deepAccess(bid, 'ortb2.site.publisher')) { + deepSetValue(req, 'site.publisher.id', bid.ortb2.site.publisher.id || bid.params.networkId); + } else if (deepAccess(bid, 'ortb2.app.publisher')) { + deepSetValue(req, 'app.publisher.id', bid.ortb2.app.publisher.id || bid.params.networkId); + } else if (deepAccess(bid, 'ortb2.dooh.publisher')) { + deepSetValue(req, 'dooh.publisher.id', bid.ortb2.dooh.publisher.id || bid.params.networkId); + } else { + deepSetValue(req, 'site.publisher.id', bid.params.networkId); + } + + return req; + }, +}); + +registerBidder(spec); diff --git a/modules/equativBidAdapter.md b/modules/equativBidAdapter.md new file mode 100644 index 00000000000..ceee6d19bdc --- /dev/null +++ b/modules/equativBidAdapter.md @@ -0,0 +1,40 @@ +# Overview + +``` +Module Name: Equativ Bidder Adapter (beta) +Module Type: Bidder Adapter +Maintainer: support@equativ.com +``` + +# Description + +Connect to Equativ for bids. + +The Equativ adapter requires setup and approval from the Equativ team. Please reach out to your technical account manager for more information. + +# Test Parameters + +## Web or In-app +```javascript +var adUnits = [ + { + code: '/589236/banner_1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'equativ', + params: { + networkId: 13, // mandatory if no ortb2.(site or app).publisher.id set + siteId: 20743, // optional + pageId: 89653, // optional + formatId: 291, // optional + } + } + ] + } +]; +``` \ No newline at end of file diff --git a/test/spec/modules/equativBidAdapter_spec.js b/test/spec/modules/equativBidAdapter_spec.js new file mode 100644 index 00000000000..c2465176ac1 --- /dev/null +++ b/test/spec/modules/equativBidAdapter_spec.js @@ -0,0 +1,425 @@ +import { spec, converter } from 'modules/equativBidAdapter.js'; + +describe('Equativ bid adapter tests', () => { + const DEFAULT_BID_REQUESTS = [ + { + adUnitCode: 'eqtv_42', + bidId: 'abcd1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bidder: 'equativ', + params: { + networkId: 111, + }, + requestId: 'efgh5678', + ortb2Imp: { + ext: { + tid: 'zsfgzzg', + }, + }, + }, + ]; + + const DEFAULT_BIDDER_REQUEST = { + bidderCode: 'equativ', + bids: DEFAULT_BID_REQUESTS, + }; + + const SAMPLE_RESPONSE = { + body: { + id: '12h712u7-k22g-8124-ab7a-h268s22dy271', + seatbid: [ + { + bid: [ + { + id: '1bh7jku7-ko2g-8654-ab72-h268shvwy271', + impid: 'r12gwgf231', + price: 0.6565, + adm: '

AD

', + adomain: ['abc.com'], + cid: '1242512', + crid: '535231', + w: 300, + h: 600, + mtype: 1, + cat: ['IAB19', 'IAB19-1'], + cattax: 1, + }, + ], + seat: '4212', + }, + ], + cur: 'USD', + statuscode: 0, + }, + }; + + // const RESPONSE_WITH_DSP_PIXELS = { + // ...SAMPLE_RESPONSE, + // body: { + // dspPixels: ['1st-pixel', '2nd-pixel', '3rd-pixel'] + // } + // }; + + describe('buildRequests', () => { + it('should build correct request using ORTB converter', () => { + const request = spec.buildRequests( + DEFAULT_BID_REQUESTS, + DEFAULT_BIDDER_REQUEST + ); + const dataFromConverter = converter.toORTB({ + bidderRequest: DEFAULT_BIDDER_REQUEST, + bidRequests: DEFAULT_BID_REQUESTS, + }); + expect(request).to.deep.equal({ + data: { ...dataFromConverter, id: request.data.id }, + method: 'POST', + url: 'https://ssb-global.smartadserver.com/api/bid?callerId=169', + }); + }); + + it('should add ext.bidder to imp object when siteId is defined', () => { + const bidRequests = [ + { ...DEFAULT_BID_REQUESTS[0], params: { siteId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].ext.bidder).to.deep.equal({ + siteId: 123, + }); + }); + + it('should add ext.bidder to imp object when pageId is defined', () => { + const bidRequests = [ + { ...DEFAULT_BID_REQUESTS[0], params: { pageId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].ext.bidder).to.deep.equal({ + pageId: 123, + }); + }); + + it('should add ext.bidder to imp object when formatId is defined', () => { + const bidRequests = [ + { ...DEFAULT_BID_REQUESTS[0], params: { formatId: 123 } }, + ]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].ext.bidder).to.deep.equal({ + formatId: 123, + }); + }); + + it('should not add ext.bidder to imp object when siteId, pageId, formatId are not defined', () => { + const bidRequests = [{ ...DEFAULT_BID_REQUESTS[0], params: {} }]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0].ext.bidder).to.be.undefined; + }); + + it('should add site.publisher.id param', () => { + const request = spec.buildRequests( + DEFAULT_BID_REQUESTS, + DEFAULT_BIDDER_REQUEST + ); + expect(request.data.site.publisher.id).to.equal(111); + }); + + it('should pass ortb2.site.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BID_REQUESTS[0], + ortb2: { + site: { + publisher: { + id: 98, + } + } + } + }]; + delete bidRequests[0].params; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.site.publisher.id).to.equal(98); + }); + + it('should pass networkId as site.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BID_REQUESTS[0], + ortb2: { + site: { + publisher: {} + } + } + }]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.site.publisher.id).to.equal(111); + }); + + it('should pass ortb2.app.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BID_REQUESTS[0], + ortb2: { + app: { + publisher: { + id: 27, + } + } + } + }]; + delete bidRequests[0].params; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.app.publisher.id).to.equal(27); + }); + + it('should pass networkId as app.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BID_REQUESTS[0], + ortb2: { + app: { + publisher: {} + } + } + }]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.app.publisher.id).to.equal(111); + }); + + it('should pass ortb2.dooh.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BID_REQUESTS[0], + ortb2: { + dooh: { + publisher: { + id: 35, + } + } + } + }]; + delete bidRequests[0].params; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.dooh.publisher.id).to.equal(35); + }); + + it('should pass networkId as dooh.publisher.id', () => { + const bidRequests = [{ + ...DEFAULT_BID_REQUESTS[0], + ortb2: { + dooh: { + publisher: {} + } + } + }]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.dooh.publisher.id).to.equal(111); + }); + + it('should send default floor of 0.0', () => { + const request = spec.buildRequests( + DEFAULT_BID_REQUESTS, + DEFAULT_BIDDER_REQUEST + ); + expect(request.data.imp[0]).to.have.property('bidfloor').that.eq(0.0); + }); + + it('should send secure connection', () => { + const request = spec.buildRequests( + DEFAULT_BID_REQUESTS, + DEFAULT_BIDDER_REQUEST + ); + expect(request.data.imp[0]).to.have.property('secure').that.within(0, 1); + }); + + it('should have tagid', () => { + const request = spec.buildRequests( + DEFAULT_BID_REQUESTS, + DEFAULT_BIDDER_REQUEST + ); + expect(request.data.imp[0]).to.have.property('tagid').that.eq(DEFAULT_BID_REQUESTS[0].adUnitCode); + }); + + it('should remove dt', () => { + const bidRequests = [ + { ...DEFAULT_BID_REQUESTS[0], ortb2Imp: { dt: 1728377558235 } } + ]; + const bidderRequest = { ...DEFAULT_BIDDER_REQUEST, bids: bidRequests }; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[0]).to.not.have.property('dt'); + }); + }); + + describe('getMinFloor', () => { + it('should return floor of 0.0 if floor module not available', () => { + const bid = { + ...DEFAULT_BID_REQUESTS[0], + getFloor: false, + }; + expect(spec.getMinFloor(bid)).to.deep.eq(0.0); + }); + + it('should return floor of 0.0 if mediaTypes not defined', () => { + const bid = { + getFloor: () => ({}) + }; + expect(bid.mediaTypes).to.be.undefined; + expect(spec.getMinFloor(bid)).to.deep.eq(0.0); + }); + + it('should return proper min floor', () => { + const bid = { + ...DEFAULT_BID_REQUESTS[0], + getFloor: data => { + if (data.size[0] === 300 && data.size[1] === 250) { + return { floor: 1.13 }; + } else if (data.size[0] === 300 && data.size[1] === 600) { + return { floor: 1.39 }; + } else { + return { floor: 0.52 }; + } + } + }; + expect(spec.getMinFloor(bid)).to.deep.eq(1.13); + }); + + it('should return global media type floor if no rule for size', () => { + const bid = { + ...DEFAULT_BID_REQUESTS[0], + getFloor: data => { + if (data.size[0] === 728 && data.size[1] === 90) { + return { floor: 1.13 }; + } else if (data.size[0] === 300 && data.size[1] === 600) { + return { floor: 1.36 }; + } else { + return { floor: 0.34 }; + } + } + }; + expect(spec.getMinFloor(bid)).to.deep.eq(0.34); + }); + + it('should return floor of 0 if no rule for size', () => { + const bid = { + ...DEFAULT_BID_REQUESTS[0], + getFloor: data => { + if (data.size[0] === 728 && data.size[1] === 90) { + return { floor: 1.13 }; + } else if (data.size[0] === 300 && data.size[1] === 600) { + return { floor: 1.36 }; + } else { + return {}; + } + } + }; + expect(spec.getMinFloor(bid)).to.deep.eq(0.0); + }); + }); + + // describe('getUserSyncs', () => { + // it('should return empty array if no pixel sync not enabled', () => { + // const syncs = spec.getUserSyncs({}, RESPONSE_WITH_DSP_PIXELS); + // expect(syncs).to.deep.equal([]); + // }); + + // it('should return empty array if no pixels available', () => { + // const syncs = spec.getUserSyncs( + // { pixelEnabled: true }, + // SAMPLE_RESPONSE + // ); + // expect(syncs).to.deep.equal([]); + // }); + + // it('should register dsp pixels', () => { + // const syncs = spec.getUserSyncs( + // { pixelEnabled: true }, + // RESPONSE_WITH_DSP_PIXELS + // ); + // expect(syncs).to.have.lengthOf(3); + // expect(syncs[1]).to.deep.equal({ + // type: 'image', + // url: '2nd-pixel', + // }); + // }); + // }); + + describe('interpretResponse', () => { + it('should return data returned by ORTB converter', () => { + const request = spec.buildRequests( + DEFAULT_BID_REQUESTS, + DEFAULT_BIDDER_REQUEST + ); + const bids = spec.interpretResponse(SAMPLE_RESPONSE, request); + expect(bids).to.deep.equal( + converter.fromORTB({ + request: request.data, + response: SAMPLE_RESPONSE.body, + }) + ); + }); + }); + + describe('isBidRequestValid', () => { + it('should return true if params.networkId is set', () => { + const bidRequest = { + params: { + networkId: 123, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true if ortb2.site.publisher.id is set', () => { + const bidRequest = { + ortb2: { + site: { + publisher: { + id: 123, + }, + }, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true if ortb2.app.publisher.id is set', () => { + const bidRequest = { + ortb2: { + app: { + publisher: { + id: 123, + }, + }, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true if ortb2.dooh.publisher.id is set', () => { + const bidRequest = { + ortb2: { + dooh: { + publisher: { + id: 123, + }, + }, + }, + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false if networkId is not set', () => { + const bidRequest = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); +});