152 lines
5.0 KiB
Python
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,
|
|
)
|