Mercury Genesys Token Calculator

This commit is contained in:
2026-08-07 14:49:39 -04:00
parent 22a5d907d6
commit 89b835d681
35 changed files with 14105 additions and 2912 deletions

View File

@@ -0,0 +1,70 @@
"""Test plumbing: import path + notebook content served from tagged cells.
Content and client data live in the notebook, never in ``.py`` — the cells
tagged ``topic-bank`` (the feature catalogue) and ``engagement-data`` (the
client's volumes and commercial terms) in
``notebooks/genesys_token_calculator.ipynb``. The fixtures below read those
cells with nbformat and exec them, so pytest pins the exact content the
deliverable ships (no kernel needed — tagged cells are self-contained by
contract).
The published Genesys rate card is deliberately NOT here: it is a vendor
record nobody in this repo authors, so it lives in ``genesyscalc/ratecard.py``
as an immutable anchor and is pinned by ``test_rate_card.py`` (see
docs/Calculator_Pattern_V1-00.md).
The sys.path insert makes genesyscalc importable even without the master
venv active (the normal setup is ``pip install -e ".[dev]"`` into the
master-local ``.venv/``).
"""
from __future__ import annotations
import pathlib
import sys
from typing import Any
import pytest
MASTER_ROOT = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(MASTER_ROOT))
NOTEBOOK = MASTER_ROOT / "notebooks" / "genesys_token_calculator.ipynb"
def tagged_cell_ns(tag: str) -> dict[str, Any]:
"""Exec the single cell carrying ``tag`` and return its namespace."""
import nbformat
nb = nbformat.read(NOTEBOOK, as_version=4)
cells = [c for c in nb.cells if tag in c.metadata.get("tags", [])]
assert len(cells) == 1, (
f"expected exactly one cell tagged {tag!r} in {NOTEBOOK.name}, "
f"found {len(cells)}"
)
ns: dict[str, Any] = {}
exec(compile(cells[0].source, f"{NOTEBOOK.name} [{tag}]", "exec"), ns)
return ns
@pytest.fixture(scope="session")
def catalogue_ns() -> dict[str, Any]:
"""The executed namespace of the notebook's topic-bank cell."""
return tagged_cell_ns("topic-bank")
@pytest.fixture(scope="session")
def features(catalogue_ns: dict[str, Any]) -> tuple[Any, ...]:
"""The FEATURES tuple as the deliverable defines it."""
return catalogue_ns["FEATURES"] # type: ignore[no-any-return]
@pytest.fixture(scope="session")
def feature_by_key(features: tuple[Any, ...]) -> dict[str, Any]:
return {f.key: f for f in features}
@pytest.fixture(scope="session")
def engagement() -> dict[str, Any]:
"""The ENGAGEMENT dict as the deliverable's engagement-data cell ships it."""
return tagged_cell_ns("engagement-data")["ENGAGEMENT"] # type: ignore[no-any-return]

View File

@@ -0,0 +1,154 @@
"""Tokens → dollars: the allowance, the rounding, and the commercial levers.
Hand-checked first, then pinned.
"""
from __future__ import annotations
import pytest
from genesyscalc.billing import (
MONTHS_PER_YEAR,
allowance_for,
billable_tokens_monthly,
cost_lines,
total_cost,
)
from genesyscalc.meters import LicenceModel, TokenPrice
NAMED = LicenceModel.NAMED
CONCURRENT = LicenceModel.CONCURRENT
# ── the free monthly allowance ───────────────────────────────────────────
def test_free_allowance_deducted_named() -> None:
"""1,000 consumption tokens, named org: 1,000 250 = 750 billable."""
assert billable_tokens_monthly(1_000, 0, NAMED) == 750
def test_free_allowance_deducted_concurrent() -> None:
"""Same consumption, concurrent org: 1,000 350 = 650."""
assert billable_tokens_monthly(1_000, 0, CONCURRENT) == 650
def test_allowance_floors_at_zero() -> None:
assert billable_tokens_monthly(120, 0, NAMED) == 0
def test_allowance_does_not_carry_over() -> None:
"""Two quiet months do not bank credit for a busy third.
120 → 0 billable. 120 → 0 billable. 400 → 150 billable, NOT 0: the
unused 130 + 130 is discarded, because the allowance "renew[s] each
month and do[es] not carry over to future months".
"""
assert billable_tokens_monthly(120, 0, NAMED) == 0
assert billable_tokens_monthly(120, 0, NAMED) == 0
assert billable_tokens_monthly(400, 0, NAMED) == 150
def test_allowance_can_be_switched_off() -> None:
"""So the gap is measurable on stage rather than merely asserted."""
assert billable_tokens_monthly(1_000, 0, NAMED, apply_allowance=False) == 1_000
assert allowance_for(NAMED, apply_allowance=False) == 0
assert allowance_for(NAMED) == 250
def test_allowance_applies_across_consumption_and_subscription() -> None:
"""It is an org-level deduction, not a per-line one.
44.12 consumption → ceil 45; + 20,000 subscription = 20,045; 250 =
19,795.
"""
assert billable_tokens_monthly(44.117647, 20_000, NAMED) == 19_795
# ── rounding ─────────────────────────────────────────────────────────────
def test_consumption_tokens_rounded_up_monthly() -> None:
"""44.117647 tokens of bot time bills as 45."""
assert billable_tokens_monthly(44.117647, 0, NAMED, apply_allowance=False) == 45
def test_per_user_tokens_are_exact_not_rounded() -> None:
"""500 named users × 40 = 20,000 exactly — no ceil on subscription."""
assert billable_tokens_monthly(0, 20_000, NAMED, apply_allowance=False) == 20_000
def test_negative_tokens_rejected() -> None:
with pytest.raises(ValueError, match="must not be negative"):
billable_tokens_monthly(-1, 0, NAMED)
# ── price, concession, currency ──────────────────────────────────────────
def test_concession_applies_to_msrp() -> None:
"""$1.00 list, 30% concession → $0.70/token; 1,000 tokens → $700/mo."""
price = TokenPrice(list_rate=1.00, concession_pct=0.30)
assert price.effective_rate() == pytest.approx(0.70)
totals = total_cost(1_000, 0, NAMED, price, apply_allowance=False)
assert totals.token_cost_monthly == pytest.approx(700.0)
assert totals.token_cost_annual == pytest.approx(8_400.0)
def test_msrp_walk_is_carried_alongside() -> None:
"""The deck-frame column: list beside effective, and the saving."""
price = TokenPrice(list_rate=1.00, concession_pct=0.30)
totals = total_cost(1_000, 0, NAMED, price, apply_allowance=False)
assert totals.list_cost_annual == pytest.approx(12_000.0)
assert totals.concession_saving_annual == pytest.approx(3_600.0)
assert totals.token_cost_annual <= totals.list_cost_annual
def test_contracted_rate_overrides_concession() -> None:
price = TokenPrice(list_rate=1.00, contracted_rate=0.55, concession_pct=0.30)
assert price.effective_rate() == pytest.approx(0.55)
def test_currency_carried_through() -> None:
price = TokenPrice(currency="JPY", list_rate=120.0)
totals = total_cost(1_000, 0, NAMED, price, apply_allowance=False)
assert totals.currency == "JPY"
assert totals.token_cost_monthly == pytest.approx(120_000.0)
def test_invalid_concession_rejected() -> None:
with pytest.raises(ValueError, match="concession_pct"):
TokenPrice(concession_pct=1.0)
# ── cost lines ───────────────────────────────────────────────────────────
def test_cost_lines_price_each_meter_at_the_effective_rate() -> None:
price = TokenPrice(list_rate=1.00, concession_pct=0.25)
lines = cost_lines(
{"ai_scoring": 100.0, "virtual_agent": 7_500.0},
{"ai_scoring": 2_000.0, "virtual_agent": 15_000.0},
price,
)
by_key = {line.key: line for line in lines}
assert by_key["ai_scoring"].cost_monthly == pytest.approx(75.0)
assert by_key["virtual_agent"].cost_monthly == pytest.approx(5_625.0)
assert by_key["virtual_agent"].cost_annual == pytest.approx(
5_625.0 * MONTHS_PER_YEAR
)
assert by_key["ai_scoring"].units_monthly == pytest.approx(2_000.0)
assert by_key["ai_scoring"].unit_label == "evaluations"
def test_cost_lines_are_ordered_by_feature_name() -> None:
lines = cost_lines(
{"virtual_agent": 1.0, "ai_scoring": 1.0}, {}, TokenPrice()
)
assert [line.feature for line in lines] == ["AI Scoring", "Virtual Agent"]
def test_totals_expose_the_allowance_that_was_applied() -> None:
totals = total_cost(1_000, 20_000, NAMED, TokenPrice())
assert totals.allowance_tokens_monthly == 250
assert totals.gross_tokens_monthly == pytest.approx(21_000.0)
assert totals.billable_tokens_monthly == 20_750

View File

@@ -0,0 +1,94 @@
"""The feature catalogue — content pins, read from the notebook by tag.
The catalogue is the master's authored content: it lives in the notebook's
``topic-bank`` cell, and these tests exec that cell rather than importing a
module. The published RATES are not here — they are the vendor's record, in
``genesyscalc/ratecard.py``, pinned by ``test_rate_card.py``.
"""
from __future__ import annotations
from typing import Any
from genesyscalc.ratecard import METERS
EXPECTED_KEYS = (
"agent_copilot_named",
"speech_and_text_analytics_named",
"bots_voice",
"virtual_agent",
"agentic_virtual_agent",
"ai_summary_and_insights",
"ai_scoring",
"bots_digital",
"ai_translate",
"predictive_routing",
"whatsapp_messaging",
"genesys_cloud_copilot",
"predictive_engagement",
)
def test_catalogue_size(features: tuple[Any, ...]) -> None:
assert len(features) == 13
def test_catalogue_keys_and_order(features: tuple[Any, ...]) -> None:
"""Keys are stable identities — widgets, notes and the export key off them."""
assert tuple(f.key for f in features) == EXPECTED_KEYS
def test_keys_are_unique(features: tuple[Any, ...]) -> None:
assert len({f.key for f in features}) == len(features)
def test_every_catalogue_key_is_a_published_meter(features: tuple[Any, ...]) -> None:
"""The cross-layer tie: a rename here silently unprices a feature."""
for f in features:
assert f.key in METERS, f"{f.key} is not a published Genesys meter"
def test_every_feature_carries_its_content(features: tuple[Any, ...]) -> None:
for f in features:
assert f.title.strip(), f"{f.key} has no title"
assert f.description.strip(), f"{f.key} has no description"
assert f.ask.strip(), f"{f.key} has no sizing question"
assert f.ask.strip().endswith(("?", ")")), (
f"{f.key}: `ask` should read as a question to put to the client"
)
def test_titles_are_distinct(features: tuple[Any, ...]) -> None:
"""Titles become widget labels; duplicates collide in the sidebar."""
titles = [f.title for f in features]
assert len(set(titles)) == len(titles)
def test_default_scenario_is_a_coherent_starting_point(
features: tuple[Any, ...],
) -> None:
"""Headless nbconvert takes every widget at its seed, so the defaults
must form a scenario the gate can pass — and a plausible one to show."""
default_on = {f.key for f in features if f.default_on}
assert len(default_on) == 8
assert "agent_copilot_named" in default_on
assert "bots_voice" in default_on
# Copilot on by default means the summary line must be visible but free —
# that pairing is the published exclusion the calculator demonstrates.
assert "ai_summary_and_insights" in default_on
def test_catalogue_uses_a_schema_not_loose_dicts(features: tuple[Any, ...]) -> None:
"""Content is structured, so the notebook and tests agree on its shape."""
first = features[0]
assert type(first).__name__ == "Feature"
assert {"key", "title", "description", "ask", "default_on"} <= set(
type(first).__dataclass_fields__
)
def test_catalogue_cell_is_self_contained(catalogue_ns: dict[str, Any]) -> None:
"""It execs with no setup cell — the conftest fixture proves it, but pin
the contract explicitly so a stray dependency is caught here."""
assert "FEATURES" in catalogue_ns
assert "Feature" in catalogue_ns

View File

@@ -0,0 +1,115 @@
"""Engagement data — SHAPE pins only.
The master ships placeholders; an engagement copy fills them in. So these
tests pin the *shape* of the cell and never its emptiness — asserting that
``client`` is blank would fail the moment the copy is used for real, which
is precisely backwards.
"""
from __future__ import annotations
from typing import Any
import nbformat
from tests.conftest import NOTEBOOK
IDENTITY_KEYS = {"client", "prepared_date", "prepared_by"}
VOLUME_KEYS = {
"voice_inbound_monthly",
"voice_outbound_monthly",
"digital_sessions_monthly",
"email_monthly",
"messaging_monthly",
"social_posts_monthly",
"social_responses_monthly",
"translations_monthly",
"evaluations_monthly",
"ai_actions_monthly",
"routed_interactions_monthly",
}
COMMERCIAL_KEYS = {
"agent_users",
"licence_model",
"currency",
"contracted_rate_per_token",
"concession_pct",
}
TTS_KEYS = {"tts_chars_per_call", "tts_voice_tier"}
def test_engagement_shape(engagement: dict[str, Any]) -> None:
assert set(engagement) == (
IDENTITY_KEYS | VOLUME_KEYS | COMMERCIAL_KEYS | TTS_KEYS
)
def test_identity_fields_are_strings(engagement: dict[str, Any]) -> None:
"""Blank in the master, filled in the copy — either way, strings."""
for key in IDENTITY_KEYS:
assert isinstance(engagement[key], str)
def test_volumes_are_non_negative_numbers(engagement: dict[str, Any]) -> None:
for key in VOLUME_KEYS:
value = engagement[key]
assert isinstance(value, (int, float)) and not isinstance(value, bool)
assert value >= 0, f"{key} must not be negative"
def test_commercial_terms_are_well_formed(engagement: dict[str, Any]) -> None:
assert isinstance(engagement["agent_users"], int)
assert engagement["agent_users"] >= 0
assert engagement["licence_model"] in ("named", "concurrent")
assert isinstance(engagement["currency"], str) and engagement["currency"]
assert 0.0 <= float(engagement["concession_pct"]) < 1.0
contracted = engagement["contracted_rate_per_token"]
assert contracted is None or float(contracted) >= 0
def test_currency_is_one_the_rate_card_publishes(engagement: dict[str, Any]) -> None:
from genesyscalc.ratecard import LIST_PRICE_BY_CURRENCY
assert engagement["currency"] in LIST_PRICE_BY_CURRENCY
def test_tts_settings(engagement: dict[str, Any]) -> None:
from genesyscalc.tts import TTS_PRICE_PER_MILLION_CHARS
assert isinstance(engagement["tts_chars_per_call"], int)
assert engagement["tts_chars_per_call"] >= 0
assert engagement["tts_voice_tier"] in TTS_PRICE_PER_MILLION_CHARS
def test_master_carries_no_client_identity(engagement: dict[str, Any]) -> None:
"""The one emptiness check that IS correct: masters stay client-clean.
This guards the master in THIS repo. An engagement copy lives outside
Palladium, so it never runs this suite — filling the cell there does not
break anything (CLAUDE.md § Confidentiality).
"""
assert engagement["client"] == "", (
"a client name in the master means the copy-out step was skipped — "
"copy the directory out of Palladium before entering client data"
)
def test_engagement_cell_sits_above_the_widget_cell() -> None:
"""Client data must not re-run on a sidebar change (Mercury re-runs only
cells below a changed widget)."""
nb = nbformat.read(NOTEBOOK, as_version=4)
eng = [
i
for i, c in enumerate(nb.cells)
if "engagement-data" in c.metadata.get("tags", [])
]
widgets = [
i
for i, c in enumerate(nb.cells)
if c.cell_type == "code" and "mr.Select(" in c.source
]
assert len(eng) == 1 and widgets
assert eng[0] < min(widgets)

View File

@@ -0,0 +1,263 @@
"""End-to-end pricing, hand-checked.
The reference scenario below is the one the notebook ships as its default.
Every figure was computed by hand from the published rates before it was
pinned here.
"""
from __future__ import annotations
import json
import pytest
from genesyscalc.appendix import result_json
from genesyscalc.meters import LicenceModel, TokenPrice
from genesyscalc.model import (
CalculatorInputs,
calculate,
compare,
cost_per_interaction,
metered_units,
)
from genesyscalc.ratecard import RATE_CARD_SOURCE_DATE
from genesyscalc.tts import TtsUsage
from genesyscalc.usage import DeflectionMix, VoiceBotUsage, Volumes
REFERENCE_ENABLED = frozenset(
{
"agent_copilot_named",
"speech_and_text_analytics_named",
"bots_voice",
"virtual_agent",
"agentic_virtual_agent",
"ai_summary_and_insights",
"ai_scoring",
"whatsapp_messaging",
"predictive_engagement",
}
)
@pytest.fixture
def volumes() -> Volumes:
return Volumes(
voice_inbound_monthly=100_000,
voice_outbound_monthly=20_000,
digital_sessions_monthly=8_000,
email_monthly=12_000,
messaging_monthly=6_000,
evaluations_monthly=2_000,
)
@pytest.fixture
def reference(volumes: Volumes) -> CalculatorInputs:
return CalculatorInputs(
volumes=volumes,
users=500,
licence=LicenceModel.NAMED,
enabled=REFERENCE_ENABLED,
mix=DeflectionMix(
bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
),
price=TokenPrice(),
voice_bot=VoiceBotUsage(
calls_per_month=30_000, avg_bot_seconds_per_call=40
),
tts=TtsUsage(calls_monthly=100_000, chars_per_call=2_000),
)
# ── the hand-checked reference scenario ──────────────────────────────────
def test_subscription_lines(reference: CalculatorInputs) -> None:
"""500 users: Copilot 500×40 = 20,000; STA 500×30 = 15,000 tokens/mo."""
by_key = {line.key: line for line in calculate(reference).lines}
assert by_key["agent_copilot_named"].tokens_monthly == pytest.approx(20_000.0)
assert by_key["speech_and_text_analytics_named"].tokens_monthly == pytest.approx(
15_000.0
)
assert by_key["agent_copilot_named"].cost_annual == pytest.approx(240_000.0)
def test_voice_bot_line(reference: CalculatorInputs) -> None:
"""30,000 calls × ceil(40/15)×15s = 45s = 0.75 min → 22,500 min.
22,500 / 17 = 1,323.53 tokens/mo → $15,882.35/yr at $1.00."""
by_key = {line.key: line for line in calculate(reference).lines}
assert by_key["bots_voice"].units_monthly == pytest.approx(22_500.0)
assert by_key["bots_voice"].tokens_monthly == pytest.approx(1_323.529412, rel=1e-6)
assert by_key["bots_voice"].cost_annual == pytest.approx(15_882.35, abs=0.01)
def test_virtual_agent_lines_use_the_partition(reference: CalculatorInputs) -> None:
"""120,000 voice: VA 15% = 18,000 × 0.5 = 9,000 tokens;
AVA 5% = 6,000 × 1.2 = 7,200 tokens."""
by_key = {line.key: line for line in calculate(reference).lines}
assert by_key["virtual_agent"].units_monthly == pytest.approx(18_000.0)
assert by_key["virtual_agent"].tokens_monthly == pytest.approx(9_000.0)
assert by_key["agentic_virtual_agent"].units_monthly == pytest.approx(6_000.0)
assert by_key["agentic_virtual_agent"].tokens_monthly == pytest.approx(7_200.0)
def test_summary_is_zeroed_because_copilot_is_on(reference: CalculatorInputs) -> None:
result = calculate(reference)
by_key = {line.key: line for line in result.lines}
assert by_key["ai_summary_and_insights"].units_monthly == 0.0
assert by_key["ai_summary_and_insights"].cost_annual == 0.0
assert any("Agent Copilot is enabled" in w for w in result.warnings)
def test_zero_rated_line_is_shown_not_hidden(reference: CalculatorInputs) -> None:
"""A published $0 is a finding — it renders as a line, at zero."""
by_key = {line.key: line for line in calculate(reference).lines}
assert "predictive_engagement" in by_key
assert by_key["predictive_engagement"].units_monthly > 0
assert by_key["predictive_engagement"].cost_annual == 0.0
def test_reference_totals(reference: CalculatorInputs) -> None:
"""consumption 17,638.53 → ceil 17,639; + 35,000 subscription = 52,639;
250 allowance = 52,389 billable → $52,389/mo → $628,668/yr.
TTS 200M chars, 20% VA-free → 160M → $3,200/mo → $38,400/yr.
Grand total $667,068/yr."""
totals = calculate(reference).totals
assert totals.consumption_tokens_monthly == pytest.approx(17_638.529412, rel=1e-6)
assert totals.subscription_tokens_monthly == pytest.approx(35_000.0)
assert totals.gross_tokens_monthly == pytest.approx(52_639.0)
assert totals.allowance_tokens_monthly == 250
assert totals.billable_tokens_monthly == 52_389
assert totals.token_cost_annual == pytest.approx(628_668.0)
def test_reference_grand_total_splits_tokens_from_tts(
reference: CalculatorInputs,
) -> None:
result = calculate(reference)
assert result.tts_cost_annual == pytest.approx(38_400.0)
assert result.grand_total_annual == pytest.approx(667_068.0)
assert result.grand_total_annual == pytest.approx(
result.token_cost_annual + result.tts_cost_annual
)
def test_cost_per_interaction(reference: CalculatorInputs) -> None:
"""$667,068 / (146,000 × 12) = $0.3807 per interaction."""
result = calculate(reference)
assert cost_per_interaction(result, reference.volumes) == pytest.approx(
0.380747, rel=1e-5
)
def test_roundup_warning_is_reported(reference: CalculatorInputs) -> None:
assert any("15-second per-call round-up" in w for w in calculate(reference).warnings)
# ── structural ties that must hold at any setting ────────────────────────
def test_lines_sum_to_the_gross_token_count(reference: CalculatorInputs) -> None:
result = calculate(reference)
assert sum(line.tokens_monthly for line in result.lines) == pytest.approx(
result.totals.consumption_tokens_monthly
+ result.totals.subscription_tokens_monthly
)
def test_effective_cost_never_exceeds_list(reference: CalculatorInputs) -> None:
discounted = reference.with_(price=TokenPrice(concession_pct=0.4))
result = calculate(discounted)
assert result.totals.token_cost_annual <= result.totals.list_cost_annual
assert result.totals.concession_saving_annual >= 0
def test_allowance_only_ever_reduces_the_bill(reference: CalculatorInputs) -> None:
with_allowance = calculate(reference).totals.billable_tokens_monthly
without = calculate(
reference.with_(apply_allowance=False)
).totals.billable_tokens_monthly
assert with_allowance == without - 250
def test_allowance_absorbing_a_month_is_reported() -> None:
quiet = CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=100),
users=0,
enabled=frozenset({"ai_scoring"}),
)
result = calculate(quiet)
assert result.totals.billable_tokens_monthly == 0
assert any("free monthly allowance" in w for w in result.warnings)
def test_unknown_feature_key_rejected(volumes: Volumes) -> None:
with pytest.raises(ValueError, match="not published meters"):
CalculatorInputs(volumes=volumes, users=1, enabled=frozenset({"telepathy"}))
def test_metered_units_covers_every_enabled_key(reference: CalculatorInputs) -> None:
assert set(metered_units(reference)) == set(reference.enabled)
def test_empty_scenario_costs_nothing() -> None:
result = calculate(CalculatorInputs(volumes=Volumes(), users=0))
assert result.grand_total_annual == 0.0
assert result.lines == ()
# ── licence model, scenarios ─────────────────────────────────────────────
def test_concurrent_licence_costs_more_per_user(reference: CalculatorInputs) -> None:
"""Copilot 40→60 and STA 30→45, so 35,000 → 52,500 subscription tokens."""
concurrent = reference.with_(
licence=LicenceModel.CONCURRENT,
enabled=frozenset(
{"agent_copilot_named", "speech_and_text_analytics_named"}
),
)
named = reference.with_(
enabled=frozenset({"agent_copilot_named", "speech_and_text_analytics_named"})
)
assert calculate(named).totals.subscription_tokens_monthly == pytest.approx(35_000.0)
assert calculate(concurrent).totals.subscription_tokens_monthly == pytest.approx(
52_500.0
)
def test_compare_is_monotone_in_deflection(reference: CalculatorInputs) -> None:
"""More virtual-agent deflection means more consumption tokens."""
rows = compare(
{
"Conservative": reference.with_(
mix=DeflectionMix(bot_only_share=0.20, virtual_agent_share=0.05)
),
"Base": reference,
"Aggressive": reference.with_(
mix=DeflectionMix(
bot_only_share=0.35,
virtual_agent_share=0.25,
agentic_va_share=0.10,
)
),
}
)
assert [r["Scenario"] for r in rows] == ["Conservative", "Base", "Aggressive"]
totals = [r["Token cost / yr"] for r in rows]
assert totals[0] < totals[1] < totals[2]
# ── the export payload ───────────────────────────────────────────────────
def test_result_json_round_trips_and_carries_provenance(
reference: CalculatorInputs,
) -> None:
payload = result_json(calculate(reference), reference, meta={"client": "Example"})
assert json.loads(json.dumps(payload)) == payload
assert payload["rate_card"]["source_date"] == RATE_CARD_SOURCE_DATE
assert payload["rate_card"]["voice_bot_roundup_seconds"] == 15
assert payload["tts_source"]["source_date"] == "2026-05-22"
assert payload["totals"]["grand_total_annual"] == pytest.approx(667_068.0)
assert payload["meta"]["client"] == "Example"
assert len(payload["lines"]) == len(REFERENCE_ENABLED)

View File

@@ -0,0 +1,174 @@
"""The vendor-change tripwire.
Every published row is pinned by its exact feature wording AND its exact
rate string, so a transcription slip or a vendor republication breaks the
build rather than quietly moving a client-facing number. When Genesys
republishes, these pins are updated in the SAME commit as the anchor — see
the re-anchoring protocol in docs/Calculator_Pattern_V1-00.md.
"""
from __future__ import annotations
import pytest
from genesyscalc.meters import Confidence, LicenceModel, MeterBasis
from genesyscalc.ratecard import (
ALLOWANCE_CARRIES_OVER,
COPILOT_COVERS_SUMMARY_RULE,
FREE_TOKENS_PER_MONTH,
HIGHEST_TIER_RULE,
LIST_PRICE_BY_CURRENCY,
METERS,
METERS_VERBATIM,
RATE_CARD_SOURCE,
RATE_CARD_SOURCE_DATE,
VOICE_BOT_ROUNDUP_SECONDS,
ZERO_RATED_VERBATIM,
meter,
rate_card_rows,
)
# The published table, as it reads on the page. Transcribed 2026-07-12.
PUBLISHED = {
"Bots (Voice)": "17 minutes per token",
"Bots (Digital)": "51 sessions per token",
"Virtual Agent": "0.5 tokens per Virtual Agent interaction",
"Agentic Virtual Agent": "1.2 tokens per interaction",
"Agent Copilot [named]": "40 tokens per user",
"Agent Copilot [concurrent]": "60 tokens per user",
"AI Scoring": "20 evaluations per token",
"AI Translate": "2 translations per token",
"AI Summary and Insights": "50 summaries/insights per token",
"Apple Messages for Business": "400 inbound or outbound messages per token",
"Facebook Messenger": "400 messages per token",
"Instagram Direct Messaging": "400 messages per token",
"WhatsApp Messaging": "400 messages per token",
"X Direct Messaging": "400 messages per token",
"Genesys Cloud Social": "400 social post ingestions per channel per token",
"Social Post Responses": "400 outbound messages per channel per token",
"Predictive Routing": "17 routes per token",
"Speech and Text Analytics [named]": "30 tokens per user",
"Speech and Text Analytics [concurrent]": "45 tokens per user",
"Genesys Cloud Copilot": "20 AI actions per token",
}
def test_published_meter_table_verbatim() -> None:
assert {m.feature: m.published_rate for m in METERS_VERBATIM} == PUBLISHED
def test_meter_count() -> None:
assert len(METERS_VERBATIM) == 20
assert len(ZERO_RATED_VERBATIM) == 2
def test_source_pinned() -> None:
"""Bumping the date without updating the pins is the failure mode."""
assert RATE_CARD_SOURCE_DATE == "2026-07-12"
assert RATE_CARD_SOURCE == (
"https://help.genesys.cloud/articles/genesys-cloud-tokens-model/"
)
def test_every_confirmed_meter_carries_source_url_and_date() -> None:
for m in METERS.values():
if m.confidence is Confidence.CONFIRMED:
assert m.source_url, f"{m.key} is 🟢 without a source URL"
assert m.source_date, f"{m.key} is 🟢 without a source date"
def test_unsourced_confirmed_meter_cannot_be_constructed() -> None:
from genesyscalc.meters import Meter
with pytest.raises(ValueError, match="source URL and date"):
Meter(
key="made_up",
feature="Made Up",
published_rate="1 per token",
basis=MeterBasis.UNITS_PER_TOKEN,
rate=1.0,
)
@pytest.mark.parametrize(
("key", "expected"),
[
("bots_voice", 1 / 17),
("bots_digital", 1 / 51),
("virtual_agent", 0.5),
("agentic_virtual_agent", 1.2),
("ai_scoring", 1 / 20),
("ai_translate", 1 / 2),
("ai_summary_and_insights", 1 / 50),
("whatsapp_messaging", 1 / 400),
("genesys_cloud_social", 1 / 400),
("predictive_routing", 1 / 17),
("genesys_cloud_copilot", 1 / 20),
],
)
def test_numeric_rates_match_their_published_strings(key: str, expected: float) -> None:
"""Catches a float drifting away from the wording beside it."""
assert meter(key).rate == pytest.approx(expected)
def test_per_user_rates() -> None:
copilot = meter("agent_copilot_named")
assert copilot.tokens_per_user_month(LicenceModel.NAMED) == 40.0
assert copilot.tokens_per_user_month(LicenceModel.CONCURRENT) == 60.0
sta = meter("speech_and_text_analytics_named")
assert sta.tokens_per_user_month(LicenceModel.NAMED) == 30.0
assert sta.tokens_per_user_month(LicenceModel.CONCURRENT) == 45.0
def test_free_allowance() -> None:
assert FREE_TOKENS_PER_MONTH[LicenceModel.NAMED] == 250
assert FREE_TOKENS_PER_MONTH[LicenceModel.CONCURRENT] == 350
assert ALLOWANCE_CARRIES_OVER is False
def test_voice_roundup_granularity() -> None:
assert VOICE_BOT_ROUNDUP_SECONDS == 15
def test_zero_rated_features() -> None:
assert {m.key for m in ZERO_RATED_VERBATIM} == {
"predictive_engagement",
"knowledge_queries",
}
assert all(m.rate == 0.0 for m in ZERO_RATED_VERBATIM)
assert all(m.tokens_for(1_000_000) == 0.0 for m in ZERO_RATED_VERBATIM)
def test_highest_tier_rule_text_verbatim() -> None:
"""It renders on stage, so the wording is part of the deliverable."""
assert HIGHEST_TIER_RULE == (
"In cases where an interaction uses multiple AI resources, such as bot "
"flows, virtual agents, and agentic virtual agents, Genesys bases "
"charges on the highest tier (price) resource that Genesys Cloud uses "
"during the interaction."
)
def test_copilot_covers_summary_rule_text_verbatim() -> None:
assert COPILOT_COVERS_SUMMARY_RULE == (
"if you enable Agent Copilot simultaneously, then Supervisor Copilot "
"summaries and insights do not consume tokens"
)
def test_list_price() -> None:
assert LIST_PRICE_BY_CURRENCY["USD"] == 1.00
assert LIST_PRICE_BY_CURRENCY["JPY"] == 120.0
def test_meter_lookup_error_is_useful() -> None:
with pytest.raises(KeyError, match="not a published Genesys meter"):
meter("nope")
def test_rate_card_rows_cover_every_published_line() -> None:
rows = rate_card_rows()
assert len(rows) == len(METERS_VERBATIM) + len(ZERO_RATED_VERBATIM)
assert rows[0]["Feature"] == "Bots (Voice)"
assert all(r["Confidence"] == "🟢" for r in rows)
assert all(r["Source"] == RATE_CARD_SOURCE_DATE for r in rows)

View File

@@ -0,0 +1,110 @@
"""Sweeps and tornado data."""
from __future__ import annotations
import pytest
from genesyscalc.meters import TokenPrice
from genesyscalc.model import CalculatorInputs, calculate
from genesyscalc.sensitivity import (
DRIVERS,
driver_value,
set_driver,
sweep,
tornado,
)
from genesyscalc.tts import TtsUsage
from genesyscalc.usage import DeflectionMix, VoiceBotUsage, Volumes
@pytest.fixture
def base() -> CalculatorInputs:
return CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=100_000, voice_outbound_monthly=20_000),
users=500,
enabled=frozenset(
{"agent_copilot_named", "bots_voice", "virtual_agent"}
),
mix=DeflectionMix(bot_only_share=0.30, virtual_agent_share=0.15),
price=TokenPrice(),
voice_bot=VoiceBotUsage(calls_per_month=30_000, avg_bot_seconds_per_call=40),
tts=TtsUsage(calls_monthly=100_000),
)
def test_driver_round_trip(base: CalculatorInputs) -> None:
assert driver_value(base, "users") == 500
assert driver_value(set_driver(base, "users", 750), "users") == 750
def test_nested_driver_round_trip(base: CalculatorInputs) -> None:
assert driver_value(base, "concession_pct") == 0.0
moved = set_driver(base, "concession_pct", 0.25)
assert moved.price.concession_pct == pytest.approx(0.25)
assert base.price.concession_pct == 0.0 # inputs are frozen; no mutation
def test_integer_drivers_stay_integers(base: CalculatorInputs) -> None:
assert isinstance(set_driver(base, "users", 512.6).users, int)
assert set_driver(base, "users", 512.6).users == 513
def test_unknown_driver_is_named(base: CalculatorInputs) -> None:
with pytest.raises(KeyError, match="not a known driver"):
driver_value(base, "vibes")
def test_sweep_is_monotone_in_token_price(base: CalculatorInputs) -> None:
rows = sweep(base, "token_price", [0.5, 1.0, 1.5, 2.0])
costs = [r["token_cost_annual"] for r in rows]
assert costs == sorted(costs)
assert len(rows) == 4
def test_sweep_carries_the_driver_value(base: CalculatorInputs) -> None:
rows = sweep(base, "concession_pct", [0.0, 0.25])
assert rows[0]["concession_pct"] == 0.0
assert rows[1]["grand_total_annual"] < rows[0]["grand_total_annual"]
def test_tornado_is_sorted_by_swing(base: CalculatorInputs) -> None:
rows = tornado(base, ["token_price", "users", "bot_seconds", "concession_pct"])
swings = [r["swing"] for r in rows]
assert swings == sorted(swings, reverse=True)
assert all(r["swing"] >= 0 for r in rows)
def test_tornado_brackets_the_baseline(base: CalculatorInputs) -> None:
baseline = calculate(base).grand_total_annual
for row in tornado(base, ["token_price", "users"]):
assert row["low"] <= baseline <= row["high"]
assert row["baseline"] == pytest.approx(baseline)
def test_tornado_skips_inactive_levers() -> None:
"""No TTS and no voice bot in the scenario — those levers are noise."""
plain = CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=1_000),
users=10,
enabled=frozenset({"agent_copilot_named"}),
)
drivers = [r["driver"] for r in tornado(plain, list(DRIVERS))]
assert "bot_seconds" not in drivers
assert "tts_chars_per_call" not in drivers
assert "users" in drivers
def test_tornado_skips_a_lever_that_would_break_the_partition() -> None:
"""Raising a share past the partition is illegal, not a data point."""
saturated = CalculatorInputs(
volumes=Volumes(voice_inbound_monthly=1_000),
users=10,
enabled=frozenset({"virtual_agent"}),
mix=DeflectionMix(bot_only_share=0.5, virtual_agent_share=0.5),
)
drivers = [r["driver"] for r in tornado(saturated, ["virtual_agent_share"])]
assert drivers == []
def test_invalid_delta_rejected(base: CalculatorInputs) -> None:
with pytest.raises(ValueError, match="delta"):
tornado(base, ["users"], delta=1.5)

View File

@@ -0,0 +1,61 @@
"""Stage/backstage detection.
Two signals mark the Mercury app view (see staging.py): the app's shadow
session name (``__mercury__`` in ``JPY_SESSION_NAME`` — per-view, primary)
and the ``MERCURY_CONFIG_DIR`` env var (server-level fallback, only set by
``mercury --working-dir``). Either one means "a client may be looking".
"""
import pytest
from genesyscalc import staging
@pytest.fixture(autouse=True)
def clean_stage_env(monkeypatch):
monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False)
monkeypatch.delenv("JPY_SESSION_NAME", raising=False)
def test_backstage_by_default(capsys):
assert not staging.on_stage()
staging.backstage("visible")
assert capsys.readouterr().out == "visible\n"
def test_plain_session_name_is_backstage(monkeypatch, capsys):
# A JupyterLab or nbconvert session: the real notebook path, no marker.
monkeypatch.setenv("JPY_SESSION_NAME", "notebooks/genesys_token_calculator.ipynb")
assert not staging.on_stage()
staging.backstage("visible")
assert capsys.readouterr().out == "visible\n"
def test_mercury_shadow_session_is_stage(monkeypatch, capsys):
# The Mercury app runs against a shadow copy: <stem>__mercury__<id>.ipynb
monkeypatch.setenv(
"JPY_SESSION_NAME", "notebooks/genesys_token_calculator__mercury__545e5520.ipynb"
)
assert staging.on_stage()
staging.backstage("hidden")
assert capsys.readouterr().out == ""
def test_config_dir_fallback_is_stage(monkeypatch, capsys):
monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app")
assert staging.on_stage()
staging.backstage("hidden")
assert capsys.readouterr().out == ""
def test_backstage_md_renders_only_off_stage(monkeypatch, capsys):
# Off stage it must emit SOMETHING (rich markdown under a kernel;
# IPython's display degrades to print under plain pytest) …
staging.backstage_md("**visible**")
assert capsys.readouterr().out != ""
# … and on stage — either signal — nothing at all.
monkeypatch.setenv(
"JPY_SESSION_NAME", "notebooks/genesys_token_calculator__mercury__ab12cd34.ipynb"
)
staging.backstage_md("**hidden**")
assert capsys.readouterr().out == ""

View File

@@ -0,0 +1,114 @@
"""Enhanced TTS — the non-token line, its round-up, and its free share."""
from __future__ import annotations
import pytest
from genesyscalc.meters import Confidence
from genesyscalc.tts import (
TTS_PRICE_PER_MILLION_CHARS,
TTS_SOURCE,
TTS_SOURCE_DATE,
TTS_STANDARD_END_OF_SALE,
TTS_STANDARD_END_OF_SUPPORT,
TtsUsage,
tts_cost,
tts_free_share,
)
from genesyscalc.usage import DeflectionMix
def test_published_rates() -> None:
assert TTS_PRICE_PER_MILLION_CHARS == {"advanced": 20.0, "standard": 5.0}
def test_source_pinned_and_distinct_from_the_tokens_article() -> None:
"""TTS has its own page and its own date — it is not a token meter."""
from genesyscalc.ratecard import RATE_CARD_SOURCE, RATE_CARD_SOURCE_DATE
assert TTS_SOURCE_DATE == "2026-05-22"
assert TTS_SOURCE.endswith("genesys-enhanced-tts-pricing/")
assert TTS_SOURCE != RATE_CARD_SOURCE
assert TTS_SOURCE_DATE != RATE_CARD_SOURCE_DATE
def test_tts_is_published_so_confirmed() -> None:
line = tts_cost(TtsUsage(calls_monthly=100_000))
assert line.confidence is Confidence.CONFIRMED
def test_hand_checked_advanced_cost() -> None:
"""100,000 calls × 2,000 chars = 200M chars → 200 units × $20 = $4,000/mo."""
line = tts_cost(TtsUsage(calls_monthly=100_000, chars_per_call=2_000))
assert line.total_chars_monthly == pytest.approx(200_000_000.0)
assert line.billed_millions_monthly == 200
assert line.cost_monthly == pytest.approx(4_000.0)
assert line.cost_annual == pytest.approx(48_000.0)
def test_hand_checked_standard_cost() -> None:
"""Same volume on standard voices: 200 × $5 = $1,000/mo."""
line = tts_cost(
TtsUsage(calls_monthly=100_000, chars_per_call=2_000, tier="standard")
)
assert line.cost_monthly == pytest.approx(1_000.0)
def test_per_million_roundup_published_examples() -> None:
"""The article's own examples: 200,000 chars = $5; 1,000,005 chars = $10.
Both on standard voices, where the unit rate is $5.
"""
small = tts_cost(TtsUsage(calls_monthly=1, chars_per_call=200_000, tier="standard"))
assert small.billed_millions_monthly == 1
assert small.cost_monthly == pytest.approx(5.0)
just_over = tts_cost(
TtsUsage(calls_monthly=1, chars_per_call=1_000_005, tier="standard")
)
assert just_over.billed_millions_monthly == 2
assert just_over.cost_monthly == pytest.approx(10.0)
def test_free_share_tracks_the_virtual_agent_tiers() -> None:
"""Free during VA and agentic VA — bot-only deflection does not qualify."""
mix = DeflectionMix(
bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
)
assert tts_free_share(mix) == pytest.approx(0.20)
def test_free_share_reduces_the_bill() -> None:
"""200M chars, 20% free → 160M billable → 160 × $20 = $3,200/mo."""
mix = DeflectionMix(virtual_agent_share=0.15, agentic_va_share=0.05)
line = tts_cost(TtsUsage(calls_monthly=100_000, chars_per_call=2_000), mix)
assert line.free_share == pytest.approx(0.20)
assert line.free_chars_monthly == pytest.approx(40_000_000.0)
assert line.billable_chars_monthly == pytest.approx(160_000_000.0)
assert line.cost_monthly == pytest.approx(3_200.0)
def test_standard_tier_warns_about_end_of_life() -> None:
line = tts_cost(TtsUsage(calls_monthly=1_000, tier="standard"))
assert len(line.warnings) == 1
assert TTS_STANDARD_END_OF_SALE in line.warnings[0]
assert TTS_STANDARD_END_OF_SUPPORT in line.warnings[0]
def test_advanced_tier_does_not_warn() -> None:
assert tts_cost(TtsUsage(calls_monthly=1_000)).warnings == ()
def test_concession_applies_to_tts_too() -> None:
line = tts_cost(TtsUsage(calls_monthly=100_000), concession_pct=0.25)
assert line.cost_monthly == pytest.approx(3_000.0)
def test_zero_volume_costs_nothing() -> None:
line = tts_cost(TtsUsage(calls_monthly=0))
assert line.billed_millions_monthly == 0
assert line.cost_monthly == 0.0
def test_unknown_tier_rejected() -> None:
with pytest.raises(ValueError, match="unknown TTS tier"):
TtsUsage(calls_monthly=1, tier="neural-ultra")

View File

@@ -0,0 +1,206 @@
"""Volumes → tokens, with the three published rules that trip the naive math.
Every number below was computed by hand first, then pinned.
"""
from __future__ import annotations
import pytest
from genesyscalc.meters import LicenceModel, Tier
from genesyscalc.usage import (
DeflectionMix,
VoiceBotUsage,
Volumes,
apply_copilot_covers_summary,
subscription_tokens,
tier_interactions,
virtual_agent_tokens,
voice_bot_billable_minutes,
voice_bot_roundup_uplift,
voice_bot_tokens,
)
# ── 1 · the 15-second per-call round-up ──────────────────────────────────
def test_voice_bot_15_second_roundup() -> None:
"""1,000 calls at a 40s average.
ceil(40/15) = 3 increments = 45s = 0.75 min/call → 750.0 min/month.
The naive 40/60 × 1,000 = 666.67 min understates by 12.5%.
"""
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)
assert voice_bot_billable_minutes(usage) == pytest.approx(750.0)
assert voice_bot_tokens(usage) == pytest.approx(750.0 / 17.0)
assert voice_bot_tokens(usage) == pytest.approx(44.117647, rel=1e-6)
def test_roundup_uplift_is_reportable() -> None:
"""The stage-facing number: 12.5% more than the raw duration implies."""
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)
assert voice_bot_roundup_uplift(usage) == pytest.approx(0.125)
def test_roundup_is_exact_on_increment_boundaries() -> None:
"""45s is exactly three increments — no inflation, uplift 0."""
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=45)
assert voice_bot_billable_minutes(usage) == pytest.approx(750.0)
assert voice_bot_roundup_uplift(usage) == pytest.approx(0.0)
def test_roundup_penalises_short_calls_hardest() -> None:
"""A 5-second bot greeting bills as 15 seconds — 3× its duration.
1,000 calls × 5s = 83.33 raw minutes, billed as 250.0.
"""
usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=5)
assert voice_bot_billable_minutes(usage) == pytest.approx(250.0)
assert voice_bot_roundup_uplift(usage) == pytest.approx(2.0)
def test_zero_bot_volume_is_free_and_does_not_divide_by_zero() -> None:
usage = VoiceBotUsage(calls_per_month=0, avg_bot_seconds_per_call=0)
assert voice_bot_billable_minutes(usage) == 0.0
assert voice_bot_roundup_uplift(usage) == 0.0
assert voice_bot_tokens(usage) == 0.0
def test_negative_bot_usage_rejected() -> None:
with pytest.raises(ValueError, match="calls_per_month"):
VoiceBotUsage(calls_per_month=-1, avg_bot_seconds_per_call=30)
# ── 2 · the highest-tier rule as a partition ─────────────────────────────
def test_tier_shares_must_partition() -> None:
"""Shares summing past 1.0 would bill some interaction twice."""
with pytest.raises(ValueError, match="sum to at most 1.0"):
DeflectionMix(
bot_only_share=0.5, virtual_agent_share=0.4, agentic_va_share=0.3
)
def test_tier_share_bounds() -> None:
with pytest.raises(ValueError, match="must be in"):
DeflectionMix(virtual_agent_share=1.4)
def test_agent_handled_share_is_the_remainder() -> None:
mix = DeflectionMix(
bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
)
assert mix.total_deflected_share == pytest.approx(0.50)
assert mix.agent_handled_share == pytest.approx(0.50)
def test_highest_tier_charges_each_interaction_once() -> None:
"""100,000 interactions: 30% bot, 15% VA, 5% agentic VA.
VA = 100,000 × 0.15 × 0.5 tokens = 7,500
AVA = 100,000 × 0.05 × 1.2 tokens = 6,000
------
13,500
Applying every tier to the whole volume — the additive reading — would
give 50,000 + 120,000 = far more, billing the same interaction at
several tiers at once.
"""
mix = DeflectionMix(
bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
)
tokens = virtual_agent_tokens(100_000, mix)
assert tokens["virtual_agent"] == pytest.approx(7_500.0)
assert tokens["agentic_virtual_agent"] == pytest.approx(6_000.0)
assert sum(tokens.values()) == pytest.approx(13_500.0)
def test_tier_interactions_never_exceed_the_volume() -> None:
mix = DeflectionMix(
bot_only_share=0.40, virtual_agent_share=0.35, agentic_va_share=0.25
)
split = tier_interactions(100_000, mix)
assert sum(split.values()) == pytest.approx(100_000.0)
assert split[Tier.BOT] == pytest.approx(40_000.0)
def test_bot_tier_is_not_priced_per_interaction() -> None:
"""Voice bots bill in minutes with a round-up, so they are absent here."""
mix = DeflectionMix(bot_only_share=1.0)
assert virtual_agent_tokens(100_000, mix) == {
"virtual_agent": 0.0,
"agentic_virtual_agent": 0.0,
}
# ── 3 · Copilot covers Supervisor summaries ──────────────────────────────
def test_summary_billed_when_copilot_off() -> None:
usage = apply_copilot_covers_summary(
{"ai_summary_and_insights": 120_000.0}, copilot_enabled=False
)
assert usage.get("ai_summary_and_insights") == pytest.approx(120_000.0)
assert usage.notes == ()
def test_copilot_covers_summary_zeroes_the_line() -> None:
"""The published exclusion, enforced — and reported, not silent."""
usage = apply_copilot_covers_summary(
{"ai_summary_and_insights": 120_000.0}, copilot_enabled=True
)
assert usage.get("ai_summary_and_insights") == 0.0
assert len(usage.notes) == 1
assert "Agent Copilot is enabled" in usage.notes[0]
def test_copilot_exclusion_leaves_other_meters_alone() -> None:
usage = apply_copilot_covers_summary(
{"ai_summary_and_insights": 100.0, "ai_scoring": 500.0}, copilot_enabled=True
)
assert usage.get("ai_scoring") == pytest.approx(500.0)
def test_copilot_exclusion_is_quiet_when_there_is_nothing_to_zero() -> None:
usage = apply_copilot_covers_summary(
{"ai_summary_and_insights": 0.0}, copilot_enabled=True
)
assert usage.notes == ()
# ── subscription tokens ──────────────────────────────────────────────────
def test_per_user_tokens_are_exact_and_licence_sensitive() -> None:
"""500 users × 40 = 20,000 named; × 60 = 30,000 concurrent."""
keys = frozenset({"agent_copilot_named"})
assert subscription_tokens(500, keys, LicenceModel.NAMED) == {
"agent_copilot_named": 20_000.0
}
assert subscription_tokens(500, keys, LicenceModel.CONCURRENT) == {
"agent_copilot_named": 30_000.0
}
def test_subscription_rejects_a_consumption_meter() -> None:
with pytest.raises(ValueError, match="not a per-user meter"):
subscription_tokens(10, frozenset({"ai_scoring"}), LicenceModel.NAMED)
# ── volumes ──────────────────────────────────────────────────────────────
def test_volume_totals() -> None:
v = Volumes(
voice_inbound_monthly=100_000,
voice_outbound_monthly=20_000,
digital_sessions_monthly=8_000,
email_monthly=12_000,
)
assert v.voice_total_monthly == pytest.approx(120_000.0)
assert v.all_interactions_monthly == pytest.approx(140_000.0)
def test_negative_volume_rejected() -> None:
with pytest.raises(ValueError, match="voice_inbound_monthly"):
Volumes(voice_inbound_monthly=-1)