UNPKG

jobnimbus-mcp-client

Version:

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

388 lines 19.4 kB
/** * Get Customer Segmentation Analytics * Comprehensive customer segmentation with RFM analysis, clustering, personas, and targeting recommendations */ import { BaseTool } from '../baseTool.js'; export class GetCustomerSegmentationAnalyticsTool extends BaseTool { get definition() { return { name: 'get_customer_segmentation_analytics', description: 'Comprehensive customer segmentation with RFM analysis, clustering, persona development, behavioral segmentation, value tiers, and targeting recommendations', inputSchema: { type: 'object', properties: { days_back: { type: 'number', default: 365, description: 'Days of history for analysis (default: 365)', }, include_personas: { type: 'boolean', default: true, description: 'Include customer personas', }, include_targeting: { type: 'boolean', default: true, description: 'Include targeting recommendations', }, min_transactions: { type: 'number', default: 1, description: 'Minimum transactions to include customer (default: 1)', }, }, }, }; } async execute(input, context) { try { const daysBack = input.days_back || 365; // const includePersonas = input.include_personas !== false; const includeTargeting = input.include_targeting !== false; const minTransactions = input.min_transactions || 1; 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(); // const cutoffDate = now - (daysBack * 24 * 60 * 60 * 1000); // Build customer data const customerData = new Map(); // Initialize from contacts for (const contact of contacts) { const contactId = contact.jnid || contact.id; if (!contactId) continue; customerData.set(contactId, { name: `${contact.first_name || ''} ${contact.last_name || ''}`.trim() || contact.company || 'Unknown', company: contact.company || 'N/A', lastPurchaseDate: 0, purchases: 0, totalValue: 0, jobTypes: [], engagementActivities: 0, }); } // Add job data 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 (!customerData.has(contactId)) continue; const customer = customerData.get(contactId); const status = (job.status_name || '').toLowerCase(); if (status.includes('complete') || status.includes('won')) { customer.purchases++; const value = parseFloat(job.total || job.value || 0); customer.totalValue += value; const completedDate = job.date_status_change || job.date_updated || 0; customer.lastPurchaseDate = Math.max(customer.lastPurchaseDate, completedDate); const jobType = job.job_type || job.type || 'General'; if (!customer.jobTypes.includes(jobType)) { customer.jobTypes.push(jobType); } } } // Add activity/engagement data 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 (!customerData.has(contactId)) continue; const customer = customerData.get(contactId); customer.engagementActivities++; } // RFM Analysis const rfmAnalyses = []; for (const [contactId, customer] of customerData.entries()) { if (customer.purchases < minTransactions) continue; // Recency (days since last purchase) const recencyDays = customer.lastPurchaseDate > 0 ? (now - customer.lastPurchaseDate) / (1000 * 60 * 60 * 24) : 999; // Frequency (number of purchases) const frequency = customer.purchases; // Monetary (total value) const monetary = customer.totalValue; // RFM Scores (1-5) const recencyScore = recencyDays <= 30 ? 5 : recencyDays <= 90 ? 4 : recencyDays <= 180 ? 3 : recencyDays <= 365 ? 2 : 1; const frequencyScore = frequency >= 10 ? 5 : frequency >= 5 ? 4 : frequency >= 3 ? 3 : frequency >= 2 ? 2 : 1; const monetaryScore = monetary >= 50000 ? 5 : monetary >= 25000 ? 4 : monetary >= 10000 ? 3 : monetary >= 5000 ? 2 : 1; const rfmScore = `${recencyScore}${frequencyScore}${monetaryScore}`; // RFM Segment const rfmSegment = this.getRFMSegment(recencyScore, frequencyScore, monetaryScore); // Lifetime value const lifetimeValue = monetary; // Recommended action const recommendedAction = this.getRFMAction(rfmSegment); rfmAnalyses.push({ customer_id: contactId, customer_name: customer.name, recency_days: Math.round(recencyDays), frequency_count: frequency, monetary_value: monetary, rfm_score: rfmScore, rfm_segment: rfmSegment, lifetime_value: lifetimeValue, recommended_action: recommendedAction, }); } rfmAnalyses.sort((a, b) => b.lifetime_value - a.lifetime_value); // Customer segments const segmentMap = new Map(); for (const rfm of rfmAnalyses) { if (!segmentMap.has(rfm.rfm_segment)) { segmentMap.set(rfm.rfm_segment, { customers: [], totalValue: 0, recency: [], frequency: [], monetary: [] }); } const segment = segmentMap.get(rfm.rfm_segment); segment.customers.push(rfm.customer_id); segment.totalValue += rfm.monetary_value; segment.recency.push(rfm.recency_days); segment.frequency.push(rfm.frequency_count); segment.monetary.push(rfm.monetary_value); } const customerSegments = []; for (const [segmentName, data] of segmentMap.entries()) { const avgRecency = data.recency.reduce((sum, r) => sum + r, 0) / data.recency.length; const avgFrequency = data.frequency.reduce((sum, f) => sum + f, 0) / data.frequency.length; const avgMonetary = data.monetary.reduce((sum, m) => sum + m, 0) / data.monetary.length; const engagementLevel = avgRecency <= 30 && avgFrequency >= 5 ? 'Very High' : avgRecency <= 90 && avgFrequency >= 3 ? 'High' : avgRecency <= 180 ? 'Medium' : 'Low'; const churnRisk = avgRecency <= 90 ? 'Low' : avgRecency <= 180 ? 'Medium' : 'High'; const growthPotential = avgMonetary >= 25000 && avgFrequency < 5 ? 'High' : avgMonetary >= 10000 && avgFrequency < 3 ? 'Medium' : 'Low'; customerSegments.push({ segment_id: segmentName.toLowerCase().replace(/\s+/g, '_'), segment_name: segmentName, description: this.getSegmentDescription(segmentName), customer_count: data.customers.length, percentage_of_total: (data.customers.length / rfmAnalyses.length) * 100, avg_customer_value: avgMonetary, total_segment_value: data.totalValue, recency_score: 5 - Math.floor(avgRecency / 100), frequency_score: Math.min(Math.floor(avgFrequency / 2), 5), monetary_score: Math.min(Math.floor(avgMonetary / 10000), 5), engagement_level: engagementLevel, churn_risk: churnRisk, growth_potential: growthPotential, recommended_strategy: this.getSegmentStrategy(segmentName), }); } customerSegments.sort((a, b) => b.total_segment_value - a.total_segment_value); // Value segments const valueSegments = [ { segment_name: 'Diamond', min_value: 50000, max_value: Infinity, customer_count: 0, total_revenue: 0, avg_purchase_frequency: 0, retention_rate: 95, upsell_potential: 50, service_level: 'VIP - dedicated account manager', }, { segment_name: 'Platinum', min_value: 25000, max_value: 49999, customer_count: 0, total_revenue: 0, avg_purchase_frequency: 0, retention_rate: 85, upsell_potential: 40, service_level: 'Premium - priority support', }, { segment_name: 'Gold', min_value: 10000, max_value: 24999, customer_count: 0, total_revenue: 0, avg_purchase_frequency: 0, retention_rate: 75, upsell_potential: 30, service_level: 'Standard Plus - quarterly check-ins', }, { segment_name: 'Silver', min_value: 5000, max_value: 9999, customer_count: 0, total_revenue: 0, avg_purchase_frequency: 0, retention_rate: 65, upsell_potential: 20, service_level: 'Standard - regular communications', }, { segment_name: 'Bronze', min_value: 0, max_value: 4999, customer_count: 0, total_revenue: 0, avg_purchase_frequency: 0, retention_rate: 50, upsell_potential: 10, service_level: 'Basic - automated communications', }, ]; for (const rfm of rfmAnalyses) { const segment = valueSegments.find(s => rfm.monetary_value >= s.min_value && rfm.monetary_value <= s.max_value); if (segment) { segment.customer_count++; segment.total_revenue += rfm.monetary_value; segment.avg_purchase_frequency += rfm.frequency_count; } } for (const segment of valueSegments) { if (segment.customer_count > 0) { segment.avg_purchase_frequency /= segment.customer_count; } } // Targeting recommendations const targetingRecommendations = []; if (includeTargeting) { for (const segment of customerSegments.slice(0, 5)) { targetingRecommendations.push({ segment: segment.segment_name, priority: segment.total_segment_value > 100000 ? 'Critical' : segment.total_segment_value > 50000 ? 'High' : 'Medium', recommended_channels: this.getRecommendedChannels(segment.segment_name), message_themes: this.getMessageThemes(segment.segment_name), offer_suggestions: this.getOfferSuggestions(segment.segment_name), expected_roi: segment.growth_potential === 'High' ? 3.5 : segment.growth_potential === 'Medium' ? 2.5 : 1.5, effort_level: segment.churn_risk === 'High' ? 'High' : 'Medium', }); } } const segmentationMetrics = { total_customers: rfmAnalyses.length, total_segments: customerSegments.length, avg_customers_per_segment: rfmAnalyses.length / Math.max(customerSegments.length, 1), most_valuable_segment: customerSegments[0]?.segment_name || 'N/A', fastest_growing_segment: customerSegments.find(s => s.growth_potential === 'High')?.segment_name || 'N/A', }; return { data_source: 'Live JobNimbus API data', analysis_timestamp: new Date().toISOString(), analysis_period_days: daysBack, segmentation_metrics: segmentationMetrics, customer_segments: customerSegments, rfm_analysis: rfmAnalyses.slice(0, 20), value_segments: valueSegments, targeting_recommendations: includeTargeting ? targetingRecommendations : undefined, key_insights: [ `${rfmAnalyses.length} active customer(s) segmented`, `${customerSegments.length} distinct segment(s) identified`, `Top segment: ${customerSegments[0]?.segment_name} (${customerSegments[0]?.customer_count} customers)`, `Total customer value: $${customerSegments.reduce((sum, s) => sum + s.total_segment_value, 0).toLocaleString()}`, ], }; } catch (error) { return { error: error instanceof Error ? error.message : 'Unknown error', status: 'Failed', }; } } getRFMSegment(r, f, m) { if (r >= 4 && f >= 4 && m >= 4) return 'Champions'; if (r >= 3 && f >= 4) return 'Loyal'; if (r >= 4 && f <= 2) return 'New'; if (r >= 3 && f >= 2 && m >= 3) return 'Potential Loyalist'; if (r >= 3 && f <= 2) return 'Promising'; if (r >= 2 && f >= 2) return 'Need Attention'; if (r === 2) return 'About to Sleep'; if (r === 1 && f >= 2) return 'At Risk'; return 'Hibernating'; } getRFMAction(segment) { const actions = { 'Champions': 'Reward with VIP program, ask for referrals', 'Loyal': 'Upsell premium services, exclusive offers', 'Potential Loyalist': 'Engage with loyalty program, personalized offers', 'New': 'Onboard properly, build relationship', 'Promising': 'Create brand awareness, offer trials', 'Need Attention': 'Reactivate with limited offers, gather feedback', 'About to Sleep': 'Win back campaign, special discounts', 'At Risk': 'Intervention required, survey dissatisfaction', 'Hibernating': 'Re-engagement campaign or retire', }; return actions[segment] || 'Monitor and engage appropriately'; } getSegmentDescription(name) { const descriptions = { 'Champions': 'Best customers - recent, frequent, high value', 'Loyal': 'Consistent customers with good frequency', 'Potential Loyalist': 'Recent customers with growth potential', 'New': 'Recent first-time buyers', 'Promising': 'Recent low-spenders with potential', 'Need Attention': 'Above average recency/frequency/value but declining', 'About to Sleep': 'Below average recency, need reactivation', 'At Risk': 'High value but haven\'t purchased recently', 'Hibernating': 'Lowest recency, frequency, and value', }; return descriptions[name] || 'Customer segment'; } getSegmentStrategy(name) { const strategies = { 'Champions': 'Retain and grow - VIP treatment', 'Loyal': 'Nurture and upsell', 'Potential Loyalist': 'Build loyalty through engagement', 'New': 'Educate and convert to repeat', 'Promising': 'Increase purchase frequency', 'Need Attention': 'Reactivate with targeted offers', 'About to Sleep': 'Win-back campaign', 'At Risk': 'Save relationship urgently', 'Hibernating': 'Minimal investment or retire', }; return strategies[name] || 'Engage appropriately'; } getRecommendedChannels(segment) { if (segment === 'Champions' || segment === 'Loyal') return ['Direct call', 'Email', 'In-person']; if (segment === 'At Risk' || segment === 'About to Sleep') return ['Email', 'Direct mail', 'Retargeting ads']; return ['Email', 'Social media', 'Content marketing']; } getMessageThemes(segment) { if (segment === 'Champions') return ['Exclusive access', 'VIP rewards', 'Referral bonuses']; if (segment === 'At Risk') return ['We miss you', 'Special comeback offer', 'What can we improve?']; return ['Value proposition', 'Customer success stories', 'New offerings']; } getOfferSuggestions(segment) { if (segment === 'Champions') return ['Early access to new services', 'Volume discounts', 'Referral commissions']; if (segment === 'At Risk') return ['30% win-back discount', 'Free consultation', 'Service upgrade']; return ['Limited-time promotion', 'Bundle deals', 'Seasonal offers']; } } //# sourceMappingURL=getCustomerSegmentationAnalytics.js.map