Files
nifodea/tests/test_currency.py
T
oqyude d58547e8f8 T8: ISO 4217 per-currency decimals + format_money()
- Add CURRENCY_DECIMALS dict (JPY=0, BHD/KWD/OMR=3, rest=2) and DEFAULT_CURRENCY_DECIMALS=2
- Add CurrencyConverter.format_money() staticmethod with ROUND_HALF_UP; symbol-prefix for known, code-prefix for unknown
- Replace 14 :,.2f literals across services.py / info.py / config_cmd.py with format_money() calls
- Refactor 2 i18n templates (transaction_add.ok, recurring_add.ok): {amount:.2f} -> {amount_str}; callers pass pre-formatted string
- New _account_currency() helper for transaction currency lookup
- Extend test_currency.py: 8 new tests (JPY/BHD/USD/CHF/negative/zero/float-int/lowercase)
- 75/75 tests pass; 0 :,.2f literals remain in infrastructure/cli/
2026-10-09 19:24:47 +03:00

113 lines
4.3 KiB
Python

from decimal import Decimal
import pytest
from domain import CurrencyConverter, CurrencyError, ExchangeRate
class TestExchangeRate:
def test_to_dict_roundtrip(self):
rate = ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0)
d = rate.model_dump(mode="json")
r2 = ExchangeRate.model_validate(d)
assert r2.from_currency == "USD"
assert r2.to_currency == "RUB"
assert r2.rate == 80.0
def test_defaults(self):
r = ExchangeRate()
assert r.from_currency == "USD"
assert r.to_currency == "RUB"
assert r.rate == 80.0
class TestCurrencyConverter:
def test_convert_usd_to_rub(self, sample_converter):
result = sample_converter.convert(100, "USD", "RUB")
assert result == 8000.0
def test_convert_rub_to_usd(self, sample_converter):
result = sample_converter.convert(8000, "RUB", "USD")
assert result == 100.0
def test_same_currency(self, sample_converter):
result = sample_converter.convert(500, "USD", "USD")
assert result == 500.0
def test_unknown_pair(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.convert(100, "USD", "RUB")
def test_negative_rate(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.set_rate("USD", "RUB", -1)
def test_zero_rate(self):
converter = CurrencyConverter()
with pytest.raises(CurrencyError):
converter.set_rate("USD", "RUB", 0)
def test_inverse_auto(self):
rate = ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0)
converter = CurrencyConverter([rate])
assert converter.convert(900, "RUB", "EUR") == 10.0
def test_get_symbol(self, sample_converter):
assert sample_converter.get_symbol("USD") == "$"
assert sample_converter.get_symbol("RUB") == "₽"
assert sample_converter.get_symbol("XYZ") == "XYZ"
def test_with_defaults(self):
converter = CurrencyConverter.with_defaults()
assert converter.convert(10, "USD", "RUB") == 800.0
def test_multiple_rates(self):
rates = [
ExchangeRate(from_currency="USD", to_currency="RUB", rate=80.0),
ExchangeRate(from_currency="EUR", to_currency="RUB", rate=90.0),
]
converter = CurrencyConverter(rates)
assert converter.convert(10, "USD", "RUB") == 800.0
assert converter.convert(10, "EUR", "RUB") == 900.0
assert converter.convert(900, "RUB", "EUR") == 10.0
class TestFormatMoney:
"""ISO 4217-aware display formatting. Rounding mode: ROUND_HALF_UP."""
def test_jpy_zero_decimals(self):
c = CurrencyConverter()
assert c.format_money(Decimal("1234.567"), "JPY") == "¥1,235"
def test_usd_two_decimals(self, sample_converter):
assert sample_converter.format_money(Decimal("1234.567"), "USD") == "$1,234.57"
def test_bhd_three_decimals(self):
# BHD has 3 minor units and is NOT in CURRENCY_SYMBOLS — code prefix with space.
c = CurrencyConverter()
assert c.format_money(Decimal("1.234"), "BHD") == "BHD 1.234"
def test_chf_unknown_code_two_decimals(self):
# Unknown code falls back to DEFAULT_CURRENCY_DECIMALS=2, layout is code prefix.
c = CurrencyConverter()
assert c.format_money(Decimal("1.234"), "CHF") == "CHF 1.23"
def test_negative_amount(self, sample_converter):
# Sign must be rendered before the symbol, not embedded in the number.
assert sample_converter.format_money(Decimal("-1200.00"), "RUB") == "-₽1,200.00"
def test_zero_amount(self, sample_converter):
assert sample_converter.format_money(Decimal("0"), "JPY") == "¥0"
assert sample_converter.format_money(Decimal("0"), "USD") == "$0.00"
def test_accepts_float_and_int(self, sample_converter):
# Defensive: callers from CLI pass float — format_money must coerce.
assert sample_converter.format_money(1500.5, "USD") == "$1,500.50"
assert sample_converter.format_money(1500, "JPY") == "¥1,500"
def test_case_insensitive_currency_code(self, sample_converter):
# Defensive: lower-case codes are accepted; storage stays upper-case.
assert sample_converter.format_money(Decimal("100"), "usd") == "$100.00"