141 lines
6.9 KiB
Python
141 lines
6.9 KiB
Python
"""Appendix-4 corrected business case — hand-check acceptance numbers."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import datetime as dt
|
||
import math
|
||
|
||
import pytest
|
||
|
||
from tokencalc import appendix4 as a4
|
||
from tokencalc.defaults import CTM_DEFAULT_SITES, DEFAULT_METERS, DEFAULT_PRICING
|
||
|
||
SITES = list(CTM_DEFAULT_SITES)
|
||
|
||
|
||
def test_verbatim_crossfoots_to_slide_totals():
|
||
df = a4.verbatim_dataframe()
|
||
for r, expect in a4.SLIDE_TOTALS["regional_3yr"].items():
|
||
got = df.loc[df.region == r, "three_yr"].sum()
|
||
assert abs(got - expect) <= a4.crossfoot_tolerance(expect), r
|
||
for c, expect in a4.SLIDE_TOTALS["capability_3yr"].items():
|
||
got = df.loc[df.capability == c, "three_yr"].sum()
|
||
assert abs(got - expect) <= a4.crossfoot_tolerance(expect), c
|
||
assert abs(df["three_yr"].sum() - a4.SLIDE_TOTALS["total_3yr"]) <= \
|
||
a4.crossfoot_tolerance(a4.SLIDE_TOTALS["total_3yr"])
|
||
|
||
|
||
def test_benefits_phase_on_the_pm_schedule():
|
||
_, _, benefit_rollout = a4.build_rollouts(SITES)
|
||
long = a4.benefits_by_year(benefit_rollout)
|
||
by_year = long.groupby("year")["benefit"].sum()
|
||
# Earliest region (NA) realizes Jan 2027 → 2026 still $0.
|
||
assert by_year[2026] == 0.0, "2026 must be $0 (NA realizes Jan 2027)"
|
||
# Scaling at the finest grain reproduces every verbatim 3-yr value exactly.
|
||
for (region, cap), (_, three_yr) in a4.VERBATIM_BENEFITS.items():
|
||
got = long.query("region == @region and capability == @cap")["benefit"].sum()
|
||
assert got == pytest.approx(three_yr)
|
||
|
||
|
||
def test_ramp_zeroes_year_one_licences():
|
||
assert a4.licence_costs_by_year(12) == {2026: 0.0, 2027: 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(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():
|
||
cs = a4.current_state_inputs(SITES)
|
||
assert cs["annual_cost"].sum() == pytest.approx(7_300_000)
|
||
by_year = a4.current_costs_by_year(cs)
|
||
assert by_year == {2026: pytest.approx(7_300_000),
|
||
2027: pytest.approx(7_300_000), 2028: 0.0}
|
||
cs.loc["NA", "contract_termination"] = dt.date(2028, 6, 30)
|
||
assert a4.current_costs_by_year(cs)[2028] == pytest.approx(
|
||
cs.loc["NA", "annual_cost"] * 6 / 12)
|
||
|
||
|
||
def test_token_hand_checks():
|
||
token_ro, email_ro, _ = a4.build_rollouts(SITES)
|
||
core, email = a4.build_scopes(SITES, copilot_includes_asia=False)
|
||
meters = {**DEFAULT_METERS, "Email AI (Auto-Respond)": a4.autorespond_meter(0.05)}
|
||
long = a4.token_costs_by_year(SITES, meters, DEFAULT_PRICING,
|
||
a4.claim_scenario(0.255), core, email,
|
||
token_ro, email_ro)
|
||
# STA 2028: every site live all 12 months under the compressed PM
|
||
# timeline (APAC live from Nov 2027) → full-year STA for all sites.
|
||
sta = long.query("cost_line == 'Speech & Text Analytics [named]'")
|
||
assert sta.query("year == 2028")["annual_cost"].sum() == pytest.approx(751_680)
|
||
# Agent Copilot 2028 (ASIA off): 1,490 users × 40 tokens × 12 months.
|
||
cp = long.query("cost_line == 'Agent Copilot [named]' and year == 2028")
|
||
assert cp["annual_cost"].sum() == pytest.approx(1_490 * 40 * 12)
|
||
# Rule 1: Copilot covers AI Summary at Copilot sites.
|
||
assert (long.query("cost_line == 'AI Summary & Insights'")["annual_cost"] == 0).all()
|
||
# Under the PM timeline NA goes live Oct 2026, so tokens DO accrue in
|
||
# 2026 (NA only — three live months); no longer a $0 year.
|
||
assert long.query("year == 2026")["annual_cost"].sum() == pytest.approx(424_350)
|
||
# PR NAM steady-month tokens.
|
||
assert math.ceil(
|
||
1_214_358 * DEFAULT_METERS["Predictive Routing"].tokens_per_unit) == 71_433
|
||
|
||
|
||
def test_impl_costs_reconcile_with_v2_doc():
|
||
_, impl_y, kb_y, steady_y = a4.build_impl_costs(SITES, "mid", 225.0,
|
||
include_kb=True)
|
||
assert sum(impl_y.values()) == pytest.approx(5_850 * 225) # V2's "$1.3M"
|
||
assert sum(kb_y.values()) == pytest.approx(1_000 * 225)
|
||
assert steady_y == {2026: 0.0, 2027: pytest.approx(700 * 225),
|
||
2028: pytest.approx(700 * 225)}
|
||
# Impl spend is fully booked by each region's implementation month.
|
||
assert a4.impl_year_fractions(18) == pytest.approx([12 / 18, 6 / 18, 0.0])
|
||
assert a4.impl_year_fractions(27) == pytest.approx([12 / 27, 12 / 27, 3 / 27])
|
||
|
||
|
||
def test_case_flows_and_kpis():
|
||
benefits = {2026: 0.0, 2027: 2_000_000.0, 2028: 12_000_000.0}
|
||
costs = {2026: 10_000_000.0, 2027: 13_000_000.0, 2028: 8_000_000.0}
|
||
inc, net = a4.case_flows(costs, benefits)
|
||
assert inc == {2026: pytest.approx(2_700_000),
|
||
2027: pytest.approx(5_700_000),
|
||
2028: pytest.approx(700_000)}
|
||
for y in a4.YEARS:
|
||
assert net[y] == pytest.approx(benefits[y] - inc[y])
|
||
kpis = a4.case_kpis(inc, net)
|
||
assert kpis["benefits_3yr"] == pytest.approx(sum(benefits.values()))
|
||
assert kpis["net_3yr"] == pytest.approx(sum(net.values()))
|
||
assert kpis["roi"] == pytest.approx(kpis["net_3yr"] / kpis["incremental_cost_3yr"])
|
||
assert kpis["discount_rate"] == 0.135
|
||
# Net cost saving → ROI undefined.
|
||
inc2 = {y: -1.0 for y in a4.YEARS}
|
||
net2 = {y: benefits[y] + 1.0 for y in a4.YEARS}
|
||
assert a4.case_kpis(inc2, net2)["roi"] is None
|
||
|
||
|
||
def test_contracted_overlays_verbatim():
|
||
assert a4.tco("ccaas_annual") == 3_200_000 # signed contract
|
||
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 (Oct 16 → Nov).
|
||
ms = a4.managed_services_by_year()
|
||
assert ms[2026] == pytest.approx(410_918.40 * 2 / 12)
|
||
assert ms[2027] == pytest.approx(410_918.40)
|
||
assert ms[2028] == pytest.approx(410_918.40)
|