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