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.
Files changed (190) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +17 -0
  3. package/IMPLEMENTATION.md +226 -0
  4. package/README.md +1133 -193
  5. package/dist/business.d.ts +62 -0
  6. package/dist/business.d.ts.map +1 -0
  7. package/dist/business.js +109 -0
  8. package/dist/business.js.map +1 -0
  9. package/dist/dollar.d.ts +60 -0
  10. package/dist/dollar.d.ts.map +1 -0
  11. package/dist/dollar.js +107 -0
  12. package/dist/dollar.js.map +1 -0
  13. package/dist/entities/assets.d.ts +21 -0
  14. package/dist/entities/assets.d.ts.map +1 -0
  15. package/dist/entities/assets.js +323 -0
  16. package/dist/entities/assets.js.map +1 -0
  17. package/dist/entities/business.d.ts +36 -0
  18. package/dist/entities/business.d.ts.map +1 -0
  19. package/dist/entities/business.js +370 -0
  20. package/dist/entities/business.js.map +1 -0
  21. package/dist/entities/communication.d.ts +21 -0
  22. package/dist/entities/communication.d.ts.map +1 -0
  23. package/dist/entities/communication.js +255 -0
  24. package/dist/entities/communication.js.map +1 -0
  25. package/dist/entities/customers.d.ts +58 -0
  26. package/dist/entities/customers.d.ts.map +1 -0
  27. package/dist/entities/customers.js +989 -0
  28. package/dist/entities/customers.js.map +1 -0
  29. package/dist/entities/financials.d.ts +59 -0
  30. package/dist/entities/financials.d.ts.map +1 -0
  31. package/dist/entities/financials.js +932 -0
  32. package/dist/entities/financials.js.map +1 -0
  33. package/dist/entities/goals.d.ts +58 -0
  34. package/dist/entities/goals.d.ts.map +1 -0
  35. package/dist/entities/goals.js +800 -0
  36. package/dist/entities/goals.js.map +1 -0
  37. package/dist/entities/index.d.ts +299 -0
  38. package/dist/entities/index.d.ts.map +1 -0
  39. package/dist/entities/index.js +198 -0
  40. package/dist/entities/index.js.map +1 -0
  41. package/dist/entities/legal.d.ts +21 -0
  42. package/dist/entities/legal.d.ts.map +1 -0
  43. package/dist/entities/legal.js +301 -0
  44. package/dist/entities/legal.js.map +1 -0
  45. package/dist/entities/market.d.ts +21 -0
  46. package/dist/entities/market.d.ts.map +1 -0
  47. package/dist/entities/market.js +301 -0
  48. package/dist/entities/market.js.map +1 -0
  49. package/dist/entities/marketing.d.ts +67 -0
  50. package/dist/entities/marketing.d.ts.map +1 -0
  51. package/dist/entities/marketing.js +1157 -0
  52. package/dist/entities/marketing.js.map +1 -0
  53. package/dist/entities/offerings.d.ts +51 -0
  54. package/dist/entities/offerings.d.ts.map +1 -0
  55. package/dist/entities/offerings.js +727 -0
  56. package/dist/entities/offerings.js.map +1 -0
  57. package/dist/entities/operations.d.ts +58 -0
  58. package/dist/entities/operations.d.ts.map +1 -0
  59. package/dist/entities/operations.js +787 -0
  60. package/dist/entities/operations.js.map +1 -0
  61. package/dist/entities/organization.d.ts +57 -0
  62. package/dist/entities/organization.d.ts.map +1 -0
  63. package/dist/entities/organization.js +807 -0
  64. package/dist/entities/organization.js.map +1 -0
  65. package/dist/entities/partnerships.d.ts +21 -0
  66. package/dist/entities/partnerships.d.ts.map +1 -0
  67. package/dist/entities/partnerships.js +300 -0
  68. package/dist/entities/partnerships.js.map +1 -0
  69. package/dist/entities/planning.d.ts +87 -0
  70. package/dist/entities/planning.d.ts.map +1 -0
  71. package/dist/entities/planning.js +271 -0
  72. package/dist/entities/planning.js.map +1 -0
  73. package/dist/entities/projects.d.ts +25 -0
  74. package/dist/entities/projects.d.ts.map +1 -0
  75. package/dist/entities/projects.js +349 -0
  76. package/dist/entities/projects.js.map +1 -0
  77. package/dist/entities/risk.d.ts +21 -0
  78. package/dist/entities/risk.d.ts.map +1 -0
  79. package/dist/entities/risk.js +293 -0
  80. package/dist/entities/risk.js.map +1 -0
  81. package/dist/entities/sales.d.ts +72 -0
  82. package/dist/entities/sales.d.ts.map +1 -0
  83. package/dist/entities/sales.js +1248 -0
  84. package/dist/entities/sales.js.map +1 -0
  85. package/dist/financials.d.ts +130 -0
  86. package/dist/financials.d.ts.map +1 -0
  87. package/dist/financials.js +297 -0
  88. package/dist/financials.js.map +1 -0
  89. package/dist/goals.d.ts +87 -0
  90. package/dist/goals.d.ts.map +1 -0
  91. package/dist/goals.js +215 -0
  92. package/dist/goals.js.map +1 -0
  93. package/dist/index.d.ts +97 -4
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +131 -1079
  96. package/dist/index.js.map +1 -1
  97. package/dist/kpis.d.ts +118 -0
  98. package/dist/kpis.d.ts.map +1 -0
  99. package/dist/kpis.js +232 -0
  100. package/dist/kpis.js.map +1 -0
  101. package/dist/metrics.d.ts +448 -0
  102. package/dist/metrics.d.ts.map +1 -0
  103. package/dist/metrics.js +325 -0
  104. package/dist/metrics.js.map +1 -0
  105. package/dist/okrs.d.ts +123 -0
  106. package/dist/okrs.d.ts.map +1 -0
  107. package/dist/okrs.js +269 -0
  108. package/dist/okrs.js.map +1 -0
  109. package/dist/organization.d.ts +585 -0
  110. package/dist/organization.d.ts.map +1 -0
  111. package/dist/organization.js +173 -0
  112. package/dist/organization.js.map +1 -0
  113. package/dist/process.d.ts +112 -0
  114. package/dist/process.d.ts.map +1 -0
  115. package/dist/process.js +241 -0
  116. package/dist/process.js.map +1 -0
  117. package/dist/product.d.ts +85 -0
  118. package/dist/product.d.ts.map +1 -0
  119. package/dist/product.js +145 -0
  120. package/dist/product.js.map +1 -0
  121. package/dist/queries.d.ts +304 -0
  122. package/dist/queries.d.ts.map +1 -0
  123. package/dist/queries.js +415 -0
  124. package/dist/queries.js.map +1 -0
  125. package/dist/roles.d.ts +340 -0
  126. package/dist/roles.d.ts.map +1 -0
  127. package/dist/roles.js +255 -0
  128. package/dist/roles.js.map +1 -0
  129. package/dist/service.d.ts +61 -0
  130. package/dist/service.d.ts.map +1 -0
  131. package/dist/service.js +140 -0
  132. package/dist/service.js.map +1 -0
  133. package/dist/types.d.ts +459 -0
  134. package/dist/types.d.ts.map +1 -0
  135. package/dist/types.js +5 -0
  136. package/dist/types.js.map +1 -0
  137. package/dist/vision.d.ts +38 -0
  138. package/dist/vision.d.ts.map +1 -0
  139. package/dist/vision.js +68 -0
  140. package/dist/vision.js.map +1 -0
  141. package/dist/workflow.d.ts +115 -0
  142. package/dist/workflow.d.ts.map +1 -0
  143. package/dist/workflow.js +247 -0
  144. package/dist/workflow.js.map +1 -0
  145. package/examples/basic-usage.ts +307 -0
  146. package/package.json +19 -60
  147. package/src/business.ts +121 -0
  148. package/src/dollar.ts +132 -0
  149. package/src/entities/assets.ts +332 -0
  150. package/src/entities/business.ts +406 -0
  151. package/src/entities/communication.ts +264 -0
  152. package/src/entities/customers.ts +1072 -0
  153. package/src/entities/financials.ts +1011 -0
  154. package/src/entities/goals.ts +871 -0
  155. package/src/entities/index.ts +383 -0
  156. package/src/entities/legal.ts +310 -0
  157. package/src/entities/market.ts +310 -0
  158. package/src/entities/marketing.ts +1249 -0
  159. package/src/entities/offerings.ts +789 -0
  160. package/src/entities/operations.ts +861 -0
  161. package/src/entities/organization.ts +876 -0
  162. package/src/entities/partnerships.ts +309 -0
  163. package/src/entities/planning.ts +307 -0
  164. package/src/entities/projects.ts +360 -0
  165. package/src/entities/risk.ts +302 -0
  166. package/src/entities/sales.ts +1352 -0
  167. package/src/financials.ts +352 -0
  168. package/src/goals.ts +250 -0
  169. package/src/index.test.ts +336 -0
  170. package/src/index.ts +530 -0
  171. package/src/kpis.ts +275 -0
  172. package/src/metrics.ts +825 -0
  173. package/src/okrs.ts +325 -0
  174. package/src/organization.ts +909 -0
  175. package/src/process.ts +272 -0
  176. package/src/product.ts +178 -0
  177. package/src/queries.ts +767 -0
  178. package/src/roles.ts +686 -0
  179. package/src/service.ts +164 -0
  180. package/src/types.ts +493 -0
  181. package/src/vision.ts +88 -0
  182. package/src/workflow.ts +280 -0
  183. package/tsconfig.json +9 -0
  184. package/dist/loaders/index.d.ts +0 -174
  185. package/dist/loaders/index.js +0 -366
  186. package/dist/loaders/index.js.map +0 -1
  187. package/dist/schema/index.d.ts +0 -146
  188. package/dist/schema/index.js +0 -716
  189. package/dist/schema/index.js.map +0 -1
  190. 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
+ }