business-as-code 2.1.1 → 2.3.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/CHANGELOG.md +18 -0
- package/README.md +2 -0
- package/package.json +7 -4
- package/src/dollar.ts +5 -2
- package/src/entities/organization.ts +31 -18
- package/src/goals.ts +78 -12
- package/src/index.ts +48 -18
- package/src/kpis.ts +62 -8
- package/src/metrics.ts +92 -79
- package/src/okrs.ts +120 -20
- package/src/organization.ts +12 -15
- package/src/process.ts +11 -12
- package/src/product.ts +8 -9
- package/src/queries.ts +238 -75
- package/src/roles.ts +62 -61
- package/src/workflow.ts +22 -15
- package/test/business.test.ts +282 -0
- package/test/dollar.test.ts +270 -0
- package/test/entities.test.ts +628 -0
- package/test/financials.test.ts +539 -0
- package/test/goals.test.ts +451 -0
- package/{src → test}/index.test.ts +1 -1
- package/test/kpis.test.ts +440 -0
- package/test/metrics.test.ts +744 -0
- package/test/okrs.test.ts +741 -0
- package/test/organization.test.ts +548 -0
- package/test/process.test.ts +503 -0
- package/test/product.test.ts +430 -0
- package/test/queries.test.ts +556 -0
- package/test/roles.test.ts +546 -0
- package/test/service.test.ts +450 -0
- package/test/types.test.ts +1141 -0
- package/test/vision.test.ts +214 -0
- package/test/workflow.test.ts +501 -0
- package/vitest.config.ts +47 -0
- package/.turbo/turbo-build.log +0 -5
- package/dist/business.d.ts +0 -62
- package/dist/business.d.ts.map +0 -1
- package/dist/business.js +0 -109
- package/dist/business.js.map +0 -1
- package/dist/dollar.d.ts +0 -60
- package/dist/dollar.d.ts.map +0 -1
- package/dist/dollar.js +0 -107
- package/dist/dollar.js.map +0 -1
- package/dist/entities/assets.d.ts +0 -21
- package/dist/entities/assets.d.ts.map +0 -1
- package/dist/entities/assets.js +0 -323
- package/dist/entities/assets.js.map +0 -1
- package/dist/entities/business.d.ts +0 -36
- package/dist/entities/business.d.ts.map +0 -1
- package/dist/entities/business.js +0 -370
- package/dist/entities/business.js.map +0 -1
- package/dist/entities/communication.d.ts +0 -21
- package/dist/entities/communication.d.ts.map +0 -1
- package/dist/entities/communication.js +0 -255
- package/dist/entities/communication.js.map +0 -1
- package/dist/entities/customers.d.ts +0 -58
- package/dist/entities/customers.d.ts.map +0 -1
- package/dist/entities/customers.js +0 -989
- package/dist/entities/customers.js.map +0 -1
- package/dist/entities/financials.d.ts +0 -59
- package/dist/entities/financials.d.ts.map +0 -1
- package/dist/entities/financials.js +0 -932
- package/dist/entities/financials.js.map +0 -1
- package/dist/entities/goals.d.ts +0 -58
- package/dist/entities/goals.d.ts.map +0 -1
- package/dist/entities/goals.js +0 -800
- package/dist/entities/goals.js.map +0 -1
- package/dist/entities/index.d.ts +0 -299
- package/dist/entities/index.d.ts.map +0 -1
- package/dist/entities/index.js +0 -198
- package/dist/entities/index.js.map +0 -1
- package/dist/entities/legal.d.ts +0 -21
- package/dist/entities/legal.d.ts.map +0 -1
- package/dist/entities/legal.js +0 -301
- package/dist/entities/legal.js.map +0 -1
- package/dist/entities/market.d.ts +0 -21
- package/dist/entities/market.d.ts.map +0 -1
- package/dist/entities/market.js +0 -301
- package/dist/entities/market.js.map +0 -1
- package/dist/entities/marketing.d.ts +0 -67
- package/dist/entities/marketing.d.ts.map +0 -1
- package/dist/entities/marketing.js +0 -1157
- package/dist/entities/marketing.js.map +0 -1
- package/dist/entities/offerings.d.ts +0 -51
- package/dist/entities/offerings.d.ts.map +0 -1
- package/dist/entities/offerings.js +0 -727
- package/dist/entities/offerings.js.map +0 -1
- package/dist/entities/operations.d.ts +0 -58
- package/dist/entities/operations.d.ts.map +0 -1
- package/dist/entities/operations.js +0 -787
- package/dist/entities/operations.js.map +0 -1
- package/dist/entities/organization.d.ts +0 -57
- package/dist/entities/organization.d.ts.map +0 -1
- package/dist/entities/organization.js +0 -807
- package/dist/entities/organization.js.map +0 -1
- package/dist/entities/partnerships.d.ts +0 -21
- package/dist/entities/partnerships.d.ts.map +0 -1
- package/dist/entities/partnerships.js +0 -300
- package/dist/entities/partnerships.js.map +0 -1
- package/dist/entities/planning.d.ts +0 -87
- package/dist/entities/planning.d.ts.map +0 -1
- package/dist/entities/planning.js +0 -271
- package/dist/entities/planning.js.map +0 -1
- package/dist/entities/projects.d.ts +0 -25
- package/dist/entities/projects.d.ts.map +0 -1
- package/dist/entities/projects.js +0 -349
- package/dist/entities/projects.js.map +0 -1
- package/dist/entities/risk.d.ts +0 -21
- package/dist/entities/risk.d.ts.map +0 -1
- package/dist/entities/risk.js +0 -293
- package/dist/entities/risk.js.map +0 -1
- package/dist/entities/sales.d.ts +0 -72
- package/dist/entities/sales.d.ts.map +0 -1
- package/dist/entities/sales.js +0 -1248
- package/dist/entities/sales.js.map +0 -1
- package/dist/financials.d.ts +0 -130
- package/dist/financials.d.ts.map +0 -1
- package/dist/financials.js +0 -297
- package/dist/financials.js.map +0 -1
- package/dist/goals.d.ts +0 -87
- package/dist/goals.d.ts.map +0 -1
- package/dist/goals.js +0 -215
- package/dist/goals.js.map +0 -1
- package/dist/index.d.ts +0 -97
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -132
- package/dist/index.js.map +0 -1
- package/dist/kpis.d.ts +0 -118
- package/dist/kpis.d.ts.map +0 -1
- package/dist/kpis.js +0 -232
- package/dist/kpis.js.map +0 -1
- package/dist/metrics.d.ts +0 -448
- package/dist/metrics.d.ts.map +0 -1
- package/dist/metrics.js +0 -325
- package/dist/metrics.js.map +0 -1
- package/dist/okrs.d.ts +0 -123
- package/dist/okrs.d.ts.map +0 -1
- package/dist/okrs.js +0 -269
- package/dist/okrs.js.map +0 -1
- package/dist/organization.d.ts +0 -585
- package/dist/organization.d.ts.map +0 -1
- package/dist/organization.js +0 -173
- package/dist/organization.js.map +0 -1
- package/dist/process.d.ts +0 -112
- package/dist/process.d.ts.map +0 -1
- package/dist/process.js +0 -241
- package/dist/process.js.map +0 -1
- package/dist/product.d.ts +0 -85
- package/dist/product.d.ts.map +0 -1
- package/dist/product.js +0 -145
- package/dist/product.js.map +0 -1
- package/dist/queries.d.ts +0 -304
- package/dist/queries.d.ts.map +0 -1
- package/dist/queries.js +0 -415
- package/dist/queries.js.map +0 -1
- package/dist/roles.d.ts +0 -340
- package/dist/roles.d.ts.map +0 -1
- package/dist/roles.js +0 -255
- package/dist/roles.js.map +0 -1
- package/dist/service.d.ts +0 -61
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -140
- package/dist/service.js.map +0 -1
- package/dist/types.d.ts +0 -459
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/dist/vision.d.ts +0 -38
- package/dist/vision.d.ts.map +0 -1
- package/dist/vision.js +0 -68
- package/dist/vision.js.map +0 -1
- package/dist/workflow.d.ts +0 -115
- package/dist/workflow.d.ts.map +0 -1
- package/dist/workflow.js +0 -247
- package/dist/workflow.js.map +0 -1
- package/src/business.js +0 -108
- package/src/dollar.js +0 -106
- package/src/entities/assets.js +0 -322
- package/src/entities/business.js +0 -369
- package/src/entities/communication.js +0 -254
- package/src/entities/customers.js +0 -988
- package/src/entities/financials.js +0 -931
- package/src/entities/goals.js +0 -799
- package/src/entities/index.js +0 -197
- package/src/entities/legal.js +0 -300
- package/src/entities/market.js +0 -300
- package/src/entities/marketing.js +0 -1156
- package/src/entities/offerings.js +0 -726
- package/src/entities/operations.js +0 -786
- package/src/entities/organization.js +0 -806
- package/src/entities/partnerships.js +0 -299
- package/src/entities/planning.js +0 -270
- package/src/entities/projects.js +0 -348
- package/src/entities/risk.js +0 -292
- package/src/entities/sales.js +0 -1247
- package/src/financials.js +0 -296
- package/src/goals.js +0 -214
- package/src/index.js +0 -131
- package/src/index.test.js +0 -274
- package/src/kpis.js +0 -231
- package/src/metrics.js +0 -324
- package/src/okrs.js +0 -268
- package/src/organization.js +0 -172
- package/src/process.js +0 -240
- package/src/product.js +0 -144
- package/src/queries.js +0 -414
- package/src/roles.js +0 -254
- package/src/service.js +0 -139
- package/src/types.js +0 -4
- package/src/vision.js +0 -67
- package/src/workflow.js +0 -246
package/src/metrics.ts
CHANGED
|
@@ -58,12 +58,12 @@ export interface TimeSeries<T = number> {
|
|
|
58
58
|
*/
|
|
59
59
|
export interface MRR {
|
|
60
60
|
total: number
|
|
61
|
-
newMRR: number
|
|
62
|
-
expansionMRR: number
|
|
63
|
-
contractionMRR: number
|
|
64
|
-
churnedMRR: number
|
|
65
|
-
reactivationMRR: number
|
|
66
|
-
netNewMRR: 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
67
|
currency: Currency
|
|
68
68
|
period: MetricPeriod
|
|
69
69
|
}
|
|
@@ -73,8 +73,8 @@ export interface MRR {
|
|
|
73
73
|
*/
|
|
74
74
|
export interface ARR {
|
|
75
75
|
total: number
|
|
76
|
-
fromMRR?: number
|
|
77
|
-
contracted?: number
|
|
76
|
+
fromMRR?: number // MRR * 12
|
|
77
|
+
contracted?: number // From annual contracts
|
|
78
78
|
currency: Currency
|
|
79
79
|
asOf: Date
|
|
80
80
|
}
|
|
@@ -83,7 +83,7 @@ export interface ARR {
|
|
|
83
83
|
* Net Revenue Retention (NRR) / Dollar-based Net Retention (DBNR)
|
|
84
84
|
*/
|
|
85
85
|
export interface NRR {
|
|
86
|
-
rate: number
|
|
86
|
+
rate: number // Percentage (e.g., 115 = 115%)
|
|
87
87
|
startingMRR: number
|
|
88
88
|
endingMRR: number
|
|
89
89
|
expansion: number
|
|
@@ -96,7 +96,7 @@ export interface NRR {
|
|
|
96
96
|
* Gross Revenue Retention (GRR)
|
|
97
97
|
*/
|
|
98
98
|
export interface GRR {
|
|
99
|
-
rate: number
|
|
99
|
+
rate: number // Percentage (max 100%)
|
|
100
100
|
startingMRR: number
|
|
101
101
|
endingMRR: number
|
|
102
102
|
contraction: number
|
|
@@ -113,7 +113,7 @@ export interface ARPU {
|
|
|
113
113
|
totalUsers: number
|
|
114
114
|
currency: Currency
|
|
115
115
|
period: MetricPeriod
|
|
116
|
-
segment?: string
|
|
116
|
+
segment?: string // Optional segment (e.g., "enterprise", "smb")
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
@@ -125,7 +125,7 @@ export interface RevenueSegment {
|
|
|
125
125
|
arr: number
|
|
126
126
|
customers: number
|
|
127
127
|
arpu: number
|
|
128
|
-
growth: number
|
|
128
|
+
growth: number // MoM or YoY growth rate
|
|
129
129
|
currency: Currency
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -151,8 +151,8 @@ export interface CAC {
|
|
|
151
151
|
export interface LTV {
|
|
152
152
|
value: number
|
|
153
153
|
arpu: number
|
|
154
|
-
grossMargin: number
|
|
155
|
-
churnRate: number
|
|
154
|
+
grossMargin: number // Percentage
|
|
155
|
+
churnRate: number // Monthly churn rate
|
|
156
156
|
averageLifetimeMonths: number
|
|
157
157
|
currency: Currency
|
|
158
158
|
}
|
|
@@ -164,8 +164,8 @@ export interface LTVtoCAC {
|
|
|
164
164
|
ratio: number
|
|
165
165
|
ltv: number
|
|
166
166
|
cac: number
|
|
167
|
-
paybackMonths: number
|
|
168
|
-
healthy: boolean
|
|
167
|
+
paybackMonths: number // CAC / (ARPU * Gross Margin)
|
|
168
|
+
healthy: boolean // > 3 is generally healthy
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/**
|
|
@@ -173,12 +173,12 @@ export interface LTVtoCAC {
|
|
|
173
173
|
*/
|
|
174
174
|
export interface Churn {
|
|
175
175
|
// Customer churn (logo churn)
|
|
176
|
-
customerChurnRate: number
|
|
176
|
+
customerChurnRate: number // Percentage
|
|
177
177
|
customersLost: number
|
|
178
178
|
customersStart: number
|
|
179
179
|
|
|
180
180
|
// Revenue churn
|
|
181
|
-
revenueChurnRate: number
|
|
181
|
+
revenueChurnRate: number // Percentage (gross churn)
|
|
182
182
|
mrrChurned: number
|
|
183
183
|
|
|
184
184
|
// Net revenue churn (can be negative with good expansion)
|
|
@@ -192,11 +192,11 @@ export interface Churn {
|
|
|
192
192
|
*/
|
|
193
193
|
export interface RetentionCohort {
|
|
194
194
|
cohortDate: Date
|
|
195
|
-
cohortLabel: string
|
|
195
|
+
cohortLabel: string // e.g., "Jan 2024"
|
|
196
196
|
initialCustomers: number
|
|
197
197
|
initialMRR: number
|
|
198
|
-
retentionByMonth: number[]
|
|
199
|
-
revenueByMonth: number[]
|
|
198
|
+
retentionByMonth: number[] // Array of retention rates by month
|
|
199
|
+
revenueByMonth: number[] // Array of MRR by month
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
// =============================================================================
|
|
@@ -207,11 +207,11 @@ export interface RetentionCohort {
|
|
|
207
207
|
* Growth rate metrics
|
|
208
208
|
*/
|
|
209
209
|
export interface GrowthRate {
|
|
210
|
-
mom: number
|
|
211
|
-
qoq: number
|
|
212
|
-
yoy: number
|
|
213
|
-
cagr?: number
|
|
214
|
-
metric: string
|
|
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
215
|
period: MetricPeriod
|
|
216
216
|
}
|
|
217
217
|
|
|
@@ -225,7 +225,7 @@ export interface QuickRatio {
|
|
|
225
225
|
expansionMRR: number
|
|
226
226
|
churnedMRR: number
|
|
227
227
|
contractionMRR: number
|
|
228
|
-
healthy: boolean
|
|
228
|
+
healthy: boolean // > 4 is good, > 1 means growing
|
|
229
229
|
period: MetricPeriod
|
|
230
230
|
}
|
|
231
231
|
|
|
@@ -241,7 +241,7 @@ export interface MagicNumber {
|
|
|
241
241
|
value: number
|
|
242
242
|
netNewARR: number
|
|
243
243
|
salesMarketingSpend: number
|
|
244
|
-
efficient: boolean
|
|
244
|
+
efficient: boolean // > 0.75 is efficient
|
|
245
245
|
period: MetricPeriod
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -253,7 +253,7 @@ export interface BurnMultiple {
|
|
|
253
253
|
value: number
|
|
254
254
|
netBurn: number
|
|
255
255
|
netNewARR: number
|
|
256
|
-
efficient: boolean
|
|
256
|
+
efficient: boolean // < 1.5 is good
|
|
257
257
|
period: MetricPeriod
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -264,8 +264,8 @@ export interface BurnMultiple {
|
|
|
264
264
|
export interface RuleOf40 {
|
|
265
265
|
score: number
|
|
266
266
|
revenueGrowthRate: number
|
|
267
|
-
profitMargin: number
|
|
268
|
-
passing: boolean
|
|
267
|
+
profitMargin: number // Or EBITDA margin
|
|
268
|
+
passing: boolean // >= 40 is passing
|
|
269
269
|
period: MetricPeriod
|
|
270
270
|
}
|
|
271
271
|
|
|
@@ -274,7 +274,7 @@ export interface RuleOf40 {
|
|
|
274
274
|
* Combines multiple efficiency metrics
|
|
275
275
|
*/
|
|
276
276
|
export interface EfficiencyScore {
|
|
277
|
-
overall: number
|
|
277
|
+
overall: number // 0-100 score
|
|
278
278
|
components: {
|
|
279
279
|
ltvCacRatio: number
|
|
280
280
|
magicNumber: number
|
|
@@ -294,10 +294,10 @@ export interface EfficiencyScore {
|
|
|
294
294
|
*/
|
|
295
295
|
export interface Pipeline {
|
|
296
296
|
totalValue: number
|
|
297
|
-
weightedValue: number
|
|
297
|
+
weightedValue: number // Probability-adjusted
|
|
298
298
|
stages: PipelineStage[]
|
|
299
|
-
velocity: number
|
|
300
|
-
conversionRate: number
|
|
299
|
+
velocity: number // Average days to close
|
|
300
|
+
conversionRate: number // Win rate
|
|
301
301
|
currency: Currency
|
|
302
302
|
asOf: Date
|
|
303
303
|
}
|
|
@@ -318,7 +318,7 @@ export interface PipelineStage {
|
|
|
318
318
|
* (Opportunities * Win Rate * Average Deal Size) / Sales Cycle Length
|
|
319
319
|
*/
|
|
320
320
|
export interface SalesVelocity {
|
|
321
|
-
value: number
|
|
321
|
+
value: number // Revenue per day
|
|
322
322
|
opportunities: number
|
|
323
323
|
winRate: number
|
|
324
324
|
averageDealSize: number
|
|
@@ -335,10 +335,10 @@ export interface SalesVelocity {
|
|
|
335
335
|
* Net Promoter Score
|
|
336
336
|
*/
|
|
337
337
|
export interface NPS {
|
|
338
|
-
score: number
|
|
339
|
-
promoters: number
|
|
340
|
-
passives: number
|
|
341
|
-
detractors: number
|
|
338
|
+
score: number // -100 to 100
|
|
339
|
+
promoters: number // 9-10
|
|
340
|
+
passives: number // 7-8
|
|
341
|
+
detractors: number // 0-6
|
|
342
342
|
responses: number
|
|
343
343
|
responseRate?: number
|
|
344
344
|
asOf: Date
|
|
@@ -348,10 +348,10 @@ export interface NPS {
|
|
|
348
348
|
* Customer health score
|
|
349
349
|
*/
|
|
350
350
|
export interface CustomerHealth {
|
|
351
|
-
averageScore: number
|
|
352
|
-
healthy: number
|
|
353
|
-
atRisk: number
|
|
354
|
-
critical: number
|
|
351
|
+
averageScore: number // 0-100
|
|
352
|
+
healthy: number // Count
|
|
353
|
+
atRisk: number // Count
|
|
354
|
+
critical: number // Count
|
|
355
355
|
factors: HealthFactor[]
|
|
356
356
|
asOf: Date
|
|
357
357
|
}
|
|
@@ -422,7 +422,8 @@ export function calculateMRR(input: {
|
|
|
422
422
|
period: MetricPeriod
|
|
423
423
|
}): MRR {
|
|
424
424
|
const reactivationMRR = input.reactivationMRR || 0
|
|
425
|
-
const netNewMRR =
|
|
425
|
+
const netNewMRR =
|
|
426
|
+
input.newMRR + input.expansionMRR - input.contractionMRR - input.churnedMRR + reactivationMRR
|
|
426
427
|
const total = input.previousMRR + netNewMRR
|
|
427
428
|
|
|
428
429
|
return {
|
|
@@ -508,22 +509,23 @@ export function calculateCACMetric(input: {
|
|
|
508
509
|
}): CAC {
|
|
509
510
|
const value = input.newCustomers > 0 ? input.salesMarketingSpend / input.newCustomers : 0
|
|
510
511
|
|
|
511
|
-
|
|
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 {
|
|
512
|
+
const result: CAC = {
|
|
520
513
|
value,
|
|
521
514
|
totalSalesMarketingSpend: input.salesMarketingSpend,
|
|
522
515
|
newCustomersAcquired: input.newCustomers,
|
|
523
516
|
currency: input.currency || 'USD',
|
|
524
517
|
period: input.period,
|
|
525
|
-
byChannel,
|
|
526
518
|
}
|
|
519
|
+
|
|
520
|
+
if (input.byChannel) {
|
|
521
|
+
const byChannel: Record<string, number> = {}
|
|
522
|
+
for (const [channel, data] of Object.entries(input.byChannel)) {
|
|
523
|
+
byChannel[channel] = data.customers > 0 ? data.spend / data.customers : 0
|
|
524
|
+
}
|
|
525
|
+
result.byChannel = byChannel
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return result
|
|
527
529
|
}
|
|
528
530
|
|
|
529
531
|
/**
|
|
@@ -537,7 +539,7 @@ export function calculateLTVMetric(input: {
|
|
|
537
539
|
}): LTV {
|
|
538
540
|
// LTV = (ARPU * Gross Margin) / Churn Rate
|
|
539
541
|
const averageLifetimeMonths = input.churnRate > 0 ? 1 / input.churnRate : 0
|
|
540
|
-
const value = input.churnRate > 0 ? (input.arpu * input.grossMargin / 100
|
|
542
|
+
const value = input.churnRate > 0 ? (input.arpu * input.grossMargin) / 100 / input.churnRate : 0
|
|
541
543
|
|
|
542
544
|
return {
|
|
543
545
|
value,
|
|
@@ -554,9 +556,8 @@ export function calculateLTVMetric(input: {
|
|
|
554
556
|
*/
|
|
555
557
|
export function calculateLTVtoCACRatio(ltv: LTV, cac: CAC): LTVtoCAC {
|
|
556
558
|
const ratio = cac.value > 0 ? ltv.value / cac.value : 0
|
|
557
|
-
const paybackMonths =
|
|
558
|
-
? cac.value / (ltv.arpu * ltv.grossMargin / 100)
|
|
559
|
-
: 0
|
|
559
|
+
const paybackMonths =
|
|
560
|
+
ltv.arpu > 0 && ltv.grossMargin > 0 ? cac.value / ((ltv.arpu * ltv.grossMargin) / 100) : 0
|
|
560
561
|
|
|
561
562
|
return {
|
|
562
563
|
ratio,
|
|
@@ -654,15 +655,18 @@ export function calculateGrowthRates(input: {
|
|
|
654
655
|
metric: string
|
|
655
656
|
period: MetricPeriod
|
|
656
657
|
}): GrowthRate {
|
|
657
|
-
const mom =
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
658
|
+
const mom =
|
|
659
|
+
input.previousMonth && input.previousMonth > 0
|
|
660
|
+
? ((input.current - input.previousMonth) / input.previousMonth) * 100
|
|
661
|
+
: 0
|
|
662
|
+
const qoq =
|
|
663
|
+
input.previousQuarter && input.previousQuarter > 0
|
|
664
|
+
? ((input.current - input.previousQuarter) / input.previousQuarter) * 100
|
|
665
|
+
: 0
|
|
666
|
+
const yoy =
|
|
667
|
+
input.previousYear && input.previousYear > 0
|
|
668
|
+
? ((input.current - input.previousYear) / input.previousYear) * 100
|
|
669
|
+
: 0
|
|
666
670
|
|
|
667
671
|
return {
|
|
668
672
|
mom,
|
|
@@ -684,15 +688,11 @@ export function calculateChurnMetrics(input: {
|
|
|
684
688
|
expansionMRR: number
|
|
685
689
|
period: MetricPeriod
|
|
686
690
|
}): Churn {
|
|
687
|
-
const customerChurnRate =
|
|
688
|
-
? (input.customersLost / input.customersStart) * 100
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
? (input.mrrChurned / input.mrrStart) * 100
|
|
692
|
-
: 0
|
|
693
|
-
const netRevenueChurnRate = input.mrrStart > 0
|
|
694
|
-
? ((input.mrrChurned - input.expansionMRR) / input.mrrStart) * 100
|
|
695
|
-
: 0
|
|
691
|
+
const customerChurnRate =
|
|
692
|
+
input.customersStart > 0 ? (input.customersLost / input.customersStart) * 100 : 0
|
|
693
|
+
const revenueChurnRate = input.mrrStart > 0 ? (input.mrrChurned / input.mrrStart) * 100 : 0
|
|
694
|
+
const netRevenueChurnRate =
|
|
695
|
+
input.mrrStart > 0 ? ((input.mrrChurned - input.expansionMRR) / input.mrrStart) * 100 : 0
|
|
696
696
|
|
|
697
697
|
return {
|
|
698
698
|
customerChurnRate,
|
|
@@ -728,7 +728,7 @@ export function aggregateTimeSeries<T extends number>(
|
|
|
728
728
|
const aggregation = series.aggregation || 'sum'
|
|
729
729
|
|
|
730
730
|
for (const [key, points] of buckets) {
|
|
731
|
-
const values = points.map(p => p.value as number)
|
|
731
|
+
const values = points.map((p) => p.value as number)
|
|
732
732
|
let aggregatedValue: number
|
|
733
733
|
|
|
734
734
|
switch (aggregation) {
|
|
@@ -809,7 +809,20 @@ export function createMetricPeriod(
|
|
|
809
809
|
* Format period label
|
|
810
810
|
*/
|
|
811
811
|
function formatPeriodLabel(period: TimePeriod, start: Date, end: Date): string {
|
|
812
|
-
const monthNames = [
|
|
812
|
+
const monthNames = [
|
|
813
|
+
'Jan',
|
|
814
|
+
'Feb',
|
|
815
|
+
'Mar',
|
|
816
|
+
'Apr',
|
|
817
|
+
'May',
|
|
818
|
+
'Jun',
|
|
819
|
+
'Jul',
|
|
820
|
+
'Aug',
|
|
821
|
+
'Sep',
|
|
822
|
+
'Oct',
|
|
823
|
+
'Nov',
|
|
824
|
+
'Dec',
|
|
825
|
+
]
|
|
813
826
|
|
|
814
827
|
switch (period) {
|
|
815
828
|
case 'monthly':
|
package/src/okrs.ts
CHANGED
|
@@ -1,8 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Objectives and Key Results (OKRs) management
|
|
3
|
+
*
|
|
4
|
+
* Uses org.ai OKR types for standardized OKR definitions across the ecosystem.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import type { OKRDefinition, KeyResult } from './types.js'
|
|
8
|
+
import type { OKR as OrgOKR, KeyResult as OrgKeyResult, OKRStatus, KeyResultStatus } from 'org.ai'
|
|
9
|
+
|
|
10
|
+
// Re-export org.ai OKR types for convenience
|
|
11
|
+
export type { OrgOKR, OrgKeyResult, OKRStatus, KeyResultStatus }
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert a business-as-code KeyResult to an org.ai KeyResult
|
|
15
|
+
*
|
|
16
|
+
* @param kr - Business key result
|
|
17
|
+
* @param id - Optional identifier
|
|
18
|
+
* @returns org.ai KeyResult object
|
|
19
|
+
*/
|
|
20
|
+
export function toOrgKeyResult(kr: KeyResult, id?: string): OrgKeyResult {
|
|
21
|
+
const result: OrgKeyResult = {
|
|
22
|
+
description: kr.description,
|
|
23
|
+
}
|
|
24
|
+
if (id !== undefined) result.id = id
|
|
25
|
+
if (kr.description) result.name = kr.description
|
|
26
|
+
if (kr.metric !== undefined) result.metric = kr.metric
|
|
27
|
+
if (kr.startValue !== undefined) result.startValue = kr.startValue
|
|
28
|
+
if (kr.targetValue !== undefined) {
|
|
29
|
+
result.targetValue = kr.targetValue
|
|
30
|
+
result.target = kr.targetValue
|
|
31
|
+
}
|
|
32
|
+
if (kr.currentValue !== undefined) {
|
|
33
|
+
result.currentValue = kr.currentValue
|
|
34
|
+
result.current = kr.currentValue
|
|
35
|
+
}
|
|
36
|
+
if (kr.unit !== undefined) result.unit = kr.unit
|
|
37
|
+
if (kr.progress !== undefined) result.progress = kr.progress
|
|
38
|
+
return result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert an org.ai KeyResult to a business-as-code KeyResult
|
|
43
|
+
*
|
|
44
|
+
* @param kr - org.ai KeyResult object
|
|
45
|
+
* @returns Business key result
|
|
46
|
+
*/
|
|
47
|
+
export function fromOrgKeyResult(kr: OrgKeyResult): KeyResult {
|
|
48
|
+
const result: KeyResult = {
|
|
49
|
+
description: kr.description || kr.name || '',
|
|
50
|
+
metric: kr.metric || '',
|
|
51
|
+
targetValue: kr.targetValue ?? kr.target ?? 0,
|
|
52
|
+
}
|
|
53
|
+
if (kr.startValue !== undefined) result.startValue = kr.startValue
|
|
54
|
+
if (kr.currentValue !== undefined) result.currentValue = kr.currentValue
|
|
55
|
+
else if (kr.current !== undefined) result.currentValue = kr.current
|
|
56
|
+
if (kr.unit !== undefined) result.unit = kr.unit
|
|
57
|
+
if (kr.progress !== undefined) result.progress = kr.progress
|
|
58
|
+
return result
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert a business-as-code OKRDefinition to an org.ai OKR
|
|
63
|
+
*
|
|
64
|
+
* @param definition - Business OKR definition
|
|
65
|
+
* @param id - Optional unique identifier
|
|
66
|
+
* @returns org.ai OKR object
|
|
67
|
+
*/
|
|
68
|
+
export function toOrgOKR(definition: OKRDefinition, id?: string): OrgOKR {
|
|
69
|
+
const result: OrgOKR = {
|
|
70
|
+
objective: definition.objective,
|
|
71
|
+
keyResults: definition.keyResults?.map((kr, i) => toOrgKeyResult(kr, `${id}_kr_${i}`)) || [],
|
|
72
|
+
}
|
|
73
|
+
if (id !== undefined) result.id = id
|
|
74
|
+
if (definition.description !== undefined) result.description = definition.description
|
|
75
|
+
if (definition.owner !== undefined) result.owner = definition.owner
|
|
76
|
+
if (definition.period !== undefined) result.period = definition.period
|
|
77
|
+
if (definition.status !== undefined) result.status = definition.status as OKRStatus
|
|
78
|
+
result.progress = calculateOKRProgress(definition)
|
|
79
|
+
if (definition.confidence !== undefined) result.confidence = definition.confidence
|
|
80
|
+
if (definition.metadata !== undefined) result.metadata = definition.metadata
|
|
81
|
+
return result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Convert an org.ai OKR to a business-as-code OKRDefinition
|
|
86
|
+
*
|
|
87
|
+
* @param okr - org.ai OKR object
|
|
88
|
+
* @returns Business OKR definition
|
|
89
|
+
*/
|
|
90
|
+
export function fromOrgOKR(okr: OrgOKR): OKRDefinition {
|
|
91
|
+
const result: OKRDefinition = {
|
|
92
|
+
objective: okr.objective,
|
|
93
|
+
keyResults: okr.keyResults.map(fromOrgKeyResult),
|
|
94
|
+
}
|
|
95
|
+
if (okr.description !== undefined) result.description = okr.description
|
|
96
|
+
if (okr.owner !== undefined) result.owner = okr.owner
|
|
97
|
+
if (okr.period !== undefined) result.period = okr.period
|
|
98
|
+
const st = okr.status
|
|
99
|
+
if (st !== undefined) result.status = st as NonNullable<OKRDefinition['status']>
|
|
100
|
+
if (okr.confidence !== undefined) result.confidence = okr.confidence
|
|
101
|
+
if (okr.metadata !== undefined) result.metadata = okr.metadata
|
|
102
|
+
return result
|
|
103
|
+
}
|
|
6
104
|
|
|
7
105
|
/**
|
|
8
106
|
* Define Objectives and Key Results for goal tracking
|
|
@@ -51,7 +149,7 @@ import type { OKRDefinition, KeyResult } from './types.js'
|
|
|
51
149
|
* ```
|
|
52
150
|
*/
|
|
53
151
|
export function okrs(definitions: OKRDefinition[]): OKRDefinition[] {
|
|
54
|
-
return definitions.map(okr => validateAndNormalizeOKR(okr))
|
|
152
|
+
return definitions.map((okr) => validateAndNormalizeOKR(okr))
|
|
55
153
|
}
|
|
56
154
|
|
|
57
155
|
/**
|
|
@@ -70,18 +168,19 @@ function validateAndNormalizeOKR(okr: OKRDefinition): OKRDefinition {
|
|
|
70
168
|
}
|
|
71
169
|
|
|
72
170
|
// Calculate progress for key results if not set
|
|
73
|
-
const keyResults = okr.keyResults?.map(kr => ({
|
|
171
|
+
const keyResults = okr.keyResults?.map((kr) => ({
|
|
74
172
|
...kr,
|
|
75
173
|
progress: kr.progress ?? calculateKeyResultProgress(kr),
|
|
76
174
|
}))
|
|
77
175
|
|
|
78
|
-
|
|
176
|
+
const result: OKRDefinition = {
|
|
79
177
|
...okr,
|
|
80
|
-
keyResults,
|
|
81
178
|
status: okr.status || 'not-started',
|
|
82
179
|
confidence: okr.confidence ?? calculateConfidence(keyResults || []),
|
|
83
180
|
metadata: okr.metadata || {},
|
|
84
181
|
}
|
|
182
|
+
if (keyResults !== undefined) result.keyResults = keyResults
|
|
183
|
+
return result
|
|
85
184
|
}
|
|
86
185
|
|
|
87
186
|
/**
|
|
@@ -136,7 +235,7 @@ export function updateKeyResult(
|
|
|
136
235
|
krDescription: string,
|
|
137
236
|
currentValue: number
|
|
138
237
|
): OKRDefinition {
|
|
139
|
-
const keyResults = okr.keyResults?.map(kr => {
|
|
238
|
+
const keyResults = okr.keyResults?.map((kr) => {
|
|
140
239
|
if (kr.description === krDescription) {
|
|
141
240
|
const updatedKR = { ...kr, currentValue }
|
|
142
241
|
return {
|
|
@@ -148,24 +247,24 @@ export function updateKeyResult(
|
|
|
148
247
|
})
|
|
149
248
|
|
|
150
249
|
// Recalculate overall status and confidence
|
|
151
|
-
const
|
|
250
|
+
const okrWithKeyResults: OKRDefinition = { ...okr }
|
|
251
|
+
if (keyResults !== undefined) okrWithKeyResults.keyResults = keyResults
|
|
252
|
+
const progress = calculateOKRProgress(okrWithKeyResults)
|
|
152
253
|
const status = determineOKRStatus(progress, okr.confidence || 0)
|
|
153
254
|
|
|
154
|
-
|
|
255
|
+
const result: OKRDefinition = {
|
|
155
256
|
...okr,
|
|
156
|
-
keyResults,
|
|
157
|
-
status,
|
|
158
257
|
confidence: calculateConfidence(keyResults || []),
|
|
159
258
|
}
|
|
259
|
+
if (keyResults !== undefined) result.keyResults = keyResults
|
|
260
|
+
if (status !== undefined) result.status = status
|
|
261
|
+
return result
|
|
160
262
|
}
|
|
161
263
|
|
|
162
264
|
/**
|
|
163
265
|
* Determine OKR status based on progress and confidence
|
|
164
266
|
*/
|
|
165
|
-
function determineOKRStatus(
|
|
166
|
-
progress: number,
|
|
167
|
-
confidence: number
|
|
168
|
-
): OKRDefinition['status'] {
|
|
267
|
+
function determineOKRStatus(progress: number, confidence: number): OKRDefinition['status'] {
|
|
169
268
|
if (progress === 0) return 'not-started'
|
|
170
269
|
if (progress === 100) return 'completed'
|
|
171
270
|
if (confidence < 50 || progress < 30) return 'at-risk'
|
|
@@ -199,21 +298,21 @@ export function getKeyResultsOnTrack(okr: OKRDefinition): KeyResult[] {
|
|
|
199
298
|
* Get key results that are at risk
|
|
200
299
|
*/
|
|
201
300
|
export function getKeyResultsAtRisk(okr: OKRDefinition): KeyResult[] {
|
|
202
|
-
return okr.keyResults?.filter(kr => !isKeyResultOnTrack(kr)) || []
|
|
301
|
+
return okr.keyResults?.filter((kr) => !isKeyResultOnTrack(kr)) || []
|
|
203
302
|
}
|
|
204
303
|
|
|
205
304
|
/**
|
|
206
305
|
* Get OKRs by owner
|
|
207
306
|
*/
|
|
208
307
|
export function getOKRsByOwner(okrs: OKRDefinition[], owner: string): OKRDefinition[] {
|
|
209
|
-
return okrs.filter(okr => okr.owner === owner)
|
|
308
|
+
return okrs.filter((okr) => okr.owner === owner)
|
|
210
309
|
}
|
|
211
310
|
|
|
212
311
|
/**
|
|
213
312
|
* Get OKRs by period
|
|
214
313
|
*/
|
|
215
314
|
export function getOKRsByPeriod(okrs: OKRDefinition[], period: string): OKRDefinition[] {
|
|
216
|
-
return okrs.filter(okr => okr.period === period)
|
|
315
|
+
return okrs.filter((okr) => okr.period === period)
|
|
217
316
|
}
|
|
218
317
|
|
|
219
318
|
/**
|
|
@@ -223,7 +322,7 @@ export function getOKRsByStatus(
|
|
|
223
322
|
okrs: OKRDefinition[],
|
|
224
323
|
status: OKRDefinition['status']
|
|
225
324
|
): OKRDefinition[] {
|
|
226
|
-
return okrs.filter(okr => okr.status === status)
|
|
325
|
+
return okrs.filter((okr) => okr.status === status)
|
|
227
326
|
}
|
|
228
327
|
|
|
229
328
|
/**
|
|
@@ -232,9 +331,10 @@ export function getOKRsByStatus(
|
|
|
232
331
|
export function calculateSuccessRate(okrs: OKRDefinition[]): number {
|
|
233
332
|
if (okrs.length === 0) return 0
|
|
234
333
|
|
|
235
|
-
const avgProgress =
|
|
236
|
-
|
|
237
|
-
|
|
334
|
+
const avgProgress =
|
|
335
|
+
okrs.reduce((sum, okr) => {
|
|
336
|
+
return sum + calculateOKRProgress(okr)
|
|
337
|
+
}, 0) / okrs.length
|
|
238
338
|
|
|
239
339
|
return avgProgress
|
|
240
340
|
}
|
package/src/organization.ts
CHANGED
|
@@ -741,7 +741,7 @@ export function resolvePermissions(
|
|
|
741
741
|
// Search through hierarchy
|
|
742
742
|
for (const dept of org.departments || []) {
|
|
743
743
|
for (const t of dept.teams || []) {
|
|
744
|
-
const pos = t.positions?.find(p => p.id === positionId)
|
|
744
|
+
const pos = t.positions?.find((p) => p.id === positionId)
|
|
745
745
|
if (pos) {
|
|
746
746
|
position = pos
|
|
747
747
|
team = t
|
|
@@ -755,7 +755,7 @@ export function resolvePermissions(
|
|
|
755
755
|
// Also check standalone teams
|
|
756
756
|
if (!position) {
|
|
757
757
|
for (const t of org.teams || []) {
|
|
758
|
-
const pos = t.positions?.find(p => p.id === positionId)
|
|
758
|
+
const pos = t.positions?.find((p) => p.id === positionId)
|
|
759
759
|
if (pos) {
|
|
760
760
|
position = pos
|
|
761
761
|
team = t
|
|
@@ -767,7 +767,7 @@ export function resolvePermissions(
|
|
|
767
767
|
if (!position) return null
|
|
768
768
|
|
|
769
769
|
// Find the role
|
|
770
|
-
const role = org.roles?.find(r => r
|
|
770
|
+
const role = org.roles?.find((r) => r['id'] === position.roleId)
|
|
771
771
|
|
|
772
772
|
// Build inheritance chain
|
|
773
773
|
const inheritanceChain: string[] = []
|
|
@@ -803,16 +803,16 @@ export function resolvePermissions(
|
|
|
803
803
|
|
|
804
804
|
// 4. Role permissions
|
|
805
805
|
if (role?.permissions) {
|
|
806
|
-
inheritanceChain.push(`role:${role
|
|
806
|
+
inheritanceChain.push(`role:${role['id']}`)
|
|
807
807
|
mergePermissions(permissions, role.permissions)
|
|
808
808
|
}
|
|
809
809
|
|
|
810
810
|
// 5. Role capabilities
|
|
811
|
-
if (role?.canApprove) {
|
|
812
|
-
canApprove.push(...role
|
|
811
|
+
if (role?.['canApprove']) {
|
|
812
|
+
canApprove.push(...role['canApprove'])
|
|
813
813
|
}
|
|
814
|
-
if (role?.canHandle) {
|
|
815
|
-
canHandle.push(...role
|
|
814
|
+
if (role?.['canHandle']) {
|
|
815
|
+
canHandle.push(...role['canHandle'])
|
|
816
816
|
}
|
|
817
817
|
|
|
818
818
|
// 6. Position-specific permissions
|
|
@@ -867,7 +867,7 @@ export function getApprovalChainForRequest(
|
|
|
867
867
|
requestType: string,
|
|
868
868
|
amount?: number
|
|
869
869
|
): ApproverSpec[] {
|
|
870
|
-
const chain = org.approvalChains?.find(c => c.type === requestType && c.active !== false)
|
|
870
|
+
const chain = org.approvalChains?.find((c) => c.type === requestType && c.active !== false)
|
|
871
871
|
if (!chain) return []
|
|
872
872
|
|
|
873
873
|
// Find the appropriate level based on amount
|
|
@@ -886,19 +886,16 @@ export function getApprovalChainForRequest(
|
|
|
886
886
|
/**
|
|
887
887
|
* Find manager for a position (follows reportsTo chain)
|
|
888
888
|
*/
|
|
889
|
-
export function findManager(
|
|
890
|
-
org: Organization,
|
|
891
|
-
positionId: string
|
|
892
|
-
): Position | null {
|
|
889
|
+
export function findManager(org: Organization, positionId: string): Position | null {
|
|
893
890
|
// Find the position
|
|
894
891
|
for (const dept of org.departments || []) {
|
|
895
892
|
for (const team of dept.teams || []) {
|
|
896
|
-
const position = team.positions?.find(p => p.id === positionId)
|
|
893
|
+
const position = team.positions?.find((p) => p.id === positionId)
|
|
897
894
|
if (position?.reportsTo) {
|
|
898
895
|
// Find the manager position
|
|
899
896
|
for (const d of org.departments || []) {
|
|
900
897
|
for (const t of d.teams || []) {
|
|
901
|
-
const manager = t.positions?.find(p => p.id === position.reportsTo)
|
|
898
|
+
const manager = t.positions?.find((p) => p.id === position.reportsTo)
|
|
902
899
|
if (manager) return manager
|
|
903
900
|
}
|
|
904
901
|
}
|