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/
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from domain import CurrencyConverter, CurrencyError, ExchangeRate
|
||||
@@ -70,3 +72,41 @@ class TestCurrencyConverter:
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user