diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js
new file mode 100644
index 00000000000..dfc5f514cf3
--- /dev/null
+++ b/modules/eplanningBidAdapter.js
@@ -0,0 +1,156 @@
+import * as utils from 'src/utils';
+import { registerBidder } from 'src/adapters/bidderFactory';
+
+const BIDDER_CODE = 'eplanning';
+const rnd = Math.random();
+const DEFAULT_SV = 'ads.us.e-planning.net';
+const DEFAULT_ISV = 'i.e-planning.net';
+const PARAMS = ['ci', 'sv', 't'];
+const DOLLARS = 'USD';
+const NET_REVENUE = true;
+const TTL = 120;
+const NULL_SIZE = '1x1';
+const FILE = 'file';
+
+export const spec = {
+ code: BIDDER_CODE,
+ isBidRequestValid: function(bid) {
+ return Boolean(bid.params.ci) || Boolean(bid.params.t);
+ },
+ buildRequests: function(bidRequests) {
+ const method = 'GET';
+ const dfpClientId = '1';
+ const sec = 'ROS';
+ let url;
+ let params;
+ const urlConfig = getUrlConfig(bidRequests);
+
+ if (urlConfig.t) {
+ url = urlConfig.isv + '/layers/t_pbjs_2.json';
+ params = {};
+ } else {
+ url = '//' + (urlConfig.sv || DEFAULT_SV) + '/hb/1/' + urlConfig.ci + '/' + dfpClientId + '/' + (utils.getTopWindowLocation().hostname || FILE) + '/' + sec;
+ const referrerUrl = utils.getTopWindowReferrer();
+ const spacesString = getSpacesString(bidRequests);
+ params = {
+ rnd: rnd,
+ e: spacesString,
+ ur: utils.getTopWindowUrl() || FILE,
+ r: 'pbjs',
+ pbv: '$prebid.version$',
+ ncb: '1'
+ };
+ if (referrerUrl) {
+ params.fr = referrerUrl;
+ }
+ }
+
+ return {
+ method: method,
+ url: url,
+ data: params,
+ adUnitToBidId: getBidIdMap(bidRequests),
+ };
+ },
+ interpretResponse: function(serverResponse, request) {
+ const response = serverResponse.body;
+ let bidResponses = [];
+
+ if (response && !utils.isEmpty(response.sp)) {
+ response.sp.forEach(space => {
+ if (!utils.isEmpty(space.a)) {
+ space.a.forEach(ad => {
+ const bidResponse = {
+ requestId: request.adUnitToBidId[space.k],
+ cpm: ad.pr,
+ width: ad.w,
+ height: ad.h,
+ ad: ad.adm,
+ ttl: TTL,
+ creativeId: ad.crid,
+ netRevenue: NET_REVENUE,
+ currency: DOLLARS,
+ };
+ bidResponses.push(bidResponse);
+ });
+ }
+ });
+ }
+
+ return bidResponses;
+ },
+ getUserSyncs: function(syncOptions, serverResponses) {
+ const syncs = [];
+ const response = !utils.isEmpty(serverResponses) && serverResponses[0].body;
+
+ if (response && !utils.isEmpty(response.cs)) {
+ const responseSyncs = response.cs;
+ responseSyncs.forEach(sync => {
+ if (typeof sync === 'string' && syncOptions.pixelEnabled) {
+ syncs.push({
+ type: 'image',
+ url: sync,
+ });
+ } else if (typeof sync === 'object' && sync.ifr && syncOptions.iframeEnabled) {
+ syncs.push({
+ type: 'iframe',
+ url: sync.u,
+ })
+ }
+ });
+ }
+
+ return syncs;
+ },
+}
+
+function cleanName(name) {
+ return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)/g, '_').replace(/^_+|_+$/g, '');
+}
+function getUrlConfig(bidRequests) {
+ if (isTestRequest(bidRequests)) {
+ return getTestConfig(bidRequests.filter(br => br.params.t));
+ }
+
+ let config = {};
+ bidRequests.forEach(bid => {
+ PARAMS.forEach(param => {
+ if (bid.params[param] && !config[param]) {
+ config[param] = bid.params[param];
+ }
+ });
+ });
+
+ if (config.sv) {
+ config.sv = '//' + config.sv;
+ }
+
+ return config;
+}
+function isTestRequest(bidRequests) {
+ let isTest = false;
+ bidRequests.forEach(bid => isTest = bid.params.t);
+ return isTest;
+}
+function getTestConfig(bidRequests) {
+ let isv;
+ bidRequests.forEach(br => isv = isv || br.params.isv);
+ return {
+ t: true,
+ isv: '//' + (isv || DEFAULT_ISV)
+ };
+}
+function getSpacesString(bids) {
+ const spacesString = bids.map(bid =>
+ cleanName(bid.adUnitCode) + ':' + (bid.sizes && bid.sizes.length ? utils.parseSizesInput(bid.sizes).join(',') : NULL_SIZE)
+ ).join('+');
+
+ return spacesString;
+}
+function getBidIdMap(bidRequests) {
+ let map = {};
+ bidRequests.forEach(bid => map[cleanName(bid.adUnitCode)] = bid.bidId);
+ return map;
+}
+
+registerBidder(spec);
diff --git a/modules/eplanningBidAdapter.md b/modules/eplanningBidAdapter.md
new file mode 100644
index 00000000000..b6cfbb535b6
--- /dev/null
+++ b/modules/eplanningBidAdapter.md
@@ -0,0 +1,25 @@
+# Overview
+
+```
+Module Name: E-Planning Bid Adapter
+Module Type: Bidder Adapter
+Maintainer: ainsua@e-planning.net
+```
+
+# Description
+
+Connects to E-Planning exchange for bids.
+
+# Test Parameters
+```
+var adUnits = [{
+ code: 'div-gpt-ad-1460505748561-0',
+ sizes: [[300, 250]],
+ bids: [{
+ bidder: 'eplanning',
+ params: {
+ t: 1
+ }
+ }]
+}];
+```
diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js
new file mode 100644
index 00000000000..2ec7f482edd
--- /dev/null
+++ b/test/spec/modules/eplanningBidAdapter_spec.js
@@ -0,0 +1,350 @@
+import { expect } from 'chai';
+import { spec } from 'modules/eplanningBidAdapter';
+import { newBidder } from 'src/adapters/bidderFactory';
+import * as utils from 'src/utils';
+
+describe('E-Planning Adapter', () => {
+ const adapter = newBidder('spec');
+ const CI = '12345';
+ const ADUNIT_CODE = 'adunit-code';
+ const ADUNIT_CODE2 = 'adunit-code-dos';
+ const CLEAN_ADUNIT_CODE2 = 'adunitcodedos';
+ const CLEAN_ADUNIT_CODE = 'adunitcode';
+ const BID_ID = '123456789';
+ const BID_ID2 = '987654321';
+ const CPM = 1.3;
+ const W = '300';
+ const H = '250';
+ const ADM = '
This is an ad
';
+ const I_ID = '7854abc56248f873';
+ const CRID = '1234567890';
+ const TEST_ISV = 'leles.e-planning.net';
+ const validBid = {
+ 'bidder': 'eplanning',
+ 'bidId': BID_ID,
+ 'params': {
+ 'ci': CI,
+ },
+ 'adUnitCode': ADUNIT_CODE,
+ 'sizes': [[300, 250], [300, 600]],
+ };
+ const validBid2 = {
+ 'bidder': 'eplanning',
+ 'bidId': BID_ID2,
+ 'params': {
+ 'ci': CI,
+ },
+ 'adUnitCode': ADUNIT_CODE2,
+ 'sizes': [[300, 250], [300, 600]],
+ };
+ const testBid = {
+ 'bidder': 'eplanning',
+ 'params': {
+ 't': 1,
+ 'isv': TEST_ISV
+ },
+ 'adUnitCode': ADUNIT_CODE,
+ 'sizes': [[300, 250], [300, 600]],
+ };
+ const invalidBid = {
+ 'bidder': 'eplanning',
+ 'params': {
+ },
+ 'adUnitCode': 'adunit-code',
+ 'sizes': [[300, 250], [300, 600]],
+ };
+ const response = {
+ body: {
+ 'sI': {
+ 'k': '12345'
+ },
+ 'sec': {
+ 'k': 'ROS'
+ },
+ 'sp': [{
+ 'k': CLEAN_ADUNIT_CODE,
+ 'a': [{
+ 'adm': ADM,
+ 'id': '7854abc56248f874',
+ 'i': I_ID,
+ 'fi': '7854abc56248f872',
+ 'ip': '45621afd87462104',
+ 'w': W,
+ 'h': H,
+ 'crid': CRID,
+ 'pr': CPM
+ }],
+ }],
+ 'cs': [
+ 'http://a-sync-url.com/',
+ {
+ 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east',
+ 'ifr': true
+ }
+ ]
+ }
+ };
+ const responseWithTwoAdunits = {
+ body: {
+ 'sI': {
+ 'k': '12345'
+ },
+ 'sec': {
+ 'k': 'ROS'
+ },
+ 'sp': [{
+ 'k': CLEAN_ADUNIT_CODE,
+ 'a': [{
+ 'adm': ADM,
+ 'id': '7854abc56248f874',
+ 'i': I_ID,
+ 'fi': '7854abc56248f872',
+ 'ip': '45621afd87462104',
+ 'w': W,
+ 'h': H,
+ 'crid': CRID,
+ 'pr': CPM
+ }]
+ }, {
+ 'k': CLEAN_ADUNIT_CODE2,
+ 'a': [{
+ 'adm': ADM,
+ 'id': '7854abc56248f874',
+ 'i': I_ID,
+ 'fi': '7854abc56248f872',
+ 'ip': '45621afd87462104',
+ 'w': W,
+ 'h': H,
+ 'crid': CRID,
+ 'pr': CPM
+ }],
+ },
+ ],
+ 'cs': [
+ 'http://a-sync-url.com/',
+ {
+ 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east',
+ 'ifr': true
+ }
+ ]
+ }
+ };
+ const responseWithNoAd = {
+ body: {
+ 'sI': {
+ 'k': '12345'
+ },
+ 'sec': {
+ 'k': 'ROS'
+ },
+ 'sp': [{
+ 'k': 'spname',
+ }],
+ 'cs': [
+ 'http://a-sync-url.com/',
+ {
+ 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east',
+ 'ifr': true
+ }
+ ]
+ }
+ };
+ const responseWithNoSpace = {
+ body: {
+ 'sI': {
+ 'k': '12345'
+ },
+ 'sec': {
+ 'k': 'ROS'
+ },
+ 'cs': [
+ 'http://a-sync-url.com/',
+ {
+ 'u': 'http://another-sync-url.com/test.php?&partner=123456&endpoint=us-east',
+ 'ifr': true
+ }
+ ]
+ }
+ };
+
+ describe('inherited functions', () => {
+ it('exists and is a function', () => {
+ expect(adapter.callBids).to.exist.and.to.be.a('function');
+ });
+ });
+
+ describe('isBidRequestValid', () => {
+ it('should return true when bid has ci parameter', () => {
+ expect(spec.isBidRequestValid(validBid)).to.equal(true);
+ });
+
+ it('should return false when bid does not have ci parameter and is not a test bid', () => {
+ expect(spec.isBidRequestValid(invalidBid)).to.equal(false);
+ });
+
+ it('should return true when bid does not have ci parameter but is a test bid'), () => {
+ expect(spec.isBidRequestValid(testBid).to.equal(true));
+ }
+ });
+
+ describe('buildRequests', () => {
+ let bidRequests = [validBid];
+
+ it('should create the url correctly', () => {
+ const url = spec.buildRequests(bidRequests).url;
+ expect(url).to.equal('//ads.us.e-planning.net/hb/1/' + CI + '/1/localhost/ROS');
+ });
+
+ it('should return GET method', () => {
+ const method = spec.buildRequests(bidRequests).method;
+ expect(method).to.equal('GET');
+ });
+
+ it('should return r parameter with value pbjs', () => {
+ const r = spec.buildRequests(bidRequests).data.r;
+ expect(r).to.equal('pbjs');
+ });
+
+ it('should return pbv parameter with value prebid version', () => {
+ const pbv = spec.buildRequests(bidRequests).data.pbv;
+ expect(pbv).to.equal('$prebid.version$');
+ });
+
+ it('should return e parameter with value according to the adunit sizes', () => {
+ const e = spec.buildRequests(bidRequests).data.e;
+ expect(e).to.equal(CLEAN_ADUNIT_CODE + ':300x250,300x600');
+ });
+
+ it('should return correct e parameter with more than one adunit', () => {
+ const NEW_CODE = ADUNIT_CODE + '2';
+ const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2';
+ const anotherBid = {
+ 'bidder': 'eplanning',
+ 'params': {
+ 'ci': CI,
+ },
+ 'adUnitCode': NEW_CODE,
+ 'sizes': [[100, 100]],
+ };
+ bidRequests.push(anotherBid);
+
+ const e = spec.buildRequests(bidRequests).data.e;
+ expect(e).to.equal(CLEAN_ADUNIT_CODE + ':300x250,300x600+' + CLEAN_NEW_CODE + ':100x100');
+ });
+
+ it('should return correct e parameter when the adunit has no size', () => {
+ const noSizeBid = {
+ 'bidder': 'eplanning',
+ 'params': {
+ 'ci': CI,
+ },
+ 'adUnitCode': ADUNIT_CODE,
+ };
+
+ const e = spec.buildRequests([noSizeBid]).data.e;
+ expect(e).to.equal(CLEAN_ADUNIT_CODE + ':1x1');
+ });
+
+ it('should return ur parameter with current window url', () => {
+ const ur = spec.buildRequests(bidRequests).data.ur;
+ expect(ur).to.equal(utils.getTopWindowUrl());
+ });
+
+ it('should return fr parameter when there is a referrer', () => {
+ const referrer = 'thisisafakereferrer';
+ const stubGetReferrer = sinon.stub(utils, 'getTopWindowReferrer').returns(referrer);
+ after(() => stubGetReferrer.restore());
+
+ const fr = spec.buildRequests(bidRequests).data.fr;
+ expect(fr).to.equal(referrer);
+ });
+
+ it('should return the testing url when the request has the t parameter', () => {
+ const url = spec.buildRequests([testBid]).url;
+ const expectedUrl = '//' + TEST_ISV + '/layers/t_pbjs_2.json';
+ expect(url).to.equal(expectedUrl);
+ });
+
+ it('should return the parameter ncb with value 1', () => {
+ const ncb = spec.buildRequests(bidRequests).data.ncb;
+ expect(ncb).to.equal('1');
+ });
+ });
+
+ describe('interpretResponse', () => {
+ it('should return an empty array when there is no ads in the response', () => {
+ const bidResponses = spec.interpretResponse(responseWithNoAd);
+ expect(bidResponses).to.be.empty;
+ });
+
+ it('should return an empty array when there is no spaces in the response', () => {
+ const bidResponses = spec.interpretResponse(responseWithNoSpace);
+ expect(bidResponses).to.be.empty;
+ });
+
+ it('should correctly map the parameters in the response', () => {
+ const bidResponse = spec.interpretResponse(response, { adUnitToBidId: { [CLEAN_ADUNIT_CODE]: BID_ID } })[0];
+ const expectedResponse = {
+ requestId: BID_ID,
+ cpm: CPM,
+ width: W,
+ height: H,
+ ad: ADM,
+ ttl: 120,
+ creativeId: CRID,
+ netRevenue: true,
+ currency: 'USD',
+ };
+ expect(bidResponse).to.deep.equal(expectedResponse);
+ });
+ });
+
+ describe('getUserSyncs', () => {
+ const sOptionsAllEnabled = {
+ pixelEnabled: true,
+ iframeEnabled: true
+ };
+ const sOptionsAllDisabled = {
+ pixelEnabled: false,
+ iframeEnabled: false
+ };
+ const sOptionsOnlyPixel = {
+ pixelEnabled: true,
+ iframeEnabled: false
+ };
+ const sOptionsOnlyIframe = {
+ pixelEnabled: false,
+ iframeEnabled: true
+ };
+
+ it('should return an empty array if the response has no syncs', () => {
+ const noSyncsResponse = { cs: [] };
+ const syncs = spec.getUserSyncs(sOptionsAllEnabled, [noSyncsResponse]);
+ expect(syncs).to.be.empty;
+ });
+
+ it('should return an empty array if there is no sync options enabled', () => {
+ const syncs = spec.getUserSyncs(sOptionsAllDisabled, [response]);
+ expect(syncs).to.be.empty;
+ });
+
+ it('should only return pixels if iframe is not enabled', () => {
+ const syncs = spec.getUserSyncs(sOptionsOnlyPixel, [response]);
+ syncs.forEach(sync => expect(sync.type).to.equal('image'));
+ });
+
+ it('should only return iframes if pixel is not enabled', () => {
+ const syncs = spec.getUserSyncs(sOptionsOnlyIframe, [response]);
+ syncs.forEach(sync => expect(sync.type).to.equal('iframe'));
+ });
+ });
+
+ describe('adUnits mapping to bidId', () => {
+ it('should correctly map the bidId to the adunit', () => {
+ const requests = spec.buildRequests([validBid, validBid2]);
+ const responses = spec.interpretResponse(responseWithTwoAdunits, requests);
+ expect(responses[0].requestId).to.equal(BID_ID);
+ expect(responses[1].requestId).to.equal(BID_ID2);
+ });
+ });
+});