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:
2026-07-09 16:38:51 -04:00
parent a420af230b
commit e88449d15a
54 changed files with 8462 additions and 6427 deletions

Binary file not shown.

View File

@@ -1,51 +0,0 @@
# Genesys CX Cloud TEI — December 2025
Source: Forrester, *The Total Economic Impact™ Of CX Cloud — Cost Savings And
Business Benefits Enabled By Genesys And Salesforce* (commissioned by Genesys
and Salesforce, December 2025). PDF in `docs/`.
## Headline (published, 3-yr risk-adjusted PV @ 10%)
| Metric | Value |
|---|---|
| Benefits PV | $14,840,638 |
| Costs PV | $4,057,170 |
| **NPV** | **$10,783,468** |
| **ROI** | **266%** |
| Payback | ~4 months (computed; not headlined in the study) |
Composite: global supply company, $2.5B revenue, 10,000 employees, 600 CX
agents (400 concurrent licenses), 80,000 weekly interactions @ 12 min.
## Structure
4 benefits (legacy retirement ↓5%, self-service savings ↓15%, agent
efficiency ↓10%, agent-assist sales ↓5%) and 3 published costs (licenses ↑5%,
implementation ↑10% — initial-only, ongoing management ↑10%), **plus one
Palladium addition**: `genesys_ai_tokens`, an AI Experience token consumption
line the published study omits (it models $0 AI cost while three of four
benefits depend on AI). Stored exactly as Athena stores it — a single annual
cost value, entered from the Genesys quote in `01_business_case.ipynb` (which
includes a sensitivity sweep), with quote details kept in the field notes.
Seeded at $0 to reproduce the published totals.
## Study quirks (documented, handled)
- p.14 prints implementation initial as $1,304,600; correct figure is
$1,309,000 (= 1,190,000 × 1.10) per the detail table and cash-flow analysis.
- B7's printed formula cites B2 (15%) where the 12-minute interaction length
is meant; the result (40 FTEs) is correct.
- The initial cost is ~32% of cost PV, so Athena's discount-initial-as-Year-1
behaviour shifts ROI to ~277%. Verification matches `ATHENA_EXPECTED`
tightly, then reconciles to `PUBLISHED` with this explained delta.
## Notebooks
| Notebook | Purpose |
|---|---|
| `00_provision.ipynb` | Create template + fields + tool in Athena (client/proposal selection), seed, calculate, verify |
| `01_business_case.ipynb` | Working business case + Genesys AI token quantity × price sensitivity |
Env keys are study-scoped: `PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID`,
`PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID`, `PALLADIUM_GENESYSCX_PROPOSAL_ID` /
`PALLADIUM_GENESYSCX_ENGAGEMENT_ID`.

View File

@@ -1,38 +0,0 @@
"""
Study configuration for the Genesys CX Cloud TEI (Forrester, December 2025).
Env keys are *study-scoped* (PALLADIUM_GENESYSCX_*) so this study can coexist
with the Amazon Connect tool IDs in the same .env. 00_provision.ipynb writes
them for you.
"""
from __future__ import annotations
import os
#: Human-friendly study identifier — used in export metadata + filenames.
STUDY_SLUG = "202512_GenesysCX"
def _int_env(name: str) -> int | None:
raw = os.getenv(name, "").strip()
return int(raw) if raw else None
#: TEI Report template public_id (12-char short UUID).
REPORT_PUBLIC_ID: str = os.getenv("PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID", "")
#: TEI Tool instance public_id.
TOOL_PUBLIC_ID: str = os.getenv("PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID", "")
#: Default discount rate used for local validation of the study numbers.
DISCOUNT_RATE = 0.10
#: Analysis horizon (years).
ANALYSIS_YEARS = 3
#: Athena Proposal PK (a TEI tool attaches to a Proposal OR an Engagement).
PROPOSAL_ID: int | None = _int_env("PALLADIUM_GENESYSCX_PROPOSAL_ID")
#: Athena Engagement PK (alternative attachment point).
ENGAGEMENT_ID: int | None = _int_env("PALLADIUM_GENESYSCX_ENGAGEMENT_ID")

View File

@@ -1,934 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "41520e77",
"metadata": {},
"source": [
"# 00 · Provision — Genesys CX Cloud TEI in Athena\n",
"\n",
"Source study: Forrester, *The Total Economic Impact™ Of CX Cloud* (Genesys +\n",
"Salesforce, December 2025). Published headline: **NPV \\$10.78M · ROI 266%**.\n",
"\n",
"This notebook creates everything the study needs in the Athena sandbox:\n",
"\n",
"1. **Report template** *CX Cloud (Genesys + Salesforce) 2025* + **field definitions** — 4 benefits, 3 published costs, **plus the `genesys_ai_tokens` consumption line the published study omits**\n",
"2. **Client selection** from the CRM (profile pulled, no re-entry)\n",
"3. **Attachment** to a Proposal or Engagement\n",
"4. **Seed values** + server-side **calculation**\n",
"5. **Two-tier verification**: exact match vs Athena-methodology expectations, then reconciliation to the published totals (explained Year-0 discounting delta)\n",
"6. Persists study-scoped IDs (`PALLADIUM_GENESYSCX_*`) to `.env`\n",
"\n",
"Safe to re-run — every step finds existing objects before creating new ones."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "1b6f1117",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Athena connected — https://athena.ouranos.helu.ca (2 report templates visible)\n",
"📁 Study: 202512_GenesysCX\n"
]
}
],
"source": [
"import sys, pathlib # path shim: works on a fresh kernel\n",
"for _p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (_p / \"pyproject.toml\").exists():\n",
" sys.path.insert(0, str(_p)); break\n",
"\n",
"import pandas as pd\n",
"from core.bootstrap import init, update_env\n",
"\n",
"pal = init(study=\"202512_GenesysCX\")\n",
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
"assert pal.connection.get(\"status\") == \"ok\", \"Fix the connection first → 00_setup.ipynb\""
]
},
{
"cell_type": "markdown",
"id": "c1f8b6bd",
"metadata": {},
"source": [
"## 1 · Report template (find or create)"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cc81e408",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Found existing report template UCb2hSJprSBx (status: active)\n"
]
}
],
"source": [
"REPORT_NAME, VENDOR = \"CX Cloud (Genesys + Salesforce) 2025\", \"Genesys\"\n",
"\n",
"report = next(\n",
" (r for r in client.list_reports()\n",
" if r.get(\"name\") == REPORT_NAME and r.get(\"vendor\") == VENDOR),\n",
" None,\n",
")\n",
"if report is None:\n",
" report = client.create_report(\n",
" name=REPORT_NAME,\n",
" vendor=VENDOR,\n",
" version=\"1.0\",\n",
" description=(\n",
" \"Forrester TEI of CX Cloud (Genesys + Salesforce), Dec 2025. \"\n",
" \"Includes Palladium's genesys_ai_tokens consumption line, \"\n",
" \"which the published study omits.\"\n",
" ),\n",
" analysis_period_years=seed.ASSUMPTIONS[\"analysis_years\"],\n",
" discount_rate=seed.ASSUMPTIONS[\"discount_rate\"],\n",
" status=\"draft\",\n",
" )\n",
" print(f\"Created report template {report['id']}\")\n",
"else:\n",
" print(f\"Found existing report template {report['id']} (status: {report.get('status')})\")\n",
"\n",
"REPORT_ID = report[\"id\"]"
]
},
{
"cell_type": "markdown",
"id": "e31bbd8b",
"metadata": {},
"source": [
"## 2 · Field definitions\n",
"\n",
"Same Palladium conventions as the Amazon Connect study: benefit risk\n",
"adjustments live on the field; cost values get pushed pre-multiplied by\n",
"`(1 + risk_adj)`; Year-0 amounts use companion `*_initial` fields.\n",
"The `genesys_ai_tokens` line is seeded \\$0 (reproduces the published study) —\n",
"the annual cost gets entered per deal, from the Genesys quote, in\n",
"`03_business_case.ipynb`."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "55e69828",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0 fields created, 12 already existed.\n"
]
}
],
"source": [
"def field_defs():\n",
" defs, sort = [], 0\n",
" for b in seed.BENEFITS:\n",
" sort += 1\n",
" defs.append({\n",
" \"table\": \"benefits\",\n",
" \"field_key\": b[\"field_key\"],\n",
" \"label\": b[\"label\"],\n",
" \"description\": b[\"notes\"][:200],\n",
" \"field_type\": \"currency\",\n",
" \"category\": b[\"category\"],\n",
" \"is_annual\": True,\n",
" \"risk_adjustment\": str(b[\"risk_adjustment\"]),\n",
" \"sort_order\": sort,\n",
" \"is_required\": True,\n",
" \"source_notes\": b[\"notes\"],\n",
" })\n",
" for c in seed.COSTS:\n",
" sort += 1\n",
" defs.append({\n",
" \"table\": \"costs\",\n",
" \"field_key\": c[\"field_key\"],\n",
" \"label\": c[\"label\"],\n",
" \"description\": c[\"notes\"][:200],\n",
" \"field_type\": \"currency\",\n",
" \"category\": c[\"category\"],\n",
" \"is_annual\": True,\n",
" \"risk_adjustment\": \"0\", # cost risk adj applied client-side\n",
" \"sort_order\": sort,\n",
" \"is_required\": False,\n",
" \"source_notes\": c[\"notes\"],\n",
" })\n",
" sort += 1\n",
" defs.append({\n",
" \"table\": \"costs\",\n",
" \"field_key\": f\"{c['field_key']}_initial\",\n",
" \"label\": f\"{c['label']} — initial (Year 0)\",\n",
" \"description\": \"One-time Year-0 amount (companion field).\",\n",
" \"field_type\": \"currency\",\n",
" \"category\": c[\"category\"],\n",
" \"is_annual\": False,\n",
" \"risk_adjustment\": \"0\",\n",
" \"sort_order\": sort,\n",
" \"is_required\": False,\n",
" \"source_notes\": \"Year-0 lump sum; Athena treats non-annual values as Year 1.\",\n",
" })\n",
" return defs\n",
"\n",
"existing = {f[\"field_key\"] for f in client.list_fields(REPORT_ID)}\n",
"created = 0\n",
"for d in field_defs():\n",
" if d[\"field_key\"] not in existing:\n",
" client.create_field(REPORT_ID, d)\n",
" created += 1\n",
"print(f\"{created} fields created, {len(existing)} already existed.\")\n",
"\n",
"if report.get(\"status\") == \"draft\":\n",
" client.update_report(REPORT_ID, status=\"active\")\n",
" print(\"Report template activated.\")"
]
},
{
"cell_type": "markdown",
"id": "96b360d3",
"metadata": {},
"source": [
"## 3 · Select the client"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "5a0a701f",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>id</th>\n",
" <th>name</th>\n",
" <th>vertical</th>\n",
" <th>client_type</th>\n",
" <th>employee_count</th>\n",
" <th>contact_center_agent_count</th>\n",
" <th>supervisor_count</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>2</td>\n",
" <td>Global Guardian Insurance</td>\n",
" <td>None</td>\n",
" <td>For-Profit</td>\n",
" <td>12000</td>\n",
" <td>2500</td>\n",
" <td>None</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>3</td>\n",
" <td>Eudaimonix</td>\n",
" <td>None</td>\n",
" <td>For-Profit</td>\n",
" <td>1500</td>\n",
" <td>300</td>\n",
" <td>None</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>4</td>\n",
" <td>Aetherium Forge</td>\n",
" <td>None</td>\n",
" <td>For-Profit</td>\n",
" <td>500</td>\n",
" <td>42</td>\n",
" <td>None</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" id name vertical client_type employee_count \\\n",
"0 2 Global Guardian Insurance None For-Profit 12000 \n",
"1 3 Eudaimonix None For-Profit 1500 \n",
"2 4 Aetherium Forge None For-Profit 500 \n",
"\n",
" contact_center_agent_count supervisor_count \n",
"0 2500 None \n",
"1 300 None \n",
"2 42 None "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"CLIENT_SEARCH = \"\" # e.g. \"Acme\" — empty lists everyone\n",
"\n",
"clients = client.list_clients(search=CLIENT_SEARCH or None)\n",
"if clients:\n",
" display(pd.DataFrame(clients)[\n",
" [c for c in (\"id\", \"name\", \"vertical\", \"client_type\", \"employee_count\",\n",
" \"contact_center_agent_count\", \"supervisor_count\")\n",
" if c in clients[0]]\n",
" ])\n",
"else:\n",
" print(\"No clients found — create one in the Athena UI (Orbit → Clients) and re-run.\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "1e375b54",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>Global Guardian Insurance</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>id</th>\n",
" <td>2</td>\n",
" </tr>\n",
" <tr>\n",
" <th>name</th>\n",
" <td>Global Guardian Insurance</td>\n",
" </tr>\n",
" <tr>\n",
" <th>abbreviated_name</th>\n",
" <td>GGI</td>\n",
" </tr>\n",
" <tr>\n",
" <th>vertical</th>\n",
" <td>None</td>\n",
" </tr>\n",
" <tr>\n",
" <th>client_type</th>\n",
" <td>For-Profit</td>\n",
" </tr>\n",
" <tr>\n",
" <th>employee_count</th>\n",
" <td>12000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>revenue</th>\n",
" <td>4500000000.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>contact_center_agent_count</th>\n",
" <td>2500</td>\n",
" </tr>\n",
" <tr>\n",
" <th>service_desk_agent_count</th>\n",
" <td>300</td>\n",
" </tr>\n",
" <tr>\n",
" <th>supervisor_count</th>\n",
" <td>None</td>\n",
" </tr>\n",
" <tr>\n",
" <th>location_count</th>\n",
" <td>120</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Global Guardian Insurance\n",
"id 2\n",
"name Global Guardian Insurance\n",
"abbreviated_name GGI\n",
"vertical None\n",
"client_type For-Profit\n",
"employee_count 12000\n",
"revenue 4500000000.0\n",
"contact_center_agent_count 2500\n",
"service_desk_agent_count 300\n",
"supervisor_count None\n",
"location_count 120"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"CRM agent count: 2500 (composite: 600) — indicative scale 4.17×\n",
"CRM revenue: $4,500,000,000 (composite: $2,500,000,000)\n"
]
}
],
"source": [
"CLIENT_ID = 2 # ← set from the `id` column above, or leave for auto-pick\n",
"\n",
"if CLIENT_ID is None and len(clients) == 1:\n",
" CLIENT_ID = clients[0][\"id\"]\n",
" print(f\"Auto-selected the only client: {clients[0]['name']} (id={CLIENT_ID})\")\n",
"assert CLIENT_ID is not None, \"Set CLIENT_ID from the table above and re-run this cell.\"\n",
"\n",
"profile = client.client_profile(CLIENT_ID)\n",
"CLIENT_NAME = profile[\"name\"]\n",
"display(pd.DataFrame([profile]).T.rename(columns={0: CLIENT_NAME}))\n",
"\n",
"# Client data → study scaling levers (no re-entry)\n",
"CLIENT_ASSUMPTIONS = dict(seed.ASSUMPTIONS)\n",
"if profile.get(\"contact_center_agent_count\"):\n",
" CLIENT_ASSUMPTIONS[\"agents_fte\"] = profile[\"contact_center_agent_count\"]\n",
" scale = CLIENT_ASSUMPTIONS[\"agents_fte\"] / seed.ASSUMPTIONS[\"agents_fte\"]\n",
" print(f\"CRM agent count: {CLIENT_ASSUMPTIONS['agents_fte']} \"\n",
" f\"(composite: {seed.ASSUMPTIONS['agents_fte']}) — \"\n",
" f\"indicative scale {scale:.2f}×\")\n",
"if profile.get(\"revenue\"):\n",
" CLIENT_ASSUMPTIONS[\"annual_revenue\"] = float(profile[\"revenue\"])\n",
" print(f\"CRM revenue: ${CLIENT_ASSUMPTIONS['annual_revenue']:,.0f} \"\n",
" f\"(composite: ${seed.ASSUMPTIONS['annual_revenue']:,.0f})\")"
]
},
{
"cell_type": "markdown",
"id": "2ff83486",
"metadata": {},
"source": [
"## 4 · Pick the attachment — Proposal or Engagement"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "584e01dd",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Proposals for Global Guardian Insurance:\n"
]
},
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>id</th>\n",
" <th>name</th>\n",
" <th>status</th>\n",
" <th>opportunity</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>1</td>\n",
" <td>Secure Cloud Infrastructure Modernization</td>\n",
" <td>Draft</td>\n",
" <td>Secure Cloud Infrastructure Modernization</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" id name status \\\n",
"0 1 Secure Cloud Infrastructure Modernization Draft \n",
"\n",
" opportunity \n",
"0 Secure Cloud Infrastructure Modernization "
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"proposals = client.proposals_for_client(CLIENT_ID)\n",
"engagements = client.engagements_for_client(CLIENT_NAME)\n",
"\n",
"if proposals:\n",
" print(f\"Proposals for {CLIENT_NAME}:\")\n",
" display(pd.DataFrame([\n",
" {\"id\": p[\"id\"], \"name\": p.get(\"name\"), \"status\": p.get(\"status\"),\n",
" \"opportunity\": (p.get(\"opportunity\") or {}).get(\"name\")}\n",
" for p in proposals\n",
" ]))\n",
"if engagements:\n",
" print(f\"Engagements for {CLIENT_NAME}:\")\n",
" display(pd.DataFrame([\n",
" {\"id\": e[\"id\"], \"name\": e.get(\"name\"), \"status\": e.get(\"status\")}\n",
" for e in engagements\n",
" ]))\n",
"if not proposals and not engagements:\n",
" print(f\"{CLIENT_NAME} has no proposals or engagements yet — \"\n",
" \"the next cell can create a sandbox opportunity + proposal.\")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "e04b1676",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Attaching via: {'proposal': 1}\n"
]
}
],
"source": [
"# Set exactly ONE (ids from above). Leave both None to auto-pick — a single\n",
"# existing option wins; otherwise a sandbox opportunity + proposal is created.\n",
"PROPOSAL_ID = config.PROPOSAL_ID # or e.g. 42\n",
"ENGAGEMENT_ID = config.ENGAGEMENT_ID # or e.g. 7\n",
"\n",
"if PROPOSAL_ID is None and ENGAGEMENT_ID is None:\n",
" if len(proposals) == 1 and not engagements:\n",
" PROPOSAL_ID = proposals[0][\"id\"]\n",
" print(f\"Auto-selected proposal {PROPOSAL_ID}: {proposals[0].get('name')}\")\n",
" elif len(engagements) == 1 and not proposals:\n",
" ENGAGEMENT_ID = engagements[0][\"id\"]\n",
" print(f\"Auto-selected engagement {ENGAGEMENT_ID}: {engagements[0].get('name')}\")\n",
" elif not proposals and not engagements:\n",
" opp = client.create_opportunity(\n",
" name=f\"{CLIENT_NAME} — CX Cloud Modernization (sandbox)\",\n",
" client_id=CLIENT_ID,\n",
" description=\"Created by Palladium 00_provision for the Genesys CX Cloud TEI.\",\n",
" )\n",
" prop = client.create_proposal(\n",
" name=f\"{CLIENT_NAME} — Genesys CX Cloud TEI (sandbox)\",\n",
" opportunity_id=opp[\"id\"],\n",
" status=\"Draft\",\n",
" )\n",
" PROPOSAL_ID = prop[\"id\"]\n",
" print(f\"Created opportunity {opp['id']} and proposal {PROPOSAL_ID} for {CLIENT_NAME}.\")\n",
" else:\n",
" raise SystemExit(\"Multiple options — set PROPOSAL_ID or ENGAGEMENT_ID above and re-run.\")\n",
"\n",
"assert (PROPOSAL_ID is None) != (ENGAGEMENT_ID is None), \\\n",
" \"Set exactly one of PROPOSAL_ID / ENGAGEMENT_ID.\"\n",
"attach = {\"proposal\": PROPOSAL_ID} if PROPOSAL_ID else {\"engagement\": ENGAGEMENT_ID}\n",
"print(f\"Attaching via: {attach}\")"
]
},
{
"cell_type": "markdown",
"id": "2b4fcb45",
"metadata": {},
"source": [
"## 5 · Tool instance & seed the published values"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "0655d1fc",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Found existing tool 3rzDgVdsjhVv (status: draft)\n"
]
}
],
"source": [
"from core.tei_client import AthenaAPIError\n",
"\n",
"def _report_id_of(t):\n",
" r = t.get(\"report\")\n",
" return r.get(\"id\") if isinstance(r, dict) else r\n",
"\n",
"def _matches_attachment(t):\n",
" if PROPOSAL_ID is not None:\n",
" opp = t.get(\"opportunity\") or {}\n",
" return t.get(\"proposal\") == PROPOSAL_ID or opp.get(\"proposal_id\") == PROPOSAL_ID\n",
" eng = t.get(\"engagement\")\n",
" eng_id = eng.get(\"id\") if isinstance(eng, dict) else eng\n",
" return eng_id == ENGAGEMENT_ID\n",
"\n",
"candidates = [t for t in client.list_tools() if _report_id_of(t) == REPORT_ID]\n",
"tool = next((t for t in candidates if _matches_attachment(t)),\n",
" candidates[0] if len(candidates) == 1 else None)\n",
"\n",
"if tool is None:\n",
" try:\n",
" tool = client.create_tool(\n",
" report_public_id=REPORT_ID,\n",
" name=f\"{CLIENT_NAME} — Genesys CX Cloud TEI\",\n",
" **attach,\n",
" )\n",
" print(f\"Created tool {tool['id']} attached to {attach}\")\n",
" except AthenaAPIError as e:\n",
" if e.status_code == 409: # DUPLICATE_INSTANCE\n",
" raise SystemExit(\n",
" \"An active tool already exists for this report + attachment. \"\n",
" \"Find it with client.list_tools() or pick a different proposal/engagement.\"\n",
" ) from e\n",
" raise\n",
"else:\n",
" print(f\"Found existing tool {tool['id']} (status: {tool.get('status')})\")\n",
"\n",
"TOOL_ID = tool[\"id\"]"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "86443d76",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Pushed values for 8 fields (genesys_ai_tokens seeded at $0 — published-study baseline).\n"
]
}
],
"source": [
"payload = []\n",
"for b in seed.BENEFITS: # nominal; Athena risk-adjusts via the field definition\n",
" payload.append({\n",
" \"field_key\": b[\"field_key\"],\n",
" \"year_values\": b[\"year_values\"],\n",
" \"notes\": b[\"notes\"],\n",
" })\n",
"for c in seed.COSTS: # risk-adjusted UP client-side (Forrester methodology)\n",
" factor = 1 + c[\"risk_adjustment\"]\n",
" payload.append({\n",
" \"field_key\": c[\"field_key\"],\n",
" \"year_values\": {y: round(v * factor, 2) for y, v in c[\"year_values\"].items()},\n",
" \"initial\": round(c[\"initial\"] * factor, 2),\n",
" \"notes\": c[\"notes\"],\n",
" })\n",
"\n",
"client.update_values(TOOL_ID, payload)\n",
"print(f\"Pushed values for {len(payload)} fields \"\n",
" f\"(genesys_ai_tokens seeded at $0 — published-study baseline).\")"
]
},
{
"cell_type": "markdown",
"id": "509b52be",
"metadata": {},
"source": [
"## 6 · Calculate & verify\n",
"\n",
"**Tier 1 — pipeline correctness:** Athena must match `seed.ATHENA_EXPECTED`\n",
"(the published model re-discounted under Athena's Year-0-as-Year-1 rule)\n",
"within 0.5%.\n",
"\n",
"**Tier 2 — reconciliation:** show Athena vs the published totals. The\n",
"implementation initial (\\$1.309M, ~32% of cost PV) is discounted by Athena\n",
"but not by Forrester, so costs PV reads ~\\$119k lower and ROI ~11pp higher\n",
"than published. That delta is methodology, not data error."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "0728b42e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"════════════════════════════════════════════════════════\n",
" TEI Financial Summary\n",
"════════════════════════════════════════════════════════\n",
" Total Benefits (PV): $ 14,840,637\n",
" Total Costs (PV): $ 3,938,170\n",
"────────────────────────────────────────────────────────\n",
" Net Present Value: $ 10,902,466\n",
" ROI: 277%\n",
" Payback: 4.0 months\n",
"════════════════════════════════════════════════════════\n"
]
}
],
"source": [
"summary = client.calculate(TOOL_ID)\n",
"client.print_summary(TOOL_ID)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "aba8fc21",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>metric</th>\n",
" <th>published (Forrester)</th>\n",
" <th>expected (Athena methodology)</th>\n",
" <th>athena actual</th>\n",
" <th>vs expected</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>total_benefits_pv</td>\n",
" <td>14,840,638</td>\n",
" <td>14,840,640</td>\n",
" <td>14,840,637</td>\n",
" <td>-0.00%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>total_costs_pv</td>\n",
" <td>4,057,170</td>\n",
" <td>3,938,170</td>\n",
" <td>3,938,170</td>\n",
" <td>+0.00%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>net_present_value</td>\n",
" <td>10,783,468</td>\n",
" <td>10,902,470</td>\n",
" <td>10,902,466</td>\n",
" <td>-0.00%</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>roi_percentage</td>\n",
" <td>266</td>\n",
" <td>277</td>\n",
" <td>277</td>\n",
" <td>+0.01%</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" metric published (Forrester) expected (Athena methodology) \\\n",
"0 total_benefits_pv 14,840,638 14,840,640 \n",
"1 total_costs_pv 4,057,170 3,938,170 \n",
"2 net_present_value 10,783,468 10,902,470 \n",
"3 roi_percentage 266 277 \n",
"\n",
" athena actual vs expected \n",
"0 14,840,637 -0.00% \n",
"1 3,938,170 +0.00% \n",
"2 10,902,466 -0.00% \n",
"3 277 +0.01% "
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"Payback: 4 months (expected ≈ 4)\n",
"✅ Tier 1 passed — pipeline reproduces the study under Athena's discounting.\n",
" Tier 2: published ROI 266% vs Athena ~277% — explained Year-0 delta (see above).\n"
]
}
],
"source": [
"rows, ok = [], True\n",
"for key in (\"total_benefits_pv\", \"total_costs_pv\", \"net_present_value\", \"roi_percentage\"):\n",
" actual = float(summary.get(key) or 0)\n",
" expected = seed.ATHENA_EXPECTED[key]\n",
" published = seed.PUBLISHED[key]\n",
" diff = (actual - expected) / expected\n",
" rows.append({\n",
" \"metric\": key,\n",
" \"published (Forrester)\": f\"{published:,.0f}\",\n",
" \"expected (Athena methodology)\": f\"{expected:,.0f}\",\n",
" \"athena actual\": f\"{actual:,.0f}\",\n",
" \"vs expected\": f\"{diff:+.2%}\",\n",
" })\n",
" ok &= abs(diff) <= 0.005\n",
"\n",
"display(pd.DataFrame(rows))\n",
"print(f\"Payback: {summary.get('payback_period_months')} months (expected ≈ 4)\")\n",
"assert ok, \"Athena diverged >0.5% from its own expected methodology — investigate.\"\n",
"print(\"✅ Tier 1 passed — pipeline reproduces the study under Athena's discounting.\")\n",
"print(\" Tier 2: published ROI 266% vs Athena ~277% — explained Year-0 delta (see above).\")"
]
},
{
"cell_type": "markdown",
"id": "181c7b55",
"metadata": {},
"source": [
"## 7 · Save a baseline version & persist IDs"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "d8102590",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Saved to /Users/robert/git/palladium/.env:\n",
" PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID=UCb2hSJprSBx\n",
" PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID=3rzDgVdsjhVv\n",
" PALLADIUM_GENESYSCX_PROPOSAL_ID=1\n",
"\n",
"Next → 01_benefits.ipynb (walk through the four Forrester benefits).\n"
]
}
],
"source": [
"if not client.list_versions(TOOL_ID):\n",
" client.save_version(TOOL_ID, note=(\n",
" \"Baseline — published Forrester CX Cloud TEI figures (Dec 2025). \"\n",
" \"genesys_ai_tokens at $0 per the published study; set the annual \"\n",
" \"cost from the Genesys quote in 03_business_case before client use.\"\n",
" ))\n",
" print(\"Saved version 1 (baseline).\")\n",
"\n",
"ids = {\n",
" \"PALLADIUM_GENESYSCX_REPORT_PUBLIC_ID\": REPORT_ID,\n",
" \"PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID\": TOOL_ID,\n",
"}\n",
"if PROPOSAL_ID is not None:\n",
" ids[\"PALLADIUM_GENESYSCX_PROPOSAL_ID\"] = str(PROPOSAL_ID)\n",
"if ENGAGEMENT_ID is not None:\n",
" ids[\"PALLADIUM_GENESYSCX_ENGAGEMENT_ID\"] = str(ENGAGEMENT_ID)\n",
"\n",
"env_path = update_env(**ids)\n",
"print(f\"Saved to {env_path}:\")\n",
"for k, v in ids.items():\n",
" print(f\" {k}={v}\")\n",
"print(\"\\nNext → 01_benefits.ipynb (walk through the four Forrester benefits).\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4fc81c99-f073-486a-9f65-f207e96e59cd",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "13acdc34-71f6-4220-8675-4e1527cb8e39",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,382 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "g3-md-intro",
"metadata": {},
"source": [
"# 03 \u2014 Business Case\n",
"\n",
"Combine the benefits and costs into the consolidated TEI summary,\n",
"render the cash-flow exhibit, run scenario analysis, **and price the\n",
"Genesys AI Experience tokens line that the published study omits**.\n",
"This notebook should reproduce the headline numbers from the PDF\n",
"Financial Summary:\n",
"\n",
"* **NPV \\$10.78M \u2022 ROI 266% \u2022 Payback \u2248 4 months**\n",
"\n",
"It then exposes a sensitivity sweep for the AI-tokens annual cost so\n",
"you can see exactly what an honest deal looks like before sending it\n",
"to a client."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "03-bootstrap",
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib # path shim: works on a fresh kernel\n",
"for _p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (_p / \"pyproject.toml\").exists():\n",
" sys.path.insert(0, str(_p)); break\n",
"\n",
"from core.bootstrap import init\n",
"\n",
"pal = init(study=\"202512_GenesysCX\")\n",
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
"\n",
"STUDY = pal.root / 'studies' / '202512_GenesysCX'\n",
"ROOT = pal.root\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-imports",
"metadata": {},
"outputs": [],
"source": [
"from core.export.report_data import _compute_summary\n",
"from core.notebook_helpers import charts, display, tables"
]
},
{
"cell_type": "markdown",
"id": "g3-md-summary",
"metadata": {},
"source": [
"## Local summary (no Athena round-trip)\n",
"\n",
"Compute the moderate-case TEI summary directly from `seed_data` so the\n",
"notebook produces results even before the Athena tool is provisioned.\n",
"Headline numbers should match the published study."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-summary",
"metadata": {},
"outputs": [],
"source": [
"summary = _compute_summary(\n",
" seed.BENEFITS,\n",
" seed.COSTS,\n",
" config.DISCOUNT_RATE,\n",
" config.ANALYSIS_YEARS,\n",
")\n",
"# `_compute_summary` returns roi_pct; expose it as `roi` for kpi_cards.\n",
"summary['roi'] = summary.get('roi_pct')\n",
"display.kpi_cards(summary, title='Forrester composite \u2014 moderate case')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-cashflow-table",
"metadata": {},
"outputs": [],
"source": [
"df_cash = tables.cashflow_table(summary)\n",
"df_cash.style.format({c: '${:,.0f}' for c in df_cash.columns if c != 'Year'})"
]
},
{
"cell_type": "markdown",
"id": "g3-md-cashflow",
"metadata": {},
"source": [
"## Cash flow chart\n",
"\n",
"Mirrors the Forrester *Cash Flow Chart* exhibit: stacked benefits/costs\n",
"by year + cumulative-net line. Payback hits inside Year 1."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-cashflow-chart",
"metadata": {},
"outputs": [],
"source": [
"charts.cashflow_chart(\n",
" summary['yearly_breakdown'],\n",
" initial_cost=summary.get('initial_costs', 0),\n",
").show()"
]
},
{
"cell_type": "markdown",
"id": "g3-md-waterfall",
"metadata": {},
"source": [
"## Waterfall: Benefits PV \u2192 Costs PV \u2192 NPV"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-waterfall",
"metadata": {},
"outputs": [],
"source": [
"charts.waterfall([\n",
" ('Benefits PV', summary['total_benefits_pv']),\n",
" ('Costs PV', -summary['total_costs_pv']),\n",
" ('NPV', summary['npv']),\n",
"]).show()"
]
},
{
"cell_type": "markdown",
"id": "g3-md-scenarios",
"metadata": {},
"source": [
"## Scenario analysis\n",
"\n",
"Apply the default Palladium multipliers (see `core.calculations.SCENARIOS`):\n",
"\n",
"* **Conservative** \u2014 lower adoption, higher risk on benefits / lower on costs\n",
"* **Moderate** \u2014 base case (= the published Forrester study)\n",
"* **Aggressive** \u2014 full adoption, lower risk on benefits / higher on costs"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-scenarios",
"metadata": {},
"outputs": [],
"source": [
"from core.calculations import apply_scenario\n",
"import pandas as pd\n",
"\n",
"scenario_summaries = {}\n",
"for name in ('conservative', 'moderate', 'aggressive'):\n",
" sb = apply_scenario(seed.BENEFITS, name, table='benefits')\n",
" sc = apply_scenario(seed.COSTS, name, table='costs')\n",
" scenario_summaries[name] = _compute_summary(sb, sc, config.DISCOUNT_RATE, config.ANALYSIS_YEARS)\n",
"\n",
"scen_df = pd.DataFrame([\n",
" {\n",
" 'Scenario': k,\n",
" 'Benefits PV': v['total_benefits_pv'],\n",
" 'Costs PV': v['total_costs_pv'],\n",
" 'NPV': v['npv'],\n",
" 'ROI %': v['roi_pct'],\n",
" 'Payback (mo)': round(v['payback_months'], 1) if v['payback_months'] is not None else None,\n",
" }\n",
" for k, v in scenario_summaries.items()\n",
"])\n",
"scen_df.style.format({\n",
" 'Benefits PV': '${:,.0f}', 'Costs PV': '${:,.0f}', 'NPV': '${:,.0f}', 'ROI %': '{:,.0f}%'\n",
"})"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-scenario-chart",
"metadata": {},
"outputs": [],
"source": [
"charts.scenario_comparison(scenario_summaries).show()"
]
},
{
"cell_type": "markdown",
"id": "g3-md-tokens-intro",
"metadata": {},
"source": [
"## Genesys AI Experience tokens \u2014 annual cost\n",
"\n",
"Token pricing is tiered, capability-dependent, and deal-specific \u2014\n",
"Athena stores a single annual cost value per line, and so does the\n",
"seed. Enter the negotiated annual cost from the Genesys quote here.\n",
"Quote details (volume, unit price, tier) go into the field notes for\n",
"the audit trail.\n",
"\n",
"For sizing context, the study's own drivers imply roughly **1,040,000**\n",
"self-service interactions/yr and **3,120,000** agent-assisted\n",
"interactions/yr would draw tokens \u2014 bring the actual figure from the\n",
"quote, not a derivation."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-token-input",
"metadata": {},
"outputs": [],
"source": [
"# \u2500\u2500 Deal inputs \u2500\u2500\n",
"AI_TOKEN_ANNUAL_COST = 0.0 # $/yr from the Genesys quote \u2014 0 reproduces the published study\n",
"AI_TOKEN_QUOTE_NOTE = \"\" # e.g. \"Quote #1234: 4.2M tokens/yr @ $0.05, tier 2 commit\"\n",
"\n",
"print(f'AI token line: ${AI_TOKEN_ANNUAL_COST:,.0f}/yr')"
]
},
{
"cell_type": "markdown",
"id": "g3-md-sensitivity",
"metadata": {},
"source": [
"### Sensitivity \u2014 what the AI line does to NPV and ROI\n",
"\n",
"An annual cost `\u0394` raises Costs PV by `\u0394 \u00d7 2.4869` (the 3-year, 10%\n",
"annuity factor) and lowers NPV by the same amount. The sweep below\n",
"shows where the deal stops being attractive \u2014 and quantifies how much\n",
"of the published 266% ROI was *contingent on Forrester modelling \\$0\n",
"of token spend*."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-sensitivity",
"metadata": {},
"outputs": [],
"source": [
"ANNUITY = sum(1 / 1.10**n for n in (1, 2, 3)) # 2.4869\n",
"\n",
"base_benefits_pv = float(summary['total_benefits_pv'])\n",
"base_costs_pv = float(summary['total_costs_pv'])\n",
"\n",
"sweep = [0, 100_000, 250_000, 500_000, 750_000, 1_000_000, 1_500_000, 2_000_000]\n",
"if AI_TOKEN_ANNUAL_COST and AI_TOKEN_ANNUAL_COST not in sweep:\n",
" sweep = sorted(sweep + [AI_TOKEN_ANNUAL_COST])\n",
"\n",
"rows = []\n",
"for ai_annual in sweep:\n",
" costs_pv = base_costs_pv + ai_annual * ANNUITY\n",
" npv_v = base_benefits_pv - costs_pv\n",
" roi_pct = (npv_v / costs_pv * 100) if costs_pv else 0\n",
" rows.append({\n",
" 'AI cost/yr': f\"${ai_annual:,.0f}\" + (' \u2190 your input' if ai_annual == AI_TOKEN_ANNUAL_COST and ai_annual else ''),\n",
" 'Costs PV': f'${costs_pv:,.0f}',\n",
" 'NPV': f'${npv_v:,.0f}',\n",
" 'ROI': f'{roi_pct:,.0f}%',\n",
" })\n",
"\n",
"pd.DataFrame(rows)"
]
},
{
"cell_type": "markdown",
"id": "g3-md-tokens-push",
"metadata": {},
"source": [
"### Push the AI-tokens cost to Athena\n",
"\n",
"When `AI_TOKEN_ANNUAL_COST` is set and `TOOL_PUBLIC_ID` exists, write\n",
"the annual cost into the `genesys_ai_tokens` field, with the quote\n",
"details preserved in the field notes."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-tokens-push",
"metadata": {},
"outputs": [],
"source": [
"PUSH = False # \u2190 set True once AI_TOKEN_ANNUAL_COST is final\n",
"\n",
"if PUSH and config.TOOL_PUBLIC_ID:\n",
" from core.tei_client import TEIClient\n",
"\n",
" note = (\n",
" f'AI Experience tokens: ${AI_TOKEN_ANNUAL_COST:,.0f}/yr. '\n",
" + (f'{AI_TOKEN_QUOTE_NOTE} ' if AI_TOKEN_QUOTE_NOTE else '')\n",
" + 'Line absent from the published Forrester study.'\n",
" )\n",
" client = TEIClient()\n",
" client.update_values(config.TOOL_PUBLIC_ID, [{\n",
" 'field_key': 'genesys_ai_tokens',\n",
" 'year_values': {'1': round(AI_TOKEN_ANNUAL_COST, 2),\n",
" '2': round(AI_TOKEN_ANNUAL_COST, 2),\n",
" '3': round(AI_TOKEN_ANNUAL_COST, 2)},\n",
" 'notes': note,\n",
" }])\n",
" client.calculate(config.TOOL_PUBLIC_ID)\n",
" client.print_summary(config.TOOL_PUBLIC_ID)\n",
" client.save_version(config.TOOL_PUBLIC_ID, note=f'AI token cost set: {note}')\n",
" display.alert('Pushed, recalculated, and versioned.', 'success')\n",
"else:\n",
" display.alert('Dry run \u2014 set <code>PUSH = True</code> and ensure '\n",
" '<code>TOOL_PUBLIC_ID</code> is configured to write to Athena.', 'info')"
]
},
{
"cell_type": "markdown",
"id": "g3-md-crosscheck",
"metadata": {},
"source": [
"## Cross-check vs Athena (optional)\n",
"\n",
"When `TOOL_PUBLIC_ID` is set, ask Athena to recalculate the summary on\n",
"the server side and confirm it matches our local computation (modulo\n",
"the documented Year-0 discounting delta \u2014 see `02_costs.ipynb`)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g3-code-crosscheck",
"metadata": {},
"outputs": [],
"source": [
"if config.TOOL_PUBLIC_ID:\n",
" from core.tei_client import TEIClient\n",
"\n",
" client = TEIClient()\n",
" client.calculate(config.TOOL_PUBLIC_ID)\n",
" server_summary = client.get_summary(config.TOOL_PUBLIC_ID)\n",
" display.kpi_cards(server_summary, title='Athena server-side summary')\n",
"else:\n",
" display.alert('Set TOOL_PUBLIC_ID to compare Athena vs local.', 'info')"
]
},
{
"cell_type": "markdown",
"id": "g3-md-next",
"metadata": {},
"source": [
"Continue with [`04_export.ipynb`](04_export.ipynb) \u2192"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -1,195 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "g4-md-intro",
"metadata": {},
"source": [
"# 04 \u2014 Export for the report pipeline\n",
"\n",
"Build the structured JSON envelope consumed by the html2docx report\n",
"generation pipeline (Peitho). Output goes to `exports/export.json`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "04-bootstrap",
"metadata": {},
"outputs": [],
"source": [
"import sys, pathlib # path shim: works on a fresh kernel\n",
"for _p in [pathlib.Path.cwd(), *pathlib.Path.cwd().parents]:\n",
" if (_p / \"pyproject.toml\").exists():\n",
" sys.path.insert(0, str(_p)); break\n",
"\n",
"from core.bootstrap import init\n",
"\n",
"pal = init(study=\"202512_GenesysCX\")\n",
"client, seed, config = pal.client, pal.seed_data, pal.config\n",
"\n",
"STUDY = pal.root / 'studies' / '202512_GenesysCX'\n",
"ROOT = pal.root\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-imports",
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"from datetime import datetime, timezone\n",
"from core import __version__\n",
"from core.calculations import apply_scenario\n",
"from core.export.report_data import _compute_summary\n",
"from core.notebook_helpers import display"
]
},
{
"cell_type": "markdown",
"id": "g4-md-build",
"metadata": {},
"source": [
"## Build the envelope\n",
"\n",
"Two paths:\n",
"\n",
"* **Live** \u2014 `core.export.build_report_data(client, public_id)` pulls\n",
" authoritative values + summary from Athena and stamps it.\n",
"* **Local** \u2014 when no `TOOL_PUBLIC_ID` is configured, build the envelope\n",
" directly from `seed_data` so this notebook is always runnable."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-build",
"metadata": {},
"outputs": [],
"source": [
"if config.TOOL_PUBLIC_ID:\n",
" from core.export import build_report_data\n",
" from core.tei_client import TEIClient\n",
"\n",
" client = TEIClient()\n",
" envelope = build_report_data(\n",
" client,\n",
" config.TOOL_PUBLIC_ID,\n",
" include_scenarios=True,\n",
" study_slug=config.STUDY_SLUG,\n",
" )\n",
" source = 'live (Athena)'\n",
"else:\n",
" summary = _compute_summary(\n",
" seed.BENEFITS, seed.COSTS, config.DISCOUNT_RATE, config.ANALYSIS_YEARS\n",
" )\n",
" summary['roi'] = summary.get('roi_pct')\n",
" scenarios = {}\n",
" for name in ('conservative', 'moderate', 'aggressive'):\n",
" sb = apply_scenario(seed.BENEFITS, name, table='benefits')\n",
" sc = apply_scenario(seed.COSTS, name, table='costs')\n",
" scenarios[name] = _compute_summary(sb, sc, config.DISCOUNT_RATE, config.ANALYSIS_YEARS)\n",
" envelope = {\n",
" 'metadata': {\n",
" 'study_slug': config.STUDY_SLUG,\n",
" 'tool_public_id': '',\n",
" 'tool_name': 'CX Cloud (Genesys + Salesforce) TEI (local seed)',\n",
" 'report_name': 'Total Economic Impact\u2122 Of CX Cloud \u2014 Genesys + Salesforce',\n",
" 'report_vendor': 'Genesys',\n",
" 'report_version': '1.0',\n",
" 'generated_at': datetime.now(timezone.utc).isoformat(),\n",
" 'generator': f'palladium core {__version__} (offline)',\n",
" },\n",
" 'report': {\n",
" 'name': 'Total Economic Impact\u2122 Of CX Cloud \u2014 Genesys + Salesforce',\n",
" 'vendor': 'Genesys',\n",
" 'version': '1.0',\n",
" 'discount_rate': config.DISCOUNT_RATE,\n",
" 'analysis_period_years': config.ANALYSIS_YEARS,\n",
" },\n",
" 'values': {'benefits': seed.BENEFITS, 'costs': seed.COSTS},\n",
" 'summary': summary,\n",
" 'scenarios': scenarios,\n",
" 'assumptions': seed.ASSUMPTIONS,\n",
" }\n",
" source = 'offline seed data'\n",
"\n",
"display.alert(f'Envelope built from <b>{source}</b>.', 'info')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-write",
"metadata": {},
"outputs": [],
"source": [
"out_path = STUDY / 'exports' / 'export.json'\n",
"out_path.parent.mkdir(parents=True, exist_ok=True)\n",
"out_path.write_text(json.dumps(envelope, indent=2, default=str))\n",
"size_kb = out_path.stat().st_size / 1024\n",
"display.alert(f'Wrote <code>{out_path.relative_to(ROOT)}</code> ({size_kb:.1f} KB).', 'success')"
]
},
{
"cell_type": "markdown",
"id": "g4-md-shape",
"metadata": {},
"source": [
"## Envelope shape\n",
"\n",
"Top-level keys consumed by the report pipeline:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "g4-code-shape",
"metadata": {},
"outputs": [],
"source": [
"for key in envelope:\n",
" sub = envelope[key]\n",
" if isinstance(sub, dict):\n",
" print(f' {key}: dict with keys {list(sub.keys())}')\n",
" elif isinstance(sub, list):\n",
" print(f' {key}: list[{len(sub)}]')\n",
" else:\n",
" print(f' {key}: {type(sub).__name__}')"
]
},
{
"cell_type": "markdown",
"id": "g4-md-done",
"metadata": {},
"source": [
"Done. Hand off `exports/export.json` to **Peitho** / **html2docx** to produce the final Word report.\n",
"\n",
"**CLI alternative:** `python -m palladium export $PALLADIUM_GENESYSCX_TOOL_PUBLIC_ID -o studies/202512_GenesysCX/exports/export.json`"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.7"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,115 @@
# 202512 — Genesys CX Cloud TEI
Self-contained reproduction of Forrester's *The Total Economic Impact™ Of
CX Cloud — Cost Savings And Business Benefits Enabled By Genesys And
Salesforce* (December 2025, commissioned by Genesys and Salesforce), built
on the [Mercury Notebook Deliverable Pattern](../../docs/Mercury_Notebook_Pattern_V1-00.md)
as **Variant 4 — TEI composite reproduction**: Forrester's composite
organization is the never-edited verbatim anchor, the in-notebook gate
proves the engine reproduces the published totals, and 🟡 client drivers
rescale the composite live.
## Source
The full Forrester study is at
[`docs/The-Total-Economic-Impact-Of-CX-Cloud.pdf`](docs/The-Total-Economic-Impact-Of-CX-Cloud.pdf);
[`docs/Genesys-Token-Metering.md`](docs/Genesys-Token-Metering.md) covers
the AI Experience token pricing the study omits.
Published composite totals (3-yr risk-adjusted PV @ 10%), reproduced by
`teicalc` to within $2:
| Metric | Published | Engine |
|---|---|---|
| Benefits PV | **$14,840,638** | $14,840,637 |
| Costs PV | **$4,057,170** | $4,057,170 |
| NPV | **$10,783,468** | $10,783,466 |
| ROI | **266%** | 265.79% |
| Payback | *not headlined* | 3.3 months |
## Composite organization (the verbatim anchor 🟢)
* Global supply company, $2.5B revenue, 10,000 employees
* 600 CX agents (400 concurrent licenses)
* 80,000 weekly interactions @ 12 minutes
* Self-service completion 15% → 25%
## The $0 AI line (🔴)
The published study models **zero Genesys AI Experience token
consumption**, even though the self-service (B), agent-efficiency (C), and
agent-assist (D) benefits all depend on token-billed AI capabilities. The
anchor keeps the $0 verbatim so the reproduction matches the PDF; the
notebook exposes `ai_tokens_annual` as a direct 🔴 sidebar input — price it
from the Genesys quote and the case re-derives live. (This critique is what
grew into the CTM token-calculator engagement, `../202607_CTM_GenesysCX/`.)
## Client overlay (🟡)
A first-order linear rescale — "the composite at your size", not "your
TEI". The composite's trajectory is flat (Y2 = Y3), so there is no growth
re-base:
| Row | Driver | Confidence |
|---|---|---|
| Legacy retirement · CX Cloud licenses | agents | 🟡 |
| Self-service savings · agent efficiency | interactions | 🟡 |
| Agent-assist sales | revenue | 🟡 |
| Implementation · ongoing management | fixed | 🟡 project-based |
| Genesys AI tokens | direct $/yr input | 🔴 $0 until quoted |
## Study quirks (documented in the anchor, verbatim)
- p.14 prints the implementation initial as $1,304,600; the correct figure
is $1,309,000 (= 1,190,000 × 1.10) per the detail table and cash-flow
analysis.
- B7's printed formula cites B2 (15%) where the 12-minute interaction
length is meant; the result (40 FTEs) is correct.
## Layout
```
202512_TEI_Genesys_CX_Cloud/
├── teicalc/ ← ALL math (stdlib-only) — notebooks hold none
│ ├── anchor.py ← Forrester's tables, VERBATIM, never edited
│ ├── model.py ← NPV/ROI/payback, risk adjustment, compute_summary
│ ├── overlay.py ← ClientDrivers + driver map + the AI-token input
│ ├── scenarios.py ← conservative / moderate / aggressive
│ └── staging.py ← on_stage()/backstage() (Mercury vs nbconvert)
├── notebooks/business_case.ipynb ← THE deliverable
├── scripts/export_report.py
├── tests/ ← hand-checked pinned acceptance numbers
├── config.toml ← Mercury theme (NTT DATA brand)
├── pyproject.toml ← full toolchain as core deps — no requirements.txt
├── docs/ ← the Forrester PDF + token-metering notes
└── exports/ ← generated .html/.md; gitignored
```
## Run
```bash
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
```
| Task | Command |
|---|---|
| Tests | `pytest` |
| Serve (the stage) | `mercury --working-dir notebooks/` (run from this project root so `config.toml` loads) |
| Analyst view (backstage) | `jupyter lab` |
| Headless check | `jupyter nbconvert --to notebook --execute --inplace notebooks/business_case.ipynb` |
| Export for LLMs | `python scripts/export_report.py` |
The data appendix (markdown tables + JSON model state) rides inside
`exports/business_case.md` — the payload for the Athena study-repository
roadmap.
## History
This study previously ran on the shared `core/` package with an
Athena-workflow notebook chain (provision → push → calculate) and
study-scoped `PALLADIUM_GENESYSCX_*` env keys. That workflow — including
the `ATHENA_EXPECTED` reconciliation for Athena's discount-initial-as-
Year-1 convention — was retired when the study migrated to the pattern
(git history preserves it); the engine reproduces the published totals
locally under Forrester's own conventions, pinned in `tests/`.

View File

@@ -0,0 +1,81 @@
# Mercury app-shell theme — NTT DATA brand (light), modern surfaces.
# See docs/brand.md for the source palette.
#
# Loaded from the directory where you launch `mercury` (this project root);
# restart the server to apply changes. Only keys in mercury/config.py
# CSS_VARIABLE_MAP emit a CSS variable — anything else in DEFAULT_THEME is
# either derived or component-baked (e.g. success/warning/danger, slider
# track, widget bg) and silently no-ops here. Omitted keys are derived
# from the ones below.
[main]
title = "Genesys CX Cloud TEI — Business Case"
favicon_emoji = "📊"
footer = "Genesys CX Cloud TEI study (Forrester, Dec 2025)"
notebooks_button_label = "Analyses"
[welcome]
header = "Genesys CX Cloud TEI"
message = """
Interactive reproduction of Forrester's *Total Economic Impact™ Of CX
Cloud* composite ($10.8M NPV · 266% ROI). The published study is the
verbatim anchor — including the AI-token line it models at $0; tune the
🟡 client drivers live (and price the tokens), then export the
personalized report source with `python scripts/export_report.py`.
"""
[theme]
# ── Type — Georgia headings, Arial body. Both web-safe system fonts,
# so no font_url / network fetch. Georgia ships only normal+bold, so
# heading weight is 700 (the default 800 would render as faux-bold). ──
font_family = "Arial, 'Helvetica Neue', Helvetica, sans-serif"
heading_font_family = "Georgia, 'Times New Roman', Times, serif"
font_size = "15px"
font_weight = "normal"
heading_font_weight = "700"
# ── Text — NTT ink scale ──
text_color = "#2e404d" # body
muted_text_color = "#586671" # captions / secondary
# ── Surfaces — white content floating on a soft neutral canvas (depth).
# For a strictly-white page instead, set background_color = "#ffffff". ──
background_color = "#f4f5f6" # outer page
content_background_color = "#ffffff" # notebook column
surface_color = "#ffffff"
card_background_color = "#f8f8f8" # brand card
border_color = "#d5d9db" # brand border
border_radius = "10px" # modern rounding
# ── Accents — Future Blue. primary_color also drives the Run button + focus. ──
primary_color = "#0072bc"
accent_color = "#0072bc"
focus_border_color = "#0072bc"
hover_background_color = "#eef5fb" # light blue tint
selected_background_color = "#dcecfa"
# ── Sidebar — clean white, hairline divider ──
sidebar_background_color = "#ffffff"
sidebar_text_color = "#2e404d"
sidebar_title_color = "#151d2c"
sidebar_shadow = "1px 0 0 #d5d9db"
# ── Top bar — deep NTT navy (brand heading-primary) ──
topbar_background_color = "#151d2c"
topbar_text_color = "#ffffff"
topbar_border_color = "rgba(255,255,255,0.08)"
# ── Footer ──
footer_background_color = "#ffffff"
footer_text_color = "#586671"
footer_border_color = "#d5d9db"
# ── Run button — subtle brand-blue gradient (else derives from primary) ──
run_button_background = "linear-gradient(180deg, #0087dc 0%, #0072bc 100%)"
run_button_background_hover = "linear-gradient(180deg, #1a93e6 0%, #0079c8 100%)"
run_button_text_color = "#ffffff"
# ── Depth — soft, navy-tinted shadows (modern) ──
shadow_sm = "0 1px 2px rgba(21,29,44,0.05)"
shadow_md = "0 6px 18px rgba(21,29,44,0.08)"
shadow_lg = "0 16px 40px rgba(21,29,44,0.10)"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "teicalc"
version = "0.1.0"
description = "Genesys CX Cloud TEI (Forrester, Dec 2025) — composite reproduction + client overlay incl. the AI-token line"
requires-python = ">=3.10"
# The notebook is the deliverable (served with Mercury, exported via
# nbconvert, tables via tabulate) — the whole toolchain is a required
# runtime dependency, not an extra. `pip install -e .` must be enough.
dependencies = [
"pandas>=2.0",
"plotly>=5.18",
"openpyxl>=3.1",
"mercury>=3.2",
"jupyterlab>=4.0",
"ipywidgets>=8.0",
"nbconvert>=7",
"tabulate>=0.9",
]
[project.optional-dependencies]
dev = ["pytest>=7.4", "mypy>=1.8"]
[tool.setuptools.packages.find]
include = ["teicalc*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"
[tool.mypy]
strict = true
packages = ["teicalc"]

View File

@@ -0,0 +1,47 @@
"""Export the deliverable notebooks as LLM-readable report sources.
Executes each notebook fresh (widget defaults — or whatever defaults you edit in),
then writes both formats to exports/:
exports/<notebook>.html — human-reviewable, tables render
exports/<notebook>.md — leanest LLM input
Plotly figures export as JavaScript an LLM cannot read; each notebook's
machine-readable appendix section carries every number behind them.
Run from the project root: python scripts/export_report.py [name-filter]
An optional argument exports only notebooks whose filename contains it.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
NOTEBOOKS = [
ROOT / "notebooks" / "business_case.ipynb",
]
EXPORTS = ROOT / "exports"
def main() -> None:
picked = [nb for nb in NOTEBOOKS
if len(sys.argv) < 2 or sys.argv[1] in nb.name]
if not picked:
sys.exit(f"no notebook matches {sys.argv[1]!r}")
EXPORTS.mkdir(exist_ok=True)
for nb in picked:
for fmt in ("html", "markdown"):
subprocess.run(
[sys.executable, "-m", "nbconvert", "--execute",
"--to", fmt, "--output-dir", str(EXPORTS), str(nb)],
check=True, cwd=ROOT,
)
for p in sorted(EXPORTS.iterdir()):
if p.suffix in (".html", ".md"):
print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size / 1024:,.0f} KB)")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
"""
teicalc — self-contained engine for the Genesys CX Cloud TEI study
(Forrester, December 2025). Mercury Notebook Pattern, Variant 4:
verbatim composite anchor → published-totals gate → client overlay
(including the AI-token line the published study left at $0).
"""
from .anchor import ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM, PUBLISHED
from .model import (
X_LABELS,
YEAR_INDEX,
YEARS,
benefits_by_year,
by_calendar,
compute_summary,
costs_by_year,
discount_factor,
html_money,
initial_costs,
money,
month_label,
npv,
payback_label,
payback_months,
payback_years,
present_value,
risk_adjust_benefit,
risk_adjust_cost,
risk_adjusted_rows,
roi_pct,
)
from .overlay import (
BENEFIT_DRIVERS,
COMPOSITE,
COST_DRIVERS,
ClientDrivers,
overlay_rows,
scale_factor,
)
from .scenarios import SCENARIOS, apply_scenario
__version__ = "0.1.0"
__all__ = [
"ASSUMPTIONS", "BENEFITS_VERBATIM", "COSTS_VERBATIM", "PUBLISHED",
"YEARS", "YEAR_INDEX", "X_LABELS",
"by_calendar", "month_label",
"discount_factor", "present_value", "npv", "roi_pct",
"payback_years", "payback_months", "payback_label",
"risk_adjust_benefit", "risk_adjust_cost", "risk_adjusted_rows",
"benefits_by_year", "costs_by_year", "initial_costs",
"compute_summary", "money", "html_money",
"ClientDrivers", "COMPOSITE", "BENEFIT_DRIVERS", "COST_DRIVERS",
"scale_factor", "overlay_rows",
"SCENARIOS", "apply_scenario",
]

View File

@@ -1,45 +1,38 @@
"""
Seed dataset for the Genesys CX Cloud TEI (Forrester, Dec 2025).
The verbatim anchor Forrester *The Total Economic Impact Of CX Cloud
Cost Savings And Business Benefits Enabled By Genesys And Salesforce*
(December 2025, commissioned by Genesys and Salesforce).
"The Total Economic Impact™ Of CX Cloud — Cost Savings And Business
Benefits Enabled By Genesys And Salesforce" (commissioned by Genesys and
Salesforce). Composite: global supply company, $2.5B revenue, 10,000
employees, 600 CX agents (400 concurrent licenses), 80,000 weekly
interactions averaging 12 minutes.
VERBATIM, do not edit. These are Forrester's published composite-organization
tables and financial summary, transplanted unchanged from the study PDF
(``docs/The-Total-Economic-Impact-Of-CX-Cloud.pdf``). Client personalization
lives in :mod:`teicalc.overlay`; scenario stress lives in
:mod:`teicalc.scenarios` both deep-copy, neither mutates this record.
Each row uses the friendly value shape accepted by
``core.tei_client.TEIClient.update_values``. Benefit values are *nominal*
(pre-risk-adjustment); Athena applies the field-level risk adjustment.
Cost values are nominal too push them pre-multiplied by
``(1 + risk_adjustment)`` per the Palladium convention (Athena never
risk-adjusts costs).
Rows keep Forrester's own year-index keys (``"1"``/``"2"``/``"3"``);
:mod:`teicalc.model` maps them to calendar years (20262028). Values are
*nominal* (pre-risk-adjustment); the risk factor is stored per row and
applied by the model (benefits ×(1rf), costs ×(1+rf), per the TEI
methodology).
Published headline (3-yr risk-adjusted, 10% discount)::
Two study-specific footnotes, preserved from the source review:
Benefits PV $14,840,638
Costs PV $ 4,057,170
NPV $10,783,468
ROI 266%
Payback ~4 months (computed; the study does not headline it)
Athena discounts Year-0 "Initial" amounts as Year-1 cashflows (Forrester
leaves Year 0 undiscounted). With this study's large initial cost
($1,309,000 risk-adjusted) that difference is material, so this module
also exports ``ATHENA_EXPECTED`` the totals Athena *should* produce
under its own discounting. Verification: match ATHENA_EXPECTED tightly
(pipeline correctness), then reconcile to PUBLISHED with the explained
Year-0 delta.
NOTE on the published PDF: the Total Costs table (p.14) prints the
implementation initial as $1,304,600, but the detail table, the cash-flow
analysis, and the math (1,190,000 × 1.10) all give $1,309,000 the p.14
figure is a typo in the study.
* The published Total Costs table (p.14) prints the implementation initial
as $1,304,600, but the detail table, the cash-flow analysis, and the math
(1,190,000 × 1.10) all give **$1,309,000** the p.14 figure is a typo in
the study.
* ``genesys_ai_tokens`` is **not in the published study** Forrester
modeled $0 AI consumption even though benefits B (self-service uplift),
C (agent efficiency), and D (agent assist upsell) all depend on AI
capabilities that Genesys bills via AI Experience tokens. The row is
anchored at $0 so the reproduction matches the published totals; client
cases price it via the overlay's ``ai_tokens_annual`` driver.
"""
from __future__ import annotations
#: 3-year nominal benefit cashflows. Risk adjustment stored separately.
BENEFITS: list[dict] = [
#: 3-year nominal benefit cashflows — 🟢 published.
BENEFITS_VERBATIM: list[dict] = [
{
"field_key": "legacy_retirement",
"table": "benefits",
@@ -98,9 +91,10 @@ BENEFITS: list[dict] = [
]
#: Costs are nominal; push × (1 + risk_adjustment). "initial" is the
#: Year-0 component (companion non-annual field in Athena).
COSTS: list[dict] = [
#: Costs include an ``initial`` (year-0, undiscounted) component for
#: implementation. Cost risk adjustments are applied *upward*. 🟢 published
#: (except the ``genesys_ai_tokens`` line — see the module docstring).
COSTS_VERBATIM: list[dict] = [
{
"field_key": "cx_cloud_licenses",
"table": "costs",
@@ -158,16 +152,17 @@ COSTS: list[dict] = [
"consumption even though benefits B (self-service uplift), "
"C (AI coaching/assist), and D (agent assist upsell) all "
"depend on AI capabilities that Genesys bills via AI "
"Experience tokens. Seeded at $0 to reproduce the published "
"Experience tokens. Anchored at $0 to reproduce the published "
"totals. For client cases, enter the negotiated annual token "
"cost from the Genesys quote and document the quote details "
"(token volume, unit price, tier) in these notes."
"cost from the Genesys quote (the overlay's ai_tokens_annual "
"driver) and document the quote details (token volume, unit "
"price, tier)."
),
},
]
#: Composite-organization drivers — for scaling to a specific client.
#: Composite-organization drivers — 🟢 published (PDF "Composite Organization").
ASSUMPTIONS: dict = {
"annual_revenue": 2_500_000_000,
"employees": 10_000,
@@ -188,44 +183,15 @@ ASSUMPTIONS: dict = {
}
# ────────────────────────────────────────────────────────────────────
# Genesys AI Experience tokens
#
# Genesys bills AI consumption in "AI Experience tokens" — pricing is
# tiered, capability-dependent, and deal-specific. Athena stores a
# single annual cost value per line, and so do we: enter the negotiated
# annual figure from the Genesys quote into ``genesys_ai_tokens`` and
# document the quote details (volume, unit price, tier) in the field
# notes. For sizing context, the study's own drivers imply ~1,040,000
# self-service interactions/yr (B5 × 52) and ~3,120,000 agent-assisted
# interactions/yr (C1 × 52) would draw tokens.
# ────────────────────────────────────────────────────────────────────
# ────────────────────────────────────────────────────────────────────
# Verification targets
# ────────────────────────────────────────────────────────────────────
#: Published Forrester totals (3-yr risk-adjusted PV @ 10%).
#: The PDF's Financial Summary — the gate's reproduction target. 🟢 published.
#: The engine reproduces these to within $2 (Forrester's own rounding).
#: Forrester does not headline a payback for this study; the engine computes
#: 3.3 months from the cash-flow table.
PUBLISHED: dict = {
"total_benefits_pv": 14_840_638,
"total_costs_pv": 4_057_170,
"net_present_value": 10_783_468,
"roi_percentage": 266,
"benefits_pv": 14_840_638,
"costs_pv": 4_057_170,
"npv": 10_783_468,
"roi_pct": 266,
"discount_rate": 0.10,
"analysis_years": 3,
}
#: What Athena should produce given its own discounting (Year-0 initial
#: treated as a Year-1 cashflow: implementation PV = 1,309,000 / 1.10 =
#: 1,190,000 instead of 1,309,000). Match these tightly; the difference
#: vs PUBLISHED is methodology, not error.
ATHENA_EXPECTED: dict = {
"total_benefits_pv": 14_840_640,
"total_costs_pv": 3_938_170,
"net_present_value": 10_902_470,
"roi_percentage": 276.8,
}
def all_values() -> list[dict]:
"""Return BENEFITS + COSTS — single-call payload for update_values."""
return BENEFITS + COSTS

View File

@@ -0,0 +1,267 @@
"""
Finance engine — the single source of truth for every number in the notebook.
Transplanted from the retired shared ``core/calculations`` and
``core/export/report_data.py`` so the study is self-contained (Mercury
Notebook Pattern, Required §2/§7). Conventions match the Forrester TEI
methodology:
* The *Initial* investment is **not** discounted — it occurs at time zero.
* Year-N cash flows are discounted at the end of the year:
``PV = CF_n / (1 + r) ** n``.
* Benefits are risk-adjusted *down* (``×(1rf)``), costs *up* (``×(1+rf)``).
* Payback runs on risk-adjusted **undiscounted** flows (the PDF's
"<6 months" uses the Cash Flow Analysis table's nominal RA rows).
Everything this module returns for display is keyed by **calendar year**
(Forrester Year 1/2/3 → 2026/2027/2028); ``initial`` stays a Year-0 scalar
and never appears inside a ``*_by_year`` dict.
This module is stdlib-only on purpose — the repo-root test suite imports it
without the study's venv.
"""
from __future__ import annotations
import math
from collections.abc import Iterable, Sequence
from copy import deepcopy
# ── Timeline ─────────────────────────────────────────────────────────
YEARS: list[int] = [2026, 2027, 2028] # Forrester Year 1/2/3; window opens Jan 2026
YEAR_INDEX: dict[int, int] = {y: i for i, y in enumerate(YEARS, start=1)}
X_LABELS: list[str] = ["Initial"] + [str(y) for y in YEARS]
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def month_label(m: int) -> str:
"""Calendar label for a 1-indexed month from Jan of YEARS[0]."""
return f"{_MONTHS[(m - 1) % 12]} {YEARS[0] + (m - 1) // 12}"
def by_calendar(year_values: dict[str, float]) -> dict[int, float]:
"""Map Forrester's ``{"1": v, …}`` year-index keys to calendar years."""
return {YEARS[int(k) - 1]: float(v or 0) for k, v in year_values.items()}
# ── Discounting primitives ───────────────────────────────────────────
def discount_factor(year_index: int, discount_rate: float) -> float:
"""``1 / (1 + r) ** n``. Year 0 → 1.0 (no discount)."""
if year_index < 0:
raise ValueError("year_index must be >= 0")
return 1.0 / ((1.0 + discount_rate) ** year_index)
def present_value(amount: float, year_index: int, discount_rate: float) -> float:
"""Discount ``amount`` from end-of-year ``year_index`` to present."""
return amount * discount_factor(year_index, discount_rate)
def npv(cashflows: Iterable[float], discount_rate: float,
initial: float = 0.0) -> float:
"""``initial + Σ CF_n / (1 + r)^n`` — initial undiscounted (TEI)."""
return initial + sum(
present_value(float(cf), n, discount_rate)
for n, cf in enumerate(cashflows, start=1)
)
def roi_pct(benefits_pv: float, costs_pv: float) -> float:
"""``(Benefits Costs) / Costs`` as a percentage; 0 when costs ≤ 0."""
if costs_pv <= 0:
return 0.0
return (benefits_pv - costs_pv) / costs_pv * 100.0
# ── Payback ──────────────────────────────────────────────────────────
def payback_years(initial_cost: float,
yearly_net: Sequence[float]) -> float | None:
"""
Years until cumulative net benefits cover the initial cost, with linear
interpolation inside the crossing year. ``None`` if never reached.
"""
remaining = float(initial_cost)
if remaining <= 0:
return 0.0
for i, cf in enumerate(yearly_net):
cf = float(cf)
if cf <= 0:
remaining += -cf # a net-loss year widens the gap
continue
if cf >= remaining:
return i + remaining / cf
remaining -= cf
return None
def payback_months(initial_cost: float,
yearly_net: Sequence[float]) -> float | None:
"""Same as :func:`payback_years`, in months."""
yrs = payback_years(initial_cost, yearly_net)
return yrs * 12.0 if yrs is not None else None
def payback_label(months: float | None) -> str:
"""Human label: ``"0.7 months (~Jan 2026)"`` / ``"immediate"`` / ``"beyond 2028"``."""
if months is None:
return f"beyond {YEARS[-1]}"
if months <= 0:
return "immediate"
return f"{months:.1f} months (~{month_label(max(1, math.ceil(months)))})"
# ── Risk adjustment (TEI: benefits down, costs up) ───────────────────
def risk_adjust_benefit(amount: float, risk_factor: float) -> float:
"""``amount × (1 rf)``, rf clamped to [0, 1]."""
rf = max(0.0, min(1.0, float(risk_factor)))
return amount * (1.0 - rf)
def risk_adjust_cost(amount: float, risk_factor: float) -> float:
"""``amount × (1 + rf)``, rf clamped to [0, 1]."""
rf = max(0.0, min(1.0, float(risk_factor)))
return amount * (1.0 + rf)
def risk_adjusted_rows(rows: list[dict], table: str) -> list[dict]:
"""Deep-copied rows with the per-row risk factor applied to every value."""
adjust = risk_adjust_benefit if table == "benefits" else risk_adjust_cost
out: list[dict] = []
for raw in rows:
row = deepcopy(raw)
rf = float(row.get("risk_adjustment") or 0.0)
row["year_values"] = {
k: adjust(float(v or 0), rf) for k, v in row["year_values"].items()
}
if row.get("initial"):
# Only costs carry an initial; TEI adjusts it upward like the years.
row["initial"] = risk_adjust_cost(float(row["initial"]), rf) \
if table == "costs" else float(row["initial"])
out.append(row)
return out
# ── Aggregation (calendar-keyed) ─────────────────────────────────────
def _totals_by_year(ra_rows: list[dict]) -> dict[int, float]:
totals = {y: 0.0 for y in YEARS}
for row in ra_rows:
for y, v in by_calendar(row["year_values"]).items():
totals[y] += v
return totals
def benefits_by_year(rows: list[dict]) -> dict[int, float]:
"""Risk-adjusted benefit totals per calendar year."""
return _totals_by_year(risk_adjusted_rows(rows, "benefits"))
def costs_by_year(rows: list[dict]) -> dict[int, float]:
"""Risk-adjusted cost totals per calendar year (excludes ``initial``)."""
return _totals_by_year(risk_adjusted_rows(rows, "costs"))
def initial_costs(rows: list[dict]) -> float:
"""Risk-adjusted Year-0 outlay (undiscounted)."""
return sum(
float(row.get("initial") or 0)
for row in risk_adjusted_rows(rows, "costs")
)
# ── Composite summary ────────────────────────────────────────────────
def compute_summary(benefits: list[dict], costs: list[dict],
discount_rate: float = 0.10) -> dict:
"""
The full business-case readout for one set of value rows.
Returns KPIs (``benefits_pv``/``costs_pv``/``npv``/``roi_pct``/
``payback_months``/``payback_label``/``initial_costs``/nominal totals),
calendar-keyed schedules (``benefits_by_year``/``costs_by_year``/
``net_by_year``/``cumulative_net_by_year`` — cumulative subtracts the
initial outlay), and a per-row breakdown under ``rows``.
"""
ben_ra = risk_adjusted_rows(benefits, "benefits")
cost_ra = risk_adjusted_rows(costs, "costs")
ben_by = _totals_by_year(ben_ra)
cost_by = _totals_by_year(cost_ra)
initial = sum(float(r.get("initial") or 0) for r in cost_ra)
benefits_pv = npv([ben_by[y] for y in YEARS], discount_rate)
costs_pv = npv([cost_by[y] for y in YEARS], discount_rate, initial=initial)
net_by = {y: ben_by[y] - cost_by[y] for y in YEARS}
cum, cum_by = -initial, {}
for y in YEARS:
cum += net_by[y]
cum_by[y] = cum
pb_months = payback_months(initial, [net_by[y] for y in YEARS])
def _row_breakdown(ra_rows: list[dict], table: str) -> list[dict]:
out = []
for row in ra_rows:
ra_by = by_calendar(row["year_values"])
init_ra = float(row.get("initial") or 0)
entry = {
"field_key": row["field_key"],
"label": row["label"],
"category": row["category"],
"risk_adjustment": row["risk_adjustment"],
"ra_by_year": ra_by,
"three_yr_ra": sum(ra_by.values()),
"pv": npv([ra_by[y] for y in YEARS], discount_rate,
initial=init_ra if table == "costs" else 0.0),
}
if table == "costs":
entry["initial_ra"] = init_ra
out.append(entry)
return out
return {
"discount_rate": discount_rate,
"benefits_pv": benefits_pv,
"costs_pv": costs_pv,
"npv": benefits_pv - costs_pv,
"roi_pct": roi_pct(benefits_pv, costs_pv),
"payback_months": pb_months,
"payback_label": payback_label(pb_months),
"initial_costs": initial,
"benefits_nominal": sum(ben_by.values()),
"costs_nominal": sum(cost_by.values()) + initial,
"benefits_by_year": ben_by,
"costs_by_year": cost_by,
"net_by_year": net_by,
"cumulative_net_by_year": cum_by,
"rows": {
"benefits": _row_breakdown(ben_ra, "benefits"),
"costs": _row_breakdown(cost_ra, "costs"),
},
}
# ── Display helpers ──────────────────────────────────────────────────
def money(v: float) -> str:
sign, a = ("-" if v < 0 else ""), abs(v)
return f"{sign}${a/1e6:,.1f}M" if a >= 1e6 else f"{sign}${a/1e3:,.0f}K"
def html_money(v: float) -> str:
"""Plotly text with two or more bare ``$`` triggers MathJax math mode —
annotations holding several amounts must use the HTML entity instead."""
return money(v).replace("$", "&#36;")

View File

@@ -0,0 +1,107 @@
"""
Client overlay — Variant 4's personalization layer.
The verbatim anchor is Forrester's *composite organization* ($2.5B revenue,
600 CX agents, 80k weekly interactions). This module rescales that composite
to a client's size: a 🟡 **first-order linear rescale**, answering "what does
the composite look like at your scale?", not "what is your TEI?".
Each verbatim row is tied to the driver that dominates its derivation in the
PDF (see ``BENEFIT_DRIVERS``/``COST_DRIVERS``); rows scale linearly with
their driver, project-based costs stay fixed. This composite's trajectory is
flat (Y2 = Y3), so there is no growth re-base; linear scaling preserves the
legacy-retirement ramp shape.
The one non-ratio driver is ``ai_tokens_annual``: the published study models
**$0** Genesys AI Experience token consumption (see the anchor's footnote),
so a client case prices that line directly — the negotiated annual figure
from the Genesys quote replaces the row's year values outright.
``overlay_rows(COMPOSITE)`` is the identity — it reproduces the verbatim
numbers exactly (tokens included, at $0), so headless widget defaults form
the published-study reproduction the gate expects. The anchor is never
mutated: every function deep-copies.
"""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass
from .anchor import ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM
@dataclass(frozen=True)
class ClientDrivers:
"""Client inputs; defaults are the Forrester composite (identity overlay)."""
agents_fte: int = ASSUMPTIONS["agents_fte"] # 600 (400 concurrent licenses)
weekly_interactions: int = ASSUMPTIONS["weekly_interactions"] # 80,000 @ 12 min
annual_revenue: float = ASSUMPTIONS["annual_revenue"] # $2.5B
ai_tokens_annual: float = 0.0 # 🔴 published study models $0 AI consumption
discount_rate: float = ASSUMPTIONS["discount_rate"] # 0.10
COMPOSITE = ClientDrivers()
#: 🟡 Which driver each verbatim row scales with, per its PDF derivation.
BENEFIT_DRIVERS: dict[str, str] = {
"legacy_retirement": "agents", # seat-scoped legacy platform costs
"self_service_savings": "interactions", # deflected volume → FTEs
"agent_efficiency": "interactions", # MTTR saving × handled volume
"agent_assist_sales": "revenue", # 20% of revenue × lift × margin
}
COST_DRIVERS: dict[str, str] = {
"cx_cloud_licenses": "agents", # 400 concurrent of 600 agents
"implementation": "fixed", # 10-week project — does not scale
"ongoing_management": "fixed", # small fixed team
"genesys_ai_tokens": "ai_tokens", # 🔴 direct annual input, not a ratio
}
def scale_factor(driver: str, d: ClientDrivers) -> float:
"""Linear size ratio vs the composite for one ratio-driver kind."""
if driver == "agents":
return d.agents_fte / ASSUMPTIONS["agents_fte"]
if driver == "interactions":
return d.weekly_interactions / ASSUMPTIONS["weekly_interactions"]
if driver == "revenue":
return d.annual_revenue / ASSUMPTIONS["annual_revenue"]
if driver == "fixed":
return 1.0
raise KeyError(f"Unknown driver: {driver!r}")
def overlay_rows(d: ClientDrivers = COMPOSITE) -> tuple[list[dict], list[dict]]:
"""
Deep-copied (benefits, costs) rows rescaled to the client's drivers.
Ratio-driven rows: ``year_values[n] ×= scale_factor(driver)`` (and
``initial`` likewise). Fixed rows are untouched. The ``ai_tokens`` row
takes ``d.ai_tokens_annual`` as each year's value directly — the
negotiated quote figure, not a rescale of the anchor's $0.
Risk factors, labels, and notes are unchanged everywhere.
"""
def _apply(rows: list[dict], drivers: dict[str, str]) -> list[dict]:
out = []
for raw in rows:
row = deepcopy(raw)
driver = drivers[row["field_key"]]
if driver == "ai_tokens":
row["year_values"] = {
k: float(d.ai_tokens_annual) for k in row["year_values"]
}
elif driver != "fixed":
s = scale_factor(driver, d)
row["year_values"] = {
k: float(v) * s for k, v in row["year_values"].items()
}
if row.get("initial"):
row["initial"] = float(row["initial"]) * s
out.append(row)
return out
return (_apply(BENEFITS_VERBATIM, BENEFIT_DRIVERS),
_apply(COSTS_VERBATIM, COST_DRIVERS))

View File

@@ -0,0 +1,67 @@
"""
Scenario stress — transplanted from the retired shared ``core/calculations/scenarios.py``
with identical semantics.
Forrester TEI risk-adjusts benefits *down* and costs *up*; scenarios stress
both levers:
* ``adoption`` scales nominal values (``year_values`` and ``initial``).
* ``risk_delta`` is *added* to a benefit's risk factor and *subtracted*
from a cost's (conservative = more uncertainty on benefits, less padding
on costs), then clamped to [0, 1].
``"moderate"`` is the identity — the headless default reproduces the
published study. Note the counterintuitive corollary: the conservative
scenario *lowers* costs PV, because 80% adoption shrinks consumption-priced
usage and the clamp caps cost padding.
"""
from __future__ import annotations
from copy import deepcopy
SCENARIOS: dict[str, dict[str, float]] = {
"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 apply_scenario(
items: list[dict],
scenario: str = "moderate",
*,
multipliers: dict[str, dict[str, float]] | None = None,
table: str | None = None,
) -> list[dict]:
"""
Deep-copied value rows with the scenario applied; inputs are not mutated.
Each row needs ``year_values`` (year-string → float), optionally
``initial`` and ``risk_adjustment``, and a ``table`` of ``"benefits"``
or ``"costs"`` (or pass ``table=`` to force one) — the table decides the
sign of ``risk_delta``.
"""
cfg = (multipliers or SCENARIOS).get(scenario)
if cfg is None:
raise KeyError(f"Unknown scenario: {scenario!r}")
adoption = float(cfg.get("adoption", 1.0))
risk_delta = float(cfg.get("risk_delta", 0.0))
out: list[dict] = []
for raw in items:
item = deepcopy(raw)
item_table = item.get("table") or table or "benefits"
item["table"] = item_table
item["year_values"] = {
k: float(v) * adoption for k, v in item["year_values"].items()
}
if item.get("initial") is not None:
item["initial"] = float(item["initial"]) * adoption
ra = float(item.get("risk_adjustment") or 0.0)
new_ra = ra + risk_delta if item_table == "benefits" else ra - risk_delta
item["risk_adjustment"] = max(0.0, min(1.0, new_ra))
out.append(item)
return out

View File

@@ -0,0 +1,29 @@
"""
Stage vs backstage — is this notebook render stakeholder-facing?
The Mercury CLI (``mercury --working-dir …``) exports ``MERCURY_CONFIG_DIR``
into the server process so the widget library can locate ``config.toml``
(see ``mercury/config.py``); every kernel that server spawns inherits it.
JupyterLab and nbconvert kernels don't have it. That makes the variable a
reliable signal for "the audience is looking" (the stage) versus an
analyst session or a headless export run (backstage).
Diagnostics routed through :func:`backstage` stay visible in JupyterLab
and land in the nbconvert exports (where the machine-readable appendix
must appear for LLM consumption) but never render in the Mercury app.
"""
from __future__ import annotations
import os
def on_stage() -> bool:
"""True when running under the Mercury app (stakeholder-facing)."""
return os.getenv("MERCURY_CONFIG_DIR") is not None
def backstage(*args, **kwargs) -> None:
"""``print`` that renders only backstage (JupyterLab, nbconvert)."""
if not on_stage():
print(*args, **kwargs)

View File

@@ -0,0 +1,7 @@
"""Make teicalc importable even without the study venv active (the normal
setup is ``pip install -e ".[dev]"`` into the study-local ``.venv/``)."""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))

View File

@@ -0,0 +1,104 @@
"""The verbatim anchor is Forrester's published record — pinned value by
value, and proven immutable under every engine code path."""
from copy import deepcopy
from teicalc import (
ASSUMPTIONS,
BENEFITS_VERBATIM,
COMPOSITE,
COSTS_VERBATIM,
PUBLISHED,
ClientDrivers,
apply_scenario,
compute_summary,
overlay_rows,
)
def _row(rows, key):
return next(r for r in rows if r["field_key"] == key)
def test_benefit_rows_verbatim():
assert [r["field_key"] for r in BENEFITS_VERBATIM] == [
"legacy_retirement",
"self_service_savings",
"agent_efficiency",
"agent_assist_sales",
]
expected = {
"legacy_retirement": ({"1": 680_000, "2": 930_000, "3": 930_000}, 0.05),
"self_service_savings": ({"1": 2_329_600, "2": 2_329_600, "3": 2_329_600}, 0.15),
"agent_efficiency": ({"1": 2_912_000, "2": 2_912_000, "3": 2_912_000}, 0.10),
"agent_assist_sales": ({"1": 600_000, "2": 600_000, "3": 600_000}, 0.05),
}
for key, (years, rf) in expected.items():
row = _row(BENEFITS_VERBATIM, key)
assert row["year_values"] == years
assert row["risk_adjustment"] == rf
assert row["table"] == "benefits"
def test_cost_rows_verbatim():
expected = {
"cx_cloud_licenses": ({"1": 840_000, "2": 840_000, "3": 840_000}, 0.05, 0),
"implementation": ({"1": 0, "2": 0, "3": 0}, 0.10, 1_190_000),
"ongoing_management": ({"1": 202_800, "2": 202_800, "3": 202_800}, 0.10, 0),
"genesys_ai_tokens": ({"1": 0, "2": 0, "3": 0}, 0.0, 0),
}
for key, (years, rf, initial) in expected.items():
row = _row(COSTS_VERBATIM, key)
assert row["year_values"] == years
assert row["risk_adjustment"] == rf
assert row["initial"] == initial
assert row["table"] == "costs"
def test_ai_token_line_is_anchored_at_zero():
"""The published study models $0 AI consumption — the study's blind spot,
preserved verbatim so the reproduction matches the published totals."""
row = _row(COSTS_VERBATIM, "genesys_ai_tokens")
assert all(v == 0 for v in row["year_values"].values())
assert row["initial"] == 0 and row["risk_adjustment"] == 0.0
assert "NOT in the published study" in row["notes"]
def test_assumptions_and_published():
assert ASSUMPTIONS["annual_revenue"] == 2_500_000_000
assert ASSUMPTIONS["agents_fte"] == 600
assert ASSUMPTIONS["concurrent_licenses"] == 400
assert ASSUMPTIONS["weekly_interactions"] == 80_000
assert ASSUMPTIONS["discount_rate"] == 0.10
assert ASSUMPTIONS["analysis_years"] == 3
assert PUBLISHED["benefits_pv"] == 14_840_638
assert PUBLISHED["costs_pv"] == 4_057_170
assert PUBLISHED["npv"] == 10_783_468
assert PUBLISHED["roi_pct"] == 266
assert "payback" not in str(sorted(PUBLISHED)) # study doesn't headline one
# The composite drivers ARE the anchor assumptions (tokens at $0).
assert COMPOSITE.agents_fte == ASSUMPTIONS["agents_fte"]
assert COMPOSITE.weekly_interactions == ASSUMPTIONS["weekly_interactions"]
assert COMPOSITE.annual_revenue == ASSUMPTIONS["annual_revenue"]
assert COMPOSITE.ai_tokens_annual == 0.0
assert COMPOSITE.discount_rate == ASSUMPTIONS["discount_rate"]
def test_anchor_is_never_mutated():
"""Exercise every engine code path, then prove the record unchanged."""
ben_snap = deepcopy(BENEFITS_VERBATIM)
cost_snap = deepcopy(COSTS_VERBATIM)
overlay_rows()
overlay_rows(ClientDrivers(agents_fte=137, weekly_interactions=5_000,
annual_revenue=9e9, ai_tokens_annual=450_000))
for scenario in ("conservative", "moderate", "aggressive"):
apply_scenario(BENEFITS_VERBATIM, scenario)
apply_scenario(COSTS_VERBATIM, scenario)
compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.10)
compute_summary(BENEFITS_VERBATIM, COSTS_VERBATIM, 0.08)
assert BENEFITS_VERBATIM == ben_snap
assert COSTS_VERBATIM == cost_snap

View File

@@ -0,0 +1,122 @@
"""Engine pins — every number hand-checked before pinning.
RA_benefit = v×(1rf), 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)

View 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

View File

@@ -0,0 +1,74 @@
"""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

View File

@@ -0,0 +1,15 @@
"""Stage/backstage detection — Mercury kernels carry MERCURY_CONFIG_DIR."""
from teicalc import staging
def test_backstage_prints_only_off_stage(monkeypatch, capsys):
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
assert not staging.on_stage()
staging.backstage("visible")
assert capsys.readouterr().out == "visible\n"
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
assert staging.on_stage()
staging.backstage("hidden")
assert capsys.readouterr().out == ""