studies/202512_GenesysCX -> studies/202512_TEI_Genesys_CX_Cloud, rebuilt as pattern Variant 4 (TEI composite reproduction): - teicalc/ self-contained engine: Forrester's tables as the never-edited verbatim anchor (incl. the p.14 typo note and the $0 AI-token line), generic model/scenarios/staging carried over from the Amazon Connect study, ClientDrivers overlay (agents / weekly interactions / revenue, flat composite so no growth re-base) with ai_tokens_annual as a direct input for the token line the published study models at $0 - one deliverable notebook (business_case.ipynb): widget-pair sidebar drivers incl. the AI-token price, published-vs-overlay KPI columns, cash-flow/waterfall/scenario charts, verification gate, backstage JSON data appendix - gate + tests reproduce the published totals within $2: NPV $10.8M / ROI 266% (engine $10,783,466 / 265.79%; payback 3.3 months, not headlined in the PDF); 29 study tests, headless nbconvert green, stage simulation leak-free, exports carry the appendix - old Athena workflow (00_provision..04_export, config.py, seed_data.py, PALLADIUM_GENESYSCX_* keys, ATHENA_EXPECTED reconciliation) deleted; git history preserves it With the last legacy study migrated, the retirement lands too: - app/ (Streamlit UI) and core/notebook_helpers deleted; nothing else imported them - streamlit stripped from pyproject extras, requirements.txt, Makefile; .env.example reduced to the Athena keys; 00_setup.ipynb and core/bootstrap.py repointed at the pattern studies - root README reworked: self-contained studies + slim core/ Athena toolkit (tei_client, calculations, export, cli) All suites green: Genesys 29, Amazon Connect 27, CTM 55, template 7, root 58. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75 lines
2.5 KiB
Python
75 lines
2.5 KiB
Python
"""Scenario pins — hand-checked composite results per scenario, clamp
|
|
behaviour, and copy semantics."""
|
|
|
|
import pytest
|
|
|
|
from teicalc import (
|
|
BENEFITS_VERBATIM,
|
|
COSTS_VERBATIM,
|
|
SCENARIOS,
|
|
apply_scenario,
|
|
compute_summary,
|
|
)
|
|
|
|
|
|
def _summary(scenario):
|
|
return compute_summary(
|
|
apply_scenario(BENEFITS_VERBATIM, scenario),
|
|
apply_scenario(COSTS_VERBATIM, scenario),
|
|
0.10,
|
|
)
|
|
|
|
|
|
def test_scenario_definitions():
|
|
assert SCENARIOS == {
|
|
"conservative": {"adoption": 0.80, "risk_delta": 0.10},
|
|
"moderate": {"adoption": 1.00, "risk_delta": 0.00},
|
|
"aggressive": {"adoption": 1.15, "risk_delta": -0.05},
|
|
}
|
|
|
|
|
|
def test_moderate_is_identity():
|
|
got = _summary("moderate")
|
|
want = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
|
assert got["benefits_pv"] == pytest.approx(want["benefits_pv"], abs=0.01)
|
|
assert got["costs_pv"] == pytest.approx(want["costs_pv"], abs=0.01)
|
|
|
|
|
|
def test_conservative_pins():
|
|
s = _summary("conservative")
|
|
assert s["benefits_pv"] == pytest.approx(10_543_493.91, abs=1)
|
|
assert s["costs_pv"] == pytest.approx(3_026_631.40, abs=1)
|
|
assert s["npv"] == pytest.approx(7_516_862.51, abs=1)
|
|
assert s["roi_pct"] == pytest.approx(248.36, abs=0.01)
|
|
assert s["payback_months"] == pytest.approx(3.464, abs=0.001)
|
|
|
|
|
|
def test_aggressive_pins():
|
|
s = _summary("aggressive")
|
|
assert s["benefits_pv"] == pytest.approx(18_021_962.25, abs=1)
|
|
assert s["costs_pv"] == pytest.approx(4_883_285.09, abs=1)
|
|
assert s["npv"] == pytest.approx(13_138_677.16, abs=1)
|
|
assert s["roi_pct"] == pytest.approx(269.05, abs=0.01)
|
|
assert s["payback_months"] == pytest.approx(3.294, abs=0.001)
|
|
|
|
|
|
def test_risk_delta_clamps_at_zero():
|
|
"""Conservative subtracts 0.10 from cost risk; every cost rf clamps to 0
|
|
(licenses 0.05, implementation 0.10, ongoing 0.10, tokens 0.0)."""
|
|
rows = apply_scenario(COSTS_VERBATIM, "conservative")
|
|
assert all(r["risk_adjustment"] == 0.0 for r in rows)
|
|
impl = next(r for r in rows if r["field_key"] == "implementation")
|
|
assert impl["initial"] == pytest.approx(1_190_000 * 0.80) # adoption scales initial
|
|
|
|
|
|
def test_unknown_scenario_raises():
|
|
with pytest.raises(KeyError):
|
|
apply_scenario(BENEFITS_VERBATIM, "wildly_optimistic")
|
|
|
|
|
|
def test_inputs_not_mutated():
|
|
apply_scenario(BENEFITS_VERBATIM, "aggressive")
|
|
apply_scenario(COSTS_VERBATIM, "conservative")
|
|
assert BENEFITS_VERBATIM[0]["year_values"]["1"] == 680_000
|
|
assert COSTS_VERBATIM[1]["initial"] == 1_190_000
|