@tpmjs/churn-risk-score 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2025 TPMJS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,61 @@
1
+ import * as ai from 'ai';
2
+
3
+ /**
4
+ * Churn Risk Scoring Tool for TPMJS
5
+ * Scores customer churn risk based on usage, engagement, and support signals
6
+ *
7
+ * This is a proper AI SDK v6 tool that can be used with streamText()
8
+ * Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
9
+ */
10
+ interface CustomerData {
11
+ id: string;
12
+ name: string;
13
+ subscriptionStartDate: string;
14
+ lastLoginDate?: string;
15
+ loginCount30Days?: number;
16
+ activeUsersCount?: number;
17
+ totalSeats?: number;
18
+ supportTicketsCount30Days?: number;
19
+ negativeTicketsCount30Days?: number;
20
+ npsScore?: number;
21
+ billingIssues?: boolean;
22
+ contractEndDate?: string;
23
+ }
24
+ interface RiskFactor {
25
+ factor: string;
26
+ impact: 'high' | 'medium' | 'low';
27
+ score: number;
28
+ description: string;
29
+ }
30
+ interface ChurnRiskScore {
31
+ customerId: string;
32
+ customerName: string;
33
+ riskScore: number;
34
+ riskLevel: 'critical' | 'high' | 'medium' | 'low';
35
+ riskFactors: RiskFactor[];
36
+ recommendations: string[];
37
+ summary: string;
38
+ }
39
+ /**
40
+ * Input type for Churn Risk Score Tool
41
+ */
42
+ type ChurnRiskScoreInput = {
43
+ customer: CustomerData;
44
+ };
45
+ /**
46
+ * Churn Risk Score Tool
47
+ * Scores customer churn risk based on multiple signals
48
+ *
49
+ * This is a proper AI SDK v6 tool that can be used with streamText()
50
+ */
51
+ declare const churnRiskScoreTool: ai.Tool<ChurnRiskScoreInput, {
52
+ customerId: string;
53
+ customerName: string;
54
+ riskScore: number;
55
+ riskLevel: "high" | "medium" | "low" | "critical";
56
+ riskFactors: RiskFactor[];
57
+ recommendations: string[];
58
+ summary: string;
59
+ }>;
60
+
61
+ export { type ChurnRiskScore, type CustomerData, type RiskFactor, churnRiskScoreTool, churnRiskScoreTool as default };
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ import { tool, jsonSchema } from 'ai';
2
+
3
+ // src/index.ts
4
+ function daysBetween(date1, date2) {
5
+ const d1 = new Date(date1);
6
+ const d2 = new Date(date2);
7
+ return Math.abs(d2.getTime() - d1.getTime()) / (1e3 * 60 * 60 * 24);
8
+ }
9
+ function calculateUsageRisk(customer) {
10
+ const factors = [];
11
+ if (customer.lastLoginDate) {
12
+ const daysSinceLogin = daysBetween(customer.lastLoginDate, (/* @__PURE__ */ new Date()).toISOString());
13
+ if (daysSinceLogin > 30) {
14
+ factors.push({
15
+ factor: "Inactive User",
16
+ impact: "high",
17
+ score: 25,
18
+ description: `No login in ${Math.round(daysSinceLogin)} days`
19
+ });
20
+ } else if (daysSinceLogin > 14) {
21
+ factors.push({
22
+ factor: "Low Activity",
23
+ impact: "medium",
24
+ score: 15,
25
+ description: `Last login ${Math.round(daysSinceLogin)} days ago`
26
+ });
27
+ }
28
+ }
29
+ if (customer.loginCount30Days !== void 0) {
30
+ if (customer.loginCount30Days === 0) {
31
+ factors.push({
32
+ factor: "Zero Logins",
33
+ impact: "high",
34
+ score: 30,
35
+ description: "No logins in the last 30 days"
36
+ });
37
+ } else if (customer.loginCount30Days < 5) {
38
+ factors.push({
39
+ factor: "Low Login Frequency",
40
+ impact: "medium",
41
+ score: 15,
42
+ description: `Only ${customer.loginCount30Days} logins in 30 days`
43
+ });
44
+ }
45
+ }
46
+ if (customer.activeUsersCount !== void 0 && customer.totalSeats !== void 0) {
47
+ const utilization = customer.activeUsersCount / customer.totalSeats;
48
+ if (utilization < 0.3) {
49
+ factors.push({
50
+ factor: "Low Seat Utilization",
51
+ impact: "medium",
52
+ score: 12,
53
+ description: `Only ${Math.round(utilization * 100)}% of seats are active`
54
+ });
55
+ }
56
+ }
57
+ return factors;
58
+ }
59
+ function calculateEngagementRisk(customer) {
60
+ const factors = [];
61
+ if (customer.npsScore !== void 0) {
62
+ if (customer.npsScore <= 6) {
63
+ factors.push({
64
+ factor: "Detractor (NPS)",
65
+ impact: "high",
66
+ score: 20,
67
+ description: `NPS score of ${customer.npsScore} indicates dissatisfaction`
68
+ });
69
+ } else if (customer.npsScore <= 8) {
70
+ factors.push({
71
+ factor: "Passive (NPS)",
72
+ impact: "medium",
73
+ score: 10,
74
+ description: `NPS score of ${customer.npsScore} shows passive satisfaction`
75
+ });
76
+ }
77
+ }
78
+ if (customer.contractEndDate) {
79
+ const daysUntilEnd = daysBetween((/* @__PURE__ */ new Date()).toISOString(), customer.contractEndDate);
80
+ if (daysUntilEnd < 30) {
81
+ factors.push({
82
+ factor: "Contract Ending Soon",
83
+ impact: "high",
84
+ score: 15,
85
+ description: `Contract ends in ${Math.round(daysUntilEnd)} days`
86
+ });
87
+ } else if (daysUntilEnd < 60) {
88
+ factors.push({
89
+ factor: "Contract Renewal Approaching",
90
+ impact: "medium",
91
+ score: 8,
92
+ description: `Contract ends in ${Math.round(daysUntilEnd)} days`
93
+ });
94
+ }
95
+ }
96
+ return factors;
97
+ }
98
+ function calculateSupportRisk(customer) {
99
+ const factors = [];
100
+ if (customer.supportTicketsCount30Days !== void 0) {
101
+ if (customer.supportTicketsCount30Days > 10) {
102
+ factors.push({
103
+ factor: "High Support Volume",
104
+ impact: "medium",
105
+ score: 12,
106
+ description: `${customer.supportTicketsCount30Days} support tickets in 30 days`
107
+ });
108
+ }
109
+ }
110
+ if (customer.negativeTicketsCount30Days !== void 0 && customer.negativeTicketsCount30Days > 0) {
111
+ factors.push({
112
+ factor: "Negative Support Experience",
113
+ impact: "high",
114
+ score: 18,
115
+ description: `${customer.negativeTicketsCount30Days} negative support tickets`
116
+ });
117
+ }
118
+ if (customer.billingIssues) {
119
+ factors.push({
120
+ factor: "Billing Issues",
121
+ impact: "high",
122
+ score: 20,
123
+ description: "Active billing or payment issues"
124
+ });
125
+ }
126
+ return factors;
127
+ }
128
+ function generateRecommendations(factors) {
129
+ const recommendations = [];
130
+ const factorNames = factors.map((f) => f.factor);
131
+ if (factorNames.includes("Inactive User") || factorNames.includes("Zero Logins")) {
132
+ recommendations.push("Schedule an urgent check-in call to understand barriers to adoption");
133
+ }
134
+ if (factorNames.includes("Detractor (NPS)")) {
135
+ recommendations.push("Escalate to account manager for immediate intervention");
136
+ }
137
+ if (factorNames.includes("Low Seat Utilization")) {
138
+ recommendations.push("Offer onboarding sessions to increase team adoption");
139
+ }
140
+ if (factorNames.includes("Negative Support Experience")) {
141
+ recommendations.push("Review support tickets and follow up on unresolved issues");
142
+ }
143
+ if (factorNames.includes("Billing Issues")) {
144
+ recommendations.push("Resolve billing issues immediately - top churn indicator");
145
+ }
146
+ if (factorNames.includes("Contract Ending Soon")) {
147
+ recommendations.push("Initiate renewal conversation with decision maker");
148
+ }
149
+ if (factorNames.includes("High Support Volume")) {
150
+ recommendations.push("Identify root cause of support issues and provide proactive solutions");
151
+ }
152
+ if (recommendations.length === 0) {
153
+ recommendations.push("Continue regular engagement and monitor for changes in usage patterns");
154
+ }
155
+ return recommendations;
156
+ }
157
+ var churnRiskScoreTool = tool({
158
+ description: "Scores customer churn risk based on usage, engagement, and support signals. Provides risk score (0-100) with detailed contributing factors and recommendations.",
159
+ inputSchema: jsonSchema({
160
+ type: "object",
161
+ properties: {
162
+ customer: {
163
+ type: "object",
164
+ description: "Customer data with activity metrics",
165
+ properties: {
166
+ id: { type: "string", description: "Customer ID" },
167
+ name: { type: "string", description: "Customer name" },
168
+ subscriptionStartDate: {
169
+ type: "string",
170
+ description: "Subscription start date (ISO format)"
171
+ },
172
+ lastLoginDate: { type: "string", description: "Last login date (ISO format)" },
173
+ loginCount30Days: { type: "number", description: "Number of logins in last 30 days" },
174
+ activeUsersCount: { type: "number", description: "Number of active users" },
175
+ totalSeats: { type: "number", description: "Total licensed seats" },
176
+ supportTicketsCount30Days: {
177
+ type: "number",
178
+ description: "Support tickets in last 30 days"
179
+ },
180
+ negativeTicketsCount30Days: {
181
+ type: "number",
182
+ description: "Negative support tickets in last 30 days"
183
+ },
184
+ npsScore: { type: "number", description: "NPS score (0-10)" },
185
+ billingIssues: {
186
+ type: "boolean",
187
+ description: "Whether there are active billing issues"
188
+ },
189
+ contractEndDate: { type: "string", description: "Contract end date (ISO format)" }
190
+ },
191
+ required: ["id", "name", "subscriptionStartDate"]
192
+ }
193
+ },
194
+ required: ["customer"],
195
+ additionalProperties: false
196
+ }),
197
+ async execute({ customer }) {
198
+ if (!customer.id || !customer.name) {
199
+ throw new Error("Customer ID and name are required");
200
+ }
201
+ const usageFactors = calculateUsageRisk(customer);
202
+ const engagementFactors = calculateEngagementRisk(customer);
203
+ const supportFactors = calculateSupportRisk(customer);
204
+ const allFactors = [...usageFactors, ...engagementFactors, ...supportFactors];
205
+ const totalScore = Math.min(
206
+ 100,
207
+ allFactors.reduce((sum, factor) => sum + factor.score, 0)
208
+ );
209
+ let riskLevel;
210
+ if (totalScore >= 70) {
211
+ riskLevel = "critical";
212
+ } else if (totalScore >= 50) {
213
+ riskLevel = "high";
214
+ } else if (totalScore >= 25) {
215
+ riskLevel = "medium";
216
+ } else {
217
+ riskLevel = "low";
218
+ }
219
+ const recommendations = generateRecommendations(allFactors);
220
+ const highImpactFactors = allFactors.filter((f) => f.impact === "high");
221
+ let summary = `${customer.name} has a ${riskLevel} churn risk with a score of ${totalScore}/100.`;
222
+ if (highImpactFactors.length > 0) {
223
+ summary += ` Key concerns: ${highImpactFactors.map((f) => f.factor).join(", ")}.`;
224
+ } else {
225
+ summary += " No critical risk factors identified.";
226
+ }
227
+ return {
228
+ customerId: customer.id,
229
+ customerName: customer.name,
230
+ riskScore: totalScore,
231
+ riskLevel,
232
+ riskFactors: allFactors,
233
+ recommendations,
234
+ summary
235
+ };
236
+ }
237
+ });
238
+ var index_default = churnRiskScoreTool;
239
+
240
+ export { churnRiskScoreTool, index_default as default };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@tpmjs/churn-risk-score",
3
+ "version": "0.1.0",
4
+ "description": "Scores customer churn risk based on usage, engagement, and support signals",
5
+ "type": "module",
6
+ "keywords": [
7
+ "tpmjs",
8
+ "cx",
9
+ "churn",
10
+ "retention",
11
+ "customer-success",
12
+ "analytics"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "devDependencies": {
24
+ "tsup": "^8.3.5",
25
+ "typescript": "^5.9.3",
26
+ "@tpmjs/tsconfig": "0.0.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/ajaxdavis/tpmjs.git",
34
+ "directory": "packages/tools/official/churn-risk-score"
35
+ },
36
+ "homepage": "https://tpmjs.com",
37
+ "license": "MIT",
38
+ "tpmjs": {
39
+ "category": "cx",
40
+ "frameworks": [
41
+ "vercel-ai"
42
+ ],
43
+ "tools": [
44
+ {
45
+ "exportName": "churnRiskScoreTool",
46
+ "description": "Scores customer churn risk based on usage, engagement, and support signals. Provides risk score (0-100) with detailed contributing factors and recommendations.",
47
+ "parameters": [
48
+ {
49
+ "name": "customer",
50
+ "type": "object",
51
+ "description": "Customer data with activity metrics including usage, engagement, and support interactions",
52
+ "required": true
53
+ }
54
+ ],
55
+ "returns": {
56
+ "type": "ChurnRiskScore",
57
+ "description": "Risk score with contributing factors, risk level, and retention recommendations"
58
+ },
59
+ "aiAgent": {
60
+ "useCase": "Use this tool to identify at-risk customers, prioritize retention efforts, and proactively reduce churn. Ideal for customer success teams and account managers.",
61
+ "limitations": "Requires comprehensive customer data. Risk scoring is heuristic-based and should be combined with human judgment for critical decisions.",
62
+ "examples": [
63
+ "Identify customers at high risk of churning",
64
+ "Prioritize outreach for retention campaigns",
65
+ "Monitor customer health scores over time"
66
+ ]
67
+ }
68
+ }
69
+ ]
70
+ },
71
+ "dependencies": {
72
+ "ai": "6.0.0-beta.124"
73
+ },
74
+ "scripts": {
75
+ "build": "tsup",
76
+ "dev": "tsup --watch",
77
+ "type-check": "tsc --noEmit",
78
+ "clean": "rm -rf dist .turbo"
79
+ }
80
+ }