94 lines
3.9 KiB
Python
94 lines
3.9 KiB
Python
"""Migration + WFM (no-AI) scenario — hand-check acceptance numbers."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from tokencalc import appendix4 as a4
|
||
from tokencalc import migration_wfm as mw
|
||
from tokencalc.defaults import CTM_DEFAULT_SITES
|
||
|
||
SITES = list(CTM_DEFAULT_SITES)
|
||
|
||
|
||
def _default_benefit_rollout():
|
||
_, _, benefit_rollout = a4.build_rollouts(SITES)
|
||
return benefit_rollout
|
||
|
||
|
||
def test_wfm_scope_and_verbatim_total():
|
||
ben = mw.wfm_benefits_by_year(_default_benefit_rollout())
|
||
assert set(ben["capability"]) == {"WFM"}
|
||
assert set(ben["region"]) == set(mw.DEFAULT_WFM_REGIONS)
|
||
assert "EMEA" not in set(ben["region"]), "EMEA WFM is out of scope"
|
||
# NA $0 (migration) + ANZ $1.4M + ASIA $914K — verbatim, exact.
|
||
assert ben["benefit"].sum() == pytest.approx(2_314_000)
|
||
|
||
|
||
def test_wfm_phasing_on_pm_schedule():
|
||
ben = mw.wfm_benefits_by_year(_default_benefit_rollout())
|
||
by_year = ben.groupby("year")["benefit"].sum()
|
||
assert by_year[2026] == 0.0
|
||
# PM timeline: ANZ realizes Sep 2027 → 4 of 16 live months in 2027
|
||
# (Sep-Dec); ASIA realizes Jan 2028 → all in 2028.
|
||
assert by_year[2027] == pytest.approx(1_400_000 * 4 / 16)
|
||
assert by_year[2028] == pytest.approx(2_314_000 - 1_400_000 * 4 / 16)
|
||
|
||
|
||
def test_runrate_saving_annual():
|
||
# (7.3M − 4.3M) licence + 1.3M ANZ + 1.6M ASIA + 0 NA = 5.9M.
|
||
assert mw.wfm_annual_runrate() == pytest.approx(2_900_000)
|
||
assert mw.runrate_saving_annual() == pytest.approx(5_900_000)
|
||
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}
|
||
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 == "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_468_770, 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():
|
||
positive = {2026: 1_000_000.0, 2027: 0.0, 2028: 0.0}
|
||
assert mw.runrate_breakeven_label(positive, 5_900_000) == \
|
||
a4.payback_label(positive)
|
||
negative = {2026: -1_000_000.0, 2027: 0.0, 2028: 0.0}
|
||
assert mw.runrate_breakeven_label(negative, 0.0) == \
|
||
"never at current run-rate"
|