/** * Tests del módulo domain/roi.ts. * Cubren: happy path, edge cases de inversión cero, ahorro cero/negativo, * división por cero, escenarios de largo plazo y corto plazo. * Todas las fórmulas son nominales simples (ADR-014). */ import { describe, it, expect } from 'vitest' import { calculateRoi } from '@/domain/roi' import type { RoiInput } from '@/domain/roi' // ─── Fixture base ───────────────────────────────────────────────────────────── function baseInput(overrides: Partial = {}): RoiInput { return { costPerExecutionActual: 100, costPerExecutionAutomated: 20, annualFrequency: 1000, analysisHorizonYears: 3, automationInvestment: 50_000, ...overrides, } } // ─── Happy path ─────────────────────────────────────────────────────────────── describe('calculateRoi — happy path', () => { it('calcula ahorro por ejecución correctamente', () => { const r = calculateRoi(baseInput()) expect(r.savingsPerExecution).toBeCloseTo(80) }) it('calcula ahorro por ejecución en %', () => { const r = calculateRoi(baseInput()) expect(r.savingsPerExecutionPercent).toBeCloseTo(80) // 80/100 = 80% }) it('calcula ahorro anual = ahorro/ejecución × frecuencia', () => { const r = calculateRoi(baseInput()) expect(r.annualSavings).toBeCloseTo(80_000) // 80 × 1000 }) it('calcula payback en meses correctamente', () => { const r = calculateRoi(baseInput()) // $50.000 / $80.000/año × 12 = 7.5 meses expect(r.paybackMonths).toBeCloseTo(7.5) }) it('calcula payback en años', () => { const r = calculateRoi(baseInput()) expect(r.paybackYears).toBeCloseTo(0.625) }) it('calcula ahorro acumulado neto al horizonte', () => { const r = calculateRoi(baseInput()) // ($80.000 × 3 años) - $50.000 = $190.000 expect(r.cumulativeSavingsHorizon).toBeCloseTo(190_000) }) it('el payback dentro del horizonte es true cuando paybackYears < horizonYears', () => { const r = calculateRoi(baseInput()) // payback ~0.625 < 3 años expect(r.breaksEvenInHorizon).toBe(true) }) it('calcula ROI anualizado %', () => { const r = calculateRoi(baseInput()) // ($80.000 / $50.000) × 100 = 160% expect(r.roiAnnualPercent).toBeCloseTo(160) }) }) // ─── Edge case: inversión cero con ahorro positivo ──────────────────────────── describe('calculateRoi — inversión cero', () => { it('payback es 0 meses (recuperación instantánea)', () => { const r = calculateRoi(baseInput({ automationInvestment: 0 })) expect(r.paybackMonths).toBe(0) expect(r.paybackYears).toBe(0) }) it('breaksEvenInHorizon es true', () => { const r = calculateRoi(baseInput({ automationInvestment: 0 })) expect(r.breaksEvenInHorizon).toBe(true) }) it('roiAnnualPercent es Infinity', () => { const r = calculateRoi(baseInput({ automationInvestment: 0 })) expect(r.roiAnnualPercent).toBe(Infinity) }) it('inversión cero con ahorro cero: roiAnnualPercent es 0', () => { const r = calculateRoi(baseInput({ automationInvestment: 0, costPerExecutionActual: 100, costPerExecutionAutomated: 100, })) expect(r.roiAnnualPercent).toBe(0) }) }) // ─── Edge case: ahorro cero o negativo ──────────────────────────────────────── describe('calculateRoi — ahorro negativo o cero', () => { it('si el costo automatizado = costo actual, ahorro = 0', () => { const r = calculateRoi(baseInput({ costPerExecutionAutomated: 100 })) expect(r.savingsPerExecution).toBeCloseTo(0) expect(r.annualSavings).toBeCloseTo(0) }) it('ahorro cero → payback Infinity', () => { const r = calculateRoi(baseInput({ costPerExecutionAutomated: 100 })) expect(r.paybackMonths).toBe(Infinity) expect(r.paybackYears).toBe(Infinity) }) it('ahorro cero → breaksEvenInHorizon false', () => { const r = calculateRoi(baseInput({ costPerExecutionAutomated: 100 })) expect(r.breaksEvenInHorizon).toBe(false) }) it('ahorro negativo (automatizado más caro): payback Infinity', () => { const r = calculateRoi(baseInput({ costPerExecutionAutomated: 150 })) expect(r.annualSavings).toBeLessThan(0) expect(r.paybackMonths).toBe(Infinity) expect(r.breaksEvenInHorizon).toBe(false) }) it('ahorro negativo: cumulativeSavings también es negativo', () => { const r = calculateRoi(baseInput({ costPerExecutionAutomated: 150 })) expect(r.cumulativeSavingsHorizon).toBeLessThan(0) }) }) // ─── Edge case: costo actual cero (evitar div/0) ────────────────────────────── describe('calculateRoi — costo actual cero', () => { it('savingsPerExecutionPercent es 0, no NaN ni Infinity', () => { const r = calculateRoi(baseInput({ costPerExecutionActual: 0, costPerExecutionAutomated: 0, })) expect(r.savingsPerExecutionPercent).toBe(0) expect(Number.isFinite(r.savingsPerExecutionPercent)).toBe(true) }) }) // ─── Escenario de largo plazo (horizonte 10 años) ───────────────────────────── describe('calculateRoi — horizonte extendido', () => { it('payback fuera del horizonte corto → breaksEvenInHorizon false', () => { // savingsPerExec=10, freq=1000 → annualSavings=$10.000; payback=$500.000/$10.000=50 años const r = calculateRoi(baseInput({ automationInvestment: 500_000, costPerExecutionActual: 100, costPerExecutionAutomated: 90, annualFrequency: 1000, })) expect(r.paybackYears).toBeGreaterThan(3) expect(r.breaksEvenInHorizon).toBe(false) }) })