business-as-code 0.2.1 → 2.0.2
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/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +17 -0
- package/IMPLEMENTATION.md +226 -0
- package/README.md +1133 -193
- package/dist/business.d.ts +62 -0
- package/dist/business.d.ts.map +1 -0
- package/dist/business.js +109 -0
- package/dist/business.js.map +1 -0
- package/dist/dollar.d.ts +60 -0
- package/dist/dollar.d.ts.map +1 -0
- package/dist/dollar.js +107 -0
- package/dist/dollar.js.map +1 -0
- package/dist/entities/assets.d.ts +21 -0
- package/dist/entities/assets.d.ts.map +1 -0
- package/dist/entities/assets.js +323 -0
- package/dist/entities/assets.js.map +1 -0
- package/dist/entities/business.d.ts +36 -0
- package/dist/entities/business.d.ts.map +1 -0
- package/dist/entities/business.js +370 -0
- package/dist/entities/business.js.map +1 -0
- package/dist/entities/communication.d.ts +21 -0
- package/dist/entities/communication.d.ts.map +1 -0
- package/dist/entities/communication.js +255 -0
- package/dist/entities/communication.js.map +1 -0
- package/dist/entities/customers.d.ts +58 -0
- package/dist/entities/customers.d.ts.map +1 -0
- package/dist/entities/customers.js +989 -0
- package/dist/entities/customers.js.map +1 -0
- package/dist/entities/financials.d.ts +59 -0
- package/dist/entities/financials.d.ts.map +1 -0
- package/dist/entities/financials.js +932 -0
- package/dist/entities/financials.js.map +1 -0
- package/dist/entities/goals.d.ts +58 -0
- package/dist/entities/goals.d.ts.map +1 -0
- package/dist/entities/goals.js +800 -0
- package/dist/entities/goals.js.map +1 -0
- package/dist/entities/index.d.ts +299 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/index.js +198 -0
- package/dist/entities/index.js.map +1 -0
- package/dist/entities/legal.d.ts +21 -0
- package/dist/entities/legal.d.ts.map +1 -0
- package/dist/entities/legal.js +301 -0
- package/dist/entities/legal.js.map +1 -0
- package/dist/entities/market.d.ts +21 -0
- package/dist/entities/market.d.ts.map +1 -0
- package/dist/entities/market.js +301 -0
- package/dist/entities/market.js.map +1 -0
- package/dist/entities/marketing.d.ts +67 -0
- package/dist/entities/marketing.d.ts.map +1 -0
- package/dist/entities/marketing.js +1157 -0
- package/dist/entities/marketing.js.map +1 -0
- package/dist/entities/offerings.d.ts +51 -0
- package/dist/entities/offerings.d.ts.map +1 -0
- package/dist/entities/offerings.js +727 -0
- package/dist/entities/offerings.js.map +1 -0
- package/dist/entities/operations.d.ts +58 -0
- package/dist/entities/operations.d.ts.map +1 -0
- package/dist/entities/operations.js +787 -0
- package/dist/entities/operations.js.map +1 -0
- package/dist/entities/organization.d.ts +57 -0
- package/dist/entities/organization.d.ts.map +1 -0
- package/dist/entities/organization.js +807 -0
- package/dist/entities/organization.js.map +1 -0
- package/dist/entities/partnerships.d.ts +21 -0
- package/dist/entities/partnerships.d.ts.map +1 -0
- package/dist/entities/partnerships.js +300 -0
- package/dist/entities/partnerships.js.map +1 -0
- package/dist/entities/planning.d.ts +87 -0
- package/dist/entities/planning.d.ts.map +1 -0
- package/dist/entities/planning.js +271 -0
- package/dist/entities/planning.js.map +1 -0
- package/dist/entities/projects.d.ts +25 -0
- package/dist/entities/projects.d.ts.map +1 -0
- package/dist/entities/projects.js +349 -0
- package/dist/entities/projects.js.map +1 -0
- package/dist/entities/risk.d.ts +21 -0
- package/dist/entities/risk.d.ts.map +1 -0
- package/dist/entities/risk.js +293 -0
- package/dist/entities/risk.js.map +1 -0
- package/dist/entities/sales.d.ts +72 -0
- package/dist/entities/sales.d.ts.map +1 -0
- package/dist/entities/sales.js +1248 -0
- package/dist/entities/sales.js.map +1 -0
- package/dist/financials.d.ts +130 -0
- package/dist/financials.d.ts.map +1 -0
- package/dist/financials.js +297 -0
- package/dist/financials.js.map +1 -0
- package/dist/goals.d.ts +87 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +215 -0
- package/dist/goals.js.map +1 -0
- package/dist/index.d.ts +97 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +131 -1079
- package/dist/index.js.map +1 -1
- package/dist/kpis.d.ts +118 -0
- package/dist/kpis.d.ts.map +1 -0
- package/dist/kpis.js +232 -0
- package/dist/kpis.js.map +1 -0
- package/dist/metrics.d.ts +448 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +325 -0
- package/dist/metrics.js.map +1 -0
- package/dist/okrs.d.ts +123 -0
- package/dist/okrs.d.ts.map +1 -0
- package/dist/okrs.js +269 -0
- package/dist/okrs.js.map +1 -0
- package/dist/organization.d.ts +585 -0
- package/dist/organization.d.ts.map +1 -0
- package/dist/organization.js +173 -0
- package/dist/organization.js.map +1 -0
- package/dist/process.d.ts +112 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +241 -0
- package/dist/process.js.map +1 -0
- package/dist/product.d.ts +85 -0
- package/dist/product.d.ts.map +1 -0
- package/dist/product.js +145 -0
- package/dist/product.js.map +1 -0
- package/dist/queries.d.ts +304 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +415 -0
- package/dist/queries.js.map +1 -0
- package/dist/roles.d.ts +340 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +255 -0
- package/dist/roles.js.map +1 -0
- package/dist/service.d.ts +61 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +140 -0
- package/dist/service.js.map +1 -0
- package/dist/types.d.ts +459 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/vision.d.ts +38 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +68 -0
- package/dist/vision.js.map +1 -0
- package/dist/workflow.d.ts +115 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +247 -0
- package/dist/workflow.js.map +1 -0
- package/examples/basic-usage.ts +307 -0
- package/package.json +19 -60
- package/src/business.ts +121 -0
- package/src/dollar.ts +132 -0
- package/src/entities/assets.ts +332 -0
- package/src/entities/business.ts +406 -0
- package/src/entities/communication.ts +264 -0
- package/src/entities/customers.ts +1072 -0
- package/src/entities/financials.ts +1011 -0
- package/src/entities/goals.ts +871 -0
- package/src/entities/index.ts +383 -0
- package/src/entities/legal.ts +310 -0
- package/src/entities/market.ts +310 -0
- package/src/entities/marketing.ts +1249 -0
- package/src/entities/offerings.ts +789 -0
- package/src/entities/operations.ts +861 -0
- package/src/entities/organization.ts +876 -0
- package/src/entities/partnerships.ts +309 -0
- package/src/entities/planning.ts +307 -0
- package/src/entities/projects.ts +360 -0
- package/src/entities/risk.ts +302 -0
- package/src/entities/sales.ts +1352 -0
- package/src/financials.ts +352 -0
- package/src/goals.ts +250 -0
- package/src/index.test.ts +336 -0
- package/src/index.ts +530 -0
- package/src/kpis.ts +275 -0
- package/src/metrics.ts +825 -0
- package/src/okrs.ts +325 -0
- package/src/organization.ts +909 -0
- package/src/process.ts +272 -0
- package/src/product.ts +178 -0
- package/src/queries.ts +767 -0
- package/src/roles.ts +686 -0
- package/src/service.ts +164 -0
- package/src/types.ts +493 -0
- package/src/vision.ts +88 -0
- package/src/workflow.ts +280 -0
- package/tsconfig.json +9 -0
- package/dist/loaders/index.d.ts +0 -174
- package/dist/loaders/index.js +0 -366
- package/dist/loaders/index.js.map +0 -1
- package/dist/schema/index.d.ts +0 -146
- package/dist/schema/index.js +0 -716
- package/dist/schema/index.js.map +0 -1
- package/dist/types-CJ9eGS_C.d.ts +0 -86
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized SaaS Metrics
|
|
3
|
+
*
|
|
4
|
+
* First-class types for common SaaS/subscription business metrics
|
|
5
|
+
* with auto-calculation over time periods.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Currency, TimePeriod } from './types.js'
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Time Period Types
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Date range for metric calculations
|
|
18
|
+
*/
|
|
19
|
+
export interface DateRange {
|
|
20
|
+
start: Date
|
|
21
|
+
end: Date
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Time period with explicit dates
|
|
26
|
+
*/
|
|
27
|
+
export interface MetricPeriod {
|
|
28
|
+
period: TimePeriod
|
|
29
|
+
range: DateRange
|
|
30
|
+
label?: string // e.g., "Q4 2024", "December 2024"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Time-series data point
|
|
35
|
+
*/
|
|
36
|
+
export interface DataPoint<T = number> {
|
|
37
|
+
timestamp: Date
|
|
38
|
+
value: T
|
|
39
|
+
metadata?: Record<string, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Time series of metric values
|
|
44
|
+
*/
|
|
45
|
+
export interface TimeSeries<T = number> {
|
|
46
|
+
metric: string
|
|
47
|
+
unit: string
|
|
48
|
+
dataPoints: DataPoint<T>[]
|
|
49
|
+
aggregation?: 'sum' | 'avg' | 'min' | 'max' | 'last' | 'first'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Revenue Metrics
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Monthly Recurring Revenue (MRR)
|
|
58
|
+
*/
|
|
59
|
+
export interface MRR {
|
|
60
|
+
total: number
|
|
61
|
+
newMRR: number // From new customers
|
|
62
|
+
expansionMRR: number // From upgrades
|
|
63
|
+
contractionMRR: number // From downgrades
|
|
64
|
+
churnedMRR: number // From cancellations
|
|
65
|
+
reactivationMRR: number // From reactivations
|
|
66
|
+
netNewMRR: number // newMRR + expansionMRR - contractionMRR - churnedMRR + reactivationMRR
|
|
67
|
+
currency: Currency
|
|
68
|
+
period: MetricPeriod
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Annual Recurring Revenue (ARR)
|
|
73
|
+
*/
|
|
74
|
+
export interface ARR {
|
|
75
|
+
total: number
|
|
76
|
+
fromMRR?: number // MRR * 12
|
|
77
|
+
contracted?: number // From annual contracts
|
|
78
|
+
currency: Currency
|
|
79
|
+
asOf: Date
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Net Revenue Retention (NRR) / Dollar-based Net Retention (DBNR)
|
|
84
|
+
*/
|
|
85
|
+
export interface NRR {
|
|
86
|
+
rate: number // Percentage (e.g., 115 = 115%)
|
|
87
|
+
startingMRR: number
|
|
88
|
+
endingMRR: number
|
|
89
|
+
expansion: number
|
|
90
|
+
contraction: number
|
|
91
|
+
churn: number
|
|
92
|
+
period: MetricPeriod
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gross Revenue Retention (GRR)
|
|
97
|
+
*/
|
|
98
|
+
export interface GRR {
|
|
99
|
+
rate: number // Percentage (max 100%)
|
|
100
|
+
startingMRR: number
|
|
101
|
+
endingMRR: number
|
|
102
|
+
contraction: number
|
|
103
|
+
churn: number
|
|
104
|
+
period: MetricPeriod
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Average Revenue Per User/Account
|
|
109
|
+
*/
|
|
110
|
+
export interface ARPU {
|
|
111
|
+
value: number
|
|
112
|
+
totalRevenue: number
|
|
113
|
+
totalUsers: number
|
|
114
|
+
currency: Currency
|
|
115
|
+
period: MetricPeriod
|
|
116
|
+
segment?: string // Optional segment (e.g., "enterprise", "smb")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Revenue by segment/cohort
|
|
121
|
+
*/
|
|
122
|
+
export interface RevenueSegment {
|
|
123
|
+
name: string
|
|
124
|
+
mrr: number
|
|
125
|
+
arr: number
|
|
126
|
+
customers: number
|
|
127
|
+
arpu: number
|
|
128
|
+
growth: number // MoM or YoY growth rate
|
|
129
|
+
currency: Currency
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// Customer Metrics
|
|
134
|
+
// =============================================================================
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Customer Acquisition Cost (CAC)
|
|
138
|
+
*/
|
|
139
|
+
export interface CAC {
|
|
140
|
+
value: number
|
|
141
|
+
totalSalesMarketingSpend: number
|
|
142
|
+
newCustomersAcquired: number
|
|
143
|
+
currency: Currency
|
|
144
|
+
period: MetricPeriod
|
|
145
|
+
byChannel?: Record<string, number>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Customer Lifetime Value (LTV/CLV)
|
|
150
|
+
*/
|
|
151
|
+
export interface LTV {
|
|
152
|
+
value: number
|
|
153
|
+
arpu: number
|
|
154
|
+
grossMargin: number // Percentage
|
|
155
|
+
churnRate: number // Monthly churn rate
|
|
156
|
+
averageLifetimeMonths: number
|
|
157
|
+
currency: Currency
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* LTV:CAC Ratio
|
|
162
|
+
*/
|
|
163
|
+
export interface LTVtoCAC {
|
|
164
|
+
ratio: number
|
|
165
|
+
ltv: number
|
|
166
|
+
cac: number
|
|
167
|
+
paybackMonths: number // CAC / (ARPU * Gross Margin)
|
|
168
|
+
healthy: boolean // > 3 is generally healthy
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Churn metrics
|
|
173
|
+
*/
|
|
174
|
+
export interface Churn {
|
|
175
|
+
// Customer churn (logo churn)
|
|
176
|
+
customerChurnRate: number // Percentage
|
|
177
|
+
customersLost: number
|
|
178
|
+
customersStart: number
|
|
179
|
+
|
|
180
|
+
// Revenue churn
|
|
181
|
+
revenueChurnRate: number // Percentage (gross churn)
|
|
182
|
+
mrrChurned: number
|
|
183
|
+
|
|
184
|
+
// Net revenue churn (can be negative with good expansion)
|
|
185
|
+
netRevenueChurnRate: number
|
|
186
|
+
|
|
187
|
+
period: MetricPeriod
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Retention cohort
|
|
192
|
+
*/
|
|
193
|
+
export interface RetentionCohort {
|
|
194
|
+
cohortDate: Date
|
|
195
|
+
cohortLabel: string // e.g., "Jan 2024"
|
|
196
|
+
initialCustomers: number
|
|
197
|
+
initialMRR: number
|
|
198
|
+
retentionByMonth: number[] // Array of retention rates by month
|
|
199
|
+
revenueByMonth: number[] // Array of MRR by month
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// =============================================================================
|
|
203
|
+
// Growth Metrics
|
|
204
|
+
// =============================================================================
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Growth rate metrics
|
|
208
|
+
*/
|
|
209
|
+
export interface GrowthRate {
|
|
210
|
+
mom: number // Month-over-month
|
|
211
|
+
qoq: number // Quarter-over-quarter
|
|
212
|
+
yoy: number // Year-over-year
|
|
213
|
+
cagr?: number // Compound annual growth rate
|
|
214
|
+
metric: string // What metric this growth rate is for
|
|
215
|
+
period: MetricPeriod
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Quick ratio (growth efficiency)
|
|
220
|
+
* (New MRR + Expansion MRR) / (Churned MRR + Contraction MRR)
|
|
221
|
+
*/
|
|
222
|
+
export interface QuickRatio {
|
|
223
|
+
ratio: number
|
|
224
|
+
newMRR: number
|
|
225
|
+
expansionMRR: number
|
|
226
|
+
churnedMRR: number
|
|
227
|
+
contractionMRR: number
|
|
228
|
+
healthy: boolean // > 4 is good, > 1 means growing
|
|
229
|
+
period: MetricPeriod
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Efficiency Metrics
|
|
234
|
+
// =============================================================================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Magic Number
|
|
238
|
+
* Net New ARR / Sales & Marketing Spend (previous quarter)
|
|
239
|
+
*/
|
|
240
|
+
export interface MagicNumber {
|
|
241
|
+
value: number
|
|
242
|
+
netNewARR: number
|
|
243
|
+
salesMarketingSpend: number
|
|
244
|
+
efficient: boolean // > 0.75 is efficient
|
|
245
|
+
period: MetricPeriod
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Burn Multiple
|
|
250
|
+
* Net Burn / Net New ARR
|
|
251
|
+
*/
|
|
252
|
+
export interface BurnMultiple {
|
|
253
|
+
value: number
|
|
254
|
+
netBurn: number
|
|
255
|
+
netNewARR: number
|
|
256
|
+
efficient: boolean // < 1.5 is good
|
|
257
|
+
period: MetricPeriod
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Rule of 40
|
|
262
|
+
* Growth Rate + Profit Margin >= 40%
|
|
263
|
+
*/
|
|
264
|
+
export interface RuleOf40 {
|
|
265
|
+
score: number
|
|
266
|
+
revenueGrowthRate: number
|
|
267
|
+
profitMargin: number // Or EBITDA margin
|
|
268
|
+
passing: boolean // >= 40 is passing
|
|
269
|
+
period: MetricPeriod
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* SaaS Efficiency Score
|
|
274
|
+
* Combines multiple efficiency metrics
|
|
275
|
+
*/
|
|
276
|
+
export interface EfficiencyScore {
|
|
277
|
+
overall: number // 0-100 score
|
|
278
|
+
components: {
|
|
279
|
+
ltvCacRatio: number
|
|
280
|
+
magicNumber: number
|
|
281
|
+
quickRatio: number
|
|
282
|
+
nrr: number
|
|
283
|
+
ruleOf40: number
|
|
284
|
+
}
|
|
285
|
+
period: MetricPeriod
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// =============================================================================
|
|
289
|
+
// Pipeline & Sales Metrics
|
|
290
|
+
// =============================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Sales pipeline metrics
|
|
294
|
+
*/
|
|
295
|
+
export interface Pipeline {
|
|
296
|
+
totalValue: number
|
|
297
|
+
weightedValue: number // Probability-adjusted
|
|
298
|
+
stages: PipelineStage[]
|
|
299
|
+
velocity: number // Average days to close
|
|
300
|
+
conversionRate: number // Win rate
|
|
301
|
+
currency: Currency
|
|
302
|
+
asOf: Date
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Pipeline stage
|
|
307
|
+
*/
|
|
308
|
+
export interface PipelineStage {
|
|
309
|
+
name: string
|
|
310
|
+
value: number
|
|
311
|
+
count: number
|
|
312
|
+
probability: number
|
|
313
|
+
averageDaysInStage: number
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Sales velocity
|
|
318
|
+
* (Opportunities * Win Rate * Average Deal Size) / Sales Cycle Length
|
|
319
|
+
*/
|
|
320
|
+
export interface SalesVelocity {
|
|
321
|
+
value: number // Revenue per day
|
|
322
|
+
opportunities: number
|
|
323
|
+
winRate: number
|
|
324
|
+
averageDealSize: number
|
|
325
|
+
salesCycleLength: number // Days
|
|
326
|
+
currency: Currency
|
|
327
|
+
period: MetricPeriod
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// =============================================================================
|
|
331
|
+
// Operational Metrics
|
|
332
|
+
// =============================================================================
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Net Promoter Score
|
|
336
|
+
*/
|
|
337
|
+
export interface NPS {
|
|
338
|
+
score: number // -100 to 100
|
|
339
|
+
promoters: number // 9-10
|
|
340
|
+
passives: number // 7-8
|
|
341
|
+
detractors: number // 0-6
|
|
342
|
+
responses: number
|
|
343
|
+
responseRate?: number
|
|
344
|
+
asOf: Date
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Customer health score
|
|
349
|
+
*/
|
|
350
|
+
export interface CustomerHealth {
|
|
351
|
+
averageScore: number // 0-100
|
|
352
|
+
healthy: number // Count
|
|
353
|
+
atRisk: number // Count
|
|
354
|
+
critical: number // Count
|
|
355
|
+
factors: HealthFactor[]
|
|
356
|
+
asOf: Date
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Health factor
|
|
361
|
+
*/
|
|
362
|
+
export interface HealthFactor {
|
|
363
|
+
name: string
|
|
364
|
+
weight: number
|
|
365
|
+
score: number
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// =============================================================================
|
|
369
|
+
// Financial Summary
|
|
370
|
+
// =============================================================================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Comprehensive SaaS metrics snapshot
|
|
374
|
+
*/
|
|
375
|
+
export interface SaaSMetrics {
|
|
376
|
+
// Revenue
|
|
377
|
+
mrr: MRR
|
|
378
|
+
arr: ARR
|
|
379
|
+
nrr: NRR
|
|
380
|
+
grr: GRR
|
|
381
|
+
arpu: ARPU
|
|
382
|
+
|
|
383
|
+
// Customers
|
|
384
|
+
cac: CAC
|
|
385
|
+
ltv: LTV
|
|
386
|
+
ltvCac: LTVtoCAC
|
|
387
|
+
churn: Churn
|
|
388
|
+
|
|
389
|
+
// Growth
|
|
390
|
+
growthRate: GrowthRate
|
|
391
|
+
quickRatio: QuickRatio
|
|
392
|
+
|
|
393
|
+
// Efficiency
|
|
394
|
+
magicNumber?: MagicNumber
|
|
395
|
+
burnMultiple?: BurnMultiple
|
|
396
|
+
ruleOf40?: RuleOf40
|
|
397
|
+
|
|
398
|
+
// Operational
|
|
399
|
+
nps?: NPS
|
|
400
|
+
customerHealth?: CustomerHealth
|
|
401
|
+
|
|
402
|
+
// Period
|
|
403
|
+
period: MetricPeriod
|
|
404
|
+
generatedAt: Date
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// =============================================================================
|
|
408
|
+
// Calculation Functions
|
|
409
|
+
// =============================================================================
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Calculate MRR from components
|
|
413
|
+
*/
|
|
414
|
+
export function calculateMRR(input: {
|
|
415
|
+
newMRR: number
|
|
416
|
+
expansionMRR: number
|
|
417
|
+
contractionMRR: number
|
|
418
|
+
churnedMRR: number
|
|
419
|
+
reactivationMRR?: number
|
|
420
|
+
previousMRR: number
|
|
421
|
+
currency?: Currency
|
|
422
|
+
period: MetricPeriod
|
|
423
|
+
}): MRR {
|
|
424
|
+
const reactivationMRR = input.reactivationMRR || 0
|
|
425
|
+
const netNewMRR = input.newMRR + input.expansionMRR - input.contractionMRR - input.churnedMRR + reactivationMRR
|
|
426
|
+
const total = input.previousMRR + netNewMRR
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
total,
|
|
430
|
+
newMRR: input.newMRR,
|
|
431
|
+
expansionMRR: input.expansionMRR,
|
|
432
|
+
contractionMRR: input.contractionMRR,
|
|
433
|
+
churnedMRR: input.churnedMRR,
|
|
434
|
+
reactivationMRR,
|
|
435
|
+
netNewMRR,
|
|
436
|
+
currency: input.currency || 'USD',
|
|
437
|
+
period: input.period,
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Calculate ARR from MRR
|
|
443
|
+
*/
|
|
444
|
+
export function calculateARRFromMRR(mrr: number, currency: Currency = 'USD'): ARR {
|
|
445
|
+
return {
|
|
446
|
+
total: mrr * 12,
|
|
447
|
+
fromMRR: mrr * 12,
|
|
448
|
+
currency,
|
|
449
|
+
asOf: new Date(),
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Calculate NRR
|
|
455
|
+
*/
|
|
456
|
+
export function calculateNRR(input: {
|
|
457
|
+
startingMRR: number
|
|
458
|
+
expansion: number
|
|
459
|
+
contraction: number
|
|
460
|
+
churn: number
|
|
461
|
+
period: MetricPeriod
|
|
462
|
+
}): NRR {
|
|
463
|
+
const endingMRR = input.startingMRR + input.expansion - input.contraction - input.churn
|
|
464
|
+
const rate = input.startingMRR > 0 ? (endingMRR / input.startingMRR) * 100 : 0
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
rate,
|
|
468
|
+
startingMRR: input.startingMRR,
|
|
469
|
+
endingMRR,
|
|
470
|
+
expansion: input.expansion,
|
|
471
|
+
contraction: input.contraction,
|
|
472
|
+
churn: input.churn,
|
|
473
|
+
period: input.period,
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Calculate GRR
|
|
479
|
+
*/
|
|
480
|
+
export function calculateGRR(input: {
|
|
481
|
+
startingMRR: number
|
|
482
|
+
contraction: number
|
|
483
|
+
churn: number
|
|
484
|
+
period: MetricPeriod
|
|
485
|
+
}): GRR {
|
|
486
|
+
const endingMRR = input.startingMRR - input.contraction - input.churn
|
|
487
|
+
const rate = input.startingMRR > 0 ? Math.min((endingMRR / input.startingMRR) * 100, 100) : 0
|
|
488
|
+
|
|
489
|
+
return {
|
|
490
|
+
rate,
|
|
491
|
+
startingMRR: input.startingMRR,
|
|
492
|
+
endingMRR,
|
|
493
|
+
contraction: input.contraction,
|
|
494
|
+
churn: input.churn,
|
|
495
|
+
period: input.period,
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Calculate CAC
|
|
501
|
+
*/
|
|
502
|
+
export function calculateCACMetric(input: {
|
|
503
|
+
salesMarketingSpend: number
|
|
504
|
+
newCustomers: number
|
|
505
|
+
currency?: Currency
|
|
506
|
+
period: MetricPeriod
|
|
507
|
+
byChannel?: Record<string, { spend: number; customers: number }>
|
|
508
|
+
}): CAC {
|
|
509
|
+
const value = input.newCustomers > 0 ? input.salesMarketingSpend / input.newCustomers : 0
|
|
510
|
+
|
|
511
|
+
let byChannel: Record<string, number> | undefined
|
|
512
|
+
if (input.byChannel) {
|
|
513
|
+
byChannel = {}
|
|
514
|
+
for (const [channel, data] of Object.entries(input.byChannel)) {
|
|
515
|
+
byChannel[channel] = data.customers > 0 ? data.spend / data.customers : 0
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
value,
|
|
521
|
+
totalSalesMarketingSpend: input.salesMarketingSpend,
|
|
522
|
+
newCustomersAcquired: input.newCustomers,
|
|
523
|
+
currency: input.currency || 'USD',
|
|
524
|
+
period: input.period,
|
|
525
|
+
byChannel,
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Calculate LTV
|
|
531
|
+
*/
|
|
532
|
+
export function calculateLTVMetric(input: {
|
|
533
|
+
arpu: number
|
|
534
|
+
grossMargin: number
|
|
535
|
+
churnRate: number
|
|
536
|
+
currency?: Currency
|
|
537
|
+
}): LTV {
|
|
538
|
+
// LTV = (ARPU * Gross Margin) / Churn Rate
|
|
539
|
+
const averageLifetimeMonths = input.churnRate > 0 ? 1 / input.churnRate : 0
|
|
540
|
+
const value = input.churnRate > 0 ? (input.arpu * input.grossMargin / 100) / input.churnRate : 0
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
value,
|
|
544
|
+
arpu: input.arpu,
|
|
545
|
+
grossMargin: input.grossMargin,
|
|
546
|
+
churnRate: input.churnRate,
|
|
547
|
+
averageLifetimeMonths,
|
|
548
|
+
currency: input.currency || 'USD',
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Calculate LTV:CAC ratio
|
|
554
|
+
*/
|
|
555
|
+
export function calculateLTVtoCACRatio(ltv: LTV, cac: CAC): LTVtoCAC {
|
|
556
|
+
const ratio = cac.value > 0 ? ltv.value / cac.value : 0
|
|
557
|
+
const paybackMonths = ltv.arpu > 0 && ltv.grossMargin > 0
|
|
558
|
+
? cac.value / (ltv.arpu * ltv.grossMargin / 100)
|
|
559
|
+
: 0
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
ratio,
|
|
563
|
+
ltv: ltv.value,
|
|
564
|
+
cac: cac.value,
|
|
565
|
+
paybackMonths,
|
|
566
|
+
healthy: ratio >= 3,
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Calculate Quick Ratio
|
|
572
|
+
*/
|
|
573
|
+
export function calculateQuickRatioMetric(mrr: MRR): QuickRatio {
|
|
574
|
+
const growth = mrr.newMRR + mrr.expansionMRR
|
|
575
|
+
const loss = mrr.churnedMRR + mrr.contractionMRR
|
|
576
|
+
const ratio = loss > 0 ? growth / loss : growth > 0 ? Infinity : 0
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
ratio,
|
|
580
|
+
newMRR: mrr.newMRR,
|
|
581
|
+
expansionMRR: mrr.expansionMRR,
|
|
582
|
+
churnedMRR: mrr.churnedMRR,
|
|
583
|
+
contractionMRR: mrr.contractionMRR,
|
|
584
|
+
healthy: ratio >= 4,
|
|
585
|
+
period: mrr.period,
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Calculate Magic Number
|
|
591
|
+
*/
|
|
592
|
+
export function calculateMagicNumberMetric(input: {
|
|
593
|
+
netNewARR: number
|
|
594
|
+
salesMarketingSpend: number
|
|
595
|
+
period: MetricPeriod
|
|
596
|
+
}): MagicNumber {
|
|
597
|
+
const value = input.salesMarketingSpend > 0 ? input.netNewARR / input.salesMarketingSpend : 0
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
value,
|
|
601
|
+
netNewARR: input.netNewARR,
|
|
602
|
+
salesMarketingSpend: input.salesMarketingSpend,
|
|
603
|
+
efficient: value >= 0.75,
|
|
604
|
+
period: input.period,
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Calculate Burn Multiple
|
|
610
|
+
*/
|
|
611
|
+
export function calculateBurnMultipleMetric(input: {
|
|
612
|
+
netBurn: number
|
|
613
|
+
netNewARR: number
|
|
614
|
+
period: MetricPeriod
|
|
615
|
+
}): BurnMultiple {
|
|
616
|
+
const value = input.netNewARR > 0 ? input.netBurn / input.netNewARR : Infinity
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
value,
|
|
620
|
+
netBurn: input.netBurn,
|
|
621
|
+
netNewARR: input.netNewARR,
|
|
622
|
+
efficient: value <= 1.5,
|
|
623
|
+
period: input.period,
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Calculate Rule of 40
|
|
629
|
+
*/
|
|
630
|
+
export function calculateRuleOf40Metric(input: {
|
|
631
|
+
revenueGrowthRate: number
|
|
632
|
+
profitMargin: number
|
|
633
|
+
period: MetricPeriod
|
|
634
|
+
}): RuleOf40 {
|
|
635
|
+
const score = input.revenueGrowthRate + input.profitMargin
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
score,
|
|
639
|
+
revenueGrowthRate: input.revenueGrowthRate,
|
|
640
|
+
profitMargin: input.profitMargin,
|
|
641
|
+
passing: score >= 40,
|
|
642
|
+
period: input.period,
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Calculate growth rates
|
|
648
|
+
*/
|
|
649
|
+
export function calculateGrowthRates(input: {
|
|
650
|
+
current: number
|
|
651
|
+
previousMonth?: number
|
|
652
|
+
previousQuarter?: number
|
|
653
|
+
previousYear?: number
|
|
654
|
+
metric: string
|
|
655
|
+
period: MetricPeriod
|
|
656
|
+
}): GrowthRate {
|
|
657
|
+
const mom = input.previousMonth && input.previousMonth > 0
|
|
658
|
+
? ((input.current - input.previousMonth) / input.previousMonth) * 100
|
|
659
|
+
: 0
|
|
660
|
+
const qoq = input.previousQuarter && input.previousQuarter > 0
|
|
661
|
+
? ((input.current - input.previousQuarter) / input.previousQuarter) * 100
|
|
662
|
+
: 0
|
|
663
|
+
const yoy = input.previousYear && input.previousYear > 0
|
|
664
|
+
? ((input.current - input.previousYear) / input.previousYear) * 100
|
|
665
|
+
: 0
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
mom,
|
|
669
|
+
qoq,
|
|
670
|
+
yoy,
|
|
671
|
+
metric: input.metric,
|
|
672
|
+
period: input.period,
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Calculate churn metrics
|
|
678
|
+
*/
|
|
679
|
+
export function calculateChurnMetrics(input: {
|
|
680
|
+
customersStart: number
|
|
681
|
+
customersLost: number
|
|
682
|
+
mrrStart: number
|
|
683
|
+
mrrChurned: number
|
|
684
|
+
expansionMRR: number
|
|
685
|
+
period: MetricPeriod
|
|
686
|
+
}): Churn {
|
|
687
|
+
const customerChurnRate = input.customersStart > 0
|
|
688
|
+
? (input.customersLost / input.customersStart) * 100
|
|
689
|
+
: 0
|
|
690
|
+
const revenueChurnRate = input.mrrStart > 0
|
|
691
|
+
? (input.mrrChurned / input.mrrStart) * 100
|
|
692
|
+
: 0
|
|
693
|
+
const netRevenueChurnRate = input.mrrStart > 0
|
|
694
|
+
? ((input.mrrChurned - input.expansionMRR) / input.mrrStart) * 100
|
|
695
|
+
: 0
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
customerChurnRate,
|
|
699
|
+
customersLost: input.customersLost,
|
|
700
|
+
customersStart: input.customersStart,
|
|
701
|
+
revenueChurnRate,
|
|
702
|
+
mrrChurned: input.mrrChurned,
|
|
703
|
+
netRevenueChurnRate,
|
|
704
|
+
period: input.period,
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// =============================================================================
|
|
709
|
+
// Aggregation Functions
|
|
710
|
+
// =============================================================================
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Aggregate time series data by period
|
|
714
|
+
*/
|
|
715
|
+
export function aggregateTimeSeries<T extends number>(
|
|
716
|
+
series: TimeSeries<T>,
|
|
717
|
+
targetPeriod: TimePeriod
|
|
718
|
+
): TimeSeries<T> {
|
|
719
|
+
const buckets = new Map<string, DataPoint<T>[]>()
|
|
720
|
+
|
|
721
|
+
for (const point of series.dataPoints) {
|
|
722
|
+
const key = getBucketKey(point.timestamp, targetPeriod)
|
|
723
|
+
const existing = buckets.get(key) || []
|
|
724
|
+
buckets.set(key, [...existing, point])
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const aggregatedPoints: DataPoint<T>[] = []
|
|
728
|
+
const aggregation = series.aggregation || 'sum'
|
|
729
|
+
|
|
730
|
+
for (const [key, points] of buckets) {
|
|
731
|
+
const values = points.map(p => p.value as number)
|
|
732
|
+
let aggregatedValue: number
|
|
733
|
+
|
|
734
|
+
switch (aggregation) {
|
|
735
|
+
case 'sum':
|
|
736
|
+
aggregatedValue = values.reduce((a, b) => a + b, 0)
|
|
737
|
+
break
|
|
738
|
+
case 'avg':
|
|
739
|
+
aggregatedValue = values.reduce((a, b) => a + b, 0) / values.length
|
|
740
|
+
break
|
|
741
|
+
case 'min':
|
|
742
|
+
aggregatedValue = Math.min(...values)
|
|
743
|
+
break
|
|
744
|
+
case 'max':
|
|
745
|
+
aggregatedValue = Math.max(...values)
|
|
746
|
+
break
|
|
747
|
+
case 'last':
|
|
748
|
+
aggregatedValue = values[values.length - 1] ?? 0
|
|
749
|
+
break
|
|
750
|
+
case 'first':
|
|
751
|
+
aggregatedValue = values[0] ?? 0
|
|
752
|
+
break
|
|
753
|
+
default:
|
|
754
|
+
aggregatedValue = values.reduce((a, b) => a + b, 0)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
aggregatedPoints.push({
|
|
758
|
+
timestamp: new Date(key),
|
|
759
|
+
value: aggregatedValue as T,
|
|
760
|
+
})
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return {
|
|
764
|
+
...series,
|
|
765
|
+
dataPoints: aggregatedPoints.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()),
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Get bucket key for time aggregation
|
|
771
|
+
*/
|
|
772
|
+
function getBucketKey(date: Date, period: TimePeriod): string {
|
|
773
|
+
switch (period) {
|
|
774
|
+
case 'daily':
|
|
775
|
+
return date.toISOString().split('T')[0] || date.toISOString()
|
|
776
|
+
case 'weekly':
|
|
777
|
+
const weekStart = new Date(date)
|
|
778
|
+
weekStart.setDate(date.getDate() - date.getDay())
|
|
779
|
+
return weekStart.toISOString().split('T')[0] || weekStart.toISOString()
|
|
780
|
+
case 'monthly':
|
|
781
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-01`
|
|
782
|
+
case 'quarterly':
|
|
783
|
+
const quarter = Math.floor(date.getMonth() / 3)
|
|
784
|
+
return `${date.getFullYear()}-Q${quarter + 1}`
|
|
785
|
+
case 'yearly':
|
|
786
|
+
return `${date.getFullYear()}-01-01`
|
|
787
|
+
default:
|
|
788
|
+
return date.toISOString()
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Create metric period from dates
|
|
794
|
+
*/
|
|
795
|
+
export function createMetricPeriod(
|
|
796
|
+
period: TimePeriod,
|
|
797
|
+
start: Date,
|
|
798
|
+
end: Date,
|
|
799
|
+
label?: string
|
|
800
|
+
): MetricPeriod {
|
|
801
|
+
return {
|
|
802
|
+
period,
|
|
803
|
+
range: { start, end },
|
|
804
|
+
label: label || formatPeriodLabel(period, start, end),
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Format period label
|
|
810
|
+
*/
|
|
811
|
+
function formatPeriodLabel(period: TimePeriod, start: Date, end: Date): string {
|
|
812
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
813
|
+
|
|
814
|
+
switch (period) {
|
|
815
|
+
case 'monthly':
|
|
816
|
+
return `${monthNames[start.getMonth()]} ${start.getFullYear()}`
|
|
817
|
+
case 'quarterly':
|
|
818
|
+
const quarter = Math.floor(start.getMonth() / 3) + 1
|
|
819
|
+
return `Q${quarter} ${start.getFullYear()}`
|
|
820
|
+
case 'yearly':
|
|
821
|
+
return `${start.getFullYear()}`
|
|
822
|
+
default:
|
|
823
|
+
return `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`
|
|
824
|
+
}
|
|
825
|
+
}
|