"""Money helpers. Every monetary value in the engine is a :class:`decimal.Decimal`. We never use ``float`` for money — floating point cannot represent cents exactly and tax authorities care about the cent. All public engine functions accept ``int``, ``str`` or ``Decimal`` and return ``Decimal`` rounded to two places using banker-free, half-up rounding (the convention the SAT and IRS expect). """ from __future__ import annotations from decimal import Decimal, ROUND_HALF_UP from typing import Union Number = Union[int, str, float, Decimal] CENTS = Decimal("0.01") def D(value: Number) -> Decimal: """Coerce any supported number into an *unrounded* ``Decimal``. ``float`` is routed through ``str`` so that ``0.1`` becomes ``Decimal("0.1")`` rather than the binary-approximation noise ``Decimal("0.1000000000000000055…")``. """ if isinstance(value, Decimal): return value return Decimal(str(value)) def money(value: Number) -> Decimal: """Coerce and round to two decimal places (half-up).""" return D(value).quantize(CENTS, rounding=ROUND_HALF_UP) def pct(value: Number) -> Decimal: """Interpret a human percentage (``16`` → ``0.16``).""" return D(value) / Decimal(100)