From d9fefcd444cbe982920cbe5ff4c9d4abf5b9791a Mon Sep 17 00:00:00 2001 From: floxis-tech Date: Thu, 25 Sep 2025 15:41:03 +0300 Subject: [PATCH 1/7] Floxis Bid Adapter : initial release --- modules/FloxisBidAdapter.md | 57 +++++++++++ modules/floxisBidAdapter.js | 112 +++++++++++++++++++++ test/spec/modules/floxisBidAdapter_spec.js | 99 ++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 modules/FloxisBidAdapter.md create mode 100644 modules/floxisBidAdapter.js create mode 100644 test/spec/modules/floxisBidAdapter_spec.js diff --git a/modules/FloxisBidAdapter.md b/modules/FloxisBidAdapter.md new file mode 100644 index 00000000000..76c7d98ea53 --- /dev/null +++ b/modules/FloxisBidAdapter.md @@ -0,0 +1,57 @@ +# Floxis Bid Adapter + +## Overview +The Floxis Bid Adapter enables integration with the Floxis programmatic advertising platform via Prebid.js. It supports banner, video, and native formats, and is designed for multi-partner, multi-region use. + +**Key Features:** +- Banner, Video and Native ad support +- OpenRTB 2.x compliant +- Privacy regulation compliance + +## Endpoint +``` +https://-.floxis.tech/pbjs +``` +- `partner`: string, required (default: 'floxis') +- `region`: string, required (default: 'us') + +## Required Params +- `partner` (string): Partner name +- `placementId` (integer): Placement identifier + +## Privacy +Privacy fields (GDPR, USP, GPP, COPPA) are handled by Prebid.js core and automatically included in the OpenRTB request. + +## Supported Media Types +- Banner +- Video +- Native + +## Example Usage +```javascript +pbjs.addAdUnits([ + { + code: 'adunit-1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bids: [{ + bidder: 'floxis', + params: { + partner: 'floxis', + placementId: 1 + } + }] + } +]); +``` + + +# Configuration +## Required Parameters + +| Name | Scope | Description | Example | Type | +| --- | --- | --- | --- | --- | +| `partner` | required | Partner identifier provided by Floxis | `floxis` | `string` | +| `placementId` | required | Placement identifier provided by Floxis | `1` | `int` | + +## Testing +Unit tests are provided in `test/spec/modules/floxisBidAdapter_spec.js` and cover validation, request building, and response interpretation. diff --git a/modules/floxisBidAdapter.js b/modules/floxisBidAdapter.js new file mode 100644 index 00000000000..b676208b54e --- /dev/null +++ b/modules/floxisBidAdapter.js @@ -0,0 +1,112 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const BIDDER_CODE = 'floxis'; + +const DEFAULT_REGION = 'us'; +const DEFAULT_PARTNER = 'floxis'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +function getFloxisUrl(partner, region = DEFAULT_REGION) { + return `https://${partner}-${region}.floxis.tech/pbjs`; +} + +function buildRequests(validBidRequests = [], bidderRequest = {}) { + if (!validBidRequests || !validBidRequests.length) { + return []; + } + const firstBid = validBidRequests[0]; + const partner = firstBid?.params?.partner || DEFAULT_PARTNER; + const region = firstBid?.params?.region || DEFAULT_REGION; + const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + req.at = 1; + req.ext = req.ext || {}; + req.ext.name = 'prebidjs'; + req.ext.version = '$prebid.version$'; + req.site = req.site || {}; + req.site.ext = req.site.ext || {}; + req.site.ext.placementId = validBidRequests[0]?.params?.placementId; + return req; + } + }); + + const ortbRequest = converter.toORTB({bidRequests: validBidRequests, bidderRequest}); + return [{ + method: 'POST', + url: getFloxisUrl(partner, region), + data: ortbRequest, + converter, // store converter instance for later use + options: { + withCredentials: true, + contentType: 'application/json;charset=UTF-8', + } + }]; +} + +// User sync not supported initially +function getUserSyncs() { + return []; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid: function(bid) { + const params = bid.params || {}; + if (typeof params.partner !== 'string' || !params.partner.length || !Number.isInteger(params.placementId)) { + return false; + } + // Must have at least one media type + if (!bid.mediaTypes || (!bid.mediaTypes.banner && !bid.mediaTypes.video && !bid.mediaTypes.native)) { + return false; + } + // Banner size validation + if (bid.mediaTypes.banner && Array.isArray(bid.mediaTypes.banner.sizes)) { + if (!bid.mediaTypes.banner.sizes.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { + return false; + } + } + // Video size validation + if (bid.mediaTypes.video && Array.isArray(bid.mediaTypes.video.playerSize)) { + if (!bid.mediaTypes.video.playerSize.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { + return false; + } + } + return true; + }, + buildRequests, + interpretResponse(response, request) { + // Use the same converter instance and request object as in buildRequests + if (!request.converter) { + throw new Error('Missing converter instance on request object'); + } + return request.converter.fromORTB({request: request.data, response: response.body}).bids; + }, + getUserSyncs, + onBidWon: function (bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl); + } + if (bid.nurl) { + utils.triggerPixel(bid.nurl); + } + }, +}; + +registerBidder(spec); diff --git a/test/spec/modules/floxisBidAdapter_spec.js b/test/spec/modules/floxisBidAdapter_spec.js new file mode 100644 index 00000000000..2298e8aff7f --- /dev/null +++ b/test/spec/modules/floxisBidAdapter_spec.js @@ -0,0 +1,99 @@ +import { expect } from 'chai'; +import { spec } from 'modules/floxisBidAdapter.js'; +import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes.js'; + +const BIDDER_CODE = 'floxis'; + +describe('floxisBidAdapter', function () { + const validBannerBid = { + bidId: '1', + adUnitCode: 'adunit-1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + params: { partner: 'floxis', placementId: 123 }, + ortb2Imp: { secure: 1 } + }; + const validVideoBid = { + bidId: '2', + adUnitCode: 'adunit-2', + mediaTypes: { video: { playerSize: [[640, 480]] } }, + params: { partner: 'floxis', placementId: 456 }, + ortb2Imp: { secure: 1 } + }; + const invalidBid = { + bidId: '3', + adUnitCode: 'adunit-3', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + params: { partner: '', placementId: 'notanint' } + }; + + it('should validate correct banner bid', function () { + expect(spec.isBidRequestValid(validBannerBid)).to.be.true; + }); + + it('should validate correct video bid', function () { + expect(spec.isBidRequestValid(validVideoBid)).to.be.true; + }); + + it('should invalidate incorrect bid', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('should build request with correct url and payload', function () { + const requests = spec.buildRequests([validBannerBid], {}); + expect(requests).to.have.lengthOf(1); + expect(requests[0].url).to.include('floxis-us.floxis.tech/pbjs'); + expect(requests[0].data).to.be.an('object'); + expect(requests[0].data.imp).to.be.an('array'); + expect(requests[0].data.site.ext.placementId).to.equal(123); + }); + + it('should handle empty bid requests', function () { + const requests = spec.buildRequests([], {}); + expect(requests).to.be.an('array').that.is.empty; + }); + + it('should invalidate bid with no media types', function () { + const noMediaTypeBid = { + bidId: '4', + adUnitCode: 'adunit-4', + params: { partner: 'floxis', placementId: 123 } + }; + expect(spec.isBidRequestValid(noMediaTypeBid)).to.be.false; + }); + + it('should validate supported media types', function () { + expect(spec.supportedMediaTypes).to.include(BANNER); + expect(spec.supportedMediaTypes).to.include(VIDEO); + expect(spec.supportedMediaTypes).to.include(NATIVE); + }); + + it('should return empty user syncs', function () { + expect(spec.getUserSyncs()).to.be.an('array').that.is.empty; + }); + + it('should interpret response correctly', function () { + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: '1', + price: 1.23, + w: 300, + h: 250, + crid: 'creative-1', + adm: '
ad
', + cur: 'USD' + }] + }] + } + }; + const requests = spec.buildRequests([validBannerBid], {}); + const bids = spec.interpretResponse(serverResponse, requests[0]); + expect(bids).to.be.an('array'); + if (bids.length > 0) { + expect(bids[0]).to.have.property('cpm'); + expect(bids[0]).to.have.property('requestId'); + expect(bids[0].cpm).to.equal(1.23); + } + }); +}); From a59906d48b5be89c0afb8e17f70ed08bc14a19eb Mon Sep 17 00:00:00 2001 From: floxis-tech Date: Thu, 25 Sep 2025 20:30:20 +0300 Subject: [PATCH 2/7] Added ORTB parameters for blocking --- modules/FloxisBidAdapter.md | 56 ++++++++++++++++++++++ modules/floxisBidAdapter.js | 39 +++++++++++++-- test/spec/modules/floxisBidAdapter_spec.js | 43 +++++++++++++++++ 3 files changed, 135 insertions(+), 3 deletions(-) diff --git a/modules/FloxisBidAdapter.md b/modules/FloxisBidAdapter.md index 76c7d98ea53..686511237bd 100644 --- a/modules/FloxisBidAdapter.md +++ b/modules/FloxisBidAdapter.md @@ -15,10 +15,66 @@ https://-.floxis.tech/pbjs - `partner`: string, required (default: 'floxis') - `region`: string, required (default: 'us') + + ## Required Params - `partner` (string): Partner name - `placementId` (integer): Placement identifier +## OpenRTB Blocking Params Support +FloxisBidAdapter supports OpenRTB blocking parameters. You can pass the following optional params in your ad unit config: +- `bcat` (array): Blocked categories +- `badv` (array): Blocked advertiser domains +- `bapp` (array): Blocked app bundle IDs +- `battr` (array): Blocked creative attributes + +These will be included in the OpenRTB request and imp objects as appropriate. + +**Example:** +```javascript +pbjs.addAdUnits([ + { + code: 'adunit-20', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bids: [{ + bidder: 'floxis', + params: { + partner: 'floxis', + placementId: 555, + bcat: ['IAB1-1', 'IAB1-2'], + badv: ['example.com', 'test.com'], + bapp: ['com.example.app'], + battr: [1, 2, 3] + } + }] + } +]); +``` + +## Floors Module Support +FloxisBidAdapter supports Prebid.js Floors Module. If a bid request provides a floor value via the Floors Module (`getFloor` function), it will be sent in the OpenRTB request as `imp.bidfloor` and `imp.bidfloorcur`. If not, you can also set a static floor using `params.bidFloor`. + +**Example with Floors Module:** +```javascript +pbjs.addAdUnits([ + { + code: 'adunit-1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bids: [{ + bidder: 'floxis', + params: { + partner: 'floxis', + placementId: 1, + bidFloor: 2.5 // optional static floor + }, + getFloor: function({currency, mediaType, size}) { + return { floor: 2.5, currency: 'USD' }; + } + }] + } +]); +``` + ## Privacy Privacy fields (GDPR, USP, GPP, COPPA) are handled by Prebid.js core and automatically included in the OpenRTB request. diff --git a/modules/floxisBidAdapter.js b/modules/floxisBidAdapter.js index b676208b54e..be1742c2560 100644 --- a/modules/floxisBidAdapter.js +++ b/modules/floxisBidAdapter.js @@ -31,6 +31,28 @@ function buildRequests(validBidRequests = [], bidderRequest = {}) { imp(buildImp, bidRequest, context) { let imp = buildImp(bidRequest, context); imp.secure = bidRequest.ortb2Imp?.secure ?? 1; + // Floors Module support + let floorInfo; + if (typeof bidRequest.getFloor === 'function') { + try { + floorInfo = bidRequest.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*' + }); + } catch (e) { } + } + const floor = floorInfo && typeof floorInfo.floor === 'number' ? floorInfo.floor : bidRequest.params?.bidFloor; + const floorCur = floorInfo && typeof floorInfo.currency === 'string' ? floorInfo.currency : DEFAULT_CURRENCY; + if (typeof floor === 'number' && !isNaN(floor)) { + imp.bidfloor = floor; + imp.bidfloorcur = floorCur; + } + // ORTB blocking params (imp-level) + if (Array.isArray(bidRequest.params?.battr)) { + imp.banner = imp.banner || {}; + imp.banner.battr = bidRequest.params.battr; + } return imp; }, request(buildRequest, imps, bidderRequest, context) { @@ -42,11 +64,22 @@ function buildRequests(validBidRequests = [], bidderRequest = {}) { req.site = req.site || {}; req.site.ext = req.site.ext || {}; req.site.ext.placementId = validBidRequests[0]?.params?.placementId; + // ORTB blocking params (request-level) + const firstParams = validBidRequests[0]?.params || {}; + if (Array.isArray(firstParams.bcat)) { + req.bcat = firstParams.bcat; + } + if (Array.isArray(firstParams.badv)) { + req.badv = firstParams.badv; + } + if (Array.isArray(firstParams.bapp)) { + req.bapp = firstParams.bapp; + } return req; } }); - const ortbRequest = converter.toORTB({bidRequests: validBidRequests, bidderRequest}); + const ortbRequest = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); return [{ method: 'POST', url: getFloxisUrl(partner, region), @@ -67,7 +100,7 @@ function getUserSyncs() { export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: function(bid) { + isBidRequestValid: function (bid) { const params = bid.params || {}; if (typeof params.partner !== 'string' || !params.partner.length || !Number.isInteger(params.placementId)) { return false; @@ -96,7 +129,7 @@ export const spec = { if (!request.converter) { throw new Error('Missing converter instance on request object'); } - return request.converter.fromORTB({request: request.data, response: response.body}).bids; + return request.converter.fromORTB({ request: request.data, response: response.body }).bids; }, getUserSyncs, onBidWon: function (bid) { diff --git a/test/spec/modules/floxisBidAdapter_spec.js b/test/spec/modules/floxisBidAdapter_spec.js index 2298e8aff7f..c12f88d6622 100644 --- a/test/spec/modules/floxisBidAdapter_spec.js +++ b/test/spec/modules/floxisBidAdapter_spec.js @@ -96,4 +96,47 @@ describe('floxisBidAdapter', function () { expect(bids[0].cpm).to.equal(1.23); } }); + + it('should set bidfloor and bidfloorcur from Floors Module', function () { + const floorValue = 2.5; + const floorCurrency = 'USD'; + const bidWithFloor = { + bidId: '10', + adUnitCode: 'adunit-10', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + params: { partner: 'floxis', placementId: 999 }, + getFloor: function({currency, mediaType, size}) { + return { floor: floorValue, currency: floorCurrency }; + } + }; + const requests = spec.buildRequests([bidWithFloor], {}); + expect(requests).to.have.lengthOf(1); + const imp = requests[0].data.imp[0]; + expect(imp.bidfloor).to.equal(floorValue); + expect(imp.bidfloorcur).to.equal(floorCurrency); + }); + + it('should set ORTB blocking params in request and imp', function () { + const bidWithBlocking = { + bidId: '20', + adUnitCode: 'adunit-20', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + params: { + partner: 'floxis', + placementId: 555, + bcat: ['IAB1-1', 'IAB1-2'], + badv: ['example.com', 'test.com'], + bapp: ['com.example.app'], + battr: [1, 2, 3] + } + }; + const requests = spec.buildRequests([bidWithBlocking], {}); + expect(requests).to.have.lengthOf(1); + const req = requests[0].data; + expect(req.bcat).to.deep.equal(['IAB1-1', 'IAB1-2']); + expect(req.badv).to.deep.equal(['example.com', 'test.com']); + expect(req.bapp).to.deep.equal(['com.example.app']); + const imp = req.imp[0]; + expect(imp.banner.battr).to.deep.equal([1, 2, 3]); + }); }); From 874eba536afee8766715a9b0f9f23023e9cd2e29 Mon Sep 17 00:00:00 2001 From: floxis-tech Date: Thu, 25 Sep 2025 20:45:38 +0300 Subject: [PATCH 3/7] Adjusted documentation with maintainer info --- modules/FloxisBidAdapter.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/modules/FloxisBidAdapter.md b/modules/FloxisBidAdapter.md index 686511237bd..30a7a1060a6 100644 --- a/modules/FloxisBidAdapter.md +++ b/modules/FloxisBidAdapter.md @@ -1,22 +1,19 @@ -# Floxis Bid Adapter +# Overview -## Overview -The Floxis Bid Adapter enables integration with the Floxis programmatic advertising platform via Prebid.js. It supports banner, video, and native formats, and is designed for multi-partner, multi-region use. +``` +Module Name: Floxis Bidder Adapter +Module Type: Floxis Bidder Adapter +Maintainer: admin@floxis.tech +``` +# Description + +The Floxis Bid Adapter enables integration with the Floxis programmatic advertising platform via Prebid.js. It supports banner, video (instream and outstream), and native formats, and is designed for multi-partner, multi-region use. **Key Features:** - Banner, Video and Native ad support - OpenRTB 2.x compliant - Privacy regulation compliance -## Endpoint -``` -https://-.floxis.tech/pbjs -``` -- `partner`: string, required (default: 'floxis') -- `region`: string, required (default: 'us') - - - ## Required Params - `partner` (string): Partner name - `placementId` (integer): Placement identifier From 218a6963512f38c79b8f14037187b6014355d98a Mon Sep 17 00:00:00 2001 From: floxis-tech Date: Fri, 26 Sep 2025 09:37:07 +0300 Subject: [PATCH 4/7] Added more validations, extracted converter to a const --- modules/floxisBidAdapter.js | 159 ++++++++++++--------- test/spec/modules/floxisBidAdapter_spec.js | 52 ++++++- 2 files changed, 139 insertions(+), 72 deletions(-) diff --git a/modules/floxisBidAdapter.js b/modules/floxisBidAdapter.js index be1742c2560..6251bb68630 100644 --- a/modules/floxisBidAdapter.js +++ b/modules/floxisBidAdapter.js @@ -11,9 +11,66 @@ const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; -function getFloxisUrl(partner, region = DEFAULT_REGION) { - return `https://${partner}-${region}.floxis.tech/pbjs`; -} +const CONVERTER = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + imp.secure = bidRequest.ortb2Imp?.secure ?? 1; + // Floors Module support + let floorInfo; + if (typeof bidRequest.getFloor === 'function') { + try { + floorInfo = bidRequest.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*' + }); + } catch (e) { } + } + const floor = floorInfo && typeof floorInfo.floor === 'number' ? floorInfo.floor : bidRequest.params?.bidFloor; + const floorCur = floorInfo && typeof floorInfo.currency === 'string' ? floorInfo.currency : DEFAULT_CURRENCY; + if (typeof floor === 'number' && !isNaN(floor)) { + imp.bidfloor = floor; + imp.bidfloorcur = floorCur; + } + // ORTB blocking params (imp-level) + if (Array.isArray(bidRequest.params?.battr)) { + imp.banner = imp.banner || {}; + imp.banner.battr = bidRequest.params.battr; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + req.at = 1; + req.ext = req.ext || {}; + req.ext.name = 'prebidjs'; + req.ext.version = '$prebid.version$'; + req.site = req.site || {}; + req.site.ext = req.site.ext || {}; + // Set placementId from first bid + const firstBid = context.bidRequests[0]; + if (firstBid?.params?.placementId) { + req.site.ext.placementId = firstBid.params.placementId; + } + // ORTB blocking params (request-level) + const firstParams = firstBid?.params || {}; + if (Array.isArray(firstParams.bcat)) { + req.bcat = firstParams.bcat; + } + if (Array.isArray(firstParams.badv)) { + req.badv = firstParams.badv; + } + if (Array.isArray(firstParams.bapp)) { + req.bapp = firstParams.bapp; + } + return req; + } +}); function buildRequests(validBidRequests = [], bidderRequest = {}) { if (!validBidRequests || !validBidRequests.length) { @@ -22,69 +79,12 @@ function buildRequests(validBidRequests = [], bidderRequest = {}) { const firstBid = validBidRequests[0]; const partner = firstBid?.params?.partner || DEFAULT_PARTNER; const region = firstBid?.params?.region || DEFAULT_REGION; - const converter = ortbConverter({ - context: { - netRevenue: DEFAULT_NET_REVENUE, - ttl: DEFAULT_BID_TTL, - currency: DEFAULT_CURRENCY - }, - imp(buildImp, bidRequest, context) { - let imp = buildImp(bidRequest, context); - imp.secure = bidRequest.ortb2Imp?.secure ?? 1; - // Floors Module support - let floorInfo; - if (typeof bidRequest.getFloor === 'function') { - try { - floorInfo = bidRequest.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: '*', - size: '*' - }); - } catch (e) { } - } - const floor = floorInfo && typeof floorInfo.floor === 'number' ? floorInfo.floor : bidRequest.params?.bidFloor; - const floorCur = floorInfo && typeof floorInfo.currency === 'string' ? floorInfo.currency : DEFAULT_CURRENCY; - if (typeof floor === 'number' && !isNaN(floor)) { - imp.bidfloor = floor; - imp.bidfloorcur = floorCur; - } - // ORTB blocking params (imp-level) - if (Array.isArray(bidRequest.params?.battr)) { - imp.banner = imp.banner || {}; - imp.banner.battr = bidRequest.params.battr; - } - return imp; - }, - request(buildRequest, imps, bidderRequest, context) { - const req = buildRequest(imps, bidderRequest, context); - req.at = 1; - req.ext = req.ext || {}; - req.ext.name = 'prebidjs'; - req.ext.version = '$prebid.version$'; - req.site = req.site || {}; - req.site.ext = req.site.ext || {}; - req.site.ext.placementId = validBidRequests[0]?.params?.placementId; - // ORTB blocking params (request-level) - const firstParams = validBidRequests[0]?.params || {}; - if (Array.isArray(firstParams.bcat)) { - req.bcat = firstParams.bcat; - } - if (Array.isArray(firstParams.badv)) { - req.badv = firstParams.badv; - } - if (Array.isArray(firstParams.bapp)) { - req.bapp = firstParams.bapp; - } - return req; - } - }); - const ortbRequest = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); + const ortbRequest = CONVERTER.toORTB({ bidRequests: validBidRequests, bidderRequest }); return [{ method: 'POST', url: getFloxisUrl(partner, region), data: ortbRequest, - converter, // store converter instance for later use options: { withCredentials: true, contentType: 'application/json;charset=UTF-8', @@ -92,6 +92,10 @@ function buildRequests(validBidRequests = [], bidderRequest = {}) { }]; } +function getFloxisUrl(partner, region = DEFAULT_REGION) { + return `https://${partner}-${region}.floxis.tech/pbjs`; +} + // User sync not supported initially function getUserSyncs() { return []; @@ -110,14 +114,31 @@ export const spec = { return false; } // Banner size validation - if (bid.mediaTypes.banner && Array.isArray(bid.mediaTypes.banner.sizes)) { - if (!bid.mediaTypes.banner.sizes.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { + if (bid.mediaTypes.banner) { + const sizes = bid.mediaTypes.banner.sizes; + if (!Array.isArray(sizes) || !sizes.length || !sizes.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { return false; } } - // Video size validation - if (bid.mediaTypes.video && Array.isArray(bid.mediaTypes.video.playerSize)) { - if (!bid.mediaTypes.video.playerSize.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { + // Video validation + if (bid.mediaTypes.video) { + const v = bid.mediaTypes.video; + if (!Array.isArray(v.playerSize) || !v.playerSize.length || !v.playerSize.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { + return false; + } + // Check for required video params + if (!Array.isArray(v.mimes) || !v.mimes.length) { + return false; + } + if (!Array.isArray(v.protocols) || !v.protocols.length) { + return false; + } + } + // Native validation (basic) + if (bid.mediaTypes.native) { + const n = bid.mediaTypes.native; + // Require at least one asset (image, title, etc.) + if (!n || Object.keys(n).length === 0) { return false; } } @@ -125,11 +146,7 @@ export const spec = { }, buildRequests, interpretResponse(response, request) { - // Use the same converter instance and request object as in buildRequests - if (!request.converter) { - throw new Error('Missing converter instance on request object'); - } - return request.converter.fromORTB({ request: request.data, response: response.body }).bids; + return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; }, getUserSyncs, onBidWon: function (bid) { diff --git a/test/spec/modules/floxisBidAdapter_spec.js b/test/spec/modules/floxisBidAdapter_spec.js index c12f88d6622..516bc430b5b 100644 --- a/test/spec/modules/floxisBidAdapter_spec.js +++ b/test/spec/modules/floxisBidAdapter_spec.js @@ -15,7 +15,7 @@ describe('floxisBidAdapter', function () { const validVideoBid = { bidId: '2', adUnitCode: 'adunit-2', - mediaTypes: { video: { playerSize: [[640, 480]] } }, + mediaTypes: { video: { playerSize: [[640, 480]], mimes: ['video/mp4'], protocols: [2, 3] } }, params: { partner: 'floxis', placementId: 456 }, ortb2Imp: { secure: 1 } }; @@ -139,4 +139,54 @@ describe('floxisBidAdapter', function () { const imp = req.imp[0]; expect(imp.banner.battr).to.deep.equal([1, 2, 3]); }); + + it('should invalidate video bid with missing mimes', function () { + const bid = { + bidId: 'v1', + adUnitCode: 'adunit-v1', + mediaTypes: { video: { playerSize: [[640, 480]], protocols: [2, 3] } }, + params: { partner: 'floxis', placementId: 123 } + }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should invalidate video bid with missing protocols', function () { + const bid = { + bidId: 'v2', + adUnitCode: 'adunit-v2', + mediaTypes: { video: { playerSize: [[640, 480]], mimes: ['video/mp4'] } }, + params: { partner: 'floxis', placementId: 123 } + }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should validate correct video bid with mimes and protocols', function () { + const bid = { + bidId: 'v3', + adUnitCode: 'adunit-v3', + mediaTypes: { video: { playerSize: [[640, 480]], mimes: ['video/mp4'], protocols: [2, 3] } }, + params: { partner: 'floxis', placementId: 123 } + }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('should invalidate native bid with no assets', function () { + const bid = { + bidId: 'n1', + adUnitCode: 'adunit-n1', + mediaTypes: { native: {} }, + params: { partner: 'floxis', placementId: 123 } + }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should validate native bid with assets', function () { + const bid = { + bidId: 'n2', + adUnitCode: 'adunit-n2', + mediaTypes: { native: { image: { required: true, sizes: [150, 50] }, title: { required: true, len: 80 } } }, + params: { partner: 'floxis', placementId: 123 } + }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); }); From 808797acc7f8038cb3beb7eda8bd677994eddaaa Mon Sep 17 00:00:00 2001 From: floxis-tech Date: Mon, 16 Feb 2026 22:52:10 +0200 Subject: [PATCH 5/7] Floxis Bid Adapter: redesign to seat-based architecture with ortbConverter Major rewrite replacing teqblazeUtils with ortbConverter for ORTB 2.x compliance. Changes: - New params: seat (required), region (required), partner (required) - Endpoint URL: https://{subdomain}.floxis.tech/pbjs?seat={seat} - subdomain = region for 'floxis' partner - subdomain = {partner}-{region} for white-label partners - ORTB-native implementation with Floors Module support - 40 comprehensive tests with full code coverage - Updated documentation with examples Addresses all PR #13934 review comments from @osazos --- modules/FloxisBidAdapter.md | 89 +--- modules/floxisBidAdapter.js | 165 +++---- test/spec/modules/floxisBidAdapter_spec.js | 536 +++++++++++++++------ 3 files changed, 457 insertions(+), 333 deletions(-) diff --git a/modules/FloxisBidAdapter.md b/modules/FloxisBidAdapter.md index 30a7a1060a6..f36db0c6577 100644 --- a/modules/FloxisBidAdapter.md +++ b/modules/FloxisBidAdapter.md @@ -2,84 +2,31 @@ ``` Module Name: Floxis Bidder Adapter -Module Type: Floxis Bidder Adapter +Module Type: Bidder Adapter Maintainer: admin@floxis.tech ``` + # Description -The Floxis Bid Adapter enables integration with the Floxis programmatic advertising platform via Prebid.js. It supports banner, video (instream and outstream), and native formats, and is designed for multi-partner, multi-region use. +The Floxis Bid Adapter enables integration with the Floxis programmatic advertising platform via Prebid.js. It supports banner, video (instream and outstream), and native formats. **Key Features:** - Banner, Video and Native ad support - OpenRTB 2.x compliant -- Privacy regulation compliance - -## Required Params -- `partner` (string): Partner name -- `placementId` (integer): Placement identifier - -## OpenRTB Blocking Params Support -FloxisBidAdapter supports OpenRTB blocking parameters. You can pass the following optional params in your ad unit config: -- `bcat` (array): Blocked categories -- `badv` (array): Blocked advertiser domains -- `bapp` (array): Blocked app bundle IDs -- `battr` (array): Blocked creative attributes +- Privacy regulation compliance (GDPR, USP, GPP, COPPA) +- Prebid.js Floors Module support -These will be included in the OpenRTB request and imp objects as appropriate. - -**Example:** -```javascript -pbjs.addAdUnits([ - { - code: 'adunit-20', - mediaTypes: { banner: { sizes: [[300, 250]] } }, - bids: [{ - bidder: 'floxis', - params: { - partner: 'floxis', - placementId: 555, - bcat: ['IAB1-1', 'IAB1-2'], - badv: ['example.com', 'test.com'], - bapp: ['com.example.app'], - battr: [1, 2, 3] - } - }] - } -]); -``` +## Supported Media Types +- Banner +- Video +- Native ## Floors Module Support -FloxisBidAdapter supports Prebid.js Floors Module. If a bid request provides a floor value via the Floors Module (`getFloor` function), it will be sent in the OpenRTB request as `imp.bidfloor` and `imp.bidfloorcur`. If not, you can also set a static floor using `params.bidFloor`. - -**Example with Floors Module:** -```javascript -pbjs.addAdUnits([ - { - code: 'adunit-1', - mediaTypes: { banner: { sizes: [[300, 250]] } }, - bids: [{ - bidder: 'floxis', - params: { - partner: 'floxis', - placementId: 1, - bidFloor: 2.5 // optional static floor - }, - getFloor: function({currency, mediaType, size}) { - return { floor: 2.5, currency: 'USD' }; - } - }] - } -]); -``` +The Floxis Bid Adapter supports the Prebid.js [Floors Module](https://docs.prebid.org/dev-docs/modules/floors.html). Floor values are automatically included in the OpenRTB request as `imp.bidfloor` and `imp.bidfloorcur`. ## Privacy Privacy fields (GDPR, USP, GPP, COPPA) are handled by Prebid.js core and automatically included in the OpenRTB request. -## Supported Media Types -- Banner -- Video -- Native - ## Example Usage ```javascript pbjs.addAdUnits([ @@ -89,22 +36,24 @@ pbjs.addAdUnits([ bids: [{ bidder: 'floxis', params: { - partner: 'floxis', - placementId: 1 + seat: 'testSeat', + region: 'us-e', + partner: 'floxis' } }] } ]); ``` - # Configuration -## Required Parameters + +## Parameters | Name | Scope | Description | Example | Type | | --- | --- | --- | --- | --- | -| `partner` | required | Partner identifier provided by Floxis | `floxis` | `string` | -| `placementId` | required | Placement identifier provided by Floxis | `1` | `int` | +| `seat` | required | Seat identifier | `'testSeat'` | `string` | +| `region` | required | Region identifier for routing | `'us-e'` | `string` | +| `partner` | required | Partner identifier | `'floxis'` | `string` | ## Testing -Unit tests are provided in `test/spec/modules/floxisBidAdapter_spec.js` and cover validation, request building, and response interpretation. +Unit tests are provided in `test/spec/modules/floxisBidAdapter_spec.js` and cover validation, request building, response interpretation, and bid-won notifications. diff --git a/modules/floxisBidAdapter.js b/modules/floxisBidAdapter.js index 6251bb68630..9d68c48d4d6 100644 --- a/modules/floxisBidAdapter.js +++ b/modules/floxisBidAdapter.js @@ -1,16 +1,18 @@ -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import * as utils from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { triggerPixel, mergeDeep } from '../src/utils.js'; const BIDDER_CODE = 'floxis'; - -const DEFAULT_REGION = 'us'; -const DEFAULT_PARTNER = 'floxis'; const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; +function getEndpointUrl(seat, region, partner) { + const subdomain = partner === BIDDER_CODE ? region : `${partner}-${region}`; + return `https://${subdomain}.floxis.tech/pbjs?seat=${encodeURIComponent(seat)}`; +} + const CONVERTER = ortbConverter({ context: { netRevenue: DEFAULT_NET_REVENUE, @@ -18,9 +20,9 @@ const CONVERTER = ortbConverter({ currency: DEFAULT_CURRENCY }, imp(buildImp, bidRequest, context) { - let imp = buildImp(bidRequest, context); + const imp = buildImp(bidRequest, context); imp.secure = bidRequest.ortb2Imp?.secure ?? 1; - // Floors Module support + let floorInfo; if (typeof bidRequest.getFloor === 'function') { try { @@ -31,132 +33,79 @@ const CONVERTER = ortbConverter({ }); } catch (e) { } } - const floor = floorInfo && typeof floorInfo.floor === 'number' ? floorInfo.floor : bidRequest.params?.bidFloor; - const floorCur = floorInfo && typeof floorInfo.currency === 'string' ? floorInfo.currency : DEFAULT_CURRENCY; + const floor = floorInfo?.floor; + const floorCur = floorInfo?.currency || DEFAULT_CURRENCY; if (typeof floor === 'number' && !isNaN(floor)) { imp.bidfloor = floor; imp.bidfloorcur = floorCur; } - // ORTB blocking params (imp-level) - if (Array.isArray(bidRequest.params?.battr)) { - imp.banner = imp.banner || {}; - imp.banner.battr = bidRequest.params.battr; - } + return imp; }, request(buildRequest, imps, bidderRequest, context) { const req = buildRequest(imps, bidderRequest, context); - req.at = 1; - req.ext = req.ext || {}; - req.ext.name = 'prebidjs'; - req.ext.version = '$prebid.version$'; - req.site = req.site || {}; - req.site.ext = req.site.ext || {}; - // Set placementId from first bid - const firstBid = context.bidRequests[0]; - if (firstBid?.params?.placementId) { - req.site.ext.placementId = firstBid.params.placementId; - } - // ORTB blocking params (request-level) - const firstParams = firstBid?.params || {}; - if (Array.isArray(firstParams.bcat)) { - req.bcat = firstParams.bcat; - } - if (Array.isArray(firstParams.badv)) { - req.badv = firstParams.badv; - } - if (Array.isArray(firstParams.bapp)) { - req.bapp = firstParams.bapp; - } + mergeDeep(req, { + at: 1, + ext: { + prebid: { + adapter: BIDDER_CODE, + adapterVersion: '2.0.0' + } + } + }); return req; } }); -function buildRequests(validBidRequests = [], bidderRequest = {}) { - if (!validBidRequests || !validBidRequests.length) { - return []; - } - const firstBid = validBidRequests[0]; - const partner = firstBid?.params?.partner || DEFAULT_PARTNER; - const region = firstBid?.params?.region || DEFAULT_REGION; +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], - const ortbRequest = CONVERTER.toORTB({ bidRequests: validBidRequests, bidderRequest }); - return [{ - method: 'POST', - url: getFloxisUrl(partner, region), - data: ortbRequest, - options: { - withCredentials: true, - contentType: 'application/json;charset=UTF-8', - } - }]; -} + isBidRequestValid(bid) { + const params = bid?.params; + if (!params) return false; + if (typeof params.seat !== 'string' || !params.seat.length) return false; + if (typeof params.region !== 'string' || !params.region.length) return false; + if (typeof params.partner !== 'string' || !params.partner.length) return false; + return true; + }, -function getFloxisUrl(partner, region = DEFAULT_REGION) { - return `https://${partner}-${region}.floxis.tech/pbjs`; -} + buildRequests(validBidRequests = [], bidderRequest = {}) { + if (!validBidRequests.length) return []; -// User sync not supported initially -function getUserSyncs() { - return []; -} + const firstBid = validBidRequests[0]; + const { seat, region, partner } = firstBid.params; + const url = getEndpointUrl(seat, region, partner); + const data = CONVERTER.toORTB({ bidRequests: validBidRequests, bidderRequest }); -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid: function (bid) { - const params = bid.params || {}; - if (typeof params.partner !== 'string' || !params.partner.length || !Number.isInteger(params.placementId)) { - return false; - } - // Must have at least one media type - if (!bid.mediaTypes || (!bid.mediaTypes.banner && !bid.mediaTypes.video && !bid.mediaTypes.native)) { - return false; - } - // Banner size validation - if (bid.mediaTypes.banner) { - const sizes = bid.mediaTypes.banner.sizes; - if (!Array.isArray(sizes) || !sizes.length || !sizes.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { - return false; - } - } - // Video validation - if (bid.mediaTypes.video) { - const v = bid.mediaTypes.video; - if (!Array.isArray(v.playerSize) || !v.playerSize.length || !v.playerSize.every(size => Array.isArray(size) && size.length === 2 && size.every(Number.isInteger))) { - return false; - } - // Check for required video params - if (!Array.isArray(v.mimes) || !v.mimes.length) { - return false; - } - if (!Array.isArray(v.protocols) || !v.protocols.length) { - return false; + return [{ + method: 'POST', + url, + data, + options: { + withCredentials: true, + contentType: 'application/json' } - } - // Native validation (basic) - if (bid.mediaTypes.native) { - const n = bid.mediaTypes.native; - // Require at least one asset (image, title, etc.) - if (!n || Object.keys(n).length === 0) { - return false; - } - } - return true; + }]; }, - buildRequests, + interpretResponse(response, request) { + if (!response?.body) return []; return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; }, - getUserSyncs, - onBidWon: function (bid) { + + getUserSyncs() { + return []; + }, + + onBidWon(bid) { if (bid.burl) { - utils.triggerPixel(bid.burl); + triggerPixel(bid.burl); } if (bid.nurl) { - utils.triggerPixel(bid.nurl); + triggerPixel(bid.nurl); } - }, + } }; registerBidder(spec); diff --git a/test/spec/modules/floxisBidAdapter_spec.js b/test/spec/modules/floxisBidAdapter_spec.js index 516bc430b5b..80323bec818 100644 --- a/test/spec/modules/floxisBidAdapter_spec.js +++ b/test/spec/modules/floxisBidAdapter_spec.js @@ -1,192 +1,418 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import { spec } from 'modules/floxisBidAdapter.js'; import { BANNER, NATIVE, VIDEO } from 'src/mediaTypes.js'; - -const BIDDER_CODE = 'floxis'; +import * as utils from 'src/utils.js'; describe('floxisBidAdapter', function () { + const DEFAULT_PARAMS = { seat: 'Gmtb', region: 'us-e', partner: 'floxis' }; + const validBannerBid = { - bidId: '1', - adUnitCode: 'adunit-1', - mediaTypes: { banner: { sizes: [[300, 250]] } }, - params: { partner: 'floxis', placementId: 123 }, - ortb2Imp: { secure: 1 } + bidId: 'bid-1', + bidder: 'floxis', + adUnitCode: 'adunit-banner', + mediaTypes: { banner: { sizes: [[300, 250], [728, 90]] } }, + params: { ...DEFAULT_PARAMS } }; + const validVideoBid = { - bidId: '2', - adUnitCode: 'adunit-2', - mediaTypes: { video: { playerSize: [[640, 480]], mimes: ['video/mp4'], protocols: [2, 3] } }, - params: { partner: 'floxis', placementId: 456 }, - ortb2Imp: { secure: 1 } + bidId: 'bid-2', + bidder: 'floxis', + adUnitCode: 'adunit-video', + mediaTypes: { + video: { + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [2, 3], + context: 'instream' + } + }, + params: { ...DEFAULT_PARAMS } }; - const invalidBid = { - bidId: '3', - adUnitCode: 'adunit-3', - mediaTypes: { banner: { sizes: [[300, 250]] } }, - params: { partner: '', placementId: 'notanint' } + + const validNativeBid = { + bidId: 'bid-3', + bidder: 'floxis', + adUnitCode: 'adunit-native', + mediaTypes: { + native: { + image: { required: true, sizes: [150, 50] }, + title: { required: true, len: 80 } + } + }, + params: { ...DEFAULT_PARAMS } }; - it('should validate correct banner bid', function () { - expect(spec.isBidRequestValid(validBannerBid)).to.be.true; - }); + describe('isBidRequestValid', function () { + it('should return true for valid banner bid', function () { + expect(spec.isBidRequestValid(validBannerBid)).to.be.true; + }); - it('should validate correct video bid', function () { - expect(spec.isBidRequestValid(validVideoBid)).to.be.true; - }); + it('should return true for valid video bid', function () { + expect(spec.isBidRequestValid(validVideoBid)).to.be.true; + }); - it('should invalidate incorrect bid', function () { - expect(spec.isBidRequestValid(invalidBid)).to.be.false; - }); + it('should return true for valid native bid', function () { + expect(spec.isBidRequestValid(validNativeBid)).to.be.true; + }); + + it('should return false when seat is missing', function () { + const bid = { ...validBannerBid, params: { region: 'us-e' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when seat is empty string', function () { + const bid = { ...validBannerBid, params: { seat: '', region: 'us-e' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when region is missing', function () { + const bid = { ...validBannerBid, params: { seat: 'Gmtb' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when region is empty string', function () { + const bid = { ...validBannerBid, params: { seat: 'Gmtb', region: '' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when partner is missing', function () { + const bid = { ...validBannerBid, params: { seat: 'Gmtb', region: 'us-e' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when partner is empty string', function () { + const bid = { ...validBannerBid, params: { seat: 'Gmtb', region: 'us-e', partner: '' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); - it('should build request with correct url and payload', function () { - const requests = spec.buildRequests([validBannerBid], {}); - expect(requests).to.have.lengthOf(1); - expect(requests[0].url).to.include('floxis-us.floxis.tech/pbjs'); - expect(requests[0].data).to.be.an('object'); - expect(requests[0].data.imp).to.be.an('array'); - expect(requests[0].data.site.ext.placementId).to.equal(123); + it('should return false when params is missing', function () { + const bid = { bidId: 'x', mediaTypes: { banner: { sizes: [[300, 250]] } } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when seat is not a string', function () { + const bid = { ...validBannerBid, params: { seat: 123, region: 'us-e', partner: 'floxis' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return true with non-default partner', function () { + const bid = { ...validBannerBid, params: { ...DEFAULT_PARAMS, partner: 'mypartner' } }; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); }); - it('should handle empty bid requests', function () { - const requests = spec.buildRequests([], {}); - expect(requests).to.be.an('array').that.is.empty; + describe('supportedMediaTypes', function () { + it('should include banner, video, and native', function () { + expect(spec.supportedMediaTypes).to.deep.equal([BANNER, VIDEO, NATIVE]); + }); }); - it('should invalidate bid with no media types', function () { - const noMediaTypeBid = { - bidId: '4', - adUnitCode: 'adunit-4', - params: { partner: 'floxis', placementId: 123 } + describe('buildRequests', function () { + const bidderRequest = { + bidderCode: 'floxis', + auctionId: 'auction-123', + timeout: 3000 }; - expect(spec.isBidRequestValid(noMediaTypeBid)).to.be.false; - }); - it('should validate supported media types', function () { - expect(spec.supportedMediaTypes).to.include(BANNER); - expect(spec.supportedMediaTypes).to.include(VIDEO); - expect(spec.supportedMediaTypes).to.include(NATIVE); - }); + it('should return an array with one POST request', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + expect(requests).to.be.an('array').with.lengthOf(1); + expect(requests[0].method).to.equal('POST'); + }); + + it('should build URL without partner prefix when partner is floxis', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + expect(requests[0].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Gmtb'); + }); + + it('should build URL with partner prefix for non-floxis partner', function () { + const bidWithPartner = { + ...validBannerBid, + params: { ...DEFAULT_PARAMS, partner: 'mypartner' } + }; + const requests = spec.buildRequests([bidWithPartner], bidderRequest); + expect(requests[0].url).to.equal('https://mypartner-us-e.floxis.tech/pbjs?seat=Gmtb'); + }); + + it('should return empty array for empty bid requests', function () { + const requests = spec.buildRequests([], bidderRequest); + expect(requests).to.be.an('array').that.is.empty; + }); + + it('should produce valid ORTB request payload', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + const data = requests[0].data; + expect(data).to.be.an('object'); + expect(data.imp).to.be.an('array').with.lengthOf(1); + expect(data.at).to.equal(1); + }); + + it('should set ext with adapter info', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + const data = requests[0].data; + expect(data.ext.prebid.adapter).to.equal('floxis'); + expect(data.ext.prebid.adapterVersion).to.equal('2.0.0'); + }); + + it('should build banner imp correctly', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + const imp = requests[0].data.imp[0]; + expect(imp).to.have.property('banner'); + expect(imp.banner.format).to.be.an('array'); + expect(imp.secure).to.equal(1); + }); + + if (FEATURES.VIDEO) { + it('should build video imp correctly', function () { + const requests = spec.buildRequests([validVideoBid], bidderRequest); + const imp = requests[0].data.imp[0]; + expect(imp).to.have.property('video'); + expect(imp.video.mimes).to.deep.equal(['video/mp4']); + expect(imp.video.protocols).to.deep.equal([2, 3]); + }); + } + + it('should handle multiple bids in single request', function () { + const requests = spec.buildRequests([validBannerBid, validVideoBid], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].data.imp).to.have.lengthOf(2); + }); + + it('should set withCredentials option', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + expect(requests[0].options.withCredentials).to.be.true; + }); + + describe('Floors Module support', function () { + it('should set bidfloor from getFloor', function () { + const bidWithFloor = { + ...validBannerBid, + getFloor: function () { + return { floor: 2.5, currency: 'USD' }; + } + }; + const requests = spec.buildRequests([bidWithFloor], bidderRequest); + const imp = requests[0].data.imp[0]; + expect(imp.bidfloor).to.equal(2.5); + expect(imp.bidfloorcur).to.equal('USD'); + }); - it('should return empty user syncs', function () { - expect(spec.getUserSyncs()).to.be.an('array').that.is.empty; + it('should not set bidfloor when getFloor is not present', function () { + const requests = spec.buildRequests([validBannerBid], bidderRequest); + const imp = requests[0].data.imp[0]; + expect(imp.bidfloor).to.be.undefined; + }); + + it('should handle getFloor throwing an error gracefully', function () { + const bidBrokenFloor = { + ...validBannerBid, + getFloor: function () { + throw new Error('floor error'); + } + }; + const requests = spec.buildRequests([bidBrokenFloor], bidderRequest); + const imp = requests[0].data.imp[0]; + expect(imp.bidfloor).to.be.undefined; + }); + }); + + describe('ortb2 passthrough', function () { + it('should merge ortb2 data into the ORTB request', function () { + const ortb2BidderRequest = { + ...bidderRequest, + ortb2: { + regs: { ext: { gdpr: 1 } }, + user: { ext: { consent: 'consent-string-123' } } + } + }; + const requests = spec.buildRequests([validBannerBid], ortb2BidderRequest); + const data = requests[0].data; + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.user.ext.consent).to.equal('consent-string-123'); + }); + + it('should merge ortb2 USP data into the ORTB request', function () { + const uspBidderRequest = { + ...bidderRequest, + ortb2: { + regs: { ext: { us_privacy: '1YNN' } } + } + }; + const requests = spec.buildRequests([validBannerBid], uspBidderRequest); + const data = requests[0].data; + expect(data.regs.ext.us_privacy).to.equal('1YNN'); + }); + }); }); - it('should interpret response correctly', function () { - const serverResponse = { - body: { - seatbid: [{ - bid: [{ - impid: '1', - price: 1.23, - w: 300, - h: 250, - crid: 'creative-1', - adm: '
ad
', - cur: 'USD' + describe('interpretResponse', function () { + function buildRequest() { + return spec.buildRequests([validBannerBid], { + bidderCode: 'floxis', + auctionId: 'auction-123' + })[0]; + } + + it('should parse valid banner ORTB response', function () { + const request = buildRequest(); + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: validBannerBid.bidId, + price: 1.23, + w: 300, + h: 250, + crid: 'creative-1', + adm: '
ad
', + mtype: 1 + }] }] - }] - } - }; - const requests = spec.buildRequests([validBannerBid], {}); - const bids = spec.interpretResponse(serverResponse, requests[0]); - expect(bids).to.be.an('array'); - if (bids.length > 0) { - expect(bids[0]).to.have.property('cpm'); - expect(bids[0]).to.have.property('requestId'); + } + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).to.be.an('array').with.lengthOf(1); expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].width).to.equal(300); + expect(bids[0].height).to.equal(250); + expect(bids[0].creativeId).to.equal('creative-1'); + expect(bids[0].ad).to.equal('
ad
'); + expect(bids[0].requestId).to.equal(validBannerBid.bidId); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].currency).to.equal('USD'); + }); + + if (FEATURES.VIDEO) { + it('should parse valid video ORTB response', function () { + const videoRequest = spec.buildRequests([validVideoBid], { + bidderCode: 'floxis', + auctionId: 'auction-456' + })[0]; + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: validVideoBid.bidId, + price: 5.00, + w: 640, + h: 480, + crid: 'video-creative-1', + adm: '', + mtype: 2 + }] + }] + } + }; + const bids = spec.interpretResponse(serverResponse, videoRequest); + expect(bids).to.be.an('array').with.lengthOf(1); + expect(bids[0].cpm).to.equal(5.00); + expect(bids[0].vastXml).to.equal(''); + expect(bids[0].mediaType).to.equal(VIDEO); + }); } - }); - it('should set bidfloor and bidfloorcur from Floors Module', function () { - const floorValue = 2.5; - const floorCurrency = 'USD'; - const bidWithFloor = { - bidId: '10', - adUnitCode: 'adunit-10', - mediaTypes: { banner: { sizes: [[300, 250]] } }, - params: { partner: 'floxis', placementId: 999 }, - getFloor: function({currency, mediaType, size}) { - return { floor: floorValue, currency: floorCurrency }; - } - }; - const requests = spec.buildRequests([bidWithFloor], {}); - expect(requests).to.have.lengthOf(1); - const imp = requests[0].data.imp[0]; - expect(imp.bidfloor).to.equal(floorValue); - expect(imp.bidfloorcur).to.equal(floorCurrency); - }); + it('should return empty array for empty response', function () { + const request = buildRequest(); + const bids = spec.interpretResponse({ body: {} }, request); + expect(bids).to.be.an('array').that.is.empty; + }); - it('should set ORTB blocking params in request and imp', function () { - const bidWithBlocking = { - bidId: '20', - adUnitCode: 'adunit-20', - mediaTypes: { banner: { sizes: [[300, 250]] } }, - params: { - partner: 'floxis', - placementId: 555, - bcat: ['IAB1-1', 'IAB1-2'], - badv: ['example.com', 'test.com'], - bapp: ['com.example.app'], - battr: [1, 2, 3] - } - }; - const requests = spec.buildRequests([bidWithBlocking], {}); - expect(requests).to.have.lengthOf(1); - const req = requests[0].data; - expect(req.bcat).to.deep.equal(['IAB1-1', 'IAB1-2']); - expect(req.badv).to.deep.equal(['example.com', 'test.com']); - expect(req.bapp).to.deep.equal(['com.example.app']); - const imp = req.imp[0]; - expect(imp.banner.battr).to.deep.equal([1, 2, 3]); - }); + it('should return empty array for null response body', function () { + const request = buildRequest(); + const bids = spec.interpretResponse({ body: null }, request); + expect(bids).to.be.an('array').that.is.empty; + }); - it('should invalidate video bid with missing mimes', function () { - const bid = { - bidId: 'v1', - adUnitCode: 'adunit-v1', - mediaTypes: { video: { playerSize: [[640, 480]], protocols: [2, 3] } }, - params: { partner: 'floxis', placementId: 123 } - }; - expect(spec.isBidRequestValid(bid)).to.be.false; - }); + it('should return empty array for undefined response', function () { + const request = buildRequest(); + const bids = spec.interpretResponse(undefined, request); + expect(bids).to.be.an('array').that.is.empty; + }); - it('should invalidate video bid with missing protocols', function () { - const bid = { - bidId: 'v2', - adUnitCode: 'adunit-v2', - mediaTypes: { video: { playerSize: [[640, 480]], mimes: ['video/mp4'] } }, - params: { partner: 'floxis', placementId: 123 } - }; - expect(spec.isBidRequestValid(bid)).to.be.false; - }); + it('should handle multiple bids in seatbid', function () { + const bids2 = [ + { ...validBannerBid, bidId: 'bid-a' }, + { ...validBannerBid, bidId: 'bid-b', adUnitCode: 'adunit-2' } + ]; + const request = spec.buildRequests(bids2, { bidderCode: 'floxis', auctionId: 'a1' })[0]; + const serverResponse = { + body: { + seatbid: [{ + bid: [ + { impid: 'bid-a', price: 1.0, w: 300, h: 250, crid: 'c1', adm: '
1
', mtype: 1 }, + { impid: 'bid-b', price: 2.0, w: 300, h: 250, crid: 'c2', adm: '
2
', mtype: 1 } + ] + }] + } + }; + const result = spec.interpretResponse(serverResponse, request); + expect(result).to.have.lengthOf(2); + expect(result[0].cpm).to.equal(1.0); + expect(result[1].cpm).to.equal(2.0); + }); - it('should validate correct video bid with mimes and protocols', function () { - const bid = { - bidId: 'v3', - adUnitCode: 'adunit-v3', - mediaTypes: { video: { playerSize: [[640, 480]], mimes: ['video/mp4'], protocols: [2, 3] } }, - params: { partner: 'floxis', placementId: 123 } - }; - expect(spec.isBidRequestValid(bid)).to.be.true; + it('should set advertiserDomains from adomain', function () { + const request = buildRequest(); + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: validBannerBid.bidId, + price: 1.0, + w: 300, + h: 250, + crid: 'c1', + adm: '
ad
', + adomain: ['adv.com'], + mtype: 1 + }] + }] + } + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids[0].meta.advertiserDomains).to.deep.equal(['adv.com']); + }); }); - it('should invalidate native bid with no assets', function () { - const bid = { - bidId: 'n1', - adUnitCode: 'adunit-n1', - mediaTypes: { native: {} }, - params: { partner: 'floxis', placementId: 123 } - }; - expect(spec.isBidRequestValid(bid)).to.be.false; + describe('getUserSyncs', function () { + it('should return empty array', function () { + expect(spec.getUserSyncs()).to.be.an('array').that.is.empty; + }); }); - it('should validate native bid with assets', function () { - const bid = { - bidId: 'n2', - adUnitCode: 'adunit-n2', - mediaTypes: { native: { image: { required: true, sizes: [150, 50] }, title: { required: true, len: 80 } } }, - params: { partner: 'floxis', placementId: 123 } - }; - expect(spec.isBidRequestValid(bid)).to.be.true; + describe('onBidWon', function () { + let triggerPixelStub; + + beforeEach(function () { + triggerPixelStub = sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function () { + triggerPixelStub.restore(); + }); + + it('should fire burl pixel', function () { + spec.onBidWon({ burl: 'https://example.com/burl' }); + expect(triggerPixelStub.calledWith('https://example.com/burl')).to.be.true; + }); + + it('should fire nurl pixel', function () { + spec.onBidWon({ nurl: 'https://example.com/nurl' }); + expect(triggerPixelStub.calledWith('https://example.com/nurl')).to.be.true; + }); + + it('should fire both burl and nurl pixels', function () { + spec.onBidWon({ + burl: 'https://example.com/burl', + nurl: 'https://example.com/nurl' + }); + expect(triggerPixelStub.callCount).to.equal(2); + }); + + it('should not fire pixels when no urls present', function () { + spec.onBidWon({}); + expect(triggerPixelStub.called).to.be.false; + }); }); }); From ec97018fc4732c206b35d7dfca1a3f2be6f60bc5 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Wed, 18 Feb 2026 13:19:21 -0500 Subject: [PATCH 6/7] Rename FloxisBidAdapter.md to floxisBidAdapter.md --- modules/{FloxisBidAdapter.md => floxisBidAdapter.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename modules/{FloxisBidAdapter.md => floxisBidAdapter.md} (100%) diff --git a/modules/FloxisBidAdapter.md b/modules/floxisBidAdapter.md similarity index 100% rename from modules/FloxisBidAdapter.md rename to modules/floxisBidAdapter.md From 12f4630ea869133c5c9844de69593f760d16cae2 Mon Sep 17 00:00:00 2001 From: floxis-tech Date: Wed, 18 Feb 2026 21:03:39 +0200 Subject: [PATCH 7/7] Code review adjustments --- modules/floxisBidAdapter.js | 82 +++++++++++++----- test/spec/modules/floxisBidAdapter_spec.js | 99 ++++++++++++++++++++-- 2 files changed, 151 insertions(+), 30 deletions(-) diff --git a/modules/floxisBidAdapter.js b/modules/floxisBidAdapter.js index 9d68c48d4d6..c677f054a46 100644 --- a/modules/floxisBidAdapter.js +++ b/modules/floxisBidAdapter.js @@ -7,10 +7,30 @@ const BIDDER_CODE = 'floxis'; const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; +const DEFAULT_REGION = 'us-e'; +const DEFAULT_PARTNER = BIDDER_CODE; +const PARTNER_REGION_WHITELIST = { + [DEFAULT_PARTNER]: [DEFAULT_REGION], +}; + +function isAllowedPartnerRegion(partner, region) { + return PARTNER_REGION_WHITELIST[partner]?.includes(region) || false; +} function getEndpointUrl(seat, region, partner) { - const subdomain = partner === BIDDER_CODE ? region : `${partner}-${region}`; - return `https://${subdomain}.floxis.tech/pbjs?seat=${encodeURIComponent(seat)}`; + if (!isAllowedPartnerRegion(partner, region)) return null; + const host = partner === BIDDER_CODE + ? `${region}.floxis.tech` + : `${partner}-${region}.floxis.tech`; + return `https://${host}/pbjs?seat=${encodeURIComponent(seat)}`; +} + +function normalizeBidParams(params = {}) { + return { + seat: params.seat, + region: params.region ?? DEFAULT_REGION, + partner: params.partner ?? DEFAULT_PARTNER + }; } const CONVERTER = ortbConverter({ @@ -49,7 +69,7 @@ const CONVERTER = ortbConverter({ ext: { prebid: { adapter: BIDDER_CODE, - adapterVersion: '2.0.0' + version: '$prebid.version$' } } }); @@ -64,34 +84,52 @@ export const spec = { isBidRequestValid(bid) { const params = bid?.params; if (!params) return false; - if (typeof params.seat !== 'string' || !params.seat.length) return false; - if (typeof params.region !== 'string' || !params.region.length) return false; - if (typeof params.partner !== 'string' || !params.partner.length) return false; + const { seat, region, partner } = normalizeBidParams(params); + if (typeof seat !== 'string' || !seat.length) return false; + if (!isAllowedPartnerRegion(partner, region)) return false; return true; }, buildRequests(validBidRequests = [], bidderRequest = {}) { if (!validBidRequests.length) return []; + const filteredBidRequests = validBidRequests.filter((bidRequest) => spec.isBidRequestValid(bidRequest)); + if (!filteredBidRequests.length) return []; - const firstBid = validBidRequests[0]; - const { seat, region, partner } = firstBid.params; - const url = getEndpointUrl(seat, region, partner); - const data = CONVERTER.toORTB({ bidRequests: validBidRequests, bidderRequest }); - - return [{ - method: 'POST', - url, - data, - options: { - withCredentials: true, - contentType: 'application/json' - } - }]; + const bidRequestsByParams = filteredBidRequests.reduce((groups, bidRequest) => { + const { seat, region, partner } = normalizeBidParams(bidRequest.params); + const key = `${seat}|${region}|${partner}`; + groups[key] = groups[key] || []; + groups[key].push({ + ...bidRequest, + params: { + ...bidRequest.params, + seat, + region, + partner + } + }); + return groups; + }, {}); + + return Object.values(bidRequestsByParams).map((groupedBidRequests) => { + const { seat, region, partner } = groupedBidRequests[0].params; + const url = getEndpointUrl(seat, region, partner); + if (!url) return null; + return { + method: 'POST', + url, + data: CONVERTER.toORTB({ bidRequests: groupedBidRequests, bidderRequest }), + options: { + withCredentials: true, + contentType: 'text/plain' + } + }; + }).filter(Boolean); }, interpretResponse(response, request) { - if (!response?.body) return []; - return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; + if (!response?.body || !request?.data) return []; + return CONVERTER.fromORTB({ request: request.data, response: response.body })?.bids || []; }, getUserSyncs() { diff --git a/test/spec/modules/floxisBidAdapter_spec.js b/test/spec/modules/floxisBidAdapter_spec.js index 80323bec818..9cc8fe96c46 100644 --- a/test/spec/modules/floxisBidAdapter_spec.js +++ b/test/spec/modules/floxisBidAdapter_spec.js @@ -66,9 +66,9 @@ describe('floxisBidAdapter', function () { expect(spec.isBidRequestValid(bid)).to.be.false; }); - it('should return false when region is missing', function () { + it('should return true when region is missing (default region applies)', function () { const bid = { ...validBannerBid, params: { seat: 'Gmtb' } }; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.true; }); it('should return false when region is empty string', function () { @@ -76,9 +76,9 @@ describe('floxisBidAdapter', function () { expect(spec.isBidRequestValid(bid)).to.be.false; }); - it('should return false when partner is missing', function () { + it('should return true when partner is missing (default partner applies)', function () { const bid = { ...validBannerBid, params: { seat: 'Gmtb', region: 'us-e' } }; - expect(spec.isBidRequestValid(bid)).to.be.false; + expect(spec.isBidRequestValid(bid)).to.be.true; }); it('should return false when partner is empty string', function () { @@ -96,8 +96,18 @@ describe('floxisBidAdapter', function () { expect(spec.isBidRequestValid(bid)).to.be.false; }); - it('should return true with non-default partner', function () { + it('should return false when region is not in whitelist', function () { + const bid = { ...validBannerBid, params: { ...DEFAULT_PARAMS, region: 'eu-w' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return false when partner is not in whitelist', function () { const bid = { ...validBannerBid, params: { ...DEFAULT_PARAMS, partner: 'mypartner' } }; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('should return true with default partner', function () { + const bid = { ...validBannerBid, params: { ...DEFAULT_PARAMS, partner: 'floxis' } }; expect(spec.isBidRequestValid(bid)).to.be.true; }); }); @@ -126,13 +136,31 @@ describe('floxisBidAdapter', function () { expect(requests[0].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Gmtb'); }); - it('should build URL with partner prefix for non-floxis partner', function () { + it('should return no requests for non-whitelisted partner', function () { const bidWithPartner = { ...validBannerBid, params: { ...DEFAULT_PARAMS, partner: 'mypartner' } }; const requests = spec.buildRequests([bidWithPartner], bidderRequest); - expect(requests[0].url).to.equal('https://mypartner-us-e.floxis.tech/pbjs?seat=Gmtb'); + expect(requests).to.be.an('array').that.is.empty; + }); + + it('should default region to us-e when missing', function () { + const bidWithoutRegion = { + ...validBannerBid, + params: { seat: 'Gmtb', partner: 'floxis' } + }; + const requests = spec.buildRequests([bidWithoutRegion], bidderRequest); + expect(requests[0].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Gmtb'); + }); + + it('should default partner to floxis when missing', function () { + const bidWithoutPartner = { + ...validBannerBid, + params: { seat: 'Gmtb', region: 'us-e' } + }; + const requests = spec.buildRequests([bidWithoutPartner], bidderRequest); + expect(requests[0].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Gmtb'); }); it('should return empty array for empty bid requests', function () { @@ -152,7 +180,8 @@ describe('floxisBidAdapter', function () { const requests = spec.buildRequests([validBannerBid], bidderRequest); const data = requests[0].data; expect(data.ext.prebid.adapter).to.equal('floxis'); - expect(data.ext.prebid.adapterVersion).to.equal('2.0.0'); + expect(data.ext.prebid.adapterVersion).to.be.undefined; + expect(data.ext.prebid.version).to.equal('$prebid.version$'); }); it('should build banner imp correctly', function () { @@ -179,6 +208,40 @@ describe('floxisBidAdapter', function () { expect(requests[0].data.imp).to.have.lengthOf(2); }); + it('should split requests by seat when using allowed defaults', function () { + const mixedBid = { + ...validVideoBid, + params: { + seat: 'Seat2', + region: 'us-e', + partner: 'floxis' + } + }; + + const requests = spec.buildRequests([validBannerBid, mixedBid], bidderRequest); + expect(requests).to.have.lengthOf(2); + expect(requests[0].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Gmtb'); + expect(requests[1].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Seat2'); + expect(requests[0].data.imp).to.have.lengthOf(1); + expect(requests[1].data.imp).to.have.lengthOf(1); + }); + + it('should ignore non-whitelisted bids in mixed request arrays', function () { + const invalidBid = { + ...validVideoBid, + params: { + seat: 'Seat2', + region: 'eu-w', + partner: 'mypartner' + } + }; + + const requests = spec.buildRequests([validBannerBid, invalidBid], bidderRequest); + expect(requests).to.have.lengthOf(1); + expect(requests[0].url).to.equal('https://us-e.floxis.tech/pbjs?seat=Gmtb'); + expect(requests[0].data.imp).to.have.lengthOf(1); + }); + it('should set withCredentials option', function () { const requests = spec.buildRequests([validBannerBid], bidderRequest); expect(requests[0].options.withCredentials).to.be.true; @@ -330,6 +393,26 @@ describe('floxisBidAdapter', function () { expect(bids).to.be.an('array').that.is.empty; }); + it('should return empty array for undefined request', function () { + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + impid: validBannerBid.bidId, + price: 1.23, + w: 300, + h: 250, + crid: 'creative-1', + adm: '
ad
', + mtype: 1 + }] + }] + } + }; + const bids = spec.interpretResponse(serverResponse, undefined); + expect(bids).to.be.an('array').that.is.empty; + }); + it('should handle multiple bids in seatbid', function () { const bids2 = [ { ...validBannerBid, bidId: 'bid-a' },