CTM Calculators: Add PS billing milestones, Adjust Genesys RAMP

This commit is contained in:
2026-07-08 11:24:57 -04:00
parent b5af65891c
commit a991879061
13 changed files with 4264 additions and 2712 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -41,6 +41,10 @@ def test_ramp_zeroes_year_one_licences():
2028: 4_300_000.0}
assert a4.licence_costs_by_year(0)[2026] == 4_300_000.0
assert a4.licence_costs_by_year(18)[2027] == pytest.approx(4_300_000 * 6 / 12)
# The order form's ramp is 6 months — licences bill from July 2026.
assert a4.DEFAULT_RAMP_MONTHS == 6
assert a4.licence_costs_by_year() == {2026: 2_150_000.0, 2027: 4_300_000.0,
2028: 4_300_000.0}
def test_current_state_run_off():
@@ -113,3 +117,21 @@ def test_contracted_overlays_verbatim():
assert a4.TCO_VERBATIM["ccaas_annual"] == 4_300_000 # deck record intact
assert a4.tco("current_annual") == a4.TCO_VERBATIM["current_annual"]
assert a4.licence_costs_by_year(12, a4.tco("ccaas_annual"))[2027] == 3_200_000
def test_sow_milestones_and_managed_services():
assert a4.PS_CONTRACTED_TOTAL == pytest.approx(2_025_446.48)
for m in a4.PS_MILESTONES: # amounts match the shares
assert m["amount"] == pytest.approx(m["share"] * a4.PS_CONTRACTED_TOTAL,
abs=0.01)
ps = a4.ps_costs_by_year(contracted=True) # 50/50 across 2026-27
assert ps[2026] == pytest.approx(607_633.94 + 405_089.30 + 167_000)
assert ps[2027] == pytest.approx(607_633.94 + 405_089.30)
assert ps[2028] == 0.0
# The deck's verbatim year-1 lump stays intact for the as-pitched frame.
assert a4.ps_costs_by_year() == {2026: 2_567_000, 2027: 0.0, 2028: 0.0}
# Managed services bill from the month after MCX go-live (Sep 30 → Oct).
ms = a4.managed_services_by_year()
assert ms[2026] == pytest.approx(410_918.40 * 3 / 12)
assert ms[2027] == pytest.approx(410_918.40)
assert ms[2028] == pytest.approx(410_918.40)

View File

@@ -47,11 +47,9 @@ def test_email_benefit_split():
[site], FeatureScope("Email AI (Auto-Respond)", ["Small"]),
"realistic", year=1,
)
# Auto-Suggest is not a separate line — it lives inside Agent Copilot.
lines = set(df["benefit_line"])
assert lines == {
"Email Auto-Respond (displaced handling)",
"Email Auto-Suggest (drafting time)",
}
assert lines == {"Email Auto-Respond (displaced handling)"}
# auto-respond: 1,000×12 × 20% × 600s × 50% × $0.01 = $7,200
respond = df[df["benefit_line"].str.contains("Respond")]["annual_value"].sum()
assert respond == pytest.approx(7_200)

View File

@@ -28,8 +28,8 @@ def test_all_spec_meters_present():
"AI Translate",
# Genesys Cloud Copilot
"Genesys Cloud Copilot",
# Email AI (rates TBD)
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)",
# Email AI (rate TBD; Auto-Suggest is inside Agent Copilot)
"Email AI (Auto-Respond)",
}
assert expected == set(DEFAULT_METERS)
@@ -59,9 +59,7 @@ def test_confirmed_rates():
def test_unknown_meters_flagged():
unknown = {f for f, m in DEFAULT_METERS.items() if m.confidence is Confidence.UNKNOWN}
assert unknown == {
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)",
}
assert unknown == {"Email AI (Auto-Respond)"}
assert Confidence.UNKNOWN.icon == "🔴"
assert Confidence.CONFIRMED.icon == "🟢"

View File

@@ -41,20 +41,46 @@ def test_runrate_saving_annual():
assert mw.runrate_saving_annual(regions=["NA"]) == pytest.approx(3_000_000)
assert mw.runrate_saving_annual(licence_annual=4_800_000,
regions=[]) == pytest.approx(2_500_000)
# Contracted frame: managed services stay in the run-rate forever.
assert mw.runrate_saving_annual(
a4.tco("ccaas_annual"), managed_annual=a4.MANAGED_SERVICES_ANNUAL
) == pytest.approx(6_589_081.60)
def _default_wfm_benefits_by_year():
ben = mw.wfm_benefits_by_year(_default_benefit_rollout())
return {y: float(ben.loc[ben.year == y, "benefit"].sum()) for y in a4.YEARS}
def test_breakeven_extrapolates_past_window():
# Deck frame: deck licence rate (6-month ramp), verbatim PS lump,
# no managed services.
cs = a4.current_state_inputs(SITES)
cur = a4.current_costs_by_year(cs)
lic = a4.licence_costs_by_year()
ps = a4.ps_costs_by_year()
total = {y: cur[y] + lic[y] + ps[y] for y in a4.YEARS}
ben = mw.wfm_benefits_by_year(_default_benefit_rollout())
ben_y = {y: float(ben.loc[ben.year == y, "benefit"].sum()) for y in a4.YEARS}
inc, net = a4.case_flows(total, ben_y)
assert sum(net.values()) == pytest.approx(-1_553_000, abs=1_000)
inc, net = a4.case_flows(total, _default_wfm_benefits_by_year())
assert sum(net.values()) == pytest.approx(-3_703_000, abs=1_000)
label = mw.runrate_breakeven_label(net, mw.runrate_saving_annual())
assert label == "40 months (~Apr 2029, extrapolated)"
assert label == "44 months (~Aug 2029, extrapolated)"
def test_contracted_frame_with_sow_and_managed_services():
# Contracted frame: signed licence rate, SOW PS milestones, managed
# services from MCX go-live — the case the notebook leads with.
cs = a4.current_state_inputs(SITES)
cur = a4.current_costs_by_year(cs)
lic = a4.licence_costs_by_year(annual=a4.tco("ccaas_annual"))
ps = a4.ps_costs_by_year(contracted=True)
man = a4.managed_services_by_year()
total = {y: cur[y] + lic[y] + ps[y] + man[y] for y in a4.YEARS}
inc, net = a4.case_flows(total, _default_wfm_benefits_by_year())
assert sum(net.values()) == pytest.approx(-1_503_013, abs=1_000)
runrate = mw.runrate_saving_annual(
a4.tco("ccaas_annual"), managed_annual=a4.MANAGED_SERVICES_ANNUAL)
label = mw.runrate_breakeven_label(net, runrate)
assert label == "39 months (~Mar 2029, extrapolated)"
def test_breakeven_defers_in_window_and_guards_zero_runrate():

View File

@@ -107,6 +107,26 @@ TCO_CONTRACTED: dict[str, float] = {
"ccaas_annual": 3_200_000, # signed licence run-rate (deck pitched $4.3M/yr)
}
#: NTT professional services — SOW billing milestones (🟢 contractual).
#: The deck's verbatim anchor books $2.4M PS in year 1; the signed SOW
#: bills $2,025,446.48 in four milestones split 50/50 across 2026-27.
PS_MILESTONES: list[dict] = [
{"name": "SOW Effective Date", "date": dt.date(2026, 3, 15),
"share": 0.30, "amount": 607_633.94},
{"name": "Start of client UAT (first region)", "date": dt.date(2026, 9, 30),
"share": 0.20, "amount": 405_089.30},
{"name": "Start of client UAT (last region)", "date": dt.date(2027, 6, 30),
"share": 0.30, "amount": 607_633.94},
{"name": "Completion of last go-live migration", "date": dt.date(2027, 9, 30),
"share": 0.20, "amount": 405_089.30},
]
PS_CONTRACTED_TOTAL = sum(m["amount"] for m in PS_MILESTONES)
#: NTT managed services — commences billing at MCX go-live (🟢 contractual).
#: Not in the deck's TCO at all; an ongoing run-rate cost thereafter.
MANAGED_SERVICES_ANNUAL = 410_918.40
MCX_GO_LIVE = dt.date(2026, 9, 30)
def tco(key: str) -> float:
"""Contracted value where one exists, else the deck's verbatim anchor."""
@@ -120,7 +140,7 @@ 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_RAMP_MONTHS = 6 # Genesys ramp programme (🟢 order form)
DEFAULT_TERMINATION = dt.date(2027, 12, 31) # current-platform term contracts
# ── Region ⇄ site mapping ────────────────────────────────────────────
@@ -275,12 +295,42 @@ def licence_costs_by_year(
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"],
def ps_costs_by_year(contracted: bool = False) -> dict[int, float]:
"""Base professional services + training.
Verbatim: the deck's $2.4M PS lump plus training, all in year 1.
Contracted: PS phased on the SOW billing milestones (50/50 across
2026-27, $2.03M total); training stays the verbatim year-1 line —
the SOW milestones don't itemize it separately.
"""
training = TCO_VERBATIM["training_y1"]
if contracted:
ps = {y: 0.0 for y in YEARS}
for m in PS_MILESTONES:
ps[m["date"].year] += m["amount"]
return {y: ps[y] + (training if y == 2026 else 0.0) for y in YEARS}
return {2026: TCO_VERBATIM["prof_services_y1"] + training,
2027: 0.0, 2028: 0.0}
def ps_milestones_dataframe() -> pd.DataFrame:
"""The SOW billing milestones as a display table."""
return pd.DataFrame(PS_MILESTONES)
def managed_services_by_year(
annual: float = MANAGED_SERVICES_ANNUAL, start: dt.date = MCX_GO_LIVE
) -> dict[int, float]:
"""Managed services bill from the month after go-live (Sep 30 → Oct),
then run at the full annual rate — an ongoing cost with no end date
inside the model window."""
def _months(y: int) -> int:
if y < start.year:
return 0
return 12 if y > start.year else 12 - start.month
return {y: annual * _months(y) / 12 for y in YEARS}
# ── Token consumption (missing cost #1) ──────────────────────────────
@@ -293,7 +343,6 @@ def claim_scenario(email_auto_respond_rate: float = 0.255) -> Scenario:
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},
)

View File

@@ -126,11 +126,12 @@ def calculate_email_ai_benefit(
params: str = "realistic",
rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
"""Email Auto-Respond (full displacement at the respond rate) plus
Auto-Suggest (time saving × acceptance on the remainder)."""
"""Email Auto-Respond full displacement at the respond rate.
(Email Auto-Suggest is not a separate benefit line: it is included
in Agent Copilot, whose per-user meter carries the drafting help.)"""
sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
ro = rollout or NO_ROLLOUT
suggest_saving = _param("email_auto_suggest_time_saving", params)
realization = sc.realization(year)
rows = []
for s in sites:
@@ -145,11 +146,6 @@ def calculate_email_ai_benefit(
respond_seconds = (
annual_emails * respond_rate * s.email_aht_seconds * realization
)
suggest_seconds = (
annual_emails * (1 - respond_rate)
* sc.email_auto_suggest_acceptance * s.email_aht_seconds
* suggest_saving * realization
)
rate = s.agent_cost_per_second
rows.append(
{
@@ -160,15 +156,6 @@ def calculate_email_ai_benefit(
"confidence": Confidence.UNKNOWN.value, # meter rate unsourced
}
)
rows.append(
{
"benefit_line": "Email Auto-Suggest (drafting time)",
"scope": s.site_name,
"annual_value": suggest_seconds * rate
* ro.fraction_live(s.site_name, year),
"confidence": Confidence.UNKNOWN.value,
}
)
return _df(rows)

View File

@@ -86,8 +86,7 @@ def calculate_per_user_ai_cost(
use_contracted: bool = False,
rollout: RolloutPlan | None = None,
) -> pd.DataFrame:
"""Per-user-per-month AI features (STA, Agent Copilot, AI Translate,
Email Auto-Suggest).
"""Per-user-per-month AI features (STA, Agent Copilot, AI Translate).
No adoption ramp and no rounding (users × tokens/user/month is
exact) — but token usage only starts at site go-live, so the year

View File

@@ -210,15 +210,9 @@ DEFAULT_METERS: dict[str, TokenMeter] = {
),
source_url=_GENESYS_TOKEN_METERS,
),
# ── Email AI (rates not yet published) ────────────────────────
TokenMeter(
feature="Email AI (Auto-Suggest)",
meter_type=MeterType.PER_USER_PER_MONTH,
units_per_token=0.0,
tokens_per_unit=0.0,
confidence=Confidence.UNKNOWN,
notes="Requires Agent Copilot. Token rate not yet published.",
),
# ── Email AI (rate not yet published) ────────────────────────
# Email Auto-Suggest is included in Agent Copilot's per-user
# meter (no standalone SKU) — only Auto-Respond is listed here.
TokenMeter(
feature="Email AI (Auto-Respond)",
meter_type=MeterType.PER_MESSAGE,
@@ -423,7 +417,6 @@ CTM_DEFAULT_FEATURE_SCOPES: list[FeatureScope] = [
FeatureScope("AI Summary & Insights", ALL_SITE_NAMES, phase=1,
adoption_curve=_RAMP),
FeatureScope("Direct Messaging", ALL_SITE_NAMES, phase=1, adoption_curve=_RAMP),
FeatureScope("Email AI (Auto-Suggest)", ["NAM", "EMEA"], phase=2),
FeatureScope("AI Translate",
["APAC HK", "APAC SG", "APAC SH", "APAC GZ", "APAC JP", "APAC TW"],
phase=3),

View File

@@ -57,13 +57,18 @@ def runrate_saving_annual(
licence_annual: float | None = None,
regions: list[str] | None = None,
baseline_annual: float | None = None,
managed_annual: float = 0.0,
) -> float:
"""Steady-state annual saving once term contracts end and the ramp
is over: (baseline licence run-rate) + scoped WFM annual values.
is over: (baseline licence run-rate managed services) + scoped
WFM annual values.
Defaults are the deck frame (deck licence rate, no managed
services); the contracted frame passes ``a4.MANAGED_SERVICES_ANNUAL``.
"""
lic = a4.TCO_VERBATIM["ccaas_annual"] if licence_annual is None else licence_annual
base = a4.TCO_VERBATIM["current_annual"] if baseline_annual is None else baseline_annual
return (base - lic) + wfm_annual_runrate(regions)
return (base - lic - managed_annual) + wfm_annual_runrate(regions)
def runrate_breakeven_label(

View File

@@ -25,7 +25,6 @@ class Scenario:
voice_summarization_eligibility: float
voice_knowledge_eligibility: float
email_auto_respond_rate: float # share of email auto-responded
email_auto_suggest_acceptance: float
# ── Virtual Agent benefit realization factors ───────────────────
# Applied to both Voice Bot and Agentic VA deflection benefits.
@@ -75,7 +74,6 @@ BENEFIT_PARAMS: dict[str, dict[str, float]] = {
"digital_aht_reduction": {"claim": 0.18, "realistic": 0.085}, # 5-12% Y1
"digital_acw_reduction": {"claim": 1.00, "realistic": 0.40}, # 30-50% Y1
"sta_aht_reduction": {"claim": 0.04, "realistic": 0.015}, # 1-2% Y1
"email_auto_suggest_time_saving": {"claim": 0.40, "realistic": 0.30}, # × acceptance; Genesys claims 40%
# ESTIMATED lines (no Genesys claim published):
"supervisor_copilot_time_saving": {"claim": 0.10, "realistic": 0.05},
"predictive_routing_aht_reduction": {"claim": 0.04, "realistic": 0.02},
@@ -97,7 +95,6 @@ SCENARIOS: dict[str, Scenario] = {
voice_summarization_eligibility=0.50,
voice_knowledge_eligibility=0.40,
email_auto_respond_rate=0.10,
email_auto_suggest_acceptance=0.25,
# VA realization: conservative — low completion, limited staffing flex
# Combined: 0.60 × 0.70 × (1 0.05) ≈ 0.40
va_completion_rate=0.60,
@@ -113,7 +110,6 @@ SCENARIOS: dict[str, Scenario] = {
voice_summarization_eligibility=0.70,
voice_knowledge_eligibility=0.60,
email_auto_respond_rate=0.20,
email_auto_suggest_acceptance=0.40,
# VA realization: production midpoints per spec analysis
# Combined: 0.70 × 0.80 × (1 0.05) ≈ 0.53
va_completion_rate=0.70,
@@ -129,7 +125,6 @@ SCENARIOS: dict[str, Scenario] = {
voice_summarization_eligibility=0.90,
voice_knowledge_eligibility=0.80,
email_auto_respond_rate=0.50,
email_auto_suggest_acceptance=0.60,
# VA realization: optimistic — high completion, good staffing flexibility
# Combined: 0.75 × 0.85 × (1 0.03) ≈ 0.62
va_completion_rate=0.75,