155 lines
5.9 KiB
Python
155 lines
5.9 KiB
Python
"""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
|