diff --git a/modules/aceexBidAdapter.js b/modules/aceexBidAdapter.js new file mode 100644 index 0000000000..170d1ef106 --- /dev/null +++ b/modules/aceexBidAdapter.js @@ -0,0 +1,98 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { + buildRequestsBase, + buildPlacementProcessingFunction, +} from '../libraries/teqblazeUtils/bidderUtils.js'; + +import { deepAccess } from '../src/utils.js'; + +const BIDDER_CODE = 'aceex'; +const GVLID = 1387; +const AD_REQUEST_URL = 'http://bl-us.aceex.io/?secret_key=prebidjs'; + +const addCustomFieldsToPlacement = (bid, bidderRequest, placement) => { + placement.trafficType = placement.adFormat; + placement.publisherId = bid.params.publisherId; + placement.internalKey = bid.params.internalKey; + placement.bidfloor = bid.params.bidfloor; +}; + +const placementProcessingFunction = buildPlacementProcessingFunction({ addCustomFieldsToPlacement }); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid) => { + return !!(bid.bidId && bid.params?.publisherId && bid.params?.trafficType); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const base = buildRequestsBase({ adUrl: AD_REQUEST_URL, validBidRequests, bidderRequest, placementProcessingFunction }); + + base.data.cat = deepAccess(bidderRequest, 'ortb2.cat'); + base.data.keywords = deepAccess(bidderRequest, 'ortb2.keywords'); + base.data.badv = deepAccess(bidderRequest, 'ortb2.badv'); + base.data.wseat = deepAccess(bidderRequest, 'ortb2.wseat'); + base.data.bseat = deepAccess(bidderRequest, 'ortb2.bseat'); + + return base; + }, + + interpretResponse: (serverResponse, bidRequest) => { + if (!serverResponse || !serverResponse.body || !Array.isArray(serverResponse.body.seatbid)) return []; + + const repackedBids = []; + + serverResponse.body.seatbid.forEach(seatbidItem => { + seatbidItem.bid.forEach((bid) => { + const originalPlacement = bidRequest.data.placements?.find(pl => pl.bidId === bid.id); + + const repackedBid = { + cpm: bid.price, + creativeId: bid.crid, + currency: 'USD', + dealId: bid.dealid, + height: bid.h, + width: bid.w, + mediaType: originalPlacement.adFormat, + netRevenue: true, + requestId: bid.id, + ttl: 1200, + meta: { + advertiserDomains: bid.adomain + }, + }; + + switch (originalPlacement.adFormat) { + case 'video': + repackedBid.vastXml = bid.adm; + break; + + case 'banner': + repackedBid.ad = bid.adm; + break; + + case 'native': + const nativeResponse = JSON.parse(bid.adm).native; + + const { assets, imptrackers, link } = nativeResponse; + repackedBid.native = { + ortb: { assets, imptrackers, link }, + }; + break; + + default: break; + }; + + repackedBids.push(repackedBid); + }) + }); + + return repackedBids; + }, +}; + +registerBidder(spec); diff --git a/modules/aceexBidAdapter.md b/modules/aceexBidAdapter.md new file mode 100644 index 0000000000..6efa00acd4 --- /dev/null +++ b/modules/aceexBidAdapter.md @@ -0,0 +1,67 @@ +# Overview + +``` +Module Name: Aceex Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech@aceex.io +``` + +# Description + +Module that connects Prebid.JS publishers to Aceex ad-exchange + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :------------------------ | :------------------- | +| `publisherId` | required | Publisher ID on platform | 219 | +| `trafficType` | required | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | +| `internalKey` | required | Publisher hash on platform | "j1opp02hsma8119" | +| `bidfloor` | required | Bidfloor | 0.1 | + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'aceex', + params: { + publisherId: 219, + internalKey: 'j1opp02hsma8119', + trafficType: 'banner', + bidfloor: 0.2 + } + } + ] + }, + // Will return test vast video + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'aceex', + params: { + publisherId: 219, + internalKey: 'j1opp02hsma8119', + trafficType: 'video', + bidfloor: 1.1 + } + } + ] + } + ]; +``` diff --git a/test/spec/modules/aceexBidAdapter_spec.js b/test/spec/modules/aceexBidAdapter_spec.js new file mode 100644 index 0000000000..cfb695f69e --- /dev/null +++ b/test/spec/modules/aceexBidAdapter_spec.js @@ -0,0 +1,248 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/aceexBidAdapter.js'; + +describe('aceexBidAdapter', function () { + const makeBidderRequest = (ortb2 = {}) => ({ + bidderCode: 'aceex', + auctionId: 'auction-1', + bidderRequestId: 'br-1', + ortb2 + }); + + const makeBid = (overrides = {}) => ({ + bidId: overrides.bidId || 'bid-1', + bidder: 'aceex', + params: { + publisherId: 'pub-1', + trafficType: 'banner', + internalKey: 'ik-1', + bidfloor: 0.1, + ...overrides.params, + }, + mediaTypes: overrides.mediaTypes, + sizes: overrides.sizes, + ...overrides, + }); + + describe('isBidRequestValid', function () { + it('should return true when bidId, params.publisherId and params.trafficType are present', function () { + const bid = makeBid({ + bidId: 'bid-123', + params: { publisherId: 'pub-123', trafficType: 'banner' } + }); + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when bidId is missing', function () { + const bid = makeBid({ bidId: undefined }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when params.publisherId is missing', function () { + const bid = makeBid({ params: { trafficType: 'banner' } }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when params.trafficType is missing', function () { + const bid = makeBid({ params: { publisherId: 'pub-1' } }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('should build a single request object', function () { + const bidderRequest = makeBidderRequest(); + const validBidRequests = [makeBid({ bidId: 'bid-1' })]; + + const req = spec.buildRequests(validBidRequests, bidderRequest); + + expect(req).to.be.an('object'); + + expect(req).to.have.property('data'); + }); + + it('should map ortb2 fields into request.data (cat, keywords, badv, wseat, bseat)', function () { + const bidderRequest = makeBidderRequest({ + cat: ['IAB1', 'IAB1-1'], + keywords: { key1: ['v1', 'v2'] }, + badv: ['bad.com'], + wseat: ['seat1'], + bseat: ['seat2'], + }); + + const validBidRequests = [makeBid({ bidId: 'bid-1' })]; + const req = spec.buildRequests(validBidRequests, bidderRequest); + + expect(req.data.cat).to.deep.equal(['IAB1', 'IAB1-1']); + expect(req.data.keywords).to.deep.equal({ key1: ['v1', 'v2'] }); + expect(req.data.badv).to.deep.equal(['bad.com']); + expect(req.data.wseat).to.deep.equal(['seat1']); + expect(req.data.bseat).to.deep.equal(['seat2']); + }); + + it('should not throw if ortb2 fields are missing', function () { + const bidderRequest = makeBidderRequest(); + const validBidRequests = [makeBid({ bidId: 'bid-1' })]; + + expect(() => spec.buildRequests(validBidRequests, bidderRequest)).to.not.throw(); + }); + }); + + describe('interpretResponse', function () { + it('should return [] when serverResponse/body is missing', function () { + expect(spec.interpretResponse(null, {})).to.deep.equal([]); + expect(spec.interpretResponse({}, {})).to.deep.equal([]); + expect(spec.interpretResponse({ body: null }, {})).to.deep.equal([]); + }); + + it('should interpret banner bid', function () { + const bidRequest = { + data: { + placements: [ + { bidId: 'resp-bid-1', adFormat: 'banner' } + ] + } + }; + + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + id: 'resp-bid-1', + price: 1.23, + crid: 'cr-1', + dealid: 'deal-1', + h: 250, + w: 300, + adm: '
price=1.23
', + nurl: 'https://win.example.com?c=1.23', + adomain: [ 'test.com' ] + }] + }] + } + }; + + const out = spec.interpretResponse(serverResponse, bidRequest); + expect(out).to.have.lengthOf(1); + + const b = out[0]; + expect(b.requestId).to.equal('resp-bid-1'); + expect(b.cpm).to.equal(1.23); + expect(b.mediaType).to.equal('banner'); + expect(b.width).to.equal(300); + expect(b.height).to.equal(250); + expect(b.ad).to.include('1.23'); + expect(b.meta.advertiserDomains[0]).to.equal('test.com'); + }); + + it('should interpret video bid as vastXml', function () { + const bidRequest = { + data: { + placements: [ + { bidId: 'resp-bid-2', adFormat: 'video' } + ] + } + }; + + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + id: 'resp-bid-2', + price: 5, + crid: 'cr-v', + dealid: 'deal-v', + h: 360, + w: 640, + adm: '', + nurl: 'https://win.example.com?c=5', + adomain: ['test.com'] + }] + }] + } + }; + + const out = spec.interpretResponse(serverResponse, bidRequest); + expect(out).to.have.lengthOf(1); + + const b = out[0]; + expect(b.mediaType).to.equal('video'); + expect(b.vastXml).to.equal(''); + expect(b.ad).to.equal(undefined); + }); + + it('should interpret native bid into native.ortb', function () { + const bidRequest = { + data: { + placements: [ + { bidId: 'resp-bid-3', adFormat: 'native' } + ] + } + }; + + const nativeAdm = JSON.stringify({ + native: { + assets: [{ id: 1, title: { text: 'Hello' } }], + imptrackers: ['https://imp.example.com/1'], + link: { url: 'https://click.example.com' } + } + }); + + const serverResponse = { + body: { + seatbid: [{ + bid: [{ + id: 'resp-bid-3', + price: 2.5, + crid: 'cr-n', + dealid: 'deal-n', + h: 1, + w: 1, + adm: nativeAdm, + nurl: 'https://win.example.com?c=5', + adomain: ['test.com'] + }] + }] + } + }; + + const out = spec.interpretResponse(serverResponse, bidRequest); + expect(out).to.have.lengthOf(1); + + const b = out[0]; + expect(b.mediaType).to.equal('native'); + expect(b).to.have.property('native'); + expect(b.native).to.have.property('ortb'); + + expect(b.native.ortb.assets).to.deep.equal([{ id: 1, title: { text: 'Hello' } }]); + expect(b.native.ortb.imptrackers).to.deep.equal(['https://imp.example.com/1']); + expect(b.native.ortb.link).to.deep.equal({ url: 'https://click.example.com' }); + }); + + it('should handle multiple seatbids and multiple bids', function () { + const bidRequest = { + data: { + placements: [ + { bidId: 'b1', adFormat: 'banner' }, + { bidId: 'b2', adFormat: 'video' } + ] + } + }; + + const serverResponse = { + body: { + seatbid: [ + { bid: [{ id: 'b1', price: 1, crid: 'c1', dealid: 'd1', h: 250, w: 300, adm: '
', nurl: '', adomain: ['test.com'] }] }, + { bid: [{ id: 'b2', price: 2, crid: 'c2', dealid: 'd2', h: 360, w: 640, adm: '', nurl: '', adomain: ['test.com'] }] } + ] + } + }; + + const out = spec.interpretResponse(serverResponse, bidRequest); + expect(out).to.have.lengthOf(2); + expect(out.find(x => x.requestId === 'b1').mediaType).to.equal('banner'); + expect(out.find(x => x.requestId === 'b2').mediaType).to.equal('video'); + }); + }); +});