UNPKG

jobnimbus-mcp-client

Version:

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

406 lines 20.8 kB
/** * Get Customer Satisfaction Analytics * Satisfaction scoring, NPS-style metrics, feedback analysis, service quality tracking, and improvement recommendations */ import { BaseTool } from '../baseTool.js'; export class GetCustomerSatisfactionAnalyticsTool extends BaseTool { get definition() { return { name: 'get_customer_satisfaction_analytics', description: 'Customer satisfaction scoring with NPS metrics, feedback analysis, service quality tracking, churn risk identification, and improvement recommendations', inputSchema: { type: 'object', properties: { days_back: { type: 'number', default: 90, description: 'Days of history to analyze (default: 90)', }, include_churn_analysis: { type: 'boolean', default: true, description: 'Include churn risk analysis', }, min_jobs_for_feedback: { type: 'number', default: 1, description: 'Minimum jobs to include customer in analysis (default: 1)', }, nps_promoter_threshold: { type: 'number', default: 9, description: 'NPS promoter score threshold (default: 9)', }, nps_detractor_threshold: { type: 'number', default: 6, description: 'NPS detractor score threshold (default: 6)', }, }, }, }; } async execute(input, context) { try { const daysBack = input.days_back || 90; const includeChurnAnalysis = input.include_churn_analysis !== false; const minJobsForFeedback = input.min_jobs_for_feedback || 1; const promoterThreshold = input.nps_promoter_threshold || 9; const detractorThreshold = input.nps_detractor_threshold || 6; // Fetch data const [contactsResponse, jobsResponse, activitiesResponse] = await Promise.all([ this.client.get(context.apiKey, 'contacts', { size: 100 }), this.client.get(context.apiKey, 'jobs', { size: 100 }), this.client.get(context.apiKey, 'activities', { size: 100 }), ]); const contacts = contactsResponse.data?.results || []; const jobs = jobsResponse.data?.results || []; const activities = activitiesResponse.data?.activity || []; const now = Date.now(); // Build customer feedback map const customerMap = new Map(); // Initialize customer data for (const contact of contacts) { const contactId = contact.jnid || contact.id; if (!contactId) continue; customerMap.set(contactId, { name: `${contact.first_name || ''} ${contact.last_name || ''}`.trim() || contact.company || 'Unknown', company: contact.company || 'N/A', jobs: 0, completedJobs: 0, revenue: 0, lastInteraction: 0, responseTimes: [], onTimeDeliveries: 0, totalDeliveries: 0, }); } // Process jobs for (const job of jobs) { const related = job.related || []; const contactRel = related.find((r) => r.type === 'contact'); if (!contactRel || !contactRel.id) continue; const contactId = contactRel.id; if (!customerMap.has(contactId)) { customerMap.set(contactId, { name: 'Unknown', company: 'N/A', jobs: 0, completedJobs: 0, revenue: 0, lastInteraction: 0, responseTimes: [], onTimeDeliveries: 0, totalDeliveries: 0, }); } const customer = customerMap.get(contactId); customer.jobs++; const statusLower = (job.status_name || '').toLowerCase(); if (statusLower.includes('complete') || statusLower.includes('won')) { customer.completedJobs++; customer.totalDeliveries++; // On-time delivery check (if scheduled) const dateStart = job.date_start || 0; const dateEnd = job.date_end || 0; const dateCompleted = job.date_status_change || job.date_updated || 0; if (dateStart > 0 && dateEnd > 0 && dateCompleted > 0) { if (dateCompleted <= dateEnd) { customer.onTimeDeliveries++; } } } // Revenue tracking const jobValue = parseFloat(job.total || job.value || 0); if (jobValue > 0) { customer.revenue += jobValue; } // Last interaction const jobDate = job.date_updated || job.date_created || 0; customer.lastInteraction = Math.max(customer.lastInteraction, jobDate); } // Process activities for response times for (const activity of activities) { const related = activity.related || []; const contactRel = related.find((r) => r.type === 'contact'); if (!contactRel || !contactRel.id) continue; const contactId = contactRel.id; if (!customerMap.has(contactId)) continue; const customer = customerMap.get(contactId); // Response time (created to started) const dateCreated = activity.date_created || 0; const dateStarted = activity.date_start || 0; if (dateCreated > 0 && dateStarted > 0 && dateStarted > dateCreated) { const responseHours = (dateStarted - dateCreated) / (1000 * 60 * 60); customer.responseTimes.push(responseHours); } // Update last interaction const activityDate = activity.date_updated || activity.date_created || 0; customer.lastInteraction = Math.max(customer.lastInteraction, activityDate); } // Calculate satisfaction scores const customerFeedbacks = []; let promoters = 0; let passives = 0; let detractors = 0; for (const [contactId, customer] of customerMap.entries()) { // Filter: minimum jobs requirement if (customer.jobs < minJobsForFeedback) continue; // Service quality score (0-100) const completionRate = customer.jobs > 0 ? (customer.completedJobs / customer.jobs) * 100 : 0; const onTimeRate = customer.totalDeliveries > 0 ? (customer.onTimeDeliveries / customer.totalDeliveries) * 100 : 0; const serviceQualityRating = (completionRate * 0.5) + (onTimeRate * 0.5); // Response time score (0-100) const avgResponseHours = customer.responseTimes.length > 0 ? customer.responseTimes.reduce((sum, t) => sum + t, 0) / customer.responseTimes.length : 24; const responseTimeRating = Math.max(0, 100 - (avgResponseHours / 0.48)); // 48 hours = 0 score // Value for money (based on repeat business) const repeatCustomer = customer.completedJobs > 1; const valueForMoneyRating = repeatCustomer ? 85 : 70; // Overall satisfaction (weighted average) const satisfactionScore = (serviceQualityRating * 0.4) + (responseTimeRating * 0.3) + (valueForMoneyRating * 0.3); // NPS score (0-10 scale) const npsScore = Math.round((satisfactionScore / 100) * 10); // NPS category const npsCategory = npsScore >= promoterThreshold ? 'Promoter' : npsScore > detractorThreshold ? 'Passive' : 'Detractor'; if (npsCategory === 'Promoter') promoters++; else if (npsCategory === 'Passive') passives++; else detractors++; // Overall experience const overallExperience = satisfactionScore >= 85 ? 'Excellent' : satisfactionScore >= 70 ? 'Good' : satisfactionScore >= 50 ? 'Fair' : 'Poor'; // Days since last interaction const lastInteractionDays = customer.lastInteraction > 0 ? (now - customer.lastInteraction) / (1000 * 60 * 60 * 24) : 999; // Risk level const riskLevel = npsCategory === 'Detractor' && lastInteractionDays > 60 ? 'High Risk' : npsCategory === 'Detractor' ? 'Medium Risk' : npsCategory === 'Passive' && lastInteractionDays > 90 ? 'Medium Risk' : lastInteractionDays > 120 ? 'Low Risk' : 'Safe'; customerFeedbacks.push({ customer_id: contactId, customer_name: customer.name, company: customer.company, satisfaction_score: Math.round(satisfactionScore), nps_category: npsCategory, service_quality_rating: Math.round(serviceQualityRating), response_time_rating: Math.round(responseTimeRating), value_for_money_rating: valueForMoneyRating, overall_experience: overallExperience, likelihood_to_recommend: npsScore, repeat_customer: repeatCustomer, total_jobs: customer.jobs, total_revenue: customer.revenue, last_interaction_days: Math.round(lastInteractionDays), risk_level: riskLevel, }); } // Satisfaction metrics const totalCustomers = customerMap.size; const customersWithFeedback = customerFeedbacks.length; const avgSatisfactionScore = customersWithFeedback > 0 ? customerFeedbacks.reduce((sum, f) => sum + f.satisfaction_score, 0) / customersWithFeedback : 0; // NPS calculation: (% Promoters - % Detractors) const npsScore = customersWithFeedback > 0 ? ((promoters / customersWithFeedback) * 100) - ((detractors / customersWithFeedback) * 100) : 0; const repeatCustomers = customerFeedbacks.filter(f => f.repeat_customer).length; const satisfactionMetrics = { total_customers: totalCustomers, customers_with_feedback: customersWithFeedback, avg_satisfaction_score: avgSatisfactionScore, nps_score: npsScore, promoters: promoters, passives: passives, detractors: detractors, response_rate: totalCustomers > 0 ? (customersWithFeedback / totalCustomers) * 100 : 0, repeat_customer_rate: customersWithFeedback > 0 ? (repeatCustomers / customersWithFeedback) * 100 : 0, }; // Service quality metrics const serviceQualityMetrics = [ { metric_name: 'Job Completion Rate', score: customersWithFeedback > 0 ? customerFeedbacks.reduce((sum, f) => sum + f.service_quality_rating, 0) / customersWithFeedback : 0, category: 'Good', trend: 'Stable', benchmark: 90, gap: 0, }, { metric_name: 'Response Time', score: customersWithFeedback > 0 ? customerFeedbacks.reduce((sum, f) => sum + f.response_time_rating, 0) / customersWithFeedback : 0, category: 'Good', trend: 'Stable', benchmark: 85, gap: 0, }, { metric_name: 'Customer Retention', score: satisfactionMetrics.repeat_customer_rate, category: 'Needs Improvement', trend: 'Stable', benchmark: 70, gap: 0, }, ]; // Calculate gaps and categories for (const metric of serviceQualityMetrics) { metric.gap = metric.score - metric.benchmark; metric.category = metric.score >= 90 ? 'Excellent' : metric.score >= 75 ? 'Good' : metric.score >= 60 ? 'Needs Improvement' : 'Critical'; } // Feedback analysis const feedbackAnalyses = [ { sentiment: 'Positive', count: promoters, percentage: customersWithFeedback > 0 ? (promoters / customersWithFeedback) * 100 : 0, common_themes: ['Great service quality', 'Timely delivery', 'Professional team'], priority_actions: ['Maintain current service standards', 'Request testimonials'], }, { sentiment: 'Neutral', count: passives, percentage: customersWithFeedback > 0 ? (passives / customersWithFeedback) * 100 : 0, common_themes: ['Acceptable service', 'Room for improvement', 'Met basic expectations'], priority_actions: ['Follow up for detailed feedback', 'Identify improvement areas'], }, { sentiment: 'Negative', count: detractors, percentage: customersWithFeedback > 0 ? (detractors / customersWithFeedback) * 100 : 0, common_themes: ['Slow response times', 'Quality concerns', 'Poor communication'], priority_actions: ['Immediate intervention required', 'Root cause analysis', 'Service recovery plan'], }, ]; // Churn risk analysis const churnRiskAnalyses = []; if (includeChurnAnalysis) { const riskCategories = ['High Risk', 'Medium Risk', 'Low Risk']; for (const category of riskCategories) { const atRiskCustomers = customerFeedbacks.filter(f => f.risk_level === category); const totalRevenue = atRiskCustomers.reduce((sum, f) => sum + f.total_revenue, 0); const avgSat = atRiskCustomers.length > 0 ? atRiskCustomers.reduce((sum, f) => sum + f.satisfaction_score, 0) / atRiskCustomers.length : 0; const interventions = []; if (category === 'High Risk') { interventions.push('Immediate personal outreach by senior management'); interventions.push('Service recovery offer or discount'); interventions.push('Assign dedicated account manager'); } else if (category === 'Medium Risk') { interventions.push('Schedule check-in call within 7 days'); interventions.push('Send satisfaction survey'); interventions.push('Offer value-add services'); } else { interventions.push('Add to re-engagement campaign'); interventions.push('Monitor satisfaction metrics'); } churnRiskAnalyses.push({ risk_category: category, customer_count: atRiskCustomers.length, total_revenue_at_risk: totalRevenue, avg_satisfaction: avgSat, recommended_interventions: interventions, }); } } // Improvement recommendations const improvements = []; // Low NPS if (npsScore < 30) { improvements.push({ area: 'Net Promoter Score', priority: 'Critical', current_score: npsScore, target_score: 50, gap: 50 - npsScore, action_items: [ 'Conduct customer interviews to identify pain points', 'Implement service recovery program for detractors', 'Establish customer success team', ], estimated_impact: 'Increase retention by 20%, reduce churn by 15%', }); } // Low response time const responseMetric = serviceQualityMetrics.find(m => m.metric_name === 'Response Time'); if (responseMetric && responseMetric.gap < -10) { improvements.push({ area: 'Response Time', priority: 'High', current_score: responseMetric.score, target_score: responseMetric.benchmark, gap: Math.abs(responseMetric.gap), action_items: [ 'Implement automated response system', 'Set up 24-hour response SLA', 'Increase customer service staffing', ], estimated_impact: 'Improve customer satisfaction by 10-15 points', }); } // Low retention if (satisfactionMetrics.repeat_customer_rate < 50) { improvements.push({ area: 'Customer Retention', priority: 'High', current_score: satisfactionMetrics.repeat_customer_rate, target_score: 70, gap: 70 - satisfactionMetrics.repeat_customer_rate, action_items: [ 'Launch customer loyalty program', 'Implement post-job follow-up process', 'Offer repeat customer discounts', ], estimated_impact: 'Increase lifetime value by 30%', }); } return { data_source: 'Live JobNimbus API data', analysis_timestamp: new Date().toISOString(), analysis_period_days: daysBack, satisfaction_metrics: satisfactionMetrics, customer_feedbacks: customerFeedbacks.slice(0, 20), // Top 20 for brevity service_quality_metrics: serviceQualityMetrics, feedback_analysis: feedbackAnalyses, churn_risk_analysis: includeChurnAnalysis ? churnRiskAnalyses : undefined, improvement_recommendations: improvements, key_insights: [ `NPS Score: ${npsScore.toFixed(1)} (${npsScore >= 50 ? 'Excellent' : npsScore >= 30 ? 'Good' : npsScore >= 0 ? 'Fair' : 'Poor'})`, `${promoters} promoter(s), ${detractors} detractor(s)`, `Average satisfaction: ${avgSatisfactionScore.toFixed(1)}/100`, `${churnRiskAnalyses[0]?.customer_count || 0} high-risk customer(s)`, ], }; } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error', status: 'Failed', }; } } } //# sourceMappingURL=getCustomerSatisfactionAnalytics.js.map