Source code for metricengine.calculations.pricing

"""
Pricing and tax-related calculations.

This module contains calculations for pricing, tax handling, and markup/discount operations.
All calculations use the Collection namespace for proper organization.
"""

from __future__ import annotations

from decimal import Decimal

from ..exceptions import CalculationError
from ..policy import DEFAULT_POLICY, Policy
from ..policy_context import get_policy
from ..registry_collections import Collection
from ..units import Dimensionless, Money, Percent, Ratio  # phantom units
from ..value import FV
from .rules import skip_if_negative_sales

pricing = Collection("pricing")

# ── small local helpers ──────────────────────────────────────────────────────


def _resolve_policy(*fvs: FV | None) -> Policy:
    for fv in fvs:
        if isinstance(fv, FV) and fv.policy:
            return fv.policy
    return get_policy() or DEFAULT_POLICY


def _none_with_unit(unit, pol: Policy) -> FV:
    return FV(None, policy=pol, unit=unit)


def _is_zero(fv: FV) -> bool:
    d = fv.as_decimal()
    return (d is not None) and (d == 0)


def _ratio_with_policy(value: FV, pol: Policy) -> FV[Ratio]:
    """Create a ratio FinancialValue without polluting the policy with percent_style='ratio'."""
    return FV(value._value, policy=pol, unit=Ratio, _is_percentage=False)


# ── calculations ─────────────────────────────────────────────────────────────


[docs] @pricing.calc("total_cost", depends_on=("unit_cost", "quantity")) def total_cost( unit_cost: FV[Money], quantity: FV[Dimensionless], ) -> FV[Money]: """ Total cost = unit_cost * quantity """ pol = _resolve_policy(unit_cost, quantity) if unit_cost.is_none() or quantity.is_none(): return _none_with_unit(Money, pol) return unit_cost * quantity
[docs] @pricing.calc("sales_ex_tax", depends_on=("sales", "tax_rate")) @skip_if_negative_sales("sales") def sales_ex_tax( sales: FV[Money], tax_rate: FV[Percent], # tax rate as percent/ratio (e.g., 10% -> 0.10) ) -> FV[Money]: """ Sales excluding tax = sales / (1 + tax_rate) """ pol = _resolve_policy(sales, tax_rate) if sales.is_none() or tax_rate.is_none(): return _none_with_unit(Money, pol) one = FV(Decimal("1"), policy=pol, unit=Dimensionless) denom = one + tax_rate if _is_zero(denom): if pol.arithmetic_strict: raise CalculationError("sales_ex_tax undefined when 1 + tax_rate == 0") return _none_with_unit(Money, pol) return sales / denom
[docs] @pricing.calc("sales_with_tax", depends_on=("sales_ex_tax", "tax_rate")) @skip_if_negative_sales("sales_ex_tax") def sales_with_tax( sales_ex_tax: FV[Money], tax_rate: FV[Percent], ) -> FV[Money]: """ Sales including tax = sales_ex_tax * (1 + tax_rate) """ pol = _resolve_policy(sales_ex_tax, tax_rate) if sales_ex_tax.is_none() or tax_rate.is_none(): return _none_with_unit(Money, pol) one = FV(Decimal("1"), policy=pol, unit=Dimensionless) return sales_ex_tax * (one + tax_rate)
[docs] @pricing.calc("tax_amount", depends_on=("sales", "tax_rate")) @skip_if_negative_sales("sales") def tax_amount( sales: FV[Money], tax_rate: FV[Percent], ) -> FV[Money]: """ Tax amount = sales - (sales / (1 + tax_rate)) """ pol = _resolve_policy(sales, tax_rate) if sales.is_none() or tax_rate.is_none(): return _none_with_unit(Money, pol) one = FV(Decimal("1"), policy=pol, unit=Dimensionless) denom = one + tax_rate if _is_zero(denom): if pol.arithmetic_strict: raise CalculationError("tax_amount undefined when 1 + tax_rate == 0") return _none_with_unit(Money, pol) return sales - (sales / denom)
[docs] @pricing.calc("price_ex_tax", depends_on=("price_inc_tax", "tax_rate")) def price_ex_tax( price_inc_tax: FV[Money], tax_rate: FV[Percent], ) -> FV[Money]: """ Price excluding tax = price_inc_tax / (1 + tax_rate) """ pol = _resolve_policy(price_inc_tax, tax_rate) if price_inc_tax.is_none() or tax_rate.is_none(): return _none_with_unit(Money, pol) one = FV(Decimal("1"), policy=pol, unit=Dimensionless) denom = one + tax_rate if _is_zero(denom): if pol.arithmetic_strict: raise CalculationError("price_ex_tax undefined when 1 + tax_rate == 0") return _none_with_unit(Money, pol) return price_inc_tax / denom
[docs] @pricing.calc("markup_ratio", depends_on=("cost", "selling_price")) def markup_ratio( cost: FV[Money], selling_price: FV[Money], ) -> FV[Ratio]: """ Markup ratio = (selling_price - cost) / cost """ pol = _resolve_policy(cost, selling_price) if cost.is_none() or selling_price.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(cost): if pol.arithmetic_strict: raise CalculationError("markup ratio undefined for cost == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy((selling_price - cost) / cost, pol)
[docs] @pricing.calc("markup_percentage", depends_on=("markup_ratio",)) def markup_percentage(markup_ratio: FV[Ratio]) -> FV[Percent]: """ Markup as percent (e.g., 0.25 -> '25%'). """ pol = _resolve_policy(markup_ratio) if markup_ratio.is_none(): return _none_with_unit(Percent, pol) return markup_ratio.as_percentage()
[docs] @pricing.calc("discount_ratio", depends_on=("original_price", "discounted_price")) def discount_ratio( original_price: FV[Money], discounted_price: FV[Money], ) -> FV[Ratio]: """ Discount ratio = (original_price - discounted_price) / original_price """ pol = _resolve_policy(original_price, discounted_price) if original_price.is_none() or discounted_price.is_none(): return _none_with_unit(Ratio, pol) if _is_zero(original_price): if pol.arithmetic_strict: raise CalculationError("discount ratio undefined for original_price == 0") return _none_with_unit(Ratio, pol) return _ratio_with_policy((original_price - discounted_price) / original_price, pol)
[docs] @pricing.calc("discount_percentage", depends_on=("discount_ratio",)) def discount_percentage(discount_ratio: FV[Ratio]) -> FV[Percent]: """ Discount as percent (e.g., 0.20 -> '20%'). """ pol = _resolve_policy(discount_ratio) if discount_ratio.is_none(): return _none_with_unit(Percent, pol) return discount_ratio.as_percentage()