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