refactor: use Decimal instead of float for monetary calculations

- calculations.py: Use Decimal for commission percentages, exchange rates,
  and client balances. Added to_decimal() helper for safe float conversion.
  Changed from banker's rounding to ROUND_HALF_UP.

- models.py: Changed all fiat amounts, percentages, and exchange rates to
  Decimal. Added json_encoders for API serialization.

- transaction_processor.py: Convert to Decimal at data ingestion boundary
  (CSV parsing). Updated all defaults and calculations to use Decimal.

- tests: Updated to work with Decimal return types.

This prevents floating-point precision issues in financial calculations.
All 23 tests pass.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2026-01-11 14:47:56 +01:00
parent 397fd4b002
commit 6e86f53962
4 changed files with 180 additions and 101 deletions

View file

@ -10,7 +10,7 @@ from decimal import Decimal
from typing import Dict, List, Tuple
# Import from the parent package (following lnurlp pattern)
from ..calculations import calculate_commission, calculate_distribution, calculate_exchange_rate
from ..calculations import calculate_commission, calculate_distribution, calculate_exchange_rate, to_decimal
# =============================================================================
@ -245,11 +245,12 @@ class TestDistributionCalculation:
# Convert each client's sats back to fiat
total_fiat_distributed = sum(
sats / exchange_rate for sats in distributions.values()
Decimal(sats) / exchange_rate for sats in distributions.values()
)
# Should equal original fiat amount (within small rounding tolerance)
assert abs(total_fiat_distributed - fiat_amount) < 0.01, \
fiat_decimal = to_decimal(fiat_amount)
assert abs(total_fiat_distributed - fiat_decimal) < Decimal("0.01"), \
f"Fiat round-trip failed: {total_fiat_distributed:.2f} != {fiat_amount:.2f} " \
f"(crypto={crypto_atoms}, comm={comm_pct}, discount={discount})"
@ -287,11 +288,12 @@ class TestEmpiricalTransactions:
"expected_base_sats": 259029,
"expected_commission_sats": 7771,
"expected_distributions": {
# 259029 / 2 = 129514.5 → both get 129514 or 129515
# With banker's rounding: 129514.5 → 129514 (even)
# Remainder of 1 sat goes to first client by fractional sort
"client_a": 129515,
"client_b": 129514,
# 259029 / 2 = 129514.5 → both round to 129515 (ROUND_HALF_UP)
# Total = 259030, remainder = -1
# Both have same fractional (-0.5), client_a is first alphabetically
# So client_a gets -1 adjustment
"client_a": 129514,
"client_b": 129515,
},
},
# Add more scenarios from your real data!