Files
nifodea/.agent/archive/requests/req-T8.json
T
oqyude 9175d5c76b T8: define task — ISO 4217 per-currency decimals (req ready)
Follow-up to T3 (Decimal migration): display layer still hardcodes :,.2f
everywhere, which is wrong for JPY (0 dp) and 3-dp currencies like BHD/KWD/OMR.

T8 = display-only refactor:
  - CURRENCY_DECIMALS + CurrencyConverter.format_money() in domain/
  - replace 14 :,.2f literals across infrastructure/cli/
  - rework 2 i18n templates that bake the format into the message string
  - extend tests/test_currency.py with JPY/BHD/USD/CHF cases

Locked decisions (HALF_UP, ',' separator, suffix for unknown codes,
internal precision untouched) recorded in req-T8.json for the executing node.
2026-10-09 18:50:12 +03:00

96 lines
7.7 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"request_id": "req-T8",
"task_id": "T8",
"title": "ISO 4217-aware форматирование денег (per-currency decimals)",
"status": "ready_for_review",
"created_at": "2026-10-09T12:00:00Z",
"goal": "Исправить наивное форматирование денег (захардкоженные :,.2f) — ввести per-currency разрядность по ISO 4217, единый форматтер, корректное отображение JPY (0 знаков), BHD/KWD/OMR (3 знака) и остальных валют из CURRENCY_SYMBOLS.",
"decisions": {
"rounding_mode": {
"value": "ROUND_HALF_UP",
"rationale": "Совместимо с T3 и интуитивно (0.5 → 1). ROUND_HALF_EVEN (banker's rounding) — для банковской отчётности; для персональной финмодели overkill. Зафиксировать в format_money() как единственное место округления при отображении."
},
"thousands_separator": {
"value": ",",
"rationale": "Сохраняем текущее поведение (:,.2f). Locale-чувствительное форматирование (ru: пробел, en: запятая) — отдельная задача T9+, не входит в T8."
},
"unknown_currency_layout": {
"value": "suffix",
"example": "1,234.56 CHF",
"rationale": "Символ валюты-как-префикс работает только если валюта в CURRENCY_SYMBOLS. Для неизвестных (CHF, TRY, AED) — суффикс через пробел. Код валюты (USD, JPY) тоже суффиксом при отсутствии символа, чтобы не путать с уже известными."
},
"internal_precision": {
"value": "сохранить Decimal-арифметику без изменений",
"rationale": "T8 только про display. _q() в forecast.py и quantize('0.01') в convert() оставить как есть. Их влияние на точность прогноза и кросс-валютной конвертации — отдельный T9 (если будет нужен). Это решение изолирует blast radius."
}
},
"scope": {
"in_scope": [
"Добавить CURRENCY_DECIMALS в domain/currency.py (RUB/USD/EUR/GBP/CNY/JPY/KZT/UAH = 2 кроме JPY=0; BHD/KWD/OMR = 3 как примеры)",
"Реализовать CurrencyConverter.format_money(amount: Decimal, currency: str) -> str",
"Заменить все 14 литералов :,.2f в infrastructure/cli/ на вызов format_money()",
"Переработать 2 i18n-шаблона с {amount:.2f} — передавать уже отформатированную строку",
"Расширить tests/test_currency.py кейсами: JPY (0 знаков), BHD (3 знака), USD (2 знака), неизвестная валюта (suffix)"
],
"out_of_scope": [
"Изменение _q() в application/forecast.py (точность хранения)",
"Изменение quantize() в CurrencyConverter.convert() (точность кросс-валютной конвертации)",
"Locale-чувствительное форматирование (ru: пробел как разделитель)",
"Поддержка криптовалют, не-ISO 4217 кодов",
"Изменение ExcelRepository (формат ячеек в .xlsx)"
]
},
"files_to_change": [
"domain/currency.py (новые CURRENCY_DECIMALS, format_money)",
"infrastructure/cli/services.py (2 места format_money)",
"infrastructure/cli/commands/info.py (5 мест format_money)",
"infrastructure/cli/commands/config_cmd.py (5 мест format_money)",
"infrastructure/cli/i18n.py (2 шаблона, .format(amount_str=...) вместо .format(amount=...))",
"infrastructure/cli/commands/config_cmd.py (callers i18n-шаблонов — передавать amount_str)",
"tests/test_currency.py (новые кейсы format_money)"
],
"acceptance_criteria": [
"format_money(Decimal('1234.567'), 'JPY') == '¥1,235' (без десятичных)",
"format_money(Decimal('1234.567'), 'USD') == '$1,234.57' (2 знака, HALF_UP)",
"format_money(Decimal('1.234'), 'BHD') == 'BHD 1.234' (3 знака, suffix для неизвестного)",
"format_money(Decimal('1.234'), 'CHF') == 'CHF 1.23' (2 знака default, suffix)",
"format_money(Decimal('-1200.00'), 'RUB') == '-₽1,200.00' (отрицательные работают)",
"format_money(Decimal('0'), 'JPY') == '¥0'",
"Ни одного литерала :,.2f для денег в infrastructure/cli/ (можно оставить в unit-тестах format_money и в не-денежных format-строках)",
"i18n-шаблоны cmd.config.transaction_add.ok и cmd.config.recurring_add.ok больше не содержат {amount:.2f}",
"Все 67 существующих тестов проходят (без изменений кода тестов, кроме test_currency.py)",
"Минимум 5 новых тестов в test_currency.py (по одному на JPY/BHD/USD/CHF/негативный кейс)"
],
"verification": {
"tests_passed_target": "67 существующих + 5 новых = 72/72",
"lsp_clean": true,
"cf_info_with_jpy_account": "показывает целые йены, без .00",
"cf_config_transaction_add_ru": "ok-message содержит корректно отформатированную сумму (e.g. '1 200,50 ₽' или '$1,200.00' в зависимости от локали счёта)",
"no_remaining_hardcoded_decimals": "grep -rn ':\\.2f' infrastructure/cli/ | grep -v test | grep -v 'not money' не находит денежных литералов"
},
"fulfills_ac": [
"format_money() — единственная точка форматирования денег в domain-слое",
"ISO 4217-корректное отображение для JPY (0 знаков) и 3-значных валют",
"CURRENCY_DECIMALS покрывает все валюты из CURRENCY_SYMBOLS плюс BHD/KWD/OMR",
"Все денежные литералы в CLI заменены на format_money()",
"Тесты JPY/BHD/USD/CHF/негативный кейс",
"pytest 72/72"
],
"execution_hints": {
"preferred_diff_approach": "Сначала ввести CURRENCY_DECIMALS + format_money() в domain/, написать тесты, проверить зелёные. Потом по одному менять callers (services.py → info.py → config_cmd.py → i18n.py), после каждого — pytest.",
"risk_areas": [
"i18n-шаблоны: format() с kwargs — если забыть передать новый amount_str, KeyError в рантайме. Защита: f'format ... {amount_str}' с дефолтом через .get()",
"config_cmd.py: 5 мест с разным контекстом (account, transaction, recurring, asset, liability) — все нужно заменить консистентно",
"Negative amounts: f-string в Python не даёт ведущий минус по-другому, но Decimal('−1200') в format() может дать неожиданные скобки. Тест обязателен."
],
"rollback": "Если T8 ломает > 2 тестов из существующих 67 — откатить, переоценить scope. Скорее всего, проблема в callers, не в format_money()."
}
}