84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
"""
|
||
Migration + WFM scenario — the no-AI business case.
|
||
|
||
Current state → Genesys Cloud CX migration, NA users migrated onto
|
||
Genesys WFM, WFM implemented for APAC (ANZ + ASIA). Migration and WFM
|
||
are included in the base implementation price (the verbatim PS +
|
||
training), so the only cost lines are the existing-platform run-off,
|
||
the licence ramp, and base PS — no token consumption, no AI
|
||
implementation labour. The only benefits kept are the deck's verbatim
|
||
WFM lines for the regions in WFM scope.
|
||
|
||
Single source of truth behind ``notebooks/ctm_migration_wfm.ipynb``
|
||
(served with Mercury) — the presentation layer holds no math. All
|
||
primitives come from :mod:`tokencalc.appendix4`; this module only
|
||
scopes and extrapolates.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import math
|
||
|
||
import pandas as pd
|
||
|
||
from . import appendix4 as a4
|
||
|
||
#: WFM scope in this scenario. NA is a migration off its existing
|
||
#: WFM-like tool (verbatim benefit $0 — "has similar feature"); ANZ and
|
||
#: ASIA are new implementations ("APAC"); EMEA is out of scope, so the
|
||
#: deck's EMEA WFM benefit line is dropped.
|
||
DEFAULT_WFM_REGIONS = ["NA", "ANZ", "ASIA"]
|
||
|
||
|
||
def wfm_benefits_by_year(
|
||
benefit_rollout, regions: list[str] | None = None
|
||
) -> pd.DataFrame:
|
||
"""Verbatim WFM benefits for the scoped regions, phased on the
|
||
deck's deployment schedule (realize = impl + 3 months, inclusive).
|
||
|
||
Long DataFrame: region, capability, year, benefit — the WFM slice
|
||
of :func:`tokencalc.appendix4.benefits_by_year`.
|
||
"""
|
||
scope = DEFAULT_WFM_REGIONS if regions is None else regions
|
||
df = a4.benefits_by_year(benefit_rollout)
|
||
return df[(df["capability"] == "WFM")
|
||
& (df["region"].isin(scope))].reset_index(drop=True)
|
||
|
||
|
||
def wfm_annual_runrate(regions: list[str] | None = None) -> float:
|
||
"""Sum of the verbatim WFM *annual* values for the scoped regions."""
|
||
scope = DEFAULT_WFM_REGIONS if regions is None else regions
|
||
return float(sum(annual for (r, c), (annual, _t) in
|
||
a4.VERBATIM_BENEFITS.items()
|
||
if c == "WFM" and r in scope))
|
||
|
||
|
||
def runrate_saving_annual(
|
||
licence_annual: float | None = None,
|
||
regions: list[str] | None = None,
|
||
baseline_annual: float | None = None,
|
||
) -> float:
|
||
"""Steady-state annual saving once term contracts end and the ramp
|
||
is over: (baseline − licence run-rate) + scoped WFM annual values.
|
||
"""
|
||
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)
|
||
|
||
|
||
def runrate_breakeven_label(
|
||
net_by_year: dict[int, float], runrate_annual: float
|
||
) -> str:
|
||
"""Payback label, extrapolated past the model window at a run-rate.
|
||
|
||
Inside 2026-28 this defers to :func:`tokencalc.appendix4.payback_label`;
|
||
a deficit at end-2028 fills at ``runrate_annual`` per year.
|
||
"""
|
||
deficit = -sum(net_by_year[y] for y in a4.YEARS)
|
||
if deficit <= 0:
|
||
return a4.payback_label(net_by_year)
|
||
if runrate_annual <= 0:
|
||
return "never at current run-rate"
|
||
m = 12 * len(a4.YEARS) + math.ceil(12 * deficit / runrate_annual)
|
||
return f"{m} months (~{a4.month_label(m)}, extrapolated)"
|