Files
Robert Helewka e88449d15a 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>
2026-07-09 16:38:51 -04:00

96 lines
3.9 KiB
Python
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.
"""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