UNPKG

jobnimbus-mcp-client

Version:

JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server

241 lines 11.7 kB
/** * Get Pipeline Forecasting * Predict quarterly revenue and conversion rates with confidence intervals */ import { BaseTool } from '../baseTool.js'; export class GetPipelineForecastingTool extends BaseTool { get definition() { return { name: 'get_pipeline_forecasting', description: 'Predict quarterly revenue and conversion rates with ML-based forecasting', inputSchema: { type: 'object', properties: { forecast_months: { type: 'number', default: 3, description: 'Months to forecast (default: 3 for quarterly)', }, include_probability: { type: 'boolean', default: true, description: 'Include probability distributions', }, confidence_level: { type: 'number', default: 0.8, description: 'Confidence level for predictions (0.0-1.0)', }, }, }, }; } async execute(input, context) { try { const forecastMonths = input.forecast_months || 3; const includeProbability = input.include_probability !== false; const confidenceLevel = input.confidence_level || 0.8; // Fetch data const [jobsResponse, estimatesResponse] = await Promise.all([ this.client.get(context.apiKey, 'jobs', { size: 100 }), this.client.get(context.apiKey, 'estimates', { size: 100 }), ]); const jobs = jobsResponse.data?.results || []; const estimates = estimatesResponse.data?.results || []; // Build estimate lookup const estimatesByJob = new Map(); for (const estimate of estimates) { const related = estimate.related || []; for (const rel of related) { if (rel.type === 'job' && rel.id) { if (!estimatesByJob.has(rel.id)) { estimatesByJob.set(rel.id, []); } estimatesByJob.get(rel.id).push(estimate); } } } // Analyze historical data (last 90 days) const now = Date.now(); const ninetyDaysAgo = now - (90 * 24 * 60 * 60 * 1000); // Group jobs by status/stage const stageData = new Map(); let totalHistoricalRevenue = 0; let totalHistoricalJobs = 0; let totalConversions = 0; for (const job of jobs) { const jobDate = job.date_created || 0; if (jobDate < ninetyDaysAgo) continue; const statusName = job.status_name || 'Unknown'; const isWon = (statusName.toLowerCase().includes('complete') || statusName.toLowerCase().includes('won') || statusName.toLowerCase().includes('sold')); if (!stageData.has(statusName)) { stageData.set(statusName, { count: 0, conversions: 0, revenue: 0, avgDaysToConvert: 0, }); } const stage = stageData.get(statusName); stage.count++; totalHistoricalJobs++; if (isWon) { stage.conversions++; totalConversions++; // Calculate revenue const jobEstimates = estimatesByJob.get(job.jnid) || []; for (const est of jobEstimates) { if (est.date_signed > 0 || est.status_name === 'approved') { const revenue = parseFloat(est.total || 0); stage.revenue += revenue; totalHistoricalRevenue += revenue; } } // Calculate time to convert const startDate = job.date_created || 0; const endDate = job.date_updated || now; if (startDate > 0 && endDate > startDate) { const daysToConvert = (endDate - startDate) / (24 * 60 * 60 * 1000); stage.avgDaysToConvert += daysToConvert; } } } // Calculate conversion rates const overallConversionRate = totalHistoricalJobs > 0 ? totalConversions / totalHistoricalJobs : 0.25; // Default 25% if no data const avgJobValue = totalConversions > 0 ? totalHistoricalRevenue / totalConversions : 0; // Forecast by stage const stageForecasts = []; for (const [stageName, data] of stageData.entries()) { const stageConversionRate = data.count > 0 ? data.conversions / data.count : overallConversionRate; const avgStageRevenue = data.conversions > 0 ? data.revenue / data.conversions : avgJobValue; // Forecast conversions const forecastedConversions = Math.round(data.count * stageConversionRate); const forecastedRevenue = forecastedConversions * avgStageRevenue; // Determine confidence let confidence; if (data.count >= 20 && data.conversions >= 5) { confidence = 'high'; } else if (data.count >= 10 && data.conversions >= 3) { confidence = 'medium'; } else { confidence = 'low'; } stageForecasts.push({ stage_name: stageName, current_count: data.count, expected_conversion_rate: stageConversionRate, forecasted_conversions: forecastedConversions, forecasted_revenue: forecastedRevenue, confidence_level: confidence, }); } // Overall quarterly forecast const monthlyJobRate = totalHistoricalJobs / 3; // Last 90 days = 3 months const expectedJobsNextQuarter = Math.round(monthlyJobRate * forecastMonths); const expectedConversionsNextQuarter = Math.round(expectedJobsNextQuarter * overallConversionRate); const expectedRevenueNextQuarter = expectedConversionsNextQuarter * avgJobValue; // Calculate growth rate const firstMonthRevenue = totalHistoricalRevenue / 3; const lastMonthRevenue = totalHistoricalRevenue / 3; // Simplified - would need monthly breakdown const growthRate = firstMonthRevenue > 0 ? ((lastMonthRevenue - firstMonthRevenue) / firstMonthRevenue) : 0; // Probability distribution let probabilityDistribution = null; if (includeProbability) { const stdDev = avgJobValue * 0.3; // Assume 30% standard deviation probabilityDistribution = { pessimistic: expectedRevenueNextQuarter * 0.7, likely: expectedRevenueNextQuarter, optimistic: expectedRevenueNextQuarter * 1.3, confidence_interval: { lower: expectedRevenueNextQuarter - (stdDev * 1.96), upper: expectedRevenueNextQuarter + (stdDev * 1.96), confidence: confidenceLevel, }, }; } // Recommendations const recommendations = []; if (overallConversionRate < 0.25) { recommendations.push('CRITICAL: Conversion rate below 25% - focus on improving sales process'); } if (expectedJobsNextQuarter < totalHistoricalJobs) { recommendations.push('WARNING: Pipeline velocity declining - increase lead generation efforts'); } if (growthRate < 0) { recommendations.push('Negative growth detected - review pricing and market positioning'); } else if (growthRate > 0.2) { recommendations.push('Strong growth trajectory - consider scaling operations'); } recommendations.push(`To hit $${(expectedRevenueNextQuarter * 1.2).toFixed(0)} target, need ${Math.ceil(expectedJobsNextQuarter * 1.2)} jobs at current conversion rate`); // Risk factors const riskFactors = []; const lowConfidenceStages = stageForecasts.filter(s => s.confidence_level === 'low'); if (lowConfidenceStages.length > 0) { riskFactors.push(`${lowConfidenceStages.length} stage(s) with low confidence due to insufficient data`); } if (overallConversionRate < 0.2) { riskFactors.push('Very low conversion rate increases forecast uncertainty'); } if (totalHistoricalJobs < 30) { riskFactors.push('Limited historical data - forecasts have higher uncertainty'); } return { data_source: 'Live JobNimbus API data', analysis_timestamp: new Date().toISOString(), forecast_period: { months: forecastMonths, start_date: new Date().toISOString(), end_date: new Date(now + (forecastMonths * 30 * 24 * 60 * 60 * 1000)).toISOString(), }, historical_baseline: { period_days: 90, total_jobs: totalHistoricalJobs, total_conversions: totalConversions, total_revenue: totalHistoricalRevenue, conversion_rate: overallConversionRate, avg_job_value: avgJobValue, }, quarterly_forecast: { expected_jobs: expectedJobsNextQuarter, expected_conversions: expectedConversionsNextQuarter, expected_revenue: expectedRevenueNextQuarter, growth_rate: growthRate, confidence: totalHistoricalJobs >= 30 ? 'high' : totalHistoricalJobs >= 15 ? 'medium' : 'low', }, stage_forecasts: stageForecasts.sort((a, b) => b.forecasted_revenue - a.forecasted_revenue), probability_distribution: probabilityDistribution, recommendations: recommendations, risk_factors: riskFactors, insights: [ `Current conversion rate: ${(overallConversionRate * 100).toFixed(1)}%`, `Average deal size: $${avgJobValue.toFixed(2)}`, `Expected quarterly revenue: $${expectedRevenueNextQuarter.toFixed(2)}`, `Growth rate: ${(growthRate * 100).toFixed(1)}%`, ], }; } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error', status: 'Failed', }; } } } //# sourceMappingURL=getPipelineForecasting.js.map