""" Client overlay — Variant 4's personalization layer. The verbatim anchor is Forrester's *composite organization* (2,000 agents, 20M contacts, 30% growth). This module rescales that composite to a client's size: a 🟡 **first-order linear rescale**, answering "what does the composite look like at your scale?", not "what is your TEI?". Each verbatim row is tied to the driver that dominates its derivation in the PDF (see ``BENEFIT_DRIVERS``/``COST_DRIVERS``); rows scale linearly with their driver, project-based costs stay fixed. The client's growth rate re-bases the composite's Y1→Y3 trajectory (which embeds 30% YoY). ``overlay_rows(COMPOSITE)`` is the identity — it reproduces the verbatim numbers exactly, so headless widget defaults form the published-study reproduction the gate expects. The anchor is never mutated: every function deep-copies. """ from __future__ import annotations from copy import deepcopy from dataclasses import dataclass from .anchor import ASSUMPTIONS, BENEFITS_VERBATIM, COSTS_VERBATIM @dataclass(frozen=True) class ClientDrivers: """Client inputs; defaults are the Forrester composite (identity overlay).""" agents_fte: int = ASSUMPTIONS["agents_fte"] # 2,000 (+200 supervisors at 10:1) annual_contacts_y1: int = ASSUMPTIONS["annual_contacts_y1"] # 20M growth_rate: float = ASSUMPTIONS["growth_rate"] # 0.30 YoY discount_rate: float = ASSUMPTIONS["discount_rate"] # 0.10 COMPOSITE = ClientDrivers() #: 🟡 Which driver each verbatim row scales with, per its PDF derivation. BENEFIT_DRIVERS: dict[str, str] = { "ai_contact_resolution": "contacts", # AHT × volume → contact-driven "ai_content_sentiment": "contacts", # per-call summaries/QA → contact-driven "ai_forecasting_supervision": "agents", # FTE optimization + supervisor span "data_driven_profit_lift": "contacts", # 🔴 proxy — revenue-driven in the PDF; # outbound volume is the nearest linear driver "legacy_solution_savings": "agents", # $/agent-month licences (supervisors follow 10:1) } COST_DRIVERS: dict[str, str] = { "amazon_connect_usage": "contacts", # per-minute/per-message consumption "implementation_migration": "fixed", # project-based — does not scale "ongoing_management": "fixed", # small fixed team } def scale_factor(driver: str, d: ClientDrivers) -> float: """Linear size ratio vs the composite for one driver kind.""" if driver == "contacts": return d.annual_contacts_y1 / ASSUMPTIONS["annual_contacts_y1"] if driver == "agents": return d.agents_fte / ASSUMPTIONS["agents_fte"] if driver == "fixed": return 1.0 raise KeyError(f"Unknown driver: {driver!r}") def growth_multiplier(year_index: int, growth_rate: float) -> float: """ Re-base the composite's Y1→Y3 trajectory on the client's growth. The verbatim year values already embed the composite's 30% YoY growth; dividing it out and compounding the client's rate preserves the composite's *shape* while adopting the client's slope. Year 1 → 1.0. """ composite_g = ASSUMPTIONS["growth_rate"] return ((1.0 + growth_rate) / (1.0 + composite_g)) ** (year_index - 1) def overlay_rows(d: ClientDrivers = COMPOSITE) -> tuple[list[dict], list[dict]]: """ Deep-copied (benefits, costs) rows rescaled to the client's drivers. Non-fixed rows: ``year_values[n] ×= scale_factor × growth_multiplier(n)``. Fixed rows keep their year values and ``initial`` unchanged (no growth re-base either — they are project/team costs, not volume costs). Risk factors, labels, and notes are untouched. """ def _apply(rows: list[dict], drivers: dict[str, str]) -> list[dict]: out = [] for raw in rows: row = deepcopy(raw) driver = drivers[row["field_key"]] if driver != "fixed": s = scale_factor(driver, d) row["year_values"] = { k: float(v) * s * growth_multiplier(int(k), d.growth_rate) for k, v in row["year_values"].items() } if row.get("initial"): row["initial"] = float(row["initial"]) * s out.append(row) return out return (_apply(BENEFITS_VERBATIM, BENEFIT_DRIVERS), _apply(COSTS_VERBATIM, COST_DRIVERS))