CTM Calculators: Add PS billing milestones, Adjust Genesys RAMP
This commit is contained in:
@@ -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},
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user