Files

141 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)