UNPKG

business-as-code

Version:

Primitives for expressing business logic and processes as code

540 lines (446 loc) 14.8 kB
/** * Tests for financials.ts - Financial metrics and calculations */ import { describe, it, expect } from 'vitest' import { financials, calculateGrossMargin, calculateOperatingMargin, calculateNetMargin, calculateEBITDAMargin, calculateBurnRate, calculateRunway, calculateCAC, calculateLTV, calculateLTVtoCAC, calculatePaybackPeriod, calculateARR, calculateMRR, calculateGrowthRate, calculateCAGR, calculateROI, calculateROE, calculateROA, calculateQuickRatio, calculateCurrentRatio, calculateDebtToEquity, formatCurrency, createStatement, getLineItem, compareMetrics, validateFinancials, } from '../src/financials.js' import type { FinancialMetrics, FinancialStatement } from '../src/types.js' describe('Financials', () => { describe('financials()', () => { it('should calculate financial metrics from basic inputs', () => { const metrics = financials({ revenue: 1000000, cogs: 300000, operatingExpenses: 500000, }) expect(metrics.revenue).toBe(1000000) expect(metrics.cogs).toBe(300000) expect(metrics.grossProfit).toBe(700000) expect(metrics.grossMargin).toBe(70) expect(metrics.operatingIncome).toBe(200000) expect(metrics.operatingMargin).toBe(20) expect(metrics.netIncome).toBe(200000) expect(metrics.netMargin).toBe(20) }) it('should use defaults for missing inputs', () => { const metrics = financials({}) expect(metrics.revenue).toBe(0) expect(metrics.cogs).toBe(0) expect(metrics.operatingExpenses).toBe(0) expect(metrics.currency).toBe('USD') expect(metrics.period).toBe('monthly') }) it('should preserve provided values', () => { const metrics = financials({ revenue: 1000000, cogs: 300000, operatingExpenses: 500000, netIncome: 150000, ebitda: 250000, currency: 'EUR', period: 'quarterly', }) expect(metrics.netIncome).toBe(150000) expect(metrics.ebitda).toBe(250000) expect(metrics.currency).toBe('EUR') expect(metrics.period).toBe('quarterly') }) it('should handle zero revenue', () => { const metrics = financials({ revenue: 0, cogs: 0, operatingExpenses: 0, }) expect(metrics.grossMargin).toBe(0) expect(metrics.operatingMargin).toBe(0) expect(metrics.netMargin).toBe(0) }) }) describe('calculateGrossMargin()', () => { it('should calculate gross margin percentage', () => { expect(calculateGrossMargin(100, 30)).toBe(70) }) it('should return 0 for zero revenue', () => { expect(calculateGrossMargin(0, 0)).toBe(0) }) it('should handle 100% margin', () => { expect(calculateGrossMargin(100, 0)).toBe(100) }) it('should handle negative margin', () => { expect(calculateGrossMargin(100, 150)).toBe(-50) }) }) describe('calculateOperatingMargin()', () => { it('should calculate operating margin', () => { expect(calculateOperatingMargin(100, 30, 20)).toBe(50) }) it('should return 0 for zero revenue', () => { expect(calculateOperatingMargin(0, 0, 0)).toBe(0) }) it('should handle negative operating income', () => { expect(calculateOperatingMargin(100, 50, 70)).toBe(-20) }) }) describe('calculateNetMargin()', () => { it('should calculate net margin', () => { expect(calculateNetMargin(100, 25)).toBe(25) }) it('should return 0 for zero revenue', () => { expect(calculateNetMargin(0, 0)).toBe(0) }) it('should handle negative net income', () => { expect(calculateNetMargin(100, -10)).toBe(-10) }) }) describe('calculateEBITDAMargin()', () => { it('should calculate EBITDA margin', () => { expect(calculateEBITDAMargin(100, 30)).toBe(30) }) it('should return 0 for zero revenue', () => { expect(calculateEBITDAMargin(0, 0)).toBe(0) }) }) describe('calculateBurnRate()', () => { it('should calculate monthly burn rate', () => { expect(calculateBurnRate(100000, 70000, 3)).toBe(10000) }) it('should return 0 for zero months', () => { expect(calculateBurnRate(100000, 70000, 0)).toBe(0) }) it('should handle positive cash flow (negative burn)', () => { expect(calculateBurnRate(100000, 130000, 3)).toBe(-10000) }) }) describe('calculateRunway()', () => { it('should calculate runway in months', () => { expect(calculateRunway(100000, 10000)).toBe(10) }) it('should return Infinity for zero burn rate', () => { expect(calculateRunway(100000, 0)).toBe(Infinity) }) it('should return Infinity for negative burn rate (profitable)', () => { expect(calculateRunway(100000, -10000)).toBe(Infinity) }) }) describe('calculateCAC()', () => { it('should calculate customer acquisition cost', () => { expect(calculateCAC(10000, 100)).toBe(100) }) it('should return 0 for zero customers', () => { expect(calculateCAC(10000, 0)).toBe(0) }) it('should handle zero spend', () => { expect(calculateCAC(0, 100)).toBe(0) }) }) describe('calculateLTV()', () => { it('should calculate lifetime value', () => { // LTV = ARPU * Lifetime * (Gross Margin / 100) // 100 * 24 * 0.7 = 1680 expect(calculateLTV(100, 24, 70)).toBe(1680) }) it('should handle zero values', () => { expect(calculateLTV(0, 24, 70)).toBe(0) expect(calculateLTV(100, 0, 70)).toBe(0) expect(calculateLTV(100, 24, 0)).toBe(0) }) }) describe('calculateLTVtoCAC()', () => { it('should calculate LTV:CAC ratio', () => { expect(calculateLTVtoCAC(300, 100)).toBe(3) }) it('should return 0 for zero CAC', () => { expect(calculateLTVtoCAC(300, 0)).toBe(0) }) }) describe('calculatePaybackPeriod()', () => { it('should calculate payback period in months', () => { expect(calculatePaybackPeriod(1200, 100)).toBe(12) }) it('should return 0 for zero monthly revenue', () => { expect(calculatePaybackPeriod(1200, 0)).toBe(0) }) }) describe('calculateARR()', () => { it('should calculate ARR from MRR', () => { expect(calculateARR(10000)).toBe(120000) }) it('should handle zero MRR', () => { expect(calculateARR(0)).toBe(0) }) }) describe('calculateMRR()', () => { it('should calculate MRR from ARR', () => { expect(calculateMRR(120000)).toBe(10000) }) it('should handle zero ARR', () => { expect(calculateMRR(0)).toBe(0) }) }) describe('calculateGrowthRate()', () => { it('should calculate growth rate', () => { expect(calculateGrowthRate(120, 100)).toBe(20) }) it('should calculate negative growth', () => { expect(calculateGrowthRate(80, 100)).toBe(-20) }) it('should return 0 for zero previous revenue', () => { expect(calculateGrowthRate(100, 0)).toBe(0) }) it('should handle same values (no growth)', () => { expect(calculateGrowthRate(100, 100)).toBe(0) }) it('should handle doubling', () => { expect(calculateGrowthRate(200, 100)).toBe(100) }) }) describe('calculateCAGR()', () => { it('should calculate compound annual growth rate', () => { // $100 to $200 over 5 years const cagr = calculateCAGR(100, 200, 5) expect(cagr).toBeCloseTo(14.87, 1) }) it('should return 0 for zero beginning value', () => { expect(calculateCAGR(0, 200, 5)).toBe(0) }) it('should return 0 for zero years', () => { expect(calculateCAGR(100, 200, 0)).toBe(0) }) it('should handle negative growth', () => { const cagr = calculateCAGR(200, 100, 5) expect(cagr).toBeLessThan(0) }) }) describe('calculateROI()', () => { it('should calculate ROI', () => { expect(calculateROI(150, 100)).toBe(50) }) it('should calculate negative ROI', () => { expect(calculateROI(80, 100)).toBe(-20) }) it('should return 0 for zero cost', () => { expect(calculateROI(150, 0)).toBe(0) }) it('should handle 100% ROI', () => { expect(calculateROI(200, 100)).toBe(100) }) }) describe('calculateROE()', () => { it('should calculate return on equity', () => { expect(calculateROE(50000, 250000)).toBe(20) }) it('should return 0 for zero equity', () => { expect(calculateROE(50000, 0)).toBe(0) }) }) describe('calculateROA()', () => { it('should calculate return on assets', () => { expect(calculateROA(50000, 500000)).toBe(10) }) it('should return 0 for zero assets', () => { expect(calculateROA(50000, 0)).toBe(0) }) }) describe('calculateQuickRatio()', () => { it('should calculate quick ratio', () => { // (100000 - 20000) / 40000 = 2 expect(calculateQuickRatio(100000, 20000, 40000)).toBe(2) }) it('should return 0 for zero liabilities', () => { expect(calculateQuickRatio(100000, 20000, 0)).toBe(0) }) }) describe('calculateCurrentRatio()', () => { it('should calculate current ratio', () => { expect(calculateCurrentRatio(100000, 50000)).toBe(2) }) it('should return 0 for zero liabilities', () => { expect(calculateCurrentRatio(100000, 0)).toBe(0) }) }) describe('calculateDebtToEquity()', () => { it('should calculate debt-to-equity ratio', () => { expect(calculateDebtToEquity(100000, 200000)).toBe(0.5) }) it('should return 0 for zero equity', () => { expect(calculateDebtToEquity(100000, 0)).toBe(0) }) }) describe('formatCurrency()', () => { it('should format USD currency', () => { const formatted = formatCurrency(1234.56) expect(formatted).toContain('$') expect(formatted).toContain('1,234.56') }) it('should format EUR currency', () => { const formatted = formatCurrency(1234.56, 'EUR') expect(formatted).toContain('1,234.56') }) it('should format GBP currency', () => { const formatted = formatCurrency(1234.56, 'GBP') expect(formatted).toContain('1,234.56') }) it('should handle negative amounts', () => { const formatted = formatCurrency(-1234.56) expect(formatted).toContain('-') }) it('should handle zero amount', () => { const formatted = formatCurrency(0) expect(formatted).toContain('$') expect(formatted).toContain('0') }) it('should handle large numbers', () => { const formatted = formatCurrency(1000000000) expect(formatted).toContain('1,000,000,000') }) }) describe('createStatement()', () => { it('should create a financial statement', () => { const statement = createStatement('income', 'Q4 2024', { revenue: 1000000, cogs: 300000, operatingExpenses: 500000, }) expect(statement.type).toBe('income') expect(statement.period).toBe('Q4 2024') expect(statement.lineItems.revenue).toBe(1000000) expect(statement.currency).toBe('USD') }) it('should use specified currency', () => { const statement = createStatement('balance', 'Q4 2024', {}, 'EUR') expect(statement.currency).toBe('EUR') }) }) describe('getLineItem()', () => { const statement: FinancialStatement = { type: 'income', period: 'Q4 2024', lineItems: { revenue: 1000000, cogs: 300000, }, currency: 'USD', } it('should get existing line item', () => { expect(getLineItem(statement, 'revenue')).toBe(1000000) }) it('should return 0 for non-existent line item', () => { expect(getLineItem(statement, 'expenses')).toBe(0) }) }) describe('compareMetrics()', () => { it('should compare financial metrics between periods', () => { const current = financials({ revenue: 1200000, cogs: 360000, operatingExpenses: 600000, }) const previous = financials({ revenue: 1000000, cogs: 300000, operatingExpenses: 500000, }) const comparison = compareMetrics(current, previous) expect(comparison.revenue?.change).toBe(200000) expect(comparison.revenue?.changePercent).toBe(20) }) it('should handle zero previous values', () => { const current = financials({ revenue: 100000 }) const previous = financials({ revenue: 0 }) const comparison = compareMetrics(current, previous) expect(comparison.revenue?.changePercent).toBe(0) }) }) describe('validateFinancials()', () => { it('should validate valid financials', () => { const metrics = financials({ revenue: 1000000, cogs: 300000, operatingExpenses: 500000, }) const result = validateFinancials(metrics) expect(result.valid).toBe(true) expect(result.errors).toHaveLength(0) }) it('should fail for negative revenue', () => { const metrics: FinancialMetrics = { revenue: -100000, currency: 'USD', period: 'monthly', } const result = validateFinancials(metrics) expect(result.valid).toBe(false) expect(result.errors).toContain('Revenue cannot be negative') }) it('should fail for negative COGS', () => { const metrics: FinancialMetrics = { revenue: 100000, cogs: -30000, currency: 'USD', period: 'monthly', } const result = validateFinancials(metrics) expect(result.valid).toBe(false) expect(result.errors).toContain('COGS cannot be negative') }) it('should fail for negative operating expenses', () => { const metrics: FinancialMetrics = { revenue: 100000, operatingExpenses: -50000, currency: 'USD', period: 'monthly', } const result = validateFinancials(metrics) expect(result.valid).toBe(false) expect(result.errors).toContain('Operating expenses cannot be negative') }) it('should fail if COGS exceeds revenue', () => { const metrics: FinancialMetrics = { revenue: 100000, cogs: 150000, currency: 'USD', period: 'monthly', } const result = validateFinancials(metrics) expect(result.valid).toBe(false) expect(result.errors).toContain('COGS cannot exceed revenue') }) it('should allow zero values', () => { const metrics: FinancialMetrics = { revenue: 0, cogs: 0, operatingExpenses: 0, currency: 'USD', period: 'monthly', } const result = validateFinancials(metrics) expect(result.valid).toBe(true) }) }) })