Files
finance/.kilo/plans/1790055652854-savings-import-categories-blockers.md
T

26 KiB
Raw Blame History

Ближайший край: накопления, категории и блокировки бюджета

Цель и порядок

Сначала ввести корректную доменную модель системных накоплений, затем использовать её в импорте Альфа-Банка и ручной разметке операций. После этого закрыть пользовательские разрывы в управлении категориями и разборе причин, мешающих закрыть месяц.

Порядок реализации обязателен:

  1. Системный счёт и конверт «Накопления», корректировки и преобразование расходов.
  2. Автоматическая классификация «Копилки для сдачи» в Альфа debit.
  3. Полное управление пользовательскими категориями на странице бюджета.
  4. Человекочитаемые и сразу устранимые блокировки закрытия.

Что обнаружено

  • Альфа debit сейчас извлекает только направление, банковскую категорию и описание. Перевод создаётся лишь при совпадении двух импортированных операций на разных пользовательских счетах; технический счёт услуги «Копилка для сдачи» этому условию не соответствует.
  • Настоящий transfer обязан иметь два счёта и две противоположные проводки. Категория у переводов сейчас не используется бюджетом.
  • Остатки счетов производные и по принятому ADR не должны перезаписываться напрямую.
  • Backend уже имеет POST /budget/categories и сервисные функции переименования/архивации, но frontend их не вызывает, HTTP-контракт жизненного цикла неполон, архив скрыт.
  • Backend отдаёт коды IMPORT_UNRESOLVED_HOLDS и BUDGET_UNCATEGORIZED_EXPENSES, а локали содержат другие ключи в нижнем регистре, поэтому UI показывает enum. Endpoint разрешения HOLD уже есть, но frontend-действий для него нет.

Зафиксированные продуктовые решения

  • В каждом финансовом пространстве существует ровно один видимый системный счёт типа savings и один связанный системный savings_envelope с ключом savings.
  • Название локализуется как «Накопления» / “Savings”; системные счёт и категория не переименовываются, не архивируются и не удаляются.
  • Перевод с обычного счёта на системный счёт получает тип transfer и системную категорию «Накопления». Перевод обратно увеличивает общий пул.
  • Пользователь может преобразовать только подтверждённый расход в перевод на «Накопления» и отменить это преобразование. Доходы, возвраты и произвольные переводы не преобразуются этой командой.
  • Ручная смена остатка создаёт отдельную аудируемую adjustment-операцию на разницу. Она меняет активы и остаток конверта, но не доходы, расходы, cash flow или общий бюджетный пул.
  • Автоматически распознаётся только услуга Альфа-Банка «Копилка для сдачи». Снятие наличных, P2P-крипта и другие способы накопления размечаются пользователем вручную.
  • Категориями управляют внутри страницы «Бюджет»: создание, переименование, архивирование и восстановление. Системные категории показаны, но их действия заблокированы.
  • Причина блокировки содержит понятный текст, количество и действие, ведущее к месту исправления.

Обязательный дизайн-процесс

  • До изменения frontend-кода загрузить и применить pen-dev: проработать в существующем design.pen состояния управления категориями, корректировки накоплений, разметки операции и устранения бюджетных блокировок. .pen редактировать только Pencil-инструментами.
  • Затем применить design-an-interface (доступное имя скилла, соответствующее запросу design-interfaces) и сравнить минимум два заметно разных варианта структуры и interaction flow. Выбрать один вариант по числу шагов, ясности финансовых последствий, доступности и согласованности с текущим приложением, а не смешивать варианты.
  • Вёрстку выбранного варианта выполнять со скиллом frontend-design, сохраняя установленный визуальный язык проекта, существующие компоненты, типографику и плотность финансовых таблиц. Не вводить отдельный дизайн-язык или новый набор базовых UI-компонентов без необходимости.
  • После первой работающей вёрстки применить impeccable для критики и полировки UX-copy, визуальной иерархии, responsive-поведения, keyboard/focus flow, error/empty/loading states и доступности.
  • Для каждого затронутого flow проверить макет и реализацию в состояниях desktop и mobile, с данными, пустым списком, pending mutation, ошибкой, системной защищённой сущностью и архивной сущностью. Frontend-срез не считается законченным до browser screenshot review и устранения замечаний из impeccable.

1. Доменная основа накоплений

Схема и миграция

  • В backend/app/ledger/models.py добавить тип счёта savings, nullable Account.system_key и уникальность (space_id, system_key) для системного счёта.
  • Добавить тип операции adjustment. Для отображения знака получать дельту из единственной проводки корректировки, не делать amount_kopecks знаковым.
  • Добавить к операции признак обратимого преобразования из расхода и ссылку на предыдущую категорию. Это позволит отличить размеченный расход от обычного вручную созданного перевода и точно восстановить категорию.
  • Добавить уникальность системной пары категории по (space_id, system_key, linked_account_id); существующие резервы кредиток остаются допустимы.
  • Создать ревизию Alembic после 0002: безопасно заменить check constraints типов, добавить поля/индексы и создать для каждого существующего пространства системный счёт с нулевым открывающим остатком и связанную категорию. Новые пространства получают ту же пару в spaces.service.create_space.
  • Не переиспользовать существующие пользовательские счета/категории с текстом «Накопления»: идентичность задаётся только system_key, совпавшие пользовательские имена сохраняются.
  • Обновить docs/adr/0002-journal-and-budget-reserves.md: корректировка является журнальной операцией с одной проводкой; системный конверт связан со счётом; прямое изменение stored balance по-прежнему запрещено.

Сервисы и API

  • Вынести идемпотентный ensure_savings_resources(db, space_id) и использовать его при создании пространства и в доменных командах накоплений.
  • Добавить POST /accounts/{account_id}/adjust-balance с targetBalanceKopecks >= 0, датой и обязательной причиной. Под блокировкой строки счёта вычислять дельту, запрещать нулевое изменение и закрытый месяц, создавать adjustment и одну signed posting, писать old/new/delta в audit.
  • Добавить явные команды POST /operations/{id}/savings-transfer и POST /operations/{id}/savings-transfer/revert.
  • Преобразование разрешать только для confirmed expense не на системном счёте и в открытом периоде: сохранить прежнюю категорию, сменить тип на transfer, назначить counter account и системную категорию, добавить положительную проводку на накопления, сохранив исходную отрицательную проводку, description, payee, request key и import provenance.
  • Обратная команда разрешена только для помеченного преобразования: удалить проводку накоплений, вернуть expense, убрать counter account и восстановить предыдущую категорию. Обе команды аудируются и остаются tenant-scoped.
  • Обычные expense/income/import endpoints не должны использовать системный счёт как внешний счёт. Ручной create_transfer может переводить в накопления и обратно; при участии системного счёта он автоматически назначает системную категорию.
  • В API счёта вернуть systemKey/nameKey; пользовательский create-account по-прежнему принимает только cash | debit | credit.

Бюджетная семантика

  • В compute_period_state считать переводы в накопления обеспечением системного конверта и вычитать их из общего пула; переводы из накоплений считать обратным движением и возвращать в пул.
  • Для системного конверта: planned = 0, funded показывает чистое движение переводов выбранного месяца, spent = 0, available равен остатку связанного счёта на конец выбранного месяца. Корректировки меняют только available.
  • Не включать adjustment в income/expense totals, category spend, plan/fact или cash-flow. В balances/net worth системный счёт включается как актив.
  • Закрытый период защищает преобразование, возврат преобразования и корректировку тем же правилом reopen-before-change.

Frontend

  • Расширить типы, форматирование и локали для savings и adjustment; не добавлять adjustment в форму создания обычной операции.
  • В AccountsPage показать системный счёт обычной строкой с системным badge, без archive, и действием «Скорректировать». Drawer/modal содержит текущий и новый остаток, дату, причину и явно сообщает, что это не доход/расход.
  • В OperationsPage у подтверждённого расхода добавить действие «В накопления» с подтверждением; у преобразованного перевода — «Вернуть в расходы». В строке отображать исходный счёт → Накопления и категорию «Накопления».
  • Исключить системный счёт из выбора счёта выписки и форм expense/income/refund; оставить его доступным в ручной форме transfer и фильтрах.

2. Альфа debit: «Копилка для сдачи»

  • В backend/app/imports/adapters/base.py добавить к канонической SourceRow необязательную подсказку решения savings_transfer; сохранить её в source record как начальное review decision.
  • В alfa_xlsx.py распознавать нормализованную фразу Перечисление средств в рамках услуги "Копилка для сдачи" со счета <20 цифр> на счет <20 цифр>, проверять, что счёт-источник совпадает с номером счёта в заголовке выписки, и применять правило только к исходящей posted-строке. Не хардкодить пользовательские номера 408.../306...; они служат контрактным примером, а не конфигурацией одного пользователя.
  • Поднять ALFA_DEBIT_XLSX_VERSION, так как канонический результат парсинга меняется.
  • Добавить решение savings_transfer в import DTO/UI как «Перевод в накопления». Оно не использует DedupCandidate и не смешивается с существующим решением transfer, которое объединяет две банковские стороны.
  • При confirm для savings_transfer создавать перевод со счёта привязанной выписки на системный счёт накоплений, назначать системную категорию и обычный source link. До подтверждения пользователь может сменить решение на create или discard.
  • Сохранить атомарные confirm/revert и provenance: отмена импорта удаляет созданный перевод; повторный parse даёт то же решение; существующая дедупликация по external id продолжает выигрывать у создания дубля.
  • Контрактные тесты покрывают точное совпадение, пробелы/регистр/кавычки, несовпавший source account, входящую строку, HOLD и похожее описание без полного шаблона. Использовать только sanitized synthetic fixture.

3. Управление категориями

Backend-контракт

  • Оставить GET /budget/categories?month= для месячных вычисленных значений и добавить отдельный каталог определений GET /budget/category-definitions?includeArchived=true.
  • Каталог возвращает id, literal name либо чистый nameKey без префикса category., kind, systemKey, linkedAccountId, archived.
  • POST /budget/categories возвращает полный definition DTO, trim-ит имя и создаёт только monthly | savings_envelope.
  • Добавить PATCH /budget/category-definitions/{id} с optional name и archived. Системные категории отклоняют оба изменения; archived=false восстанавливает пользовательскую категорию.
  • Архивирование запрещает новые назначения/обеспечение/категоризацию, но не удаляет историю. Расчёт бюджета обязан продолжать учитывать архивную категорию, если в выбранном периоде есть plan/funding/operations, чтобы расходы не исчезали из totals.
  • Унифицировать сериализацию name/nameKey в budgeting, ledger и analytics; frontend должен получать savings, а не category.savings.

Дизайн и вёрстка

  • В секции категорий BudgetPage сделать заголовок с действием «Управлять категориями». Не добавлять новый пункт глобальной навигации.
  • Использовать существующий drawer/modal visual language. Верхняя часть: компактная форма «Название + тип + Создать». Ниже: вкладки «Активные / Архив», плотный список с badge типа и inline-действиями rename/archive/restore.
  • Системные «Накопления» и резервы кредиток отмечать badge «Системная»; вместо действий показывать короткое объяснение защиты.
  • Пустое состояние должно вести к созданию первой категории, а не предлагать только копирование прошлого месяца.
  • На mobile drawer занимает viewport, действия не зависят от hover, все inputs имеют labels, confirm archive получает фокус и возвращает его инициатору.
  • Добавить API methods, TanStack mutations, точечную invalidation budget/operations/analytics, success/error toasts и RU/EN parity.

4. Понятные и устранимые блокировки

Контракт и представление

  • Сделать BudgetBlocker discriminated union для известных кодов и данных: count для импортов/HOLD/расходов, categories для отрицательных конвертов. Машинный код остаётся в API и диагностике, но не является пользовательским текстом.
  • Вынести общий frontend-компонент/formatter для BudgetPage и OverviewPage; отказаться от динамического lookup, который при несовпадении показывает raw enum.
  • Показать отдельные строки причин с count и CTA:
    • IMPORTS_UNRESOLVED: «Не завершены импорты выписок: N» → очередь разбора;
    • IMPORT_UNRESOLVED_HOLDS: «Неподтверждённые операции без решения: N» → импорт(ы) с HOLD;
    • BUDGET_UNCATEGORIZED_EXPENSES: «Расходы без категории: N» → очередь разбора выбранного месяца;
    • BUDGET_NEGATIVE_ENVELOPE: перечислить категории → секция категорий.
  • Передавать выбранный месяц в ссылке /review?month=YYYY-MM; ReviewQueue использует его для date filters и категорий, а не всегда currentMonth().

Разрешение HOLD на frontend

  • Расширить import summary агрегированным unresolvedHoldCount, с optional month filter для ReviewQueue, чтобы confirmed-импорты с HOLD не исчезали из списка работ.
  • Расширить ImportRow полями bankStatus и holdResolution. Для HOLD не показывать обычный select решения.
  • В confirmed import для нерешённого HOLD показать два действия существующего endpoint: «Операция списалась» (confirm_as_posted) и «Операция отменена» (discard_as_cancelled). Оба открывают modal с обязательной причиной; первый использует mapped account документа.
  • После разрешения инвалидировать import detail/list, operations, accounts, budget и review queue. Закрытый месяц возвращает понятную ошибку о необходимости reopen.

Отказы и граничные случаи

  • Любая команда проверяет пространство, активность счетов, статус операции и открытость периода до изменения проводок.
  • Нельзя получить отрицательный target balance накоплений; нулевая корректировка отклоняется как no-op.
  • Повторное создание системных ресурсов идемпотентно; конфликт/дубликат системного ключа приводит к явной ошибке, а не выбору случайной записи.
  • Неизвестный blocker получает нейтральный локализованный fallback и логируемый code, но raw enum не показывается вместо сообщения.
  • Авто-правило «Копилки» fails closed: при неполном совпадении остаётся обычный расход, который пользователь может вручную отправить в накопления.
  • Архивированные категории недоступны новым операциям, но продолжают отображаться в истории и участвовать в уже созданных фактах.

Проверка

  1. Backend domain/property tests: две проводки savings-transfer суммируются в ноль; adjustment имеет одну signed posting; convert/revert восстанавливает тип, категорию, баланс и provenance; closed period и cross-space операции отклоняются.
  2. Migration tests/manual check: upgrade существующей БД создаёт ровно одну пару на пространство; fresh install не дублирует её; пользовательские одноимённые записи не захватываются.
  3. Budget tests: перевод в накопления уменьшает pool и увеличивает system envelope; обратный перевод делает обратное; adjustment не меняет pool/cash-flow; archived history не исчезает; blockers содержат корректные counts.
  4. Import tests: Alfa parser contract и parser version, confirm/revert, idempotency/dedup, HOLD и near-miss cases.
  5. API tests: CSRF для новых mutations, OpenAPI critical paths, system-resource protection, category archive/restore и HOLD resolution.
  6. Frontend Vitest: account adjustment modal, expense convert/revert, category manager CRUD/archive, blocker text/count/links для RU/EN, month-aware ReviewQueue и HOLD modal.
  7. Обновить frontend/openapi.local.json и frontend/src/api/generated/schema.d.ts, затем выполнить make backend-check, make frontend-check и полный make check.
  8. По обязательному дизайн-процессу сверить реализацию с выбранными слоями в design.pen, затем со скиллом impeccable проверить в браузере desktop/mobile: Budget category drawer, Accounts adjustment, Operations actions, Budget/Overview blocker panel, ReviewQueue и confirmed ImportDetail; проверить keyboard flow, responsive states и отсутствие console errors.
  9. После исходных изменений выполнить graphify update ..

Вне этого края

  • Несколько системных счетов накоплений, цели со сроком/суммой, иерархия или split категорий.
  • Автоматические правила для банкоматов, P2P, криптовалют и произвольных описаний; для них остаётся ручная разметка расхода.
  • Преобразование income/refund в перевод и универсальный редактор типа любой операции.
  • Пересчёт или автоматическая мутация уже закрытой истории без явного reopen.