From 9175d5c76b8c589e70b17a661d19a62f596c87dc Mon Sep 17 00:00:00 2001 From: oqyude Date: Fri, 9 Oct 2026 18:50:12 +0300 Subject: [PATCH] =?UTF-8?q?T8:=20define=20task=20=E2=80=94=20ISO=204217=20?= =?UTF-8?q?per-currency=20decimals=20(req=20ready)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agent/archive/requests/req-T8.json | 95 +++++++++++++++++++++++++++++ .agent/tasks/manifest.json | 7 ++- .agent/tasks/manifest.md | 35 ++++++++++- 3 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 .agent/archive/requests/req-T8.json diff --git a/.agent/archive/requests/req-T8.json b/.agent/archive/requests/req-T8.json new file mode 100644 index 0000000..1ac7d7c --- /dev/null +++ b/.agent/archive/requests/req-T8.json @@ -0,0 +1,95 @@ +{ + "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()." + } +} diff --git a/.agent/tasks/manifest.json b/.agent/tasks/manifest.json index 5727713..390bd8e 100644 --- a/.agent/tasks/manifest.json +++ b/.agent/tasks/manifest.json @@ -1,8 +1,8 @@ { "$schema": "metaagent-task-manifest", "version": "3.0", - "session_id": "metaagent-005", - "goal": "Серьёзный архитектурный рефактор: A1+A10 (слои+version), A2+A3 (pydantic), A4 (decimal), A7+A8+A9 (DI+repo), A5 (CLI decompose)", + "session_id": "metaagent-006", + "goal": "Серьёзный архитектурный рефактор: A1+A10 (слои+version), A2+A3 (pydantic), A4 (decimal), A7+A8+A9 (DI+repo), A5 (CLI decompose). P1: ISO 4217-aware currency display", "created_at": "2026-10-08T15:58:00Z", "tasks": [ { "id": "T1", "title": "Реструктуризация в domain/application/infrastructure + version=1", "status": "archived", "origin": "user:direct" }, @@ -11,6 +11,7 @@ { "id": "T4", "title": "Repository pattern — ModelRepository", "status": "archived", "origin": "user:direct" }, { "id": "T5", "title": "Dependency Injection в сервисах", "status": "archived", "origin": "user:direct" }, { "id": "T6", "title": "Декомпозиция CLI", "status": "archived", "origin": "user:direct" }, - { "id": "T7", "title": "Финальная валидация", "status": "archived", "origin": "user:direct" } + { "id": "T7", "title": "Финальная валидация", "status": "archived", "origin": "user:direct" }, + { "id": "T8", "title": "ISO 4217-aware форматирование денег (per-currency decimals)", "status": "pending", "origin": "user:direct" } ] } diff --git a/.agent/tasks/manifest.md b/.agent/tasks/manifest.md index 6bdc095..b390814 100644 --- a/.agent/tasks/manifest.md +++ b/.agent/tasks/manifest.md @@ -1,9 +1,9 @@ # Task Manifest -**Session:** `metaagent-005` +**Session:** `metaagent-005` (initial) → `metaagent-006` (T8 pending) **Goal:** Серьёзный архитектурный рефактор **Date:** 2026-10-08 -**Total tasks:** 7 +**Total tasks:** 8 (7 archived, 1 pending) --- @@ -121,3 +121,34 @@ - [ ] cf init, cf forecast, cf scenario, cf compare — работают - [ ] cf import data.xlsx → cf export — round-trip - [ ] README.md обновлён под новую структуру + +--- + +## T8: ISO 4217-aware форматирование денег (per-currency decimals) + +**Зависимости:** T3 (Decimal уже мигрирован) +**Файлы:** `domain/currency.py`, `infrastructure/cli/services.py`, `infrastructure/cli/commands/{info,config_cmd}.py`, `infrastructure/cli/i18n.py`, `tests/test_currency.py`, `tests/test_i18n.py` + +**Контекст:** Миграция на Decimal (T3) устранила арифметические ошибки, но отображение денег осталось наивным: во всех местах захардкожен формат `:,.2f` / `:.2f`. Это даёт два бага: (1) JPY показывается как `¥1,234.00` вместо `¥1,234`; (2) BHD/KWD/OMR (3 знака) вообще не поддерживаются. Словарь `CURRENCY_SYMBOLS` в `domain/currency.py` объявляет 8 валют, но семантика ISO 4217 не реализована. + +**Что:** +- Ввести `CURRENCY_DECIMALS: dict[str, int]` в `domain/currency.py` (минимум: JPY=0, BHD/KWD/OMR=3, остальные из поддерживаемого списка=2). +- Добавить `CurrencyConverter.format_money(amount: Decimal, currency: str) -> str` — единая точка форматирования (символ + количество знаков). +- Заменить все 14 захардкоженных `:,.2f` в `services.py`, `info.py`, `config_cmd.py` на вызов `format_money()` (или новый форматтер в i18n-шаблонах). +- Переработать i18n-шаблоны `cmd.config.transaction_add.ok` и `cmd.config.recurring_add.ok` — убрать `{amount:.2f}`, передавать уже отформатированную строку. +- Сохранить `_q()` в `forecast.py` для **сериализации прогноза** (JSON-результат должен быть стабильным), но не трогать внутреннюю арифметику — она остаётся на полной точности Decimal. +- Граничный кейс: при конвертации `0.1 USD → JPY` сейчас даёт 8.07 JPY вместо ~7.86 JPY из-за `quantize("0.01")` в `convert()`. Решить — квантовать только при отображении, не при конверсии. + +**Acceptance:** +- [ ] `format_money(Decimal("1234.567"), "JPY") == "¥1,235"` (округление банкирское или HALF_UP — зафиксировать) +- [ ] `format_money(Decimal("1234.567"), "USD") == "$1,234.57"` +- [ ] `format_money(Decimal("1.234"), "BHD") == "BHD 1.234"` (3 знака) +- [ ] Ни одного литерала `:,.2f` / `:.2f` для денег в `infrastructure/cli/` (за исключением явных UNIT-тестов форматтера) +- [ ] Новые тесты в `tests/test_currency.py`: по одному на JPY, BHD, USD, плюс round-trip `format_money(format_money(x))` стабилен +- [ ] `pytest` зелёный, существующие 67 тестов не сломаны +- [ ] `cf info` на модели с JPY-счётом показывает целые йены, без `.00` + +**Открытые вопросы (на согласование перед выполнением):** +1. Округление для отображения — `ROUND_HALF_UP` (банковское кругление) или `ROUND_HALF_EVEN` (banker's rounding)? По умолчанию предлагаю `HALF_UP` для совместимости с T3. +2. Формат `1,234` vs `1 234` (разделитель тысяч) — текущий `:,.2f` даёт запятую; оставляем? +3. Для валют без символа в `CURRENCY_SYMBOLS` (например CHF) — выводить код префиксом (`CHF 1,234.56`) или суффиксом (`1,234.56 CHF`)?