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:
95
studies/202512_TEI_Genesys_CX_Cloud/tests/test_overlay.py
Normal file
95
studies/202512_TEI_Genesys_CX_Cloud/tests/test_overlay.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Client-overlay pins — identity at the composite, linear per-driver
|
||||
scaling, the direct AI-token input, and copy semantics."""
|
||||
|
||||
import dataclasses
|
||||
|
||||
import pytest
|
||||
|
||||
from teicalc import (
|
||||
BENEFIT_DRIVERS,
|
||||
BENEFITS_VERBATIM,
|
||||
COMPOSITE,
|
||||
COST_DRIVERS,
|
||||
COSTS_VERBATIM,
|
||||
ClientDrivers,
|
||||
compute_summary,
|
||||
overlay_rows,
|
||||
scale_factor,
|
||||
)
|
||||
|
||||
|
||||
def _row(rows, key):
|
||||
return next(r for r in rows if r["field_key"] == key)
|
||||
|
||||
|
||||
def test_identity_at_composite():
|
||||
"""overlay_rows(COMPOSITE) reproduces the verbatim study to the cent."""
|
||||
ob, oc = overlay_rows(COMPOSITE)
|
||||
got = compute_summary(ob, oc, 0.10)
|
||||
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)
|
||||
assert got["npv"] == pytest.approx(want["npv"], abs=0.01)
|
||||
|
||||
|
||||
def test_driver_map_covers_every_row():
|
||||
assert set(BENEFIT_DRIVERS) == {r["field_key"] for r in BENEFITS_VERBATIM}
|
||||
assert set(COST_DRIVERS) == {r["field_key"] for r in COSTS_VERBATIM}
|
||||
|
||||
|
||||
def test_scale_factor():
|
||||
d = ClientDrivers(agents_fte=300, weekly_interactions=160_000,
|
||||
annual_revenue=5_000_000_000)
|
||||
assert scale_factor("agents", d) == pytest.approx(0.5)
|
||||
assert scale_factor("interactions", d) == pytest.approx(2.0)
|
||||
assert scale_factor("revenue", d) == pytest.approx(2.0)
|
||||
assert scale_factor("fixed", d) == 1.0
|
||||
with pytest.raises(KeyError):
|
||||
scale_factor("contacts", d)
|
||||
|
||||
|
||||
def test_half_agents_halves_agent_rows_only():
|
||||
ob, oc = overlay_rows(ClientDrivers(agents_fte=300))
|
||||
assert _row(ob, "legacy_retirement")["year_values"]["1"] == pytest.approx(340_000)
|
||||
assert _row(oc, "cx_cloud_licenses")["year_values"]["1"] == pytest.approx(420_000)
|
||||
# Interaction-, revenue-driven, and fixed rows unmoved.
|
||||
assert _row(ob, "self_service_savings")["year_values"]["1"] == pytest.approx(2_329_600)
|
||||
assert _row(ob, "agent_assist_sales")["year_values"]["1"] == pytest.approx(600_000)
|
||||
assert _row(oc, "implementation")["initial"] == 1_190_000
|
||||
|
||||
|
||||
def test_double_interactions_doubles_volume_rows_only():
|
||||
ob, oc = overlay_rows(ClientDrivers(weekly_interactions=160_000))
|
||||
assert _row(ob, "self_service_savings")["year_values"]["1"] == pytest.approx(4_659_200)
|
||||
assert _row(ob, "agent_efficiency")["year_values"]["1"] == pytest.approx(5_824_000)
|
||||
assert _row(ob, "legacy_retirement")["year_values"]["1"] == pytest.approx(680_000)
|
||||
assert _row(oc, "cx_cloud_licenses")["year_values"]["1"] == pytest.approx(840_000)
|
||||
|
||||
|
||||
def test_double_revenue_doubles_agent_assist_only():
|
||||
ob, _ = overlay_rows(ClientDrivers(annual_revenue=5_000_000_000))
|
||||
assert _row(ob, "agent_assist_sales")["year_values"]["1"] == pytest.approx(1_200_000)
|
||||
assert _row(ob, "self_service_savings")["year_values"]["1"] == pytest.approx(2_329_600)
|
||||
|
||||
|
||||
def test_ai_tokens_direct_input():
|
||||
"""The token line takes the negotiated annual figure directly (rf 0.0),
|
||||
adding annual × Σ1/1.1ⁿ = 250,000 × 2.48685… ≈ $621,713 to costs PV."""
|
||||
_, oc = overlay_rows(ClientDrivers(ai_tokens_annual=250_000))
|
||||
tokens = _row(oc, "genesys_ai_tokens")
|
||||
assert tokens["year_values"] == {"1": 250_000.0, "2": 250_000.0, "3": 250_000.0}
|
||||
base = compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
|
||||
ob, oc = overlay_rows(ClientDrivers(ai_tokens_annual=250_000))
|
||||
got = compute_summary(ob, oc, 0.10)
|
||||
assert got["costs_pv"] - base["costs_pv"] == pytest.approx(621_713.00, abs=1)
|
||||
assert got["benefits_pv"] == pytest.approx(base["benefits_pv"], abs=0.01)
|
||||
|
||||
|
||||
def test_drivers_frozen_and_rows_are_copies():
|
||||
with pytest.raises(dataclasses.FrozenInstanceError):
|
||||
COMPOSITE.agents_fte = 1 # type: ignore[misc]
|
||||
ob, oc = overlay_rows(COMPOSITE)
|
||||
ob[0]["year_values"]["1"] = -1
|
||||
oc[0]["year_values"]["1"] = -1
|
||||
assert BENEFITS_VERBATIM[0]["year_values"]["1"] == 680_000
|
||||
assert COSTS_VERBATIM[0]["year_values"]["1"] == 840_000
|
||||
Reference in New Issue
Block a user