Files

264 lines
10 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.
"""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)