""" Appendix-4 corrected business case — the Genesys/Broadreach benefits kept verbatim, with the costs the deck omitted: AI Experience token consumption, AI implementation effort (V2 LoE), and double-billing of the existing platforms until their term contracts end. Single source of truth shared by the notebook (``notebooks/ctm_business_case_corrected.ipynb``) and the Streamlit "Corrected Business Case" view — presentation layers hold no math. Sources: ``docs/Appendix 4 - CCaaS Platform Benefit Calculations (Consolidated).pptx`` (verbatim figures, deployment schedule) and ``docs/ctm_ai_labour_estimate_V2.md`` (implementation hours). """ from __future__ import annotations import dataclasses import datetime as dt import math import pandas as pd from .business_case import npv, payback_years from .cost_model import calculate_total_cost from .defaults import DEFAULT_METERS from .inputs import FeatureScope, SiteInput from .meters import Confidence, TokenMeter, TokenPricing from .rollout import RolloutPlan from .scenarios import Scenario # ── Timeline ───────────────────────────────────────────────────────── YEARS = [2026, 2027, 2028] # model years 1..3, contract start Jan 2026 YEAR_INDEX = {2026: 1, 2027: 2, 2028: 3} _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 2026 (m=21 → 'Sep 2027').""" return f"{_MONTHS[(m - 1) % 12]} {2026 + (m - 1) // 12}" # ── Verbatim Appendix 4 figures ────────────────────────────────────── REGIONS = ["NA", "ANZ", "EMEA", "ASIA"] CAPABILITIES = ["Agent Copilot", "WFM", "Email", "STA", "Predictive Routing", "Supervisor Copilot"] #: (annual_value, three_yr_value) — VERBATIM slides 12-15, do not edit. VERBATIM_BENEFITS: dict[tuple[str, str], tuple[float, float]] = { ("NA", "Agent Copilot"): (2_400_000, 3_400_000), ("NA", "Email"): (1_900_000, 2_500_000), ("NA", "STA"): (294_000, 506_000), ("NA", "Supervisor Copilot"): (218_000, 291_000), ("NA", "Predictive Routing"): (97_000, 167_000), ("NA", "WFM"): (0, 0), # NA excluded — has similar feature ("ANZ", "Agent Copilot"): (3_600_000, 3_900_000), ("ANZ", "WFM"): (1_300_000, 1_400_000), ("ANZ", "Predictive Routing"): (279_000, 302_000), ("ANZ", "Email"): (132_000, 143_000), ("ANZ", "STA"): (97_000, 105_000), ("ANZ", "Supervisor Copilot"): (25_000, 27_000), ("ASIA", "WFM"): (1_600_000, 914_000), ("ASIA", "Email"): (160_000, 93_000), ("ASIA", "STA"): (124_000, 72_000), ("ASIA", "Predictive Routing"): (87_000, 51_000), ("ASIA", "Agent Copilot"): (0, 0), ("ASIA", "Supervisor Copilot"): (0, 0), ("EMEA", "WFM"): (824_000, 687_000), ("EMEA", "Email"): (282_000, 235_000), ("EMEA", "STA"): (157_000, 131_000), ("EMEA", "Agent Copilot"): (77_000, 64_000), ("EMEA", "Supervisor Copilot"): (59_000, 49_000), ("EMEA", "Predictive Routing"): (7_000, 6_000), } #: The deck's own (rounded) summary rows — slides 8-9. SLIDE_TOTALS: dict = { "regional_3yr": {"NA": 6_900_000, "ANZ": 5_900_000, "ASIA": 1_100_000, "EMEA": 1_200_000}, "capability_3yr": {"Agent Copilot": 7_400_000, "WFM": 3_000_000, "Email": 2_900_000, "STA": 814_000, "Predictive Routing": 526_000, "Supervisor Copilot": 367_000}, "total_3yr": 15_000_000, "total_annual": 13_600_000, } #: Verbatim TCO anchors — slides 5-6. TCO_VERBATIM: dict[str, float] = { "current_annual": 7_300_000, # current global spend / yr "current_3yr": 22_000_000, "ccaas_annual": 4_300_000, # licence run-rate / yr "ccaas_3yr": 15_400_000, # deck's 3-yr CCaaS investment (no ramp, no AI costs) "prof_services_y1": 2_400_000, "training_y1": 167_000, "npv_discount_rate": 0.135, # deck's benefit-NPV rate } #: Genesys/Broadreach deployment schedule (slides 17-21), months from #: Jan 2026 inclusive. Benefits realize IMPL + 3 months. IMPL_MONTH = {"NA": 18, "ANZ": 21, "EMEA": 24, "ASIA": 27} BENEFIT_LAG_MONTHS = 3 REALIZE_MONTH = {r: m + BENEFIT_LAG_MONTHS for r, m in IMPL_MONTH.items()} #: NA Gantt exception: Email implemented Jan 2027, realizes Apr 2027. NA_EMAIL_IMPL_MONTH = 13 DEFAULT_RAMP_MONTHS = 12 # Genesys ramp programme DEFAULT_TERMINATION = dt.date(2027, 12, 31) # current-platform term contracts # ── Region ⇄ site mapping ──────────────────────────────────────────── def site_region(site_name: str) -> str: """Map a tokencalc site to its Appendix-4 region (APAC * → ASIA).""" return {"NAM": "NA", "AUZ": "ANZ", "EMEA": "EMEA"}.get(site_name, "ASIA") def region_site_names(sites: list[SiteInput]) -> dict[str, list[str]]: return {r: [s.site_name for s in sites if site_region(s.site_name) == r] for r in REGIONS} def region_agents(sites: list[SiteInput]) -> dict[str, int]: return {r: sum(s.agents for s in sites if site_region(s.site_name) == r) for r in REGIONS} # ── Verbatim benefit helpers ───────────────────────────────────────── def verbatim_dataframe() -> pd.DataFrame: """Long DataFrame of the verbatim benefits: region, capability, annual, three_yr.""" return pd.DataFrame( [{"region": r, "capability": c, "annual": a, "three_yr": t} for (r, c), (a, t) in VERBATIM_BENEFITS.items()] ) def crossfoot_tolerance(value: float) -> float: """The deck rounds to $0.1M and its own tables cross-foot ±$50-120K.""" return max(100_000, 0.015 * value) # ── Schedules & rollouts ───────────────────────────────────────────── def build_rollouts( sites: list[SiteInput], na_email_early: bool = True, ramp_months: int = DEFAULT_RAMP_MONTHS, ) -> tuple[RolloutPlan, RolloutPlan, RolloutPlan]: """(token, email_token, benefit) rollout plans on the deck's schedule. ``RolloutPlan.go_live_month = m`` means active from month m+1; the deck's labels are inclusive (NA "realizes Sep 2027" ⇒ September counts), so keys are set to label − 1. The benefit plan is keyed by region (plus ``NA_EMAIL`` for the NA Gantt exception); the token plans are keyed by site. """ token = RolloutPlan( contract_start="2026-01", build_months=max(IMPL_MONTH.values()), ramp_months=ramp_months, first_year_platform_discount=0.0, # licences are handled verbatim, not by this plan go_live_month={s.site_name: IMPL_MONTH[site_region(s.site_name)] - 1 for s in sites}, ) email = dataclasses.replace( token, go_live_month={**token.go_live_month, "NAM": (NA_EMAIL_IMPL_MONTH - 1) if na_email_early else IMPL_MONTH["NA"] - 1}, ) benefit = RolloutPlan( first_year_platform_discount=0.0, go_live_month={**{r: REALIZE_MONTH[r] - 1 for r in REGIONS}, "NA_EMAIL": (NA_EMAIL_IMPL_MONTH + BENEFIT_LAG_MONTHS - 1) if na_email_early else REALIZE_MONTH["NA"] - 1}, ) return token, email, benefit def benefits_by_year( benefit_rollout: RolloutPlan, na_email_early: bool = True ) -> pd.DataFrame: """Phase each verbatim 3-yr value by its region's realization window. Scaling is at the finest grain (region × capability), so every verbatim per-region, per-capability, and grand total is reproduced exactly. Long DataFrame: region, capability, year, benefit. """ rows = [] for (region, cap), (_annual, three_yr) in VERBATIM_BENEFITS.items(): key = ("NA_EMAIL" if (region == "NA" and cap == "Email" and na_email_early) else region) live = [benefit_rollout.live_months_in_year(key, YEAR_INDEX[y]) for y in YEARS] total_live = sum(live) for y, m in zip(YEARS, live): rows.append({"region": region, "capability": cap, "year": y, "benefit": three_yr * m / total_live if total_live else 0.0}) return pd.DataFrame(rows) # ── Base cost lines (verbatim + contract mechanics) ────────────────── def current_months_in_year(termination: dt.date, cal_year: int) -> int: """Months a term contract bills in ``cal_year`` (through its termination month).""" if cal_year < termination.year: return 12 if cal_year > termination.year: return 0 return termination.month def current_state_inputs( sites: list[SiteInput], total_annual: float | None = None, termination: dt.date = DEFAULT_TERMINATION, ) -> pd.DataFrame: """Per-region current-platform inputs, seeded by agent share of the verbatim global spend. Region-indexed; annual_cost and contract_termination are the editable columns.""" total = TCO_VERBATIM["current_annual"] if total_annual is None else total_annual agents = region_agents(sites) total_agents = sum(agents.values()) return pd.DataFrame([ {"region": r, "agents": agents[r], "share": agents[r] / total_agents, "annual_cost": total * agents[r] / total_agents, "contract_termination": termination, "confidence": "🟡 agent-share allocation of the verbatim total"} for r in REGIONS ]).set_index("region") def current_costs_by_year(current_state: pd.DataFrame) -> dict[int, float]: """Existing-platform run-off per calendar year (the double-billing line).""" return { y: float(sum( row["annual_cost"] * current_months_in_year(row["contract_termination"], y) / 12 for _, row in current_state.iterrows())) for y in YEARS } def licence_months_in_year(year_index: int, ramp_months: int) -> int: """Ramp programme: licence billing starts in calendar month ramp_months + 1.""" start, end = 12 * (year_index - 1) + 1, 12 * year_index return max(0, end - max(start, ramp_months + 1) + 1) def licence_costs_by_year( ramp_months: int = DEFAULT_RAMP_MONTHS, annual: float | None = None ) -> dict[int, float]: rate = TCO_VERBATIM["ccaas_annual"] if annual is None else annual return {y: rate * licence_months_in_year(YEAR_INDEX[y], ramp_months) / 12 for y in YEARS} def ps_costs_by_year() -> dict[int, float]: """Base professional services + training — verbatim, year 1 only.""" return {2026: TCO_VERBATIM["prof_services_y1"] + TCO_VERBATIM["training_y1"], 2027: 0.0, 2028: 0.0} # ── Token consumption (missing cost #1) ────────────────────────────── def claim_scenario(email_auto_respond_rate: float = 0.255) -> Scenario: """Claim-level scenario: deck parameters, no consumption maturity ramp.""" return Scenario( name="genesys-claim", voice_bot_deflection=0.0, voice_bot_avg_minutes=0.0, agentic_va_deflection=0.0, voice_summarization_eligibility=0.0, voice_knowledge_eligibility=0.0, # unused by the Appendix-4 scope set email_auto_respond_rate=email_auto_respond_rate, email_auto_suggest_acceptance=0.0, # Auto-Suggest is inside Copilot (V2 #1) consumption_cost_realization={1: 1.0, 2: 1.0, 3: 1.0}, ) def autorespond_meter(tokens_per_msg: float = 0.05) -> TokenMeter: """Email Auto-Respond working meter — rate unpublished (🔴→🟡). Anchor: ≈1 AI action per generated response; Genesys Cloud Copilot meters 20 AI actions per token. """ return dataclasses.replace( DEFAULT_METERS["Email AI (Auto-Respond)"], units_per_token=1.0 / tokens_per_msg, tokens_per_unit=tokens_per_msg, confidence=Confidence.ESTIMATED, notes="WORKING ASSUMPTION — rate unpublished; ≈1 AI action per generated " "response (Genesys Cloud Copilot meters 20 AI actions/token).", ) def build_scopes( sites: list[SiteInput], copilot_includes_asia: bool = False, pr_eligibility: float = 1.0, ai_translate_eligibility: float = 0.01, ) -> tuple[list[FeatureScope], list[FeatureScope]]: """(core, email) feature scopes mirroring the six deck capabilities. No ``adoption_curve`` on any scope — a curve would silently override the claim scenario's flat consumption realization. Email scopes are separate because NA Email implements early (own rollout plan). """ all_names = [s.site_name for s in sites] asia = [n for n in all_names if site_region(n) == "ASIA"] non_asia = [n for n in all_names if site_region(n) != "ASIA"] copilot_sites = non_asia + (asia if copilot_includes_asia else []) core = [ FeatureScope("Agent Copilot [named]", copilot_sites, phase=1), FeatureScope("Speech & Text Analytics [named]", all_names, phase=1), FeatureScope("Predictive Routing", all_names, phase=1, eligibility_pct=pr_eligibility), # $0 by Rule 1 (Copilot covers summarization) — kept visible. FeatureScope("AI Summary & Insights", copilot_sites, phase=1), # Supervisor Copilot small-volume proxy. FeatureScope("AI Translate", asia + ["EMEA"], phase=1, eligibility_pct=ai_translate_eligibility), ] email = [FeatureScope("Email AI (Auto-Respond)", all_names, phase=1)] return core, email def token_costs_by_year( sites: list[SiteInput], meters: dict[str, TokenMeter], pricing: dict[str, TokenPricing], scenario: Scenario, core_scopes: list[FeatureScope], email_scopes: list[FeatureScope], token_rollout: RolloutPlan, email_rollout: RolloutPlan, use_contracted: bool = False, ) -> pd.DataFrame: """Engine-computed token costs, rollout-gated, per calendar year. Long DataFrame: cost_line, scope, annual_cost, confidence, year. """ frames = [] for y in YEARS: for scopes, rollout in ((core_scopes, token_rollout), (email_scopes, email_rollout)): part = calculate_total_cost( sites, scopes, meters, pricing, scenario, YEAR_INDEX[y], include_platform=False, use_contracted=use_contracted, rollout=rollout, ) part["year"] = y frames.append(part) return pd.concat(frames, ignore_index=True) # ── AI implementation effort (missing cost #2, V2 LoE) ─────────────── #: (low, high) Y1 hours — docs/ctm_ai_labour_estimate_V2.md. AI_IMPL_HOURS: dict[str, tuple[float, float]] = { "Agent Copilot": (1_200, 1_800), # voice + digital incl. email Auto-Suggest "Email Auto-Respond": (800, 1_400), # separate flow; needs SoR integration "STA": (800, 1_200), # topics, programs, tuning × 7 languages "Supervisor Copilot": (200, 400), "Predictive Routing": (400, 700), "Cross-cutting": (1_000, 1_800), # governance, PM, test env, integration } KB_READINESS_HOURS = (500, 1_500) # prerequisite project — flagged separately STEADY_STATE_HOURS = (500, 900) # absolute h/yr, 2027-2028 DEFAULT_BLENDED_RATE = 225.0 SMELL_TEST_FLOOR = 0.15 # impl ≥ 15% of benefit claim, or flag def impl_feature_regions(copilot_includes_asia: bool = False) -> dict[str, list[str]]: """Which regions each implementation workstream serves.""" return { "Agent Copilot": (["NA", "ANZ", "EMEA"] + (["ASIA"] if copilot_includes_asia else [])), "Email Auto-Respond": list(REGIONS), "STA": list(REGIONS), "Supervisor Copilot": ["NA", "ANZ", "EMEA"], # deck: $0 SupCopilot in ASIA "Predictive Routing": list(REGIONS), "Cross-cutting": list(REGIONS), } def hours_pick(rng: tuple[float, float], mode: str) -> float: low, high = rng return {"low": low, "mid": (low + high) / 2, "high": high}[mode] def impl_year_fractions(impl_month: int) -> list[float]: """Spend spreads uniformly from contract start (month 0) to the impl month.""" prev, fracs = 0, [] for yi in (1, 2, 3): cur = min(12 * yi, impl_month) fracs.append((cur - prev) / impl_month) prev = cur return fracs def region_impl_month(feature: str, region: str, na_email_early: bool = True) -> int: if feature == "Email Auto-Respond" and region == "NA" and na_email_early: return NA_EMAIL_IMPL_MONTH return IMPL_MONTH[region] def build_impl_costs( sites: list[SiteInput], mode: str = "mid", rate: float = DEFAULT_BLENDED_RATE, include_kb: bool = True, copilot_includes_asia: bool = False, na_email_early: bool = True, ) -> tuple[pd.DataFrame, dict[int, float], dict[int, float], dict[int, float]]: """V2 hours-range × rate model (swap point for the future LoE engine). Returns (detail_df, impl_by_year, kb_by_year, steady_by_year). Hours allocate to each workstream's scoped regions by agent share; steady-state is booked program-level in 2027-2028. """ agents = region_agents(sites) feature_regions = impl_feature_regions(copilot_includes_asia) workstreams = dict(AI_IMPL_HOURS) if include_kb: workstreams["KB readiness (prerequisite)"] = KB_READINESS_HOURS rows = [] for feature, rng in workstreams.items(): regions = feature_regions.get(feature, list(REGIONS)) scope_agents = sum(agents[r] for r in regions) for r in regions: hours = hours_pick(rng, mode) * agents[r] / scope_agents fracs = impl_year_fractions( region_impl_month(feature, r, na_email_early)) rows.append({"workstream": feature, "region": r, "hours": hours, "cost": hours * rate, **{y: hours * rate * f for y, f in zip(YEARS, fracs)}}) df = pd.DataFrame(rows) is_kb = df["workstream"].str.startswith("KB") impl_y = {y: float(df.loc[~is_kb, y].sum()) for y in YEARS} kb_y = {y: float(df.loc[is_kb, y].sum()) for y in YEARS} steady = hours_pick(STEADY_STATE_HOURS, mode) * rate steady_y = {2026: 0.0, 2027: steady, 2028: steady} return df, impl_y, kb_y, steady_y # ── Business case (baseline-relative frame) ────────────────────────── def case_flows( total_cost_by_year: dict[int, float], benefit_total_by_year: dict[int, float], baseline_annual: float | None = None, ) -> tuple[dict[int, float], dict[int, float]]: """(incremental cost, net) vs the do-nothing baseline. One frame captures both the 2026-27 double-billing penalty and the post-termination cost-avoidance credit. """ base = TCO_VERBATIM["current_annual"] if baseline_annual is None else baseline_annual inc = {y: total_cost_by_year[y] - base for y in YEARS} net = {y: benefit_total_by_year[y] - inc[y] for y in YEARS} return inc, net def payback_label(net_by_year: dict[int, float]) -> str: pb = payback_years([net_by_year[y] for y in YEARS]) if pb is None: return f"beyond {YEARS[-1]}" if pb == 0: return "immediate" m = math.ceil(pb * 12) return f"{m} months (~{month_label(m)})" def case_kpis( inc: dict[int, float], net: dict[int, float], discount_rate: float | None = None, ) -> dict: """KPIs for one cost frame. Benefits are recoverable as net + inc.""" rate = TCO_VERBATIM["npv_discount_rate"] if discount_rate is None else discount_rate net_list = [net[y] for y in YEARS] inc_total = sum(inc.values()) net_total = sum(net_list) return { "benefits_3yr": net_total + inc_total, "incremental_cost_3yr": inc_total, "net_3yr": net_total, "roi": (net_total / inc_total) if inc_total > 0 else None, "npv": npv(net_list, rate), "discount_rate": rate, "payback": payback_label(net), } # ── 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("$", "$")