CTM Calculators: Add PS billing milestones, Adjust Genesys RAMP
This commit is contained in:
+1578
-1389
File diff suppressed because one or more lines are too long
+1198
-1005
File diff suppressed because one or more lines are too long
+1357
-259
File diff suppressed because one or more lines are too long
@@ -41,6 +41,10 @@ def test_ramp_zeroes_year_one_licences():
|
|||||||
2028: 4_300_000.0}
|
2028: 4_300_000.0}
|
||||||
assert a4.licence_costs_by_year(0)[2026] == 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)
|
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():
|
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_VERBATIM["ccaas_annual"] == 4_300_000 # deck record intact
|
||||||
assert a4.tco("current_annual") == a4.TCO_VERBATIM["current_annual"]
|
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
|
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)
|
||||||
|
|||||||
@@ -47,11 +47,9 @@ def test_email_benefit_split():
|
|||||||
[site], FeatureScope("Email AI (Auto-Respond)", ["Small"]),
|
[site], FeatureScope("Email AI (Auto-Respond)", ["Small"]),
|
||||||
"realistic", year=1,
|
"realistic", year=1,
|
||||||
)
|
)
|
||||||
|
# Auto-Suggest is not a separate line — it lives inside Agent Copilot.
|
||||||
lines = set(df["benefit_line"])
|
lines = set(df["benefit_line"])
|
||||||
assert lines == {
|
assert lines == {"Email Auto-Respond (displaced handling)"}
|
||||||
"Email Auto-Respond (displaced handling)",
|
|
||||||
"Email Auto-Suggest (drafting time)",
|
|
||||||
}
|
|
||||||
# auto-respond: 1,000×12 × 20% × 600s × 50% × $0.01 = $7,200
|
# auto-respond: 1,000×12 × 20% × 600s × 50% × $0.01 = $7,200
|
||||||
respond = df[df["benefit_line"].str.contains("Respond")]["annual_value"].sum()
|
respond = df[df["benefit_line"].str.contains("Respond")]["annual_value"].sum()
|
||||||
assert respond == pytest.approx(7_200)
|
assert respond == pytest.approx(7_200)
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ def test_all_spec_meters_present():
|
|||||||
"AI Translate",
|
"AI Translate",
|
||||||
# Genesys Cloud Copilot
|
# Genesys Cloud Copilot
|
||||||
"Genesys Cloud Copilot",
|
"Genesys Cloud Copilot",
|
||||||
# Email AI (rates TBD)
|
# Email AI (rate TBD; Auto-Suggest is inside Agent Copilot)
|
||||||
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)",
|
"Email AI (Auto-Respond)",
|
||||||
}
|
}
|
||||||
assert expected == set(DEFAULT_METERS)
|
assert expected == set(DEFAULT_METERS)
|
||||||
|
|
||||||
@@ -59,9 +59,7 @@ def test_confirmed_rates():
|
|||||||
|
|
||||||
def test_unknown_meters_flagged():
|
def test_unknown_meters_flagged():
|
||||||
unknown = {f for f, m in DEFAULT_METERS.items() if m.confidence is Confidence.UNKNOWN}
|
unknown = {f for f, m in DEFAULT_METERS.items() if m.confidence is Confidence.UNKNOWN}
|
||||||
assert unknown == {
|
assert unknown == {"Email AI (Auto-Respond)"}
|
||||||
"Email AI (Auto-Suggest)", "Email AI (Auto-Respond)",
|
|
||||||
}
|
|
||||||
assert Confidence.UNKNOWN.icon == "🔴"
|
assert Confidence.UNKNOWN.icon == "🔴"
|
||||||
assert Confidence.CONFIRMED.icon == "🟢"
|
assert Confidence.CONFIRMED.icon == "🟢"
|
||||||
|
|
||||||
|
|||||||
@@ -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(regions=["NA"]) == pytest.approx(3_000_000)
|
||||||
assert mw.runrate_saving_annual(licence_annual=4_800_000,
|
assert mw.runrate_saving_annual(licence_annual=4_800_000,
|
||||||
regions=[]) == pytest.approx(2_500_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():
|
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)
|
cs = a4.current_state_inputs(SITES)
|
||||||
cur = a4.current_costs_by_year(cs)
|
cur = a4.current_costs_by_year(cs)
|
||||||
lic = a4.licence_costs_by_year()
|
lic = a4.licence_costs_by_year()
|
||||||
ps = a4.ps_costs_by_year()
|
ps = a4.ps_costs_by_year()
|
||||||
total = {y: cur[y] + lic[y] + ps[y] for y in a4.YEARS}
|
total = {y: cur[y] + lic[y] + ps[y] for y in a4.YEARS}
|
||||||
ben = mw.wfm_benefits_by_year(_default_benefit_rollout())
|
inc, net = a4.case_flows(total, _default_wfm_benefits_by_year())
|
||||||
ben_y = {y: float(ben.loc[ben.year == y, "benefit"].sum()) for y in a4.YEARS}
|
assert sum(net.values()) == pytest.approx(-3_703_000, abs=1_000)
|
||||||
inc, net = a4.case_flows(total, ben_y)
|
|
||||||
assert sum(net.values()) == pytest.approx(-1_553_000, abs=1_000)
|
|
||||||
label = mw.runrate_breakeven_label(net, mw.runrate_saving_annual())
|
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():
|
def test_breakeven_defers_in_window_and_guards_zero_runrate():
|
||||||
|
|||||||
@@ -107,6 +107,26 @@ TCO_CONTRACTED: dict[str, float] = {
|
|||||||
"ccaas_annual": 3_200_000, # signed licence run-rate (deck pitched $4.3M/yr)
|
"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:
|
def tco(key: str) -> float:
|
||||||
"""Contracted value where one exists, else the deck's verbatim anchor."""
|
"""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 Gantt exception: Email implemented Jan 2027, realizes Apr 2027.
|
||||||
NA_EMAIL_IMPL_MONTH = 13
|
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
|
DEFAULT_TERMINATION = dt.date(2027, 12, 31) # current-platform term contracts
|
||||||
|
|
||||||
# ── Region ⇄ site mapping ────────────────────────────────────────────
|
# ── Region ⇄ site mapping ────────────────────────────────────────────
|
||||||
@@ -275,12 +295,42 @@ def licence_costs_by_year(
|
|||||||
for y in YEARS}
|
for y in YEARS}
|
||||||
|
|
||||||
|
|
||||||
def ps_costs_by_year() -> dict[int, float]:
|
def ps_costs_by_year(contracted: bool = False) -> dict[int, float]:
|
||||||
"""Base professional services + training — verbatim, year 1 only."""
|
"""Base professional services + training.
|
||||||
return {2026: TCO_VERBATIM["prof_services_y1"] + TCO_VERBATIM["training_y1"],
|
|
||||||
|
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}
|
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) ──────────────────────────────
|
# ── 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_summarization_eligibility=0.0,
|
||||||
voice_knowledge_eligibility=0.0, # unused by the Appendix-4 scope set
|
voice_knowledge_eligibility=0.0, # unused by the Appendix-4 scope set
|
||||||
email_auto_respond_rate=email_auto_respond_rate,
|
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},
|
consumption_cost_realization={1: 1.0, 2: 1.0, 3: 1.0},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -126,11 +126,12 @@ def calculate_email_ai_benefit(
|
|||||||
params: str = "realistic",
|
params: str = "realistic",
|
||||||
rollout: RolloutPlan | None = None,
|
rollout: RolloutPlan | None = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
"""Email Auto-Respond (full displacement at the respond rate) plus
|
"""Email Auto-Respond — full displacement at the respond rate.
|
||||||
Auto-Suggest (time saving × acceptance on the remainder)."""
|
|
||||||
|
(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
|
sc = get_scenario(scenario) if isinstance(scenario, str) else scenario
|
||||||
ro = rollout or NO_ROLLOUT
|
ro = rollout or NO_ROLLOUT
|
||||||
suggest_saving = _param("email_auto_suggest_time_saving", params)
|
|
||||||
realization = sc.realization(year)
|
realization = sc.realization(year)
|
||||||
rows = []
|
rows = []
|
||||||
for s in sites:
|
for s in sites:
|
||||||
@@ -145,11 +146,6 @@ def calculate_email_ai_benefit(
|
|||||||
respond_seconds = (
|
respond_seconds = (
|
||||||
annual_emails * respond_rate * s.email_aht_seconds * realization
|
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
|
rate = s.agent_cost_per_second
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
@@ -160,15 +156,6 @@ def calculate_email_ai_benefit(
|
|||||||
"confidence": Confidence.UNKNOWN.value, # meter rate unsourced
|
"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)
|
return _df(rows)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ def calculate_per_user_ai_cost(
|
|||||||
use_contracted: bool = False,
|
use_contracted: bool = False,
|
||||||
rollout: RolloutPlan | None = None,
|
rollout: RolloutPlan | None = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
"""Per-user-per-month AI features (STA, Agent Copilot, AI Translate,
|
"""Per-user-per-month AI features (STA, Agent Copilot, AI Translate).
|
||||||
Email Auto-Suggest).
|
|
||||||
|
|
||||||
No adoption ramp and no rounding (users × tokens/user/month is
|
No adoption ramp and no rounding (users × tokens/user/month is
|
||||||
exact) — but token usage only starts at site go-live, so the year
|
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,
|
source_url=_GENESYS_TOKEN_METERS,
|
||||||
),
|
),
|
||||||
# ── Email AI (rates not yet published) ────────────────────────
|
# ── Email AI (rate not yet published) ─────────────────────────
|
||||||
TokenMeter(
|
# Email Auto-Suggest is included in Agent Copilot's per-user
|
||||||
feature="Email AI (Auto-Suggest)",
|
# meter (no standalone SKU) — only Auto-Respond is listed here.
|
||||||
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.",
|
|
||||||
),
|
|
||||||
TokenMeter(
|
TokenMeter(
|
||||||
feature="Email AI (Auto-Respond)",
|
feature="Email AI (Auto-Respond)",
|
||||||
meter_type=MeterType.PER_MESSAGE,
|
meter_type=MeterType.PER_MESSAGE,
|
||||||
@@ -423,7 +417,6 @@ CTM_DEFAULT_FEATURE_SCOPES: list[FeatureScope] = [
|
|||||||
FeatureScope("AI Summary & Insights", ALL_SITE_NAMES, phase=1,
|
FeatureScope("AI Summary & Insights", ALL_SITE_NAMES, phase=1,
|
||||||
adoption_curve=_RAMP),
|
adoption_curve=_RAMP),
|
||||||
FeatureScope("Direct Messaging", 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",
|
FeatureScope("AI Translate",
|
||||||
["APAC HK", "APAC SG", "APAC SH", "APAC GZ", "APAC JP", "APAC TW"],
|
["APAC HK", "APAC SG", "APAC SH", "APAC GZ", "APAC JP", "APAC TW"],
|
||||||
phase=3),
|
phase=3),
|
||||||
|
|||||||
@@ -57,13 +57,18 @@ def runrate_saving_annual(
|
|||||||
licence_annual: float | None = None,
|
licence_annual: float | None = None,
|
||||||
regions: list[str] | None = None,
|
regions: list[str] | None = None,
|
||||||
baseline_annual: float | None = None,
|
baseline_annual: float | None = None,
|
||||||
|
managed_annual: float = 0.0,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Steady-state annual saving once term contracts end and the ramp
|
"""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
|
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
|
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(
|
def runrate_breakeven_label(
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class Scenario:
|
|||||||
voice_summarization_eligibility: float
|
voice_summarization_eligibility: float
|
||||||
voice_knowledge_eligibility: float
|
voice_knowledge_eligibility: float
|
||||||
email_auto_respond_rate: float # share of email auto-responded
|
email_auto_respond_rate: float # share of email auto-responded
|
||||||
email_auto_suggest_acceptance: float
|
|
||||||
|
|
||||||
# ── Virtual Agent benefit realization factors ───────────────────
|
# ── Virtual Agent benefit realization factors ───────────────────
|
||||||
# Applied to both Voice Bot and Agentic VA deflection benefits.
|
# 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_aht_reduction": {"claim": 0.18, "realistic": 0.085}, # 5-12% Y1
|
||||||
"digital_acw_reduction": {"claim": 1.00, "realistic": 0.40}, # 30-50% 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
|
"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):
|
# ESTIMATED lines (no Genesys claim published):
|
||||||
"supervisor_copilot_time_saving": {"claim": 0.10, "realistic": 0.05},
|
"supervisor_copilot_time_saving": {"claim": 0.10, "realistic": 0.05},
|
||||||
"predictive_routing_aht_reduction": {"claim": 0.04, "realistic": 0.02},
|
"predictive_routing_aht_reduction": {"claim": 0.04, "realistic": 0.02},
|
||||||
@@ -97,7 +95,6 @@ SCENARIOS: dict[str, Scenario] = {
|
|||||||
voice_summarization_eligibility=0.50,
|
voice_summarization_eligibility=0.50,
|
||||||
voice_knowledge_eligibility=0.40,
|
voice_knowledge_eligibility=0.40,
|
||||||
email_auto_respond_rate=0.10,
|
email_auto_respond_rate=0.10,
|
||||||
email_auto_suggest_acceptance=0.25,
|
|
||||||
# VA realization: conservative — low completion, limited staffing flex
|
# VA realization: conservative — low completion, limited staffing flex
|
||||||
# Combined: 0.60 × 0.70 × (1 − 0.05) ≈ 0.40
|
# Combined: 0.60 × 0.70 × (1 − 0.05) ≈ 0.40
|
||||||
va_completion_rate=0.60,
|
va_completion_rate=0.60,
|
||||||
@@ -113,7 +110,6 @@ SCENARIOS: dict[str, Scenario] = {
|
|||||||
voice_summarization_eligibility=0.70,
|
voice_summarization_eligibility=0.70,
|
||||||
voice_knowledge_eligibility=0.60,
|
voice_knowledge_eligibility=0.60,
|
||||||
email_auto_respond_rate=0.20,
|
email_auto_respond_rate=0.20,
|
||||||
email_auto_suggest_acceptance=0.40,
|
|
||||||
# VA realization: production midpoints per spec analysis
|
# VA realization: production midpoints per spec analysis
|
||||||
# Combined: 0.70 × 0.80 × (1 − 0.05) ≈ 0.53
|
# Combined: 0.70 × 0.80 × (1 − 0.05) ≈ 0.53
|
||||||
va_completion_rate=0.70,
|
va_completion_rate=0.70,
|
||||||
@@ -129,7 +125,6 @@ SCENARIOS: dict[str, Scenario] = {
|
|||||||
voice_summarization_eligibility=0.90,
|
voice_summarization_eligibility=0.90,
|
||||||
voice_knowledge_eligibility=0.80,
|
voice_knowledge_eligibility=0.80,
|
||||||
email_auto_respond_rate=0.50,
|
email_auto_respond_rate=0.50,
|
||||||
email_auto_suggest_acceptance=0.60,
|
|
||||||
# VA realization: optimistic — high completion, good staffing flexibility
|
# VA realization: optimistic — high completion, good staffing flexibility
|
||||||
# Combined: 0.75 × 0.85 × (1 − 0.03) ≈ 0.62
|
# Combined: 0.75 × 0.85 × (1 − 0.03) ≈ 0.62
|
||||||
va_completion_rate=0.75,
|
va_completion_rate=0.75,
|
||||||
|
|||||||
Reference in New Issue
Block a user