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