diff --git a/integrationExamples/gpt/pubxaiRtdProvider_example.html b/integrationExamples/gpt/pubxaiRtdProvider_example.html
new file mode 100644
index 00000000000..566cfe0c928
--- /dev/null
+++ b/integrationExamples/gpt/pubxaiRtdProvider_example.html
@@ -0,0 +1,113 @@
+
+
+ Individual Ad Unit Refresh Example
+
+
+
+
+
+
+
+
+ Individual Ad Unit Refresh Example
+ Div-1
+
+
+
+
+
+
diff --git a/modules/.submodules.json b/modules/.submodules.json
index d2a13a57330..8f8e9e5dd92 100644
--- a/modules/.submodules.json
+++ b/modules/.submodules.json
@@ -88,6 +88,7 @@
"optimeraRtdProvider",
"oxxionRtdProvider",
"permutiveRtdProvider",
+ "pubxaiRtdProvider",
"qortexRtdProvider",
"reconciliationRtdProvider",
"relevadRtdProvider",
diff --git a/modules/pubxaiRtdProvider.js b/modules/pubxaiRtdProvider.js
new file mode 100644
index 00000000000..c2594df2b1f
--- /dev/null
+++ b/modules/pubxaiRtdProvider.js
@@ -0,0 +1,144 @@
+import { ajax } from '../src/ajax.js';
+import { config } from '../src/config.js';
+import { submodule } from '../src/hook.js';
+import { deepAccess } from '../src/utils.js';
+import { createFloorsDataForAuction } from './priceFloors.js'; // eslint-disable-line prebid/validate-imports
+
+const MODULE_NAME = 'realTimeData';
+const SUBMODULE_NAME = 'pubxai';
+window.__pubxFloorRulesPromise__ = null;
+export const FloorsApiStatus = Object.freeze({
+ IN_PROGRESS: 'IN_PROGRESS',
+ SUCCESS: 'SUCCESS',
+ ERROR: 'ERROR',
+});
+export const FLOORS_EVENT_HANDLE = 'floorsApi';
+export const FLOORS_END_POINT = 'https://floor.pbxai.com/';
+export const FLOOR_PROVIDER = 'PubxFloorProvider';
+
+export const getFloorsConfig = (provider, floorsResponse) => {
+ const floorsConfig = {
+ floors: {
+ enforcement: { floorDeals: true },
+ data: floorsResponse,
+ },
+ };
+ const { floorMin, enforcement } = deepAccess(provider, 'params');
+ if (floorMin) {
+ floorsConfig.floors.floorMin = floorMin;
+ }
+ if (enforcement) {
+ floorsConfig.floors.enforcement = enforcement;
+ }
+ return floorsConfig;
+};
+
+export const setFloorsConfig = (provider, data) => {
+ if (data) {
+ const floorsConfig = getFloorsConfig(provider, data);
+ config.setConfig(floorsConfig);
+ window.__pubxLoaded__ = true;
+ window.__pubxFloorsConfig__ = floorsConfig;
+ } else {
+ config.setConfig({ floors: window.__pubxPrevFloorsConfig__ });
+ window.__pubxLoaded__ = false;
+ window.__pubxFloorsConfig__ = null;
+ }
+};
+
+export const setDefaultPriceFloors = (provider) => {
+ const { data } = deepAccess(provider, 'params');
+ if (data !== undefined) {
+ data.floorProvider = FLOOR_PROVIDER;
+ setFloorsConfig(provider, data);
+ }
+};
+
+export const setPriceFloors = async (provider) => {
+ window.__pubxPrevFloorsConfig__ = config.getConfig('floors');
+ setDefaultPriceFloors(provider);
+ return fetchFloorRules(provider)
+ .then((floorsResponse) => {
+ setFloorsConfig(provider, floorsResponse);
+ setFloorsApiStatus(FloorsApiStatus.SUCCESS);
+ })
+ .catch((_) => {
+ setFloorsApiStatus(FloorsApiStatus.ERROR);
+ });
+};
+
+export const setFloorsApiStatus = (status) => {
+ window.__pubxFloorsApiStatus__ = status;
+ window.dispatchEvent(
+ new CustomEvent(FLOORS_EVENT_HANDLE, { detail: { status } })
+ );
+};
+
+export const getUrl = (provider) => {
+ const { pubxId, endpoint } = deepAccess(provider, 'params');
+ return `${endpoint || FLOORS_END_POINT}?pubxId=${pubxId}&page=${
+ window.location.href
+ }`;
+};
+
+export const fetchFloorRules = async (provider) => {
+ return new Promise((resolve, reject) => {
+ setFloorsApiStatus(FloorsApiStatus.IN_PROGRESS);
+ // When the api call exceeds auctionDelay, the api call doesn't fail but the auction starts.
+ ajax(getUrl(provider), {
+ success: (responseText, response) => {
+ try {
+ if (response && response.response) {
+ // "content-type" = 'text/plain; charset=utf-8'
+ const floorsResponse = JSON.parse(response.response);
+ resolve(floorsResponse);
+ } else {
+ resolve(null);
+ }
+ } catch (error) {
+ reject(error);
+ }
+ },
+ error: (responseText, response) => {
+ reject(response);
+ },
+ });
+ });
+};
+
+const init = (provider) => {
+ window.__pubxFloorRulesPromise__ = setPriceFloors(provider);
+ return true;
+};
+
+const getBidRequestData = (() => {
+ let floorsAttached = false;
+ return (reqBidsConfigObj, onDone) => {
+ if (!floorsAttached) {
+ createFloorsDataForAuction(
+ reqBidsConfigObj.adUnits,
+ reqBidsConfigObj.auctionId
+ );
+ window.__pubxFloorRulesPromise__.then(() => {
+ createFloorsDataForAuction(
+ reqBidsConfigObj.adUnits,
+ reqBidsConfigObj.auctionId
+ );
+ onDone();
+ });
+ floorsAttached = true;
+ }
+ };
+})();
+
+export const pubxaiSubmodule = {
+ name: SUBMODULE_NAME,
+ init,
+ getBidRequestData,
+};
+
+export const beforeInit = () => {
+ submodule(MODULE_NAME, pubxaiSubmodule);
+};
+
+beforeInit();
diff --git a/modules/pubxaiRtdProvider.md b/modules/pubxaiRtdProvider.md
new file mode 100644
index 00000000000..eb5f2ec16e4
--- /dev/null
+++ b/modules/pubxaiRtdProvider.md
@@ -0,0 +1,63 @@
+## Overview
+
+Module Name: pubX.ai RTD Provider
+Module Type: RTD Adapter
+Maintainer: phaneendra@pubx.ai
+
+## Description
+
+RTD module for prebid provided by pubX.ai.
+
+## Usage
+
+Make sure to have the following modules listed while building prebid : `priceFloors`
+For example:
+
+```shell
+gulp build --modules=priceFloors
+```
+
+Inorder to compile the RTD module into your Prebid build:
+
+```shell
+gulp build --modules=rtdModule,pubxaiRtdProvider
+```
+
+Now to use the pubX.ai RTD module, add `realTimeData` with the below mentioned params to the Prebid config.
+
+```javascript
+const AUCTION_DELAY = 100;
+pbjs.setConfig({
+ // rest of the config
+ ...,
+ realTimeData: {
+ auctionDelay: AUCTION_DELAY,
+ dataProviders: {
+ name: "pubxai",
+ waitForIt: true,
+ params: {
+ pubxId: ``,
+ endpoint: ``, // (optional)
+ floorMin: ``, // (optional)
+ enforcement: ``, // (optional)
+ data: `` // (optional)
+ }
+ }
+ }
+ // rest of the config
+ ...,
+});
+```
+
+## Parameters
+
+| Name | Type | Description | Default |
+| :----------------- | :------ | :--------------------------------------------------------------- | :------------------------- |
+| name | String | Real time data module name | Always `pubxai` |
+| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` |
+| params | Object | | |
+| params.pubxId | String | Publisher Id | |
+| params.endpoint | String | URL to get the floor data (optional) | `https://floor.pbxai.com/` |
+| params.floorMin | Number | Mimimum CPM floor (optional) | `None` |
+| params.enforcement | Object | Enforcement behavior within the Price Floors Module (optional) | `None` |
+| params.data | Object | Default Floor data provided by pubx (optional) | `None` |
diff --git a/test/spec/modules/pubxaiRtdProvider_spec.js b/test/spec/modules/pubxaiRtdProvider_spec.js
new file mode 100644
index 00000000000..b645b830246
--- /dev/null
+++ b/test/spec/modules/pubxaiRtdProvider_spec.js
@@ -0,0 +1,397 @@
+import * as priceFloors from '../../../modules/priceFloors';
+import {
+ FLOORS_END_POINT,
+ FLOORS_EVENT_HANDLE,
+ FloorsApiStatus,
+ beforeInit,
+ fetchFloorRules,
+ getFloorsConfig,
+ getUrl,
+ pubxaiSubmodule,
+ setDefaultPriceFloors,
+ setFloorsApiStatus,
+ setFloorsConfig,
+ setPriceFloors,
+} from '../../../modules/pubxaiRtdProvider';
+import { config } from '../../../src/config';
+import * as hook from '../../../src/hook.js';
+import { server } from '../../mocks/xhr.js';
+
+const getConfig = () => ({
+ params: {
+ useRtd: true,
+ endpoint: 'http://pubxai.com:3001/floors',
+ data: {
+ currency: 'EUR',
+ floorProvider: 'PubxFloorProvider',
+ modelVersion: 'gpt-mvm_AB_0.50_dt_0.75_dwt_0.95_dnt_0.25_fm_0.50',
+ schema: { fields: ['gptSlot', 'mediaType'] },
+ values: { '*|banner': 0.02 },
+ },
+ },
+});
+
+const getFloorsResponse = () => ({
+ currency: 'USD',
+ floorProvider: 'PubxFloorProvider',
+ modelVersion: 'gpt-mvm_AB_0.50_dt_0.75_dwt_0.95_dnt_0.25_fm_0.50',
+ schema: { fields: ['gptSlot', 'mediaType'] },
+ values: { '*|banner': 0.02 },
+});
+
+const resetGlobals = () => {
+ window.__pubxLoaded__ = undefined;
+ window.__pubxPrevFloorsConfig__ = undefined;
+ window.__pubxFloorsConfig__ = undefined;
+ window.__pubxFloorsApiStatus__ = undefined;
+ window.__pubxFloorRulesPromise__ = null;
+};
+
+const fakeServer = (
+ fakeResponse = '',
+ providerConfig = undefined,
+ statusCode = 200
+) => {
+ const fakeResponseHeaders = {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Origin': '*',
+ };
+ const request = server.requests[0];
+ request.respond(
+ statusCode,
+ fakeResponseHeaders,
+ fakeResponse ? JSON.stringify(fakeResponse) : ''
+ );
+ return request;
+};
+
+const stubConfig = () => {
+ const stub = sinon.stub(config, 'setConfig');
+ return stub;
+};
+
+describe('pubxaiRtdProvider', () => {
+ describe('beforeInit', () => {
+ it('should register RTD submodule provider', function () {
+ let submoduleStub = sinon.stub(hook, 'submodule');
+ beforeInit();
+ assert(submoduleStub.calledOnceWith('realTimeData', pubxaiSubmodule));
+ submoduleStub.restore();
+ });
+ });
+ describe('submodule', () => {
+ describe('name', function () {
+ it('should be pubxai', function () {
+ expect(pubxaiSubmodule.name).to.equal('pubxai');
+ });
+ });
+ });
+ describe('init', () => {
+ let stub;
+ beforeEach(() => {
+ resetGlobals();
+ stub = stubConfig();
+ });
+ afterEach(() => {
+ stub.restore();
+ });
+ it('standard case - returns true', () => {
+ const initResult = pubxaiSubmodule.init({ params: { useRtd: true } });
+ expect(initResult).to.be.true;
+ });
+ it('setPriceFloors called when `useRtd` is true in the provider config', () => {
+ pubxaiSubmodule.init(getConfig());
+ expect(window.__pubxLoaded__).to.equal(true);
+ });
+ });
+ describe('getBidRequestData', () => {
+ const reqBidsConfigObj = {
+ adUnits: [{ code: 'ad-slot-code-0' }],
+ auctionId: 'auction-id-0',
+ };
+ let stub;
+ beforeEach(() => {
+ window.__pubxFloorRulesPromise__ = Promise.resolve();
+ stub = sinon.stub(priceFloors, 'createFloorsDataForAuction');
+ });
+ afterEach(() => {
+ resetGlobals();
+ stub.restore();
+ });
+ it('createFloorsDataForAuction called once before and once after __pubxFloorRulesPromise__. Also getBidRequestData executed only once', async () => {
+ pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {});
+ assert(priceFloors.createFloorsDataForAuction.calledOnce);
+ await window.__pubxFloorRulesPromise__;
+ assert(priceFloors.createFloorsDataForAuction.calledTwice);
+ assert(
+ priceFloors.createFloorsDataForAuction.alwaysCalledWith(
+ reqBidsConfigObj.adUnits,
+ reqBidsConfigObj.auctionId
+ )
+ );
+ pubxaiSubmodule.getBidRequestData(reqBidsConfigObj, () => {});
+ await window.__pubxFloorRulesPromise__;
+ assert(priceFloors.createFloorsDataForAuction.calledTwice);
+ });
+ });
+ describe('fetchFloorRules', () => {
+ const providerConfig = getConfig();
+ const floorsResponse = getFloorsResponse();
+ it('success with floors response', (done) => {
+ const promise = fetchFloorRules(providerConfig);
+ fakeServer(floorsResponse);
+ promise.then((res) => {
+ expect(res).to.deep.equal(floorsResponse);
+ done();
+ });
+ });
+ it('success with no floors response', (done) => {
+ const promise = fetchFloorRules(providerConfig);
+ fakeServer(undefined);
+ promise.then((res) => {
+ expect(res).to.deep.equal(null);
+ done();
+ });
+ });
+ it('API call error', (done) => {
+ const promise = fetchFloorRules(providerConfig);
+ fakeServer(undefined, undefined, 404);
+ promise
+ .then((res) => {
+ expect(true).to.be.false;
+ })
+ .catch((e) => {
+ expect(e).to.not.be.undefined;
+ })
+ .finally(() => {
+ done();
+ });
+ });
+ it('Wrong API response', (done) => {
+ const promise = fetchFloorRules(providerConfig);
+ fakeServer('floorsResponse');
+ promise
+ .then((res) => {
+ expect(true).to.be.false;
+ })
+ .catch((e) => {
+ expect(e).to.not.be.undefined;
+ })
+ .finally(() => {
+ done();
+ });
+ });
+ });
+ describe('setPriceFloors', () => {
+ const providerConfig = getConfig();
+ const floorsResponse = getFloorsResponse();
+ let stub;
+ beforeEach(() => {
+ resetGlobals();
+ stub = stubConfig();
+ });
+ afterEach(() => {
+ stub.restore();
+ });
+ it('with floors response', (done) => {
+ const floorsPromise = setPriceFloors(providerConfig);
+ fakeServer(floorsResponse);
+ expect(window.__pubxLoaded__).to.be.true;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(
+ getFloorsConfig(providerConfig, providerConfig.params.data)
+ );
+ floorsPromise.then(() => {
+ expect(window.__pubxLoaded__).to.be.true;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(
+ getFloorsConfig(providerConfig, floorsResponse)
+ );
+ done();
+ });
+ });
+ it('without floors response', (done) => {
+ const floorsPromise = setPriceFloors(providerConfig);
+ fakeServer(undefined);
+ expect(window.__pubxLoaded__).to.be.true;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(
+ getFloorsConfig(providerConfig, providerConfig.params.data)
+ );
+ floorsPromise.then(() => {
+ expect(window.__pubxLoaded__).to.be.false;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(null);
+ done();
+ });
+ });
+ it('default floors', (done) => {
+ const floorsPromise = setPriceFloors(providerConfig);
+ fakeServer(undefined, undefined, 404);
+ expect(window.__pubxLoaded__).to.be.true;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(
+ getFloorsConfig(providerConfig, providerConfig.params.data)
+ );
+ floorsPromise
+ .then(() => {
+ expect(true).to.be.false;
+ })
+ .catch((e) => {
+ expect(window.__pubxLoaded__).to.be.true;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(
+ getFloorsConfig(providerConfig, providerConfig.params.data)
+ );
+ })
+ .finally(() => {
+ done();
+ });
+ });
+ });
+ describe('setFloorsConfig', () => {
+ const providerConfig = getConfig();
+ let stub;
+ beforeEach(() => {
+ resetGlobals();
+ stub = stubConfig();
+ });
+ afterEach(function () {
+ stub.restore();
+ });
+ it('non-empty floorResponse', () => {
+ const floorsResponse = getFloorsResponse();
+ setFloorsConfig(providerConfig, floorsResponse);
+ const floorsConfig = getFloorsConfig(providerConfig, floorsResponse);
+ assert(config.setConfig.calledOnceWith(floorsConfig));
+ expect(window.__pubxLoaded__).to.be.true;
+ expect(window.__pubxFloorsConfig__).to.deep.equal(floorsConfig);
+ });
+ it('empty floorResponse', () => {
+ const floorsResponse = null;
+ setFloorsConfig(providerConfig, floorsResponse);
+ assert(config.setConfig.calledOnceWith({ floors: undefined }));
+ expect(window.__pubxLoaded__).to.be.false;
+ expect(window.__pubxFloorsConfig__).to.be.null;
+ });
+ });
+ describe('getFloorsConfig', () => {
+ let providerConfig;
+ const floorsResponse = getFloorsResponse();
+ beforeEach(() => {
+ providerConfig = getConfig();
+ });
+ it('no customizations in the provider config', () => {
+ const result = getFloorsConfig(providerConfig, floorsResponse);
+ expect(result).to.deep.equal({
+ floors: {
+ enforcement: { floorDeals: true },
+ data: floorsResponse,
+ },
+ });
+ });
+ it('only floormin in the provider config', () => {
+ providerConfig.params.floorMin = 2;
+ expect(getFloorsConfig(providerConfig, floorsResponse)).to.deep.equal({
+ floors: {
+ enforcement: { floorDeals: true },
+ floorMin: 2,
+ data: floorsResponse,
+ },
+ });
+ });
+ it('only enforcement in the provider config', () => {
+ providerConfig.params.enforcement = {
+ bidAdjustment: true,
+ enforceJS: false,
+ };
+ expect(getFloorsConfig(providerConfig, floorsResponse)).to.deep.equal({
+ floors: {
+ enforcement: {
+ bidAdjustment: true,
+ enforceJS: false,
+ },
+ data: floorsResponse,
+ },
+ });
+ });
+ it('both floorMin and enforcement in the provider config', () => {
+ providerConfig.params.floorMin = 2;
+ providerConfig.params.enforcement = {
+ bidAdjustment: true,
+ enforceJS: false,
+ };
+ expect(getFloorsConfig(providerConfig, floorsResponse)).to.deep.equal({
+ floors: {
+ enforcement: {
+ bidAdjustment: true,
+ enforceJS: false,
+ },
+ floorMin: 2,
+ data: floorsResponse,
+ },
+ });
+ });
+ });
+ describe('setDefaultPriceFloors', () => {
+ let stub;
+ beforeEach(() => {
+ resetGlobals();
+ stub = stubConfig();
+ });
+ afterEach(function () {
+ stub.restore();
+ });
+ it('should set default floors config', () => {
+ const providerConfig = getConfig();
+ setDefaultPriceFloors(providerConfig);
+ assert(
+ config.setConfig.calledOnceWith(
+ getFloorsConfig(providerConfig, providerConfig.params.data)
+ )
+ );
+ expect(window.__pubxLoaded__).to.be.true;
+ });
+ });
+ describe('setFloorsApiStatus', () => {
+ let stub;
+ beforeEach(() => {
+ resetGlobals();
+ stub = sinon.stub(window, 'dispatchEvent');
+ });
+ afterEach(function () {
+ stub.restore();
+ });
+ it('set status', () => {
+ setFloorsApiStatus(FloorsApiStatus.SUCCESS);
+ expect(window.__pubxFloorsApiStatus__).to.equal(FloorsApiStatus.SUCCESS);
+ });
+ it('dispatch event', () => {
+ setFloorsApiStatus(FloorsApiStatus.SUCCESS);
+ assert(
+ window.dispatchEvent.calledOnceWith(
+ new CustomEvent(FLOORS_EVENT_HANDLE, {
+ detail: { status: FloorsApiStatus.SUCCESS },
+ })
+ )
+ );
+ });
+ });
+ describe('getUrl', () => {
+ const provider = {
+ name: 'pubxai',
+ waitForIt: true,
+ params: {
+ pubxId: '12345',
+ },
+ };
+ it('floors end point', () => {
+ expect(FLOORS_END_POINT).to.equal('https://floor.pbxai.com/');
+ });
+ it('standard case', () => {
+ expect(getUrl(provider)).to.equal(
+ `https://floor.pbxai.com/?pubxId=12345&page=${window.location.href}`
+ );
+ });
+ it('custom url provided', () => {
+ provider.params.endpoint = 'https://custom.floor.com/';
+ expect(getUrl(provider)).to.equal(
+ `https://custom.floor.com/?pubxId=12345&page=${window.location.href}`
+ );
+ });
+ });
+});