Files
palladium/calculators/Genesys_Token_Calculator/genesyscalc/billing.py

152 lines
5.0 KiB
Python

"""Tokens → dollars.
The order of operations is the whole content of this module, and it is
pinned by tests because getting it wrong moves the bill:
1. Round consumption up to whole tokens, monthly. Per-user subscription
totals are exact and are NOT rounded — only metered consumption is.
2. Deduct the free monthly allowance (250 named / 350 concurrent).
3. Floor at zero. The allowance renews monthly and "do[es] not carry over
to future months", so an unused remainder is discarded, never banked.
Everything commercial — the contracted rate, the concession — arrives via
:class:`genesyscalc.meters.TokenPrice` from the notebook's engagement-data
cell. This module knows arithmetic, not deals.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from .meters import Confidence, LicenceModel, TokenPrice
from .ratecard import FREE_TOKENS_PER_MONTH, meter
MONTHS_PER_YEAR: int = 12
@dataclass(frozen=True)
class CostLine:
"""One feature's monthly and annual cost, ready to render."""
key: str
feature: str
units_monthly: float
unit_label: str
tokens_monthly: float
cost_monthly: float
cost_annual: float
confidence: Confidence
source_url: str | None
note: str = ""
@dataclass(frozen=True)
class CostTotals:
"""The token bill, with the allowance and the MSRP walk both visible."""
consumption_tokens_monthly: float
subscription_tokens_monthly: float
gross_tokens_monthly: float
allowance_tokens_monthly: int
billable_tokens_monthly: int
effective_rate: float
currency: str
token_cost_monthly: float
token_cost_annual: float
list_cost_annual: float
concession_saving_annual: float
def billable_tokens_monthly(
consumption_tokens: float,
subscription_tokens: float,
licence: LicenceModel,
apply_allowance: bool = True,
) -> int:
"""Whole tokens actually billed in a month.
``ceil`` lands on the consumption subtotal (Genesys bills whole tokens);
subscription totals are already whole and exact. The allowance is then
deducted and the result floored at zero — it does not carry over, so an
unused remainder is simply lost.
"""
if consumption_tokens < 0 or subscription_tokens < 0:
raise ValueError("token counts must not be negative")
gross = math.ceil(consumption_tokens) + subscription_tokens
allowance = FREE_TOKENS_PER_MONTH[licence] if apply_allowance else 0
return max(0, int(gross) - allowance)
def allowance_for(licence: LicenceModel, apply_allowance: bool = True) -> int:
"""The free monthly token allowance actually in play."""
return FREE_TOKENS_PER_MONTH[licence] if apply_allowance else 0
def cost_lines(
tokens_by_key: dict[str, float], units_by_key: dict[str, float], price: TokenPrice
) -> tuple[CostLine, ...]:
"""One :class:`CostLine` per meter, in published order, at the effective rate.
Line costs are unrounded and un-allowanced: the allowance is an
org-level deduction, not a per-feature one, so attributing it to a
single line would misstate that feature's economics. It lands in
:func:`total_cost`.
"""
rate = price.effective_rate()
lines: list[CostLine] = []
for key in sorted(tokens_by_key, key=lambda k: meter(k).feature):
m = meter(key)
tokens = tokens_by_key[key]
monthly = tokens * rate
lines.append(
CostLine(
key=key,
feature=m.feature,
units_monthly=units_by_key.get(key, 0.0),
unit_label=m.unit_label,
tokens_monthly=tokens,
cost_monthly=monthly,
cost_annual=monthly * MONTHS_PER_YEAR,
confidence=m.confidence,
source_url=m.source_url,
note=m.note,
)
)
return tuple(lines)
def total_cost(
consumption_tokens_monthly: float,
subscription_tokens_monthly: float,
licence: LicenceModel,
price: TokenPrice,
apply_allowance: bool = True,
) -> CostTotals:
"""The org-level bill: allowance deducted, MSRP walk carried alongside."""
billable = billable_tokens_monthly(
consumption_tokens_monthly,
subscription_tokens_monthly,
licence,
apply_allowance,
)
rate = price.effective_rate()
monthly = billable * rate
annual = monthly * MONTHS_PER_YEAR
list_annual = billable * price.list_rate * MONTHS_PER_YEAR
return CostTotals(
consumption_tokens_monthly=consumption_tokens_monthly,
subscription_tokens_monthly=subscription_tokens_monthly,
gross_tokens_monthly=(
math.ceil(consumption_tokens_monthly) + subscription_tokens_monthly
),
allowance_tokens_monthly=allowance_for(licence, apply_allowance),
billable_tokens_monthly=billable,
effective_rate=rate,
currency=price.currency,
token_cost_monthly=monthly,
token_cost_annual=annual,
list_cost_annual=list_annual,
concession_saving_annual=list_annual - annual,
)