/** Deterministic budget simulation from tenant ad_daily + kpi_weekly. */ import { AdDailyRow, KpiWeeklyRow } from '@/lib/metrics'; import { normalizePlatform } from '@/lib/ad-tracker-range'; export type SimChannel = 'meta' | 'google' | 'blended'; export type SimBaseline = { historical_cpl: number; conversion_rate_pct: number; avg_ticket: number; sample_spend: number; sample_leads: number; sample_deals: number; sample_revenue: number; channel: string; source: 'tenant_metrics'; }; export type SimBand = { leads: number; deals: number; revenue: number; roas: number; }; export type SimResult = { target_channel: string; baseline: SimBaseline; conservative_p10: SimBand; expected_p50: SimBand; optimistic_p90: SimBand; }; function n(v: unknown): number { const x = Number(v); return Number.isFinite(x) ? x : 0; } function matchChannel(platform: string | undefined, ch: SimChannel): boolean { if (ch === 'blended') return true; const p = normalizePlatform(platform); if (ch === 'meta') return p === 'meta'; if (ch === 'google') return p === 'google'; return false; } function band( spend: number, cpl: number, convRate: number, ticket: number, cplMult: number, convMult: number ): SimBand { const effectiveCpl = Math.max(1, cpl * cplMult); const leads = Math.round(spend / effectiveCpl); const deals = Math.round(leads * Math.min(0.95, convRate * convMult)); const revenue = deals * ticket; const roas = spend > 0 ? Math.round((revenue / spend) * 100) / 100 : 0; return { leads, deals, revenue: Math.round(revenue * 100) / 100, roas }; } /** Build simulation from this tenant's warehouse rows. */ export function simulateFromTenantMetrics(opts: { spend: number; channel: SimChannel; daily: AdDailyRow[]; weekly: KpiWeeklyRow[]; }): SimResult { const { spend, channel, daily, weekly } = opts; const dailyCh = daily.filter((r) => matchChannel(r.platform, channel)); const srcDaily = dailyCh.length ? dailyCh : daily; const sampleSpend = srcDaily.reduce((a, r) => a + n(r.spend), 0); const sampleLeads = srcDaily.reduce( (a, r) => a + n(r.platform_leads ?? r.conversions), 0 ); const weeklyCh = weekly.filter((r) => { if (channel === 'blended') return true; const c = (r.channel || '').toLowerCase(); if (channel === 'meta') { return ( c.includes('meta') || c.includes('facebook') || c.includes('instagram') || c.includes('fb') ); } if (channel === 'google') { return c.includes('google') || c.includes('search') || c.includes('pmax'); } return true; }); const srcWeekly = weeklyCh.length ? weeklyCh : weekly; const wSpend = srcWeekly.reduce((a, r) => a + n(r.total_spend), 0); const wLeads = srcWeekly.reduce((a, r) => a + n(r.total_leads), 0); const wDeals = srcWeekly.reduce((a, r) => a + n(r.cohort_deals_won), 0); const wRevenue = srcWeekly.reduce((a, r) => a + n(r.cohort_revenue_won), 0); const spendBase = sampleSpend > 0 ? sampleSpend : wSpend; const leadsBase = sampleLeads > 0 ? sampleLeads : wLeads; let historicalCpl = leadsBase > 0 ? spendBase / leadsBase : sampleSpend > 0 ? sampleSpend / Math.max(1, sampleLeads) : 25; if (!Number.isFinite(historicalCpl) || historicalCpl <= 0) historicalCpl = 25; const convRate = wLeads > 0 ? wDeals / wLeads : wDeals > 0 && leadsBase > 0 ? wDeals / leadsBase : 0.12; const avgTicket = wDeals > 0 ? wRevenue / wDeals : wRevenue > 0 ? wRevenue : 800; const baseline: SimBaseline = { historical_cpl: Math.round(historicalCpl * 100) / 100, conversion_rate_pct: Math.round(Math.max(0.01, convRate) * 1000) / 10, avg_ticket: Math.round(avgTicket * 100) / 100, sample_spend: Math.round(spendBase * 100) / 100, sample_leads: Math.round(leadsBase), sample_deals: Math.round(wDeals), sample_revenue: Math.round(wRevenue * 100) / 100, channel: channel === 'blended' ? 'blended' : channel, source: 'tenant_metrics', }; const cr = Math.max(0.01, convRate); return { target_channel: baseline.channel, baseline, conservative_p10: band(spend, historicalCpl, cr, avgTicket, 1.35, 0.75), expected_p50: band(spend, historicalCpl, cr, avgTicket, 1.0, 1.0), optimistic_p90: band(spend, historicalCpl, cr, avgTicket, 0.75, 1.2), }; }