import _ from "lodash"; import { Campaign, ChoiceRequest, CampaignGroup, HeaderBiddingType } from "../../models"; import { StepResult } from "../stepResult"; import { config } from "../../utils/config"; import logger from "../../utils/logger"; import { AbstractStep } from "../abstractStep"; import { CampaignModel } from "../../models/CampaignModel"; const CPM_TOP_LIMIT = parseInt(config.CPM_TOP_LIMIT); interface ThirdPartyFeeMap { [HeaderBiddingType.AmazonTAM]?: number; [HeaderBiddingType.MAX]?: number; [HeaderBiddingType.PreBidJS]?: number; } const defaultThirdPartyFeeMap: ThirdPartyFeeMap = { [HeaderBiddingType.AmazonTAM]: 0, [HeaderBiddingType.MAX]: 0, [HeaderBiddingType.PreBidJS]: 0, }; class BidfloorStep extends AbstractStep { constructor(private readonly campaignModel: CampaignModel) { super("bidfloor", ["has_bidfloor"]); this.autoPassCampaignGroup = CampaignGroup.Marketplace | CampaignGroup.Fallback; } async run(request: ChoiceRequest): Promise { const result: StepResult = new StepResult(); let bidfloor = request.bidfloor; result.setMetricLabel("has_bidfloor", bidfloor != null && bidfloor > 0); if (bidfloor == null) { bidfloor = 0; } const thirdPartyFeeMapString: string = this.featureConfigService.getParameterValue( request, "ms-choices.header-bidding.third-party-extra-fee", "" ); let thirdPartyFeeMap: ThirdPartyFeeMap = defaultThirdPartyFeeMap; try { if (thirdPartyFeeMapString !== "") { thirdPartyFeeMap = JSON.parse(thirdPartyFeeMapString) as ThirdPartyFeeMap; } } catch (e: any) { logger.error(e); } for (const campaign of request.campaigns) { // get bid price campaign.metadata.bid_price = this.getBidPrice(request, campaign, thirdPartyFeeMap); // filter campaigns with a bid price < bidfloor if (campaign.metadata.bid_price == null) { continue; } if (campaign.metadata.bid_price >= bidfloor) { result.passCampaigns(campaign); logger.debug( () => `[BIDFLOOR] Campaign ${campaign.id} passed because bid >= bidfloor (${campaign.metadata.bid_price} >= ${bidfloor})` ); } else { logger.debug( () => `[BIDFLOOR] Campaign ${campaign.id} filtered because bid < bidfloor (${campaign.metadata.bid_price} < ${bidfloor})` ); } } return result; } private getBidPrice( request: ChoiceRequest, campaign: Campaign, thirdPartyFeeMap: ThirdPartyFeeMap ): number { const campaignData = request.campaignData[campaign.id]; const budgetPerClick = (campaignData.budget ?? 0) / (campaignData.clicks_sold ?? 1); const contextScore = campaign.metadata.context_score != null && campaign.metadata.context_score >= 0 ? campaign.metadata.context_score : campaignData.ctr ?? 0; const margin = this.campaignModel.getMargin(campaignData, request.app?.ad_unit_type); const bidPriceWithoutFee = budgetPerClick * contextScore * (1 - margin / 100) * 1_000_000; const thirdPartyFee: number = (request.headerBiddingType ? thirdPartyFeeMap[request.headerBiddingType] ?? 0 : 0) * 1_000_000; const bidPriceWithFee: number = Math.round(bidPriceWithoutFee - thirdPartyFee); const bidPrice: number = _.clamp(bidPriceWithFee, 0, CPM_TOP_LIMIT - thirdPartyFee); logger.debug( () => `[BIDFLOOR] Campaign: ${ campaign.id } bidPriceWithoutFee: ${bidPriceWithoutFee} thirdPartyFee per imp: ${ thirdPartyFee / 1_000_000 } bidPriceWithFee: ${bidPriceWithFee} bidPrice clamped: ${bidPrice}` ); return bidPrice / 1000; } } export default BidfloorStep;