Skip to content
# ADR 0003 — Monetary amounts: exact decimals everywhere, rounding is jurisdiction policy
- Status: Accepted — 2026-08-05
- Context: docs/research/ledger-engines.md (float hazards, TigerBeetle
fixed-point design) and docs/research/canada-gst-qst.md (statutory
rounding).
# Decision
- No floats, anywhere, ever. In JSON/YAML, every amount, quantity, and
rate is a string ("19.99"); in code it is
decimal.Decimal wrapped in
the Money type (core/money.py). Construction from float raises at
every boundary: Money, the Pydantic schema (ExactDecimal), and the
ALSL loader all reject floats independently.
- Rounding is a named, per-jurisdiction ALSL policy — not an engine
default. Research falsified the assumption that banker's rounding is
universal: GST/QST rounding is half-up by statute (Excise Tax Act
s. 165.2(2): fractions < $0.005 disregarded, ≥ $0.005 deemed one cent;
Revenu Québec IN-203-V states the same for QST).
RoundingMode therefore
supports half_up and half_even, and the ca-qc policy set selects
half_up with a source citation.
- Round late. Intermediate computations (qty × unit price, base × rate,
amount × fx rate) keep full precision; quantization to the currency's
ISO 4217 minor unit happens once, when a figure becomes a posted amount —
and each quantization is recorded in the provenance graph's operation
string (e.g.
tax:QST@0.09975~half_up).
# Consequences
- Cross-currency arithmetic is a hard error (
CurrencyMismatchError);
conversion only happens in the FX pass, explicitly, with a cited rate.
- Determinism holds bit-for-bit: same document + same policies = identical
journal (property-tested in tests/property/).