""" Currency-safe aggregation. Adding two different currencies is never a rounding problem — it is a wrong number, and a silent one. It happened here: the Reconciliation Control summed each marketplace's closing balance in its own local currency (USD + EUR + GBP + PLN + SEK + CAD + AUD as one figure), understating the Jan-2026 close by USD 444,658.44 against the correctly-converted total, and that understated figure was what gated Finance sign-off. `Total` makes the mistake impossible to repeat: every amount must be added with its currency, and mixing codes raises instead of quietly producing a plausible number. """ from __future__ import annotations USD = "USD" class CurrencyMismatch(ValueError): """Raised when amounts in different currencies would be added together.""" class Total: """ An accumulator that refuses to mix currencies. t = Total() # USD-converting accumulator t.add_converted(230089.62, 1.185665) t = Total(currency="EUR") # single-currency accumulator t.add(100.0, "EUR") # ok t.add(100.0, "GBP") # raises CurrencyMismatch """ __slots__ = ("currency", "_value", "_locked") def __init__(self, currency: str | None = None): self.currency = currency self._value = 0.0 self._locked = currency is not None def add(self, amount: float | None, currency: str | None) -> "Total": """Add an amount stated in `currency`. The first add fixes the accumulator's currency.""" cur = (currency or USD).strip().upper() if self.currency is None: self.currency = cur elif cur != self.currency: raise CurrencyMismatch( f"refusing to add {cur} to a {self.currency} total " f"(convert to a common currency first)" ) self._value += float(amount or 0.0) return self def add_converted(self, amount: float | None, fx_rate: float | None) -> "Total": """Add a local amount converted to USD at `fx_rate`. Only valid on a USD total.""" if self.currency is None: self.currency = USD elif self.currency != USD: raise CurrencyMismatch( f"add_converted() produces USD but this total is {self.currency}" ) self._value += float(amount or 0.0) * float(fx_rate if fx_rate is not None else 1.0) return self @property def value(self) -> float: return round(self._value, 2) def __float__(self) -> float: return self.value def __repr__(self) -> str: return f"Total({self.value:,.2f} {self.currency or '?'})" def to_usd(amount: float | None, fx_rate: float | None) -> float: """Convert one local amount to USD.""" return round(float(amount or 0.0) * float(fx_rate if fx_rate is not None else 1.0), 2)