- Add 202512_GenesysCX TEI study (config, seed data, notebooks, README) with NPV $10.8M / ROI 266% including AI-token cost line - Add explicit `key` parameter to all chart wrappers in app/components to prevent StreamlitDuplicateElementId errors when the same figure type renders across Summary/Benefits/Costs tabs - Render benefits bar and cost pie charts on their respective tabs - Add benefits_vs_costs_by_year chart wrapper
82 lines
3.5 KiB
Python
82 lines
3.5 KiB
Python
"""
|
||
Implementation rollout & ramp model.
|
||
|
||
Captures the gap between **when Genesys starts billing** (contract
|
||
start) and **when each region actually goes live**:
|
||
|
||
- The platform licence commit bills in full from contract start; the
|
||
vendor's *ramp period* compensates with a first-year credit
|
||
(typical: 6-month ramp → 50% Y1 discount on the platform commit).
|
||
- AI token usage (per-user and consumption meters) starts only when a
|
||
site goes live, and bills for the months the site is live in each
|
||
model year.
|
||
- Benefits likewise accrue only from go-live (the scenario realization
|
||
curve then models adoption maturity *within* the live period).
|
||
|
||
A site with ``go_live_month = m`` is live for ``12*year − m`` months of
|
||
the first ``year`` years (clamped to 0..12 per year). So NAM at month 6
|
||
is live 6 months of Y1; EMEA at month 9 → 3 months; AUZ/APAC at month
|
||
12 → 0 months in Y1 and fully live from Y2.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass, field
|
||
|
||
MONTHS_PER_YEAR = 12
|
||
|
||
|
||
@dataclass
|
||
class RolloutPlan:
|
||
#: ISO date Genesys starts billing the licence commit (informational,
|
||
#: surfaced in UI/exports; the model works in months-from-start).
|
||
contract_start: str | None = None
|
||
|
||
#: Total build duration, months (informational).
|
||
build_months: int = 12
|
||
|
||
#: Vendor ramp period, months. Documentation for the Y1 credit below.
|
||
ramp_months: int = 6
|
||
|
||
#: First-year credit on the platform licence commit. Typical
|
||
#: 6-month ramp = 50% discount in year 1; years 2+ bill in full.
|
||
first_year_platform_discount: float = 0.5
|
||
|
||
#: site_name -> go-live month (months after contract start).
|
||
#: Sites absent from the map are treated as live from day 0.
|
||
go_live_month: dict[str, int] = field(default_factory=dict)
|
||
|
||
def __post_init__(self) -> None:
|
||
if not 0.0 <= self.first_year_platform_discount <= 1.0:
|
||
raise ValueError("first_year_platform_discount must be within 0-1")
|
||
if self.ramp_months < 0 or self.build_months < 0:
|
||
raise ValueError("ramp_months/build_months must be >= 0")
|
||
for site, m in self.go_live_month.items():
|
||
if m < 0:
|
||
raise ValueError(f"{site}: go_live_month must be >= 0")
|
||
|
||
# ── Availability ────────────────────────────────────────────────
|
||
|
||
def live_months_in_year(self, site_name: str, year: int) -> int:
|
||
"""Months ``site_name`` is live during model year ``year`` (1-based)."""
|
||
go_live = self.go_live_month.get(site_name, 0)
|
||
live_by_year_end = max(0, MONTHS_PER_YEAR * year - go_live)
|
||
live_by_prev_year_end = max(0, MONTHS_PER_YEAR * (year - 1) - go_live)
|
||
return min(MONTHS_PER_YEAR, live_by_year_end - live_by_prev_year_end)
|
||
|
||
def fraction_live(self, site_name: str, year: int) -> float:
|
||
return self.live_months_in_year(site_name, year) / MONTHS_PER_YEAR
|
||
|
||
# ── Billing ─────────────────────────────────────────────────────
|
||
|
||
def platform_factor(self, year: int) -> float:
|
||
"""Fraction of the full platform commit billed in ``year``."""
|
||
return 1.0 - self.first_year_platform_discount if year == 1 else 1.0
|
||
|
||
|
||
#: Behaviour identical to the pre-rollout model: everything live from
|
||
#: day 0, no ramp credit.
|
||
NO_ROLLOUT = RolloutPlan(
|
||
build_months=0, ramp_months=0, first_year_platform_discount=0.0
|
||
)
|