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);
+ });
+ });
+});