Antes de la última revisión de etapa 4. Sprint 1. Previo al ajuste de look n feel de app, reportes, nombre y colores.

This commit is contained in:
2026-05-15 22:14:53 -03:00
parent b6f31f4371
commit 8b71c6ee13
54 changed files with 6497 additions and 261 deletions

161
tests/domain/roi.test.ts Normal file
View File

@@ -0,0 +1,161 @@
/**
* 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)
})
})