jobnimbus-mcp-client
Version:
JobNimbus MCP Client - Connect Claude Desktop to remote JobNimbus MCP server
388 lines • 19.4 kB
JavaScript
/**
* 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