diff --git a/modules/tezaBidAdapter.js b/modules/tezaBidAdapter.js new file mode 100644 index 00000000000..20794702759 --- /dev/null +++ b/modules/tezaBidAdapter.js @@ -0,0 +1,144 @@ +// modules/tezaBidAdapter.js +// Minimal banner-only adapter for OpenRTB 2.x endpoint +// npm i -g gulp-cli # one time command +// npm ci # one time command +// Build: +// `gulp build --modules=tezaBidAdapter` +// Compiled output: ./build/dist/prebid.js + +// modules/tezaBidAdapter.js +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'teza'; +const ENDPOINT = 'https://dsp-us-east-1-nyc.tezatags.com/openrtb2/auction'; + +function sizesToFormat(sizes) { + return (sizes || []).map(s => ({ w: s[0], h: s[1] })); +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + return !!(bid?.params?.account); + }, + + buildRequests(validBidRequests, bidderRequest) { + if (!validBidRequests?.length) return []; + + const b0 = validBidRequests[0]; + const account = b0.params.account; + const test = b0.params.test ? 1 : 0; + + const ortb2 = bidderRequest?.ortb2 || config.getConfig('ortb2') || {}; + const eids = b0.userIdAsEids || []; + const schain = b0.schain || null; + const ri = bidderRequest?.refererInfo || {}; + + const imps = validBidRequests.map(bid => { + const sizes = bid.mediaTypes?.banner?.sizes || bid.sizes || []; + const tagid = bid.params.tagid || bid.ortb2Imp?.tagid || bid.ortb2Imp?.ext?.gpid || bid.adUnitCode; + return { + id: bid.bidId, + tagid: tagid, + secure: 1, + banner: { format: sizesToFormat(sizes) }, + bidfloor: bid.params.bidfloor || 0.01, + bidfloorcur: bid.params.bidfloorcur || 'USD' + }; + }); + + const regs = { + coppa: bidderRequest?.coppa ? 1 : 0, + ext: { + gdpr: bidderRequest?.gdprConsent?.gdprApplies ? 1 : 0, + us_privacy: bidderRequest?.uspConsent || undefined + }, + gpp: bidderRequest?.gppConsent?.gppString, + gpp_sid: bidderRequest?.gppConsent?.applicableSections + }; + + const user = { + ...ortb2.user, + ext: { + ...(ortb2.user?.ext || {}), + consent: bidderRequest?.gdprConsent?.consentString, + eids + } + }; + + const device = { + ...ortb2.device, + ua: (typeof navigator !== 'undefined' ? navigator.userAgent : ''), + language: (typeof navigator !== 'undefined' ? navigator.language : undefined), + dnt: (typeof navigator !== 'undefined' && navigator.doNotTrack === '1') ? 1 : 0 + }; + + const ortb = { + id: bidderRequest?.auctionId || String(Date.now()), + imp: imps, + at: 1, + tmax: bidderRequest?.timeout || 1000, + cur: ['USD'], + test, + site: { + ...ortb2.site, + domain: ri.domain || (typeof location !== 'undefined' ? location.hostname : ''), + page: ri.page || (typeof location !== 'undefined' ? location.href : '') + }, + device, + user, + regs, + source: schain ? { ext: { schain } } : undefined + }; + + const url = `${ENDPOINT}?test=${test}&account=${encodeURIComponent(account)}`; + return { method: 'POST', url, data: ortb }; + }, + + interpretResponse(serverResponse) { + const res = serverResponse?.body || {}; + const cur = Array.isArray(res.cur) ? (res.cur[0] || 'USD') : (res.cur || 'USD'); + const out = []; + + (res.seatbid || []).forEach(sb => { + (sb.bid || []).forEach(b => { + out.push({ + requestId: b.impid, + cpm: b.price || 0, + currency: cur, + width: b.w, + height: b.h, + creativeId: b.crid || b.id || 'teza-crid', + ttl: 30, + netRevenue: true, + ad: b.adm, + nurl: b.nurl, + burl: b.burl, + meta: { advertiserDomains: b.adomain || [] } + }); + }); + }); + + return out; + }, + + getUserSyncs() { return []; }, + + onBidWon(bid) { + let url = bid.burl || bid.nurl || ''; + if (!url) return; + + try { + url = decodeURIComponent(url); + } catch { /* ignore malformed encodings */ } + + url = url.replace(/\$\{AUCTION_PRICE\}/g, String(bid.cpm)); + new Image().src = url; + } +}; + +registerBidder(spec); diff --git a/modules/tezaBidAdapter.md b/modules/tezaBidAdapter.md new file mode 100644 index 00000000000..556285828da --- /dev/null +++ b/modules/tezaBidAdapter.md @@ -0,0 +1,41 @@ +# Overview + +``` +Module Name: Teza Bidder Adapter +Module Type: Bidder Adapter +``` + +# Description + +Minimal banner-only adapter for an OpenRTB 2.x endpoint. + +### Bid params + +| Name | Scope | Description | Example | Type | +| ------------- | -------- | --------------------------------------------------------------------------- | ---------- | --------- | +| `account` | required | Account identifier provided by Teza. | `acct123` | `string` | +| `tagid` | optional | Ad placement identifier; falls back to GPID or `adUnitCode` if not present. | `home-top` | `string` | +| `bidfloor` | optional | Minimum price to bid. Default `0.01`. | `0.10` | `number` | +| `bidfloorcur` | optional | Currency for `bidfloor`. Default `USD`. | `USD` | `string` | +| `test` | optional | When `true`, enables test mode (`test=1`) on requests. | `true` | `boolean` | + +# Test Parameters + +```js +var adUnits = [ + { + code: "div-1", + mediaTypes: { banner: { sizes: [[300, 250]] } }, + bids: [ + { + bidder: "teza", + params: { + account: "acct123", + bidfloor: 0.1, + test: true, + }, + }, + ], + }, +]; +``` diff --git a/test/spec/modules/tezaBidAdapter_spec.js b/test/spec/modules/tezaBidAdapter_spec.js new file mode 100644 index 00000000000..2147eb0e8a5 --- /dev/null +++ b/test/spec/modules/tezaBidAdapter_spec.js @@ -0,0 +1,190 @@ +// test/spec/modules/tezaBidAdapter_spec.js +import { expect } from 'chai'; +import { spec } from 'modules/tezaBidAdapter.js'; + +describe('tezaBidAdapter', function () { + const bidderRequest = { + auctionId: 'auc-1', + timeout: 1200, + refererInfo: { + domain: 'localhost', + page: 'http://localhost/test-teza.html', + }, + gdprConsent: { gdprApplies: true, consentString: 'CONSENT' }, + uspConsent: '1YNN', + gppConsent: { gppString: 'GPPSTRING', applicableSections: [7] }, + ortb2: { + user: { buyeruid: 'u1' }, + device: { dnt: 0 }, + site: { cat: ['IAB1'] }, + }, + }; + + const validBid = { + bidder: 'teza', + bidId: 'bid-1', + adUnitCode: 'div-1', + params: { tagid: 'atagid', account: 'acct123', test: true }, + schain: { ver: '1.0', complete: 1, nodes: [] }, + userIdAsEids: [{ source: 'uid.example', uids: [{ id: 'abc' }] }], + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [320, 50], + ], + }, + }, + }; + + describe('isBidRequestValid', function () { + it('returns true when tagid and account exist', function () { + expect(spec.isBidRequestValid(validBid)).to.equal(true); + }); + it('returns false when missing account', function () { + const b = { ...validBid, params: { tagid: 'atagid' } }; + expect(spec.isBidRequestValid(b)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('builds POST with account & test on query and expected ORTB fields', function () { + const req = spec.buildRequests([validBid], bidderRequest); + expect(req.method).to.equal('POST'); + expect(req.url).to.match( + /^https?:\/\/[^\s]+openrtb2\/auction\?test=1&account=acct123$/ + ); + expect(req.data).to.be.an('object'); + + const ortb = req.data; + expect(ortb.id).to.equal('auc-1'); + expect(ortb.cur).to.deep.equal(['USD']); + expect(ortb.test).to.equal(1); + + // site + expect(ortb.site.domain).to.equal('localhost'); + expect(ortb.site.page).to.match(/http:\/\/localhost\/test-teza\.html/); + + // imp + expect(ortb.imp).to.have.length(1); + expect(ortb.imp[0].tagid).to.equal('atagid'); + expect(ortb.imp[0].banner.format).to.deep.equal([ + { w: 300, h: 250 }, + { w: 320, h: 50 }, + ]); + + // user/device/regs + expect(ortb.device).to.be.an('object'); + expect(ortb.user.ext.consent).to.equal('CONSENT'); + expect(ortb.user.ext.eids).to.be.an('array').with.length(1); + expect(ortb.regs.ext.gdpr).to.equal(1); + expect(ortb.regs.ext.us_privacy).to.equal('1YNN'); + expect(ortb.regs.gpp).to.equal('GPPSTRING'); + expect(ortb.regs.gpp_sid).to.deep.equal([7]); + + // schain + expect(ortb.source.ext.schain).to.be.an('object'); + }); + }); + + describe('interpretResponse', function () { + it('maps OpenRTB seatbid to Prebid bids', function () { + const serverResponse = { + body: { + id: 'resp-1', + cur: 'USD', + seatbid: [ + { + seat: 'teza', + bid: [ + { + id: 'b1', + impid: 'bid-1', + price: 0.5, + w: 300, + h: 250, + crid: 'cr1', + adm: '