""" 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 )