Files

162 lines
6.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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> = {}): 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)
})
})