Migrate Genesys CX Cloud TEI study to the pattern; retire Streamlit app
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>
This commit is contained in:
122
studies/202512_TEI_Genesys_CX_Cloud/tests/test_model.py
Normal file
122
studies/202512_TEI_Genesys_CX_Cloud/tests/test_model.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Engine pins — every number hand-checked before pinning.
|
||||
|
||||
RA_benefit = v×(1−rf), RA_cost = v×(1+rf), PV = Σ RA_n/(1.1)^n, initial
|
||||
undiscounted. The composite reproduction lands within $2 of the published
|
||||
Financial Summary (benefits PV $1.19 low, costs PV $0.40 high) — pinned
|
||||
both engine-exact (±$1) and against PUBLISHED (±$5). Forrester does not
|
||||
headline a payback for this study; the engine computes 3.3 months.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from teicalc import (
|
||||
BENEFITS_VERBATIM,
|
||||
COSTS_VERBATIM,
|
||||
PUBLISHED,
|
||||
X_LABELS,
|
||||
YEAR_INDEX,
|
||||
YEARS,
|
||||
by_calendar,
|
||||
compute_summary,
|
||||
discount_factor,
|
||||
money,
|
||||
npv,
|
||||
payback_label,
|
||||
payback_months,
|
||||
payback_years,
|
||||
roi_pct,
|
||||
)
|
||||
|
||||
# Hand-checked risk-adjusted PVs per row (see module docstring).
|
||||
ROW_PVS = {
|
||||
"legacy_retirement": 1_981_224.64,
|
||||
"self_service_savings": 4_924_364.84,
|
||||
"agent_efficiency": 6_517_541.70,
|
||||
"agent_assist_sales": 1_417_505.63,
|
||||
"cx_cloud_licenses": 2_193_403.46,
|
||||
"implementation": 1_309_000.00,
|
||||
"ongoing_management": 554_766.94,
|
||||
"genesys_ai_tokens": 0.00,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def composite():
|
||||
return compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
||||
|
||||
|
||||
def test_calendar_mapping():
|
||||
assert YEARS == [2026, 2027, 2028]
|
||||
assert YEAR_INDEX == {2026: 1, 2027: 2, 2028: 3}
|
||||
assert X_LABELS == ["Initial", "2026", "2027", "2028"]
|
||||
assert by_calendar({"1": 10, "2": 20, "3": 30}) == {2026: 10, 2027: 20, 2028: 30}
|
||||
|
||||
|
||||
def test_primitives():
|
||||
assert discount_factor(0, 0.10) == 1.0
|
||||
assert discount_factor(1, 0.10) == pytest.approx(1 / 1.1)
|
||||
assert npv([110], 0.10) == pytest.approx(100)
|
||||
assert npv([110], 0.10, initial=-50) == pytest.approx(50)
|
||||
assert roi_pct(14_840_638, 4_057_170) == pytest.approx(265.79, abs=0.1)
|
||||
assert roi_pct(100, 0) == 0.0
|
||||
assert money(10_783_466) == "$10.8M"
|
||||
assert money(-250_000) == "-$250K"
|
||||
|
||||
|
||||
def test_payback_edges():
|
||||
assert payback_years(0, [100]) == 0.0
|
||||
assert payback_years(500, []) is None
|
||||
assert payback_years(500, [-100, 200]) is None
|
||||
assert payback_years(300, [-100, 400]) == pytest.approx(2.0)
|
||||
assert payback_months(100, [1_200]) == pytest.approx(1.0)
|
||||
assert payback_label(None) == "beyond 2028"
|
||||
assert payback_label(0.0) == "immediate"
|
||||
assert payback_label(3.3337) == "3.3 months (~Apr 2026)"
|
||||
assert payback_label(14.2) == "14.2 months (~Mar 2027)"
|
||||
|
||||
|
||||
def test_per_row_pvs(composite):
|
||||
rows = composite["rows"]["benefits"] + composite["rows"]["costs"]
|
||||
assert len(rows) == 8
|
||||
for row in rows:
|
||||
assert row["pv"] == pytest.approx(ROW_PVS[row["field_key"]], abs=1)
|
||||
|
||||
|
||||
def test_composite_totals_engine_exact(composite):
|
||||
assert composite["benefits_pv"] == pytest.approx(14_840_636.81, abs=1)
|
||||
assert composite["costs_pv"] == pytest.approx(4_057_170.40, abs=1)
|
||||
assert composite["npv"] == pytest.approx(10_783_466.42, abs=1)
|
||||
assert composite["roi_pct"] == pytest.approx(265.7879, abs=0.01)
|
||||
assert composite["payback_months"] == pytest.approx(3.3337, abs=0.001)
|
||||
assert composite["initial_costs"] == pytest.approx(1_309_000, abs=0.01)
|
||||
|
||||
|
||||
def test_composite_reproduces_published(composite):
|
||||
assert composite["benefits_pv"] == pytest.approx(PUBLISHED["benefits_pv"], abs=5)
|
||||
assert composite["costs_pv"] == pytest.approx(PUBLISHED["costs_pv"], abs=5)
|
||||
assert composite["npv"] == pytest.approx(PUBLISHED["npv"], abs=5)
|
||||
assert round(composite["roi_pct"]) == PUBLISHED["roi_pct"]
|
||||
assert composite["payback_label"] == "3.3 months (~Apr 2026)"
|
||||
|
||||
|
||||
def test_yearly_schedules(composite):
|
||||
assert composite["benefits_by_year"][2026] == pytest.approx(5_816_960.00, abs=0.01)
|
||||
assert composite["benefits_by_year"][2027] == pytest.approx(6_054_460.00, abs=0.01)
|
||||
assert composite["benefits_by_year"][2028] == pytest.approx(6_054_460.00, abs=0.01)
|
||||
for y in YEARS:
|
||||
assert composite["costs_by_year"][y] == pytest.approx(1_105_080.00, abs=0.01)
|
||||
assert composite["cumulative_net_by_year"][2026] == pytest.approx(3_402_880.00, abs=0.01)
|
||||
assert composite["cumulative_net_by_year"][2028] == pytest.approx(13_301_640.00, abs=0.01)
|
||||
|
||||
|
||||
def test_cross_foots(composite):
|
||||
assert composite["npv"] == pytest.approx(
|
||||
composite["benefits_pv"] - composite["costs_pv"], abs=0.01)
|
||||
for y in YEARS:
|
||||
assert composite["net_by_year"][y] == pytest.approx(
|
||||
composite["benefits_by_year"][y] - composite["costs_by_year"][y], abs=0.01)
|
||||
assert composite["cumulative_net_by_year"][2028] == pytest.approx(
|
||||
sum(composite["net_by_year"].values()) - composite["initial_costs"], abs=0.01)
|
||||
for table, total in (("benefits", "benefits_pv"), ("costs", "costs_pv")):
|
||||
assert sum(r["pv"] for r in composite["rows"][table]) == pytest.approx(
|
||||
composite[total], abs=0.01)
|
||||
Reference in New Issue
Block a user