diff --git a/openspec/changes/add-manual-bank-cards/.openspec.yaml b/openspec/changes/add-manual-bank-cards/.openspec.yaml new file mode 100644 index 0000000..c4c60f9 --- /dev/null +++ b/openspec/changes/add-manual-bank-cards/.openspec.yaml @@ -0,0 +1,2 @@ +schema: wealth-ledger-sdd +created: 2026-09-12 diff --git a/openspec/changes/add-manual-bank-cards/design.md b/openspec/changes/add-manual-bank-cards/design.md new file mode 100644 index 0000000..81b3521 --- /dev/null +++ b/openspec/changes/add-manual-bank-cards/design.md @@ -0,0 +1,445 @@ +## Context + +**Revision:** 1. **Status:** согласован пользователем для подготовки Tasks; +реализация не начата. +Основание — [Proposal](proposal.md) и [Delta Specs](specs/manual-bank-cards/spec.md) +с ответами пользователя Q1–Q9. Здесь определён способ реализации; проверяемое +поведение и денежные правила принадлежат Delta Specs. + +WealthLedger уже использует React/Mantine, TanStack Query, NestJS, +Prisma и локальную SQLite. Брокерские сведения приходят из T-Bank и временно +кэшируются; пользовательские банковские операции станут долговременными +данными. Их источник и правила доступа отличаются от брокерского журнала. + +Подход опирается на существующие границы: [контейнеры](../../../../docs/architecture/containers.md), +[Money](../../../../docs/glossary.md#money), [backend data](../../../../backend/.ai/conventions/backend-data.md) +и [frontend data](../../../../frontend/.ai/conventions/frontend-data.md). +Ни внешний банк, ни новый сервис для ручного учёта не требуются. + +## Goals / Non-Goals + +**Goals:** один владелец ручного учёта, точные денежные расчёты, согласованные +переводы и возвраты, безопасные исправления и повторы, сохранение истории +между запусками, два экрана в существующей визуальной системе. + +**Non-Goals:** новый стек хранения, универсальная бухгалтерская платформа, +двойная запись, event sourcing, серверный кэш банковского учёта, отдельный +изменяемый баланс или конвертация валют. Продуктовые границы перечислены +в Proposal; этот Design их не расширяет. + +## Decisions + +### 1. Отдельная область ручного банковского учёта + +Backend получает один `BankCardsModule` с controller, сервисом сценариев, +DTO и чистыми функциями расчёта. Сервис использует существующий `PrismaService`. +Controller отвечает за транспорт, сервис — за владение данными, связи +операций, проверки и транзакции. Generic repository, интерфейс для одной +реализации и отдельные сервисы на каждый арифметический шаг не нужны. + +`JwtAuthGuard` и `request.user.sub` остаются источником локальной идентичности. +Каждая карта, категория и операция выбирается с ограничением по владельцу; +все дополнительные ссылки проверяются в том же сценарии. Неизменяемый +владелец связанных записей не определяется из входного `userId`. + +`TbankAccessService` не используется: он устанавливает доступ через подключение +и счета брокера. Ручные карты доступны без T-Bank. Замена или удаление +брокерского подключения не удаляет и не инвалидирует ручную историю; +logout по-прежнему очищает данные сессии в браузере. + +### 2. Три сохраняемые модели + +| Модель | Данные и ответственность | +| --- | --- | +| `BankCard` | `id`, `userId`, имя, необязательные банк и последние четыре цифры, начальный остаток в копейках, дата начала учёта, `version`, `archivedAt`, неизменяемый fingerprint создания, служебные даты | +| `ExpenseCategory` | `id`, `userId`, название, нормализованный ключ уникальности названия, необязательный неизменяемый `seedKey`, `version` | +| `BankCardOperation` | `id`, `userId`, вид, `cardId`, необязательные `targetCardId`, `categoryId`, `originalExpenseId`, положительная сумма в копейках, дата операции, описание, `version`, `deletedAt`, неизменяемый fingerprint создания, служебные даты | + +Новые таблицы ссылаются на существующий `User`. Foreign keys сохраняют +ссылочную целостность; ограничения уникальности защищают идентификаторы +и категории при гонках. Совпадение владельцев, допустимые сочетания полей +и финансовые зависимости проверяются сервисом внутри транзакции. +Удаление карты или категории каскадом не применяется. + +При первой карте её создание и начальный набор пользовательских категорий +сохраняются согласованно. `seedKey` позволяет повторно использовать начальные +категории после переименования и удерживает роль категории по умолчанию. +Если первая запись категории создаётся раньше карты, тот же сценарий сначала +обеспечивает начальный набор. Чтение не создаёт данных, повторная инициализация +не возвращает старые названия и не добавляет дубли. + +Перевод — одна `BankCardOperation`: `cardId` задаёт источник, `targetCardId` +получателя. Две строки истории являются представлениями одной записи +относительно выбранной карты. Изменение или удаление этой записи затрагивает +оба эффекта; отдельного сохраняемого состояния «половина перевода» нет. + +Возврат ссылается на расход через `originalExpenseId`. Его карта проверяется +по исходному расходу, а категория получается через эту связь. Собственная +копия категории в возврате не хранится, поэтому исправление категории покупки +не оставляет возвраты в прежней группе. Вид операции и ссылка возврата +на исходную покупку после создания неизменны. + +### 3. Копейки в хранилище, Money на внешней границе + +Начальный остаток и суммы операций сохраняются как `BigInt` целых копеек. +Входные денежные значения проверяются по диапазону и точности Delta Specs +до преобразования и записи. Расчёты используют `bigint`; результат не проходит +через `Number`, `parseFloat` или SQLite `SUM` с ограничением 64-битного итога. +Вычисленный баланс может выходить за диапазон отдельного сохраняемого значения. + +Публичный контракт остаётся Money `{ currency, value }`, где `value` — точная +decimal string. Ограничение двух знаков относится к новому вводу RUB, +а не к существующим брокерским Money. Frontend использует существующий +[formatMoney](../../../../frontend/src/shared/lib/display.ts) и не складывает суммы. + +Уже есть [decimal helpers](../../../../backend/src/tbank/decimal.ts) и +`MoneyDto` в [portfolio.dto.ts](../../../../backend/src/tbank/portfolio.dto.ts). +Общие parsing/formatting и денежный DTO получают общий owner вне `tbank`; +protobuf-преобразование остаётся у интеграции. Существующие потребители +продолжают использовать ту же точную арифметику и тот же публичный Money-контракт. +Новая денежная библиотека и две копии парсера не нужны. + +### 4. Баланс и сводка вычисляются из первичных записей + +| Вид | Эффект на баланс | Эффект на месячную сводку | +| --- | --- | --- | +| Расход | Минус сумма у `cardId` | Расход по категории и дате покупки | +| Пополнение | Плюс сумма у `cardId` | Не включается | +| Перевод | Минус у источника, плюс у получателя | Не включается у обеих сторон | +| Возврат | Плюс сумма у карты исходного расхода | Возврат по категории покупки и собственной дате | + +Backend выбирает минимальные необходимые поля и выполняет точный fold. +Баланс использует начальный остаток и всю неудалённую историю карты; месячная +сводка использует относящиеся к месяцу расходы и возвраты. Начальный остаток +не создаёт синтетическое поступление. Архивная карта остаётся в исторической +сводке «Все карты», чтобы архивирование не переписывало месячные итоги. + +Внутри одного ответа связанные первичные данные читаются согласованно. +Если требуется несколько зависимых чтений, они используют одну read-транзакцию; +независимые запросы интерфейса не получают общей транзакции ради внешнего вида. +Frontend после подтверждённых изменений перечитывает связанные представления. + +Цена подхода — O(N) просмотр нужной ручной истории при расчёте. Для первой +версии это проще второго сохраняемого баланса, его восстановления и +инвалидации. В реализации у fold будет комментарий `ponytail:` с этим +пределом и переходом к измеренной оптимизации при росте истории. Пагинация +журнала не используется для ограничения финансового расчёта. + +Даты ручного учёта сохраняются как проверенные календарные `YYYY-MM-DD`, +а не как фиктивный момент полуночи. Сегодня и текущий месяц определяются +в Europe/Moscow; выбранный месяц — явный параметр. Это сохраняет календарный +смысл введённого дня независимо от часового пояса браузера. + +### 5. Транзакции защищают связанные изменения + +Проверка владельцев и активности всех старых и новых ссылок, проверка дат, +финансовых зависимостей и запись выполняются в одной DB-транзакции. +Изменение начальной даты карты проверяет всю зависимую историю, включая +переводы, в которых карта является получателем. Сетевых запросов к банкам +внутри транзакции нет. + +При создании, изменении или удалении возврата исходный расход участвует +в условном обновлении версии внутри той же транзакции. Проверка оставшейся +доступной к возврату суммы выполняется после получения права на изменение. +При редактировании сумма остальных неудалённых возвратов складывается +с новой суммой текущего: редактируемая запись не учитывается дважды. +Так два возврата и одновременное уменьшение исходного расхода не обходят +общий лимит, а изменение описания полного возврата остаётся допустимым. +Неудачная проверка откатывает всё действие. + +Операции, редактирование метаданных и архивирование согласуются средствами +SQLite-транзакций. Конфликт занятой базы или сериализации не превращается +в частичный успех. Для кратковременного DB-конфликта допускается ограниченный +повтор всей транзакции с тем же идентификатором намерения; пользовательский +конфликт версии не повторяется скрыто. In-memory mutex и глобальный +transaction manager не вводятся. + +Редактирование и удаление используют `version`: условное обновление по +идентификатору, владельцу и ожидаемой версии. Устаревшая форма получает +конфликт; frontend сохраняет ввод и выполняет повторный GET для получения +актуальной записи, а не ожидает финансовый DTO внутри ответа об ошибке +и не затирает изменение из другой вкладки. Начальный остаток и дата начала также защищены +версией карты. Принцип уже используется в +[AuthService](../../../../backend/src/auth/auth.service.ts) для conditional update; +финансовые правила остаются у нового владельца. + +### 6. Повтор создания не создаёт вторую карту или операцию + +При открытии формы новой карты или операции клиент создаёт UUID будущей записи +и сохраняет его до определённого результата. Повтор после сетевой ошибки +и общий auth-retry используют тот же UUID. Это идентификатор пользовательского +действия, а не транспортный `X-Request-ID`. + +Запись хранит неизменяемый fingerprint нормализованного исходного ввода, +рассчитанный стандартным `node:crypto`. Уникальный идентификатор защищён +ограничением базы. Повтор с тем же владельцем, UUID и fingerprint не применяет +создание второй раз; другой ввод с тем же UUID даёт конфликт. +После проверки сессии и формы входа уже сохранённое намерение разрешается +до проверки изменяемых условий нового создания: архива, текущей даты +и доступного лимита возврата. Полный возврат с потерянным ответом не становится +новым возвратом с нулевым лимитом, а поздний повтор создания архивированной +карты возвращает её актуальное состояние. +При коллизии создания сервис перечитывает результат и проверяет владельца +и fingerprint, не полагаясь только на предварительный lookup. + +Редактирование не меняет fingerprint создания. Удаление оставляет tombstone +с идентификатором и fingerprint: запоздалый повтор не воскрешает запись. +Для уже изменённой записи возвращается её актуальное состояние; для удалённой +— понятный безопасный отказ без нового сохранения. После определённого +отказа пользователь может сознательно начать новое действие с новым UUID; +автоматическая смена UUID при неопределённом исходе запрещена. + +В обеих моделях fingerprint остаётся неизменным; общего журнала команд +или отдельной таблицы idempotency не требуется. + +Удаление операций логическое (`deletedAt`), не полное версионирование или бухгалтерский +журнал аудита. Повтор удаления своей уже удалённой операции безопасен. +Восстановление удалённых операций отдельным пользовательским сценарием +в первую версию не добавляется. Архив карты обратим; для финансовых +исправлений архивную карту сначала возвращают в активный список. + +### 7. HTTP и frontend сохраняют существующие границы + +Новый API размещается под `/api/v1/bank-cards`. Внутри этой области находятся +ресурсы карт, категорий, операций и месячной сводки. Одно представление +перевода изменяется по идентификатору операции независимо от того, из какой +карты открыта форма. Для выбора исходного расхода возврата доступно чтение +расходов карты за всю историю, без ограничения месяцем открытого журнала. + +Runtime DTO валидируют форму, сервис — связанные ограничения. Чужие +и отсутствующие ресурсы не раскрывают финансовых сведений. Ошибки используют +существующий filter и стабильные коды для невалидного ввода, отсутствия +ресурса, архива, конфликта версии, повторного намерения и зависимых возвратов. +Конкретная исполняемая форма ответа принадлежит backend OpenAPI, а не +дублирующей таблице DTO в спецификациях. + +Frontend получает типы из нового runtime OpenAPI через существующий +[contract workflow](../../../../docs/development/runtime-openapi-verification.md). +Используются `apiClient`/`apiQueryClient`, session-aware queries и общий auth +middleware. Свой Bearer header, refresh loop или вторую систему запросов +новая область не вводит. + +После успешного изменения данных инвалидируются банковские GET-запросы: +список, детали, категории, страницы истории, выбор исходного расхода и +все месячные сводки. Такой scope покрывает обе стороны перевода, изменение +даты между месяцами и перегруппировку связанных возвратов. Брокерские кэши +не затрагиваются. Оптимистический пересчёт денежных сумм не используется; +ошибка mutation сохраняет прежние данные и введённую форму. + +Композиции принадлежат `pages/cards` и `pages/card`; каждая финансовая страница +имеет container, VM и presentation views. Общие для обеих страниц banking +queries и представления принадлежат `entities/bank-card`. Только реально +повторяющиеся формы получают общего владельца; универсальный ledger, +новый слой widgets и отдельный feature на каждое поле не нужны. + +## UI/UX Applicability and Package + +**Applicability and scope:** `new/changed` — новый раздел «Карты», список, +страница карты, формы карты/операций/категорий и месячная сводка. + +**Package revision:** 1. Пакет полностью находится в этом Markdown: схемы +композиции, состояния, действия и адаптивность. Отдельные PNG/SVG и внешние +макеты для согласования этой версии не требуются; self-hash не используется. + +**Screens, states and interactions:** + +| Экран | Состав и основные действия | +| --- | --- | +| `/cards?month=YYYY-MM` | Заголовок «Карты», «Добавить карту», список активных карт, переключатель показа архива, месячная сводка всех карт, «Категории» после появления первой карты | +| `/cards/:cardId?month=YYYY-MM` | Крошки «Карты / Название», баланс, «Добавить операцию», изменение сведений/архив, сводка и журнал выбранного месяца | +| Архивная карта | Явная отметка архива, доступные баланс/история/сводки, действие «Вернуть из архива»; финансовый ввод и исправления недоступны | + +```text +Карты [Добавить карту] +[Повседневная 24 800 ₽] [Запасная 10 000 ₽] +Баланс по внесённым операциям [Показать архив] + +Расходы по всем картам [Сентябрь 2026] +Потрачено Возвращено После возвратов +8 500 ₽ 1 000 ₽ 7 500 ₽ +Продукты … ₽ Кафе … ₽ Такси … ₽ +[Категории] + +Карты / Повседневная +Повседневная 24 800 ₽ [Добавить операцию] +Баланс по внесённым операциям [Изменить карту] +Расходы за месяц [Сентябрь 2026] +Потрачено / Возвращено / После возвратов +Категории и суммы +Операции +12.09 Расход · Кафе −500 ₽ +11.09 Перевод на «Запасная» −2 000 ₽ +10.09 Возврат · Покупки +1 000 ₽ + [Назад] [Далее] +``` + +Главная иерархия карты — название, баланс, действие, журнал. Начальные +декоративные изображения банков не загружаются: название и необязательные +последние цифры различают карты без внешнего каталога. Связанные ссылки +перевода объясняют направление; одна карта не имитирует брокерский счёт. + +Сводка показывает расходы, возвраты и разницу отдельно. Отрицательный итог +месяца или категории отображается со знаком и поясняется возвратами; +процентный круговой график для таких величин не используется. Баланс +остаётся текущим при смене месяца. Журнал упорядочен по дате операции, +затем времени создания и идентификатору; переходы не обещают неизменяемый +снимок истории между конкурентными изменениями. + +| Форма | Ввод и взаимодействие | +| --- | --- | +| Карта | Название, необязательные банк и последние четыре цифры, начальный остаток, дата начала. Пояснение: «Остаток до операций с выбранной даты». Валюта RUB фиксирована | +| Расход | Сумма, дата, предвыбранная категория по умолчанию (изначально «Другое»), необязательное описание | +| Пополнение | Сумма, дата, необязательное описание; без категории расходов | +| Перевод | Карта-источник и другая активная карта-получатель, сумма, дата, описание; одно подтверждение для обеих сторон | +| Возврат | Исходный расход за любой период, дата, сумма не выше доступной к возврату, описание; карта и категория берутся из покупки. При редактировании лимит равен сумме покупки минус остальные неудалённые возвраты | +| Категории | Начальные и свои категории одним списком; добавление и переименование, без вложенности и удаления | + +Выбор возврата не ограничивается текущим месяцем: он показывает дату, +описание/категорию покупки, сумму и оставшуюся доступную сумму. Добавление +категории из формы расхода сохраняет уже введённые поля. Изменение операции +сохраняет её вид; ограничения связанных возвратов объясняются у действия. + +Перед удалением операции подтверждение показывает сумму и последствия, +для перевода — обе карты. При конфликте версии или зависимых возвратах +форма сохраняет ввод и предлагает перечитать затронутую запись. Архивирование +не удаляет историю и имеет явное обратное действие. + +**States and recovery:** + +| Состояние | Представление и восстановление | +| --- | --- | +| Initial loading | Структурный skeleton соответствующих областей, доступное состояние занятости, без фиктивных цифр и фокуса на декорациях | +| Нет карт | «Добавьте первую карту» и действие добавления | +| Пустой месяц | Нет операций/расходов за месяц, известные нулевые суммы, сохранённый текущий баланс | +| Ошибка без данных | Понятное сообщение и повторная загрузка, без подмены данных нулями | +| Фоновое обновление или его ошибка | Последние успешные данные остаются; рядом progress или предупреждение и retry | +| Локальная ошибка сводки | Доступная карта и журнал остаются видимыми | +| Invalid / pending формы | Ошибки полей без запроса; при отправке действия формы блокируются, включая закрытие | +| Ошибка сохранения | Alert, сохранённый ввод и безопасный повтор того же действия; форма закрывается только после определённого успеха | +| Чужая/отсутствующая карта | Нейтральное «Карта не найдена» и переход к списку без раскрытия чужих данных | + +Формы используют Mantine Modal, React Hook Form, Zod и FormProvider. +Модальное окно удерживает фокус и возвращает его вызвавшему действию. +Поля имеют видимые подписи, кнопки — доступные названия, суммы — знак +и текст вида операции независимо от цвета. Продуктовые строки принадлежат +существующему русскому i18n-словарю. + +**Responsive behavior:** на широком экране карты размещаются в две колонки, +на узком — в одну; заголовки и действия переносятся. Формы становятся +одноколоночными и помещаются по ширине экрана. Длинные названия и суммы +не скрывают действия; журнал и формы не перекрываются нижней навигацией. +Пятый пункт навигации «Карты» проверяется на ширине 320–375 px. Фокус, +клавиатурное управление и reduced motion сохраняются. + +**Reused source and differences:** это новый пакет на текущей общей теме, +а не повторное использование архивного UI-пакета. Поэтому immutable locator +чужого change не требуется. Переиспользуются реальные элементы реализации: + +- [AppShell](../../../../frontend/src/shared/ui/AppShell.tsx) — один пункт + «Карты» после «Счета» для desktop и mobile; текущие разделы сохраняются. +- [ProtectedLayout](../../../../frontend/src/app/routes/ProtectedLayout.tsx) + и [AppRoutes](../../../../frontend/src/app/routes/AppRoutes.tsx) — локальная + сессия и два lazy-маршрута; крошки и заголовок документа расширяются у + существующих владельцев. +- [EntryCard](../../../../frontend/src/shared/ui/EntryCard.tsx) — представление + строки операции с banking mapping. `LedgerEntryRow` и брокерские DTO + напрямую не переиспользуются. +- [AccountOperationsPage VM](../../../../frontend/src/pages/account-operations/ui/AccountOperationsPage.vm.ts) + и [форма периода дохода](../../../../frontend/src/pages/account-income/ui/components/IncomePeriodForm.view.tsx) + — примеры организации URL/форм, без cross-page imports. +- [Дизайн-система](../../../../docs/development/design-system.md) — текущие + светлая тема, семантические цвета и размеры. Новые шрифты, палитра и + глобальная переработка оформления не вводятся. + +## Delivery Map + +| Area | Status | Architectural outcome or N/A reason | +| --- | --- | --- | +| frontend | `affected` | Раздел карт, операции/категории/сводки, формы и состояния, generated client, локальная навигация и query invalidation | +| backend | `affected` | Владелец ручного учёта, хранение, транзакции, точные расчёты, API и общие Money-компоненты без изменения брокерской семантики | +| specs | `affected` | Новый контракт `manual-bank-cards` и SDD-артефакты этого change; текущие инвестиционные requirements сохраняются | +| docs | `affected` | После поставки: область банковских карт, глоссарий, обзор продукта/навигация, расширение описания хранимых данных и восстановление | + +### Contract Gate Decision + +| Gate | Status | Reason | +| --- | --- | --- | +| runtime-openapi | `required` | Новый backend API потребляется frontend; runtime document, snapshot и generated types должны совпадать | + +## Solution Assessment + +Оценка обязательна из-за новых долговременных финансовых данных, транзакций +и миграции. Выбирается существующая инфраструктура, с небольшой доменной +реализацией внутри приложения. + +| Вариант | Fit, зрелость и безопасность | Эксплуатация, ограничения и миграция | Решение | +| --- | --- | --- | --- | +| Установленные Prisma + SQLite | Соответствуют локальному приложению; существующий DB provider, FK, unique constraints и транзакции | Без отдельного сервера; добавление таблиц сохраняет действующие данные; остаётся известная проблема migration engine | Использовать | +| Другой сервер БД, например PostgreSQL | Зрелые средства конкурентной записи; текущая нагрузка не требует смены SQLite | Новый сервис, конфигурация, резервирование и миграция всей базы | Не добавлять в эту итерацию | +| Browser storage | Нативное средство, но не соответствует сохранению серверных пользовательских данных и доступу через существующую сессию | Появляется второй источник истины и отдельная миграция из браузера | Не использовать для финансовой истории | +| Prisma Decimal поверх SQLite | Удобное имя типа не доказывает точный путь хранения | В установленном adapter наблюдалось преобразование Decimal через `parseFloat`; BigInt сохраняет целые значения | Хранить копейки в BigInt; проверить реальный roundtrip | +| Изменяемый cached balance или event-sourced ledger | Возможны при иных объёмах и требованиях аудита; сейчас добавляют второе состояние или инфраструктуру событий | Дополнительные восстановление, проекции, invalidation и миграционные обязательства | Вычислять из трёх простых моделей | + +Наблюдение adapter относится к установленной зависимости, зафиксированной +в [backend package-lock](../../../../backend/package-lock.json), и её подключению +в [PrismaService](../../../../backend/src/prisma/prisma.service.ts). +Это не заменяет будущую проверку сохранения значений больше `2^53` через +реальную БД и не требует изменения версии зависимости на этапе Design. + +## Risks / Trade-offs + +- Пропущенные ручные движения приводят к расхождению с банком. Отрицательный + учётный баланс допустим; приложение не проверяет банковский доступный лимит. +- Возврат прошлой покупки может дать отрицательный месячный итог. UI показывает + расходы и возвраты отдельно, а не скрывает это округлением или обнулением. +- Редактирование истории меняет прошлые сводки. Здесь нет неизменяемого + бухгалтерского аудита; soft deletion предотвращает повторное применение + удалённой записи, но не является полной историей всех её редакций. +- Точный fold имеет O(N) стоимость. Нужную оптимизацию выбирают после измерения + фактической истории, сохраняя одного владельца расчёта. +- В [текущих ограничениях проверки](../../../../docs/development/verification.md#известное-ограничение-сквозной-проверки) + зафиксирован `Schema engine error` Prisma 7.9.1/Node 22 на новой SQLite. + В этой фазе он не воспроизводился и не признан устранённым. Финансовую + поставку нельзя признать проверенной без реальной DB/migration границы. +- Существующая ValidationPipe не доказывает обещанные структурированные + field errors — это отмечено в [HTTP API](../../../../docs/architecture/http-api.md#error-contract). + Формы имеют локальную валидацию и безопасный общий ответ об ошибке; + исполняемое поведение нового API проверяется на настроенном HTTP-приложении. + +## Migration Plan + +Сначала подтверждаются сохранность действующей базы и применимость известных +ограничений миграций на изолированной копии. Backup включает согласованный +снимок SQLite; ключ T-Bank сохраняется отдельно и не выводится в отчёты. +Работающая база и её WAL не копируются несогласованными частями: используется +корректный SQLite backup либо остановленное приложение. + +Изменение схемы additive: три таблицы, индексы и связи с `User`. +Существующие строки User, Session и TbankConnection не переписываются. +Применённые migrations не редактируются; schema reset и обход migration +history не используются. Категории создаются для владельца при первом вводе, +без массового изменения всех существующих профилей. + +Новый backend и его публичный контракт готовятся до включения нового +frontend-потребителя. Перенос общих Money-компонентов сохраняет действующий +контракт брокерских сценариев. Документация описывает поставленный результат +после проверки, а не объявляет будущие возможности готовыми. + +Если migration engine мешает проверить additive-переход, сначала решается +это подтверждённое препятствие в допустимых границах. Unit и OpenAPI success +не подменяют миграцию на изолированной реальной базе. Условие пересмотра +известного blocker сохраняется у [владельца проверки](../../../../docs/development/verification.md#известное-ограничение-сквозной-проверки). + +### Rollback Contract + +| Field | Decision | +| --- | --- | +| Status | `required` | +| Trigger | Регрессия входа/действующих данных, неверные финансовые результаты, нарушение изоляции владельцев или неуспешная миграция | +| Actions | Прекратить новые записи, сохранить актуальную базу; вернуть совместимую предыдущую версию приложения, сохранив добавленные таблицы. Не выполнять destructive down-migration | +| Data safety | Проверенный backup до миграции и актуальный снимок после остановки записей сохраняются отдельно. Старый backup не восстанавливается поверх новой истории как штатный rollback: это потеряло бы введённые операции | +| Verification | На disposable копии подтверждаются сохранность действующих пользователей/сессий/подключения, совместимость старого приложения с расширенной схемой, доступность новых записей после возврата новой версии и восстановление согласованного снимка | + +Более детальная последовательность доставки и команды проверок принадлежат +следующему артефакту Tasks. Этот документ не разрешает менять пользовательскую +базу или начинать реализацию до согласования подготовленного решения. diff --git a/openspec/changes/add-manual-bank-cards/proposal.md b/openspec/changes/add-manual-bank-cards/proposal.md new file mode 100644 index 0000000..3ef0363 --- /dev/null +++ b/openspec/changes/add-manual-bank-cards/proposal.md @@ -0,0 +1,111 @@ +## Why + +WealthLedger показывает инвестиции и пассивный доход, но пользователю также +нужен раздел личных банковских карт: сколько денег на каждой карте, какие +операции происходили и к каким категориям относятся повседневные траты. + +Первая итерация позволяет самостоятельно добавить карты и вести операции. +Пользователь выбрал последовательную подготовку SDD-артефактов: сначала +концепция и требования, затем Design, Tasks и реализация. + +## What Changes + +**Статус:** ответы Q1–Q9 согласованы. [Delta Specs](specs/manual-bank-cards/spec.md) +дополнены правилами целостного ручного учёта для рассмотрения вместе с Design. +Конкретизация требований ниже не является дословным ответом пользователя +на каждый частный случай и не означает начала реализации. + +### Согласованный результат + +- Q1: каждая вручную добавленная карта имеет независимые баланс и историю. +- Q2: баланс рассчитывается из начального остатка и внесённых операций. +- Q3: первая версия поддерживает дебетовые карты в рублях. +- Q4: доступны расход, пополнение, перевод между своими картами и возврат + покупки. Перевод вводится одним действием и не считается расходом. +- Q5: есть начальный набор категорий; пользователь может добавлять + и переименовывать свои категории без вложенных подкатегорий. +- Q6: журнал дополняется месячной сводкой расходов по категориям для выбранной + карты и всех банковских карт пользователя вместе. +- Q7: отрицательный учётный баланс допустим, в том числе до внесения + пропущенного пополнения. +- Q8: операции можно исправлять и удалять с пересчётом результатов; + ненужную карту можно убрать в архив с сохранением истории. +- Q9: возврат уменьшает расходы месяца, в котором он произошёл; + сводка отдельно показывает расходы, возвраты и итог после возвратов. + +### Целостность пользовательского сценария + +Пользователь задаёт начало учёта и начальный остаток, вносит операции +с категориями и датами, исправляет ошибки и видит согласованные баланс, +историю и месячные сводки. Частичные возвраты связаны с исходной покупкой. +Архив скрывает неиспользуемую карту из обычного списка, сохраняя её вклад +в исторические сводки; для новых операций и финансовых исправлений карту +нужно вернуть из архива. + +Раздел банковских карт доступен после входа в WealthLedger независимо +от подключения инвестиционного счёта. Пользовательские данные сохраняются +при повторном входе и перезапуске приложения, остаются доступны только +владельцу и защищены от дублирования при повторной отправке и от потери +одновременных изменений. +Повтор создания карты или операции после потерянного ответа сохраняет один +результат, включая начальный остаток карты. + +### Термины + +**Карта:** добавленная пользователем дебетовая банковская карта в рублях, +для которой он отдельно ведёт баланс и историю операций. Она не является +брокерским счётом или общим счётом нескольких карт. + +**Баланс карты:** сумма денег по данным ручного учёта, рассчитанная из начального +остатка и внесённых операций. Она не подтверждает текущий остаток в банковском +приложении: пропущенные движения могут привести к расхождению. + +**Итог расходов после возвратов:** расходы выбранного месяца за вычетом +возвратов, произошедших в этом месяце; итог может быть отрицательным. + +После реализации определения дополнят существующий +[глоссарий](../../../../docs/glossary.md); отдельный постоянный глоссарий +не создаётся. + +### Границы первой итерации + +Не входят автоматическая загрузка данных из банков, кредитные карты, другие +валюты, общий денежный баланс нескольких карт, объединение банковских денег +с инвестициями, прогнозы и бюджеты. Не предусмотрены вложенные подкатегории, +удаление категорий и безвозвратное удаление карт. + +Произвольной операции «исправить баланс» нет: расхождение исправляется +изменением начального остатка либо внесением или исправлением реальной +операции. Месячная сводка всех карт показывает траты и возвраты, сохраняя +независимый баланс каждой карты. + +## Capabilities + +### New Capabilities + +- `specs/manual-bank-cards/spec.md`: ручное добавление и архив банковских карт, + точный баланс по операциям, расходы и категории, пополнения, переводы, + связанные возвраты, исправления и месячные сводки. + +### Modified Capabilities + +Нет. Новый раздел сохраняет поведение существующих инвестиционных +представлений и не расширяет понятие Brokerage Account банковскими картами. + +## Impact + +- Пользователь получает учёт банковских карт и повседневных трат с историей + и возможностью исправить введённые данные. +- Frontend: отдельный раздел карт, формы операций и исправлений, архив, + категории и месячные сводки. +- Backend: сохранение пользовательского ручного учёта, точный расчёт + и защита целостности, приватности и одновременных изменений. +- Specs: проверяемый контракт новой возможности через SDD. +- Docs: описание новой предметной области и обновление глоссария при поставке. + +Существующие [брокерские счета](../../../../docs/domains/accounts/overview.md) +получаются через инвестиционное подключение; банковские счета исключены их +[контрактом](../../specs/brokerage-accounts/spec.md). +Текущий [журнал операций](../../../../docs/domains/operations/overview.md) +описывает историю брокера для чтения и не рассчитывает остаток после операции. +Правила нового ручного учёта определяют Delta Specs этого изменения. diff --git a/openspec/changes/add-manual-bank-cards/specs/manual-bank-cards/spec.md b/openspec/changes/add-manual-bank-cards/specs/manual-bank-cards/spec.md new file mode 100644 index 0000000..75e2436 --- /dev/null +++ b/openspec/changes/add-manual-bank-cards/specs/manual-bank-cards/spec.md @@ -0,0 +1,688 @@ +## Purpose + +Позволяет пользователю вручную вести дебетовые банковские карты в рублях: +сохранять операции, видеть независимые балансы, исправлять записи и оценивать +месячные расходы по категориям с учётом возвратов. + +## Статус согласования + +[Proposal](../../proposal.md) фиксирует согласованные ответы Q1–Q9. +Эта редакция раскрывает их в целостный предлагаемый контракт для рассмотрения +вместе с Design. Ограничения полей, даты, категории и правила целостности ниже +являются конкретизацией требований, а не отдельными цитатами пользователя. +Открытых продуктовых развилок в этой редакции нет; наличие спецификации +не означает, что соответствующее поведение уже реализовано. + +## ADDED Requirements + +### Requirement: Отдельный раздел ручного учёта банковских карт + +Система SHALL предоставлять авторизованному пользователю раздел банковских +карт на `/cards` и просмотр отдельной своей карты на `/cards/:id`. +Раздел MUST работать без подключения T-Bank и не зависеть от доступности +инвестиционных данных. Банковские карты MUST NOT становиться брокерскими +счетами, а банковские суммы и операции MUST NOT включаться в инвестиционный +портфель, пассивный доход или прогноз выплат. + +Первая версия SHALL поддерживать только ручной учёт дебетовых карт в RUB. +Автоматическая загрузка из банков, кредитные карты, другие валюты, общий +денежный баланс нескольких карт, прогнозы и бюджеты не входят в возможность. + +#### Scenario: Работа без инвестиционного подключения + +- **WHEN** пользователь с действующей локальной сессией, но без подключения T-Bank, открывает раздел карт +- **THEN** он может просматривать и вести свои банковские карты без перехода к подключению брокера + +#### Scenario: Независимость от инвестиционных данных + +- **WHEN** пользователь сохраняет банковское пополнение или расход, а инвестиционные данные временно недоступны +- **THEN** банковская операция сохраняется по правилам ручного учёта и не меняет инвестиционные суммы или пассивный доход + +### Requirement: Ручное добавление независимых банковских карт + +Система SHALL позволять пользователю добавить карту с названием, начальным +остатком и датой начала учёта. Название MUST содержать от 1 до 100 символов +после удаления внешних пробелов. Название банка необязательно и MUST содержать +не более 100 символов после удаления внешних пробелов. Последние четыре цифры +карты необязательны; указанное значение MUST состоять ровно из четырёх цифр. +Полный номер карты, срок действия, PIN и код безопасности не запрашиваются +и не сохраняются как реквизиты этой возможности. + +Каждая карта MUST иметь собственную идентичность, баланс и историю независимо +от совпадения банка, названия или последних цифр. Пользователь SHALL иметь +возможность изменить указанные сведения о своей карте с теми же ограничениями. +Финансовые изменения начального остатка и даты начала подчиняются отдельным +требованиям исправления и архива. + +#### Scenario: Добавление карты с начальным остатком + +- **WHEN** пользователь сохраняет карту с допустимым названием, датой начала учёта и начальным остатком 10 000 ₽ +- **THEN** карта появляется в его списке с балансом 10 000 ₽ и без вручную внесённых операций + +#### Scenario: Необязательные сведения не заполнены + +- **WHEN** пользователь добавляет карту без названия банка и последних четырёх цифр +- **THEN** отсутствие этих сведений не препятствует сохранению карты при корректности обязательных значений + +#### Scenario: Несколько карт одного банка + +- **WHEN** пользователь добавляет две карты одного банка с одинаковым названием и начальными остатками 1 000 ₽ и 2 000 ₽ +- **THEN** система сохраняет две самостоятельные карты с раздельными балансами и историей + +#### Scenario: Некорректные сведения карты + +- **WHEN** название после удаления внешних пробелов пустое или длиннее 100 символов, название банка длиннее 100 символов либо указанные последние цифры не состоят ровно из четырёх цифр +- **THEN** система отклоняет сохранение, объясняет ошибку соответствующего поля и не создаёт или не изменяет карту + +### Requirement: Точные денежные значения и проверка до сохранения + +Система MUST принимать денежные значения только в RUB, с точностью не более +двух знаков после десятичного разделителя, без округления введённой суммы. +Сумма расхода, пополнения, перевода или возврата MUST быть строго положительной +и не превышать 92 233 720 368 547 758,07 ₽. Начальный остаток MUST находиться +в диапазоне от −92 233 720 368 547 758,08 ₽ до 92 233 720 368 547 758,07 ₽ +включительно; ноль и отрицательный начальный остаток допустимы. + +Значение, не представляющее корректную денежную сумму, неподдерживаемая +валюта, лишние десятичные знаки или выход за диапазон MUST приводить +к понятной ошибке без частичного сохранения. Применение ограничения +к каждому денежному вводу MUST NOT приводить к округлению, переполнению +или ограничению точного результата сложения уже сохранённых сумм. + +#### Scenario: Нулевой и отрицательный начальный остаток + +- **WHEN** пользователь сохраняет отдельные карты с начальными остатками 0 ₽ и −100 ₽ +- **THEN** система сохраняет обе карты и показывает соответствующие известные балансы + +#### Scenario: Неположительная сумма операции + +- **WHEN** пользователь пытается внести операцию с суммой 0 ₽ или −1 ₽ +- **THEN** система отклоняет операцию, сохраняя прежние историю, балансы и сводки + +#### Scenario: Точность не исправляется округлением + +- **WHEN** пользователь пытается сохранить начальный остаток или операцию со значением 1,001 ₽ +- **THEN** система сообщает о допустимых двух десятичных знаках и не заменяет введённое значение на 1 ₽ + +#### Scenario: Границы денежных значений + +- **WHEN** пользователь указывает верхнюю допустимую сумму операции или нижний допустимый начальный остаток +- **THEN** сами эти граничные значения принимаются, а выход за соответствующую границу на одну копейку отклоняется без изменения данных + +#### Scenario: Некорректная сумма или валюта + +- **WHEN** поступает попытка сохранить нечисловую сумму либо сумму в валюте, отличной от RUB +- **THEN** система объясняет ошибку и не сохраняет карту, операцию или изменение существующих данных + +### Requirement: Даты начала учёта и ручных операций + +Дата начала учёта и дата операции MUST быть существующими календарными датами +без времени. Сегодня определяется по Europe/Moscow. Дата начала учёта +MUST NOT быть позже сегодняшней даты. Начальный остаток означает деньги +до всех вручную внесённых операций начиная с указанной даты включительно. + +Дата операции MUST находиться между датой начала учёта затронутой карты +и сегодняшним днём включительно. Для перевода ограничение MUST выполняться +для обеих карт. Ввод прошедших операций внутри этого интервала SHALL быть +доступен. Необязательное описание операции MUST содержать не более +500 символов. Сохранённая календарная дата MUST NOT сдвигаться при смене +часового пояса устройства. + +#### Scenario: Операция в первый день учёта + +- **WHEN** при начальном остатке 1 000 ₽ на 1 сентября пользователь вносит расход 100 ₽ с датой 1 сентября +- **THEN** расход учитывается после начального остатка и баланс становится 900 ₽ + +#### Scenario: Прошедшая операция + +- **WHEN** пользователь вносит операцию задним числом не раньше начала учёта и не позже сегодняшнего дня +- **THEN** операция сохраняется с указанной датой и изменяет текущий баланс и сводку соответствующего месяца + +#### Scenario: Недопустимая дата или описание + +- **WHEN** дата не существует, находится до начала учёта или в будущем либо описание длиннее 500 символов +- **THEN** система объясняет ошибку и не сохраняет изменение + +#### Scenario: Перевод до начала учёта получателя + +- **WHEN** дата перевода допустима для карты-источника, но предшествует началу учёта карты-получателя +- **THEN** перевод отклоняется без изменения обеих карт + +#### Scenario: Московская календарная граница + +- **WHEN** на устройстве ещё предыдущий день, а в Europe/Moscow уже наступил следующий +- **THEN** допустимая сегодняшняя дата и текущий месяц определяются по Europe/Moscow, а сохранённые даты операций остаются прежними + +### Requirement: Баланс определяется начальным остатком и внесёнными операциями + +Баланс карты MUST равняться начальному остатку плюс денежное влияние всех +её сохранённых и не удалённых операций: пополнение и возврат увеличивают +баланс, расход уменьшает, перевод уменьшает баланс источника и увеличивает +баланс получателя. Начальный остаток MUST учитываться ровно один раз +и MUST NOT считаться расходом, возвратом или пополнением. + +Баланс MUST рассчитываться точно и может быть отрицательным. Недостаточный +учётный остаток MUST NOT препятствовать сохранению корректного расхода +или перевода. Баланс MUST NOT зависеть от выбранного месяца, категории, +показанной страницы истории или нахождения карты в архиве. + +Система SHALL представлять баланс как результат ручного учёта и MUST NOT +выдавать его за подтверждённый банком остаток или обещать полноту невнесённых +операций. Отдельной произвольной операции корректировки баланса SHALL NOT быть. + +#### Scenario: Последовательные расходы с копейками + +- **WHEN** при начальном остатке 1 ₽ сохраняются расходы 0,10 ₽ и 0,20 ₽ +- **THEN** баланс составляет ровно 0,70 ₽ + +#### Scenario: Полное расходование и отрицательный остаток + +- **WHEN** при балансе 100 ₽ пользователь сохраняет расход 100 ₽, затем расход 50 ₽ +- **THEN** первая операция даёт известный баланс 0 ₽, вторая сохраняется и даёт баланс −50 ₽ + +#### Scenario: Начальный остаток и страница истории + +- **WHEN** при начальном остатке 1 000 ₽ и расходах 100 ₽ и 200 ₽ пользователь повторно открывает карту и видит только расход 200 ₽ +- **THEN** текущий баланс остаётся 700 ₽: учитываются обе операции, а начальный остаток не добавляется повторно + +#### Scenario: Итог превышает границу отдельного ввода + +- **WHEN** на карту с начальным остатком 92 233 720 368 547 758,07 ₽ сохраняется пополнение 0,01 ₽ +- **THEN** баланс составляет точно 92 233 720 368 547 758,08 ₽ без переполнения, округления или ограничения верхней границей ввода + +### Requirement: Ручное внесение расходов и пополнений + +Система SHALL позволять вручную внести расход или пополнение по своей активной +карте с суммой, датой и необязательным описанием. Расход MUST иметь категорию; +если пользователь не выбирает другую, применяется его категория по умолчанию. +После успешного сохранения операция MUST появиться в истории соответствующей +карты и ровно один раз изменить её баланс. + +Расход SHALL уменьшать баланс на свою сумму, пополнение SHALL увеличивать. +Категория MUST NOT изменять денежное влияние расхода. Пополнение MUST NOT +включаться в расходы, возвраты или пассивный инвестиционный доход. + +#### Scenario: Расход с категорией + +- **WHEN** при балансе 10 000 ₽ пользователь сохраняет расход 500 ₽ с категорией «Кафе» +- **THEN** история показывает расход 500 ₽ с этой категорией, а баланс составляет 9 500 ₽ + +#### Scenario: Категория по умолчанию + +- **WHEN** пользователь сохраняет расход, не выбрав другую категорию +- **THEN** расход сохраняется с принадлежащей пользователю категорией по умолчанию и отображается под её текущим названием + +#### Scenario: Пополнение карты + +- **WHEN** при балансе −500 ₽ пользователь сохраняет пополнение 2 000 ₽ +- **THEN** баланс становится 1 500 ₽, история содержит пополнение, а расходы и возвраты не меняются + +#### Scenario: Независимость другой карты + +- **WHEN** при балансах A 1 000 ₽ и B 2 000 ₽ пользователь сохраняет расход 125,50 ₽ по A +- **THEN** баланс A становится 874,50 ₽, а баланс и история B остаются прежними + +### Requirement: Перевод между своими картами как одно действие + +Система SHALL позволять одним действием внести перевод между двумя разными +активными картами одного пользователя. Сумма перевода MUST одинаково уменьшать +баланс источника и увеличивать баланс получателя. История каждой стороны +MUST показывать направление и другую карту этого перевода. + +Перевод MUST сохраняться, исправляться и удаляться целиком для обеих сторон. +Нельзя сохранять только одну сторону, переводить на ту же или чужую карту. +Перевод MUST NOT увеличивать расходы или возвраты в сводках одной или всех карт. + +#### Scenario: Перевод между картами + +- **WHEN** при балансах A 5 000 ₽ и B 1 000 ₽ пользователь сохраняет перевод 2 000 ₽ из A в B +- **THEN** баланс A становится 3 000 ₽, баланс B — 3 000 ₽, обе истории показывают стороны одного перевода, а расходы не меняются + +#### Scenario: Недостаточный остаток источника + +- **WHEN** при балансах A 100 ₽ и B 0 ₽ пользователь сохраняет перевод 150 ₽ из A в B +- **THEN** перевод сохраняется целиком, баланс A становится −50 ₽, а баланс B — 150 ₽ + +#### Scenario: Недопустимый получатель + +- **WHEN** получателем указана та же карта или карта другого пользователя +- **THEN** система отклоняет перевод без изменения историй, балансов и сводок + +#### Scenario: Сбой сохранения перевода + +- **WHEN** перевод не удаётся сохранить +- **THEN** система не подтверждает успех и не оставляет сохранённой только одну его сторону + +### Requirement: Возврат связан с исходным расходом + +Возврат SHALL ссылаться на существующий не удалённый расход той же карты. +При внесении возврата пользователь SHALL иметь возможность выбрать исходный +расход карты за любой период учёта, независимо от месяца открытого журнала. +Частичные и несколько возвратов допустимы; сумма всех не удалённых возвратов +MUST NOT превышать текущую сумму исходного расхода. Дата возврата MUST быть +не раньше даты расхода и соответствовать общим ограничениям даты карты. +При редактировании возврата проверка общей возвращённой суммы MUST заменять +его прежнюю сумму новой, не учитывая обе версии одновременно. + +Возврат MUST увеличивать баланс карты и наследовать категорию исходного +расхода. Отдельный выбор категории возврата SHALL NOT предоставляться. +Изменение категории исходного расхода MUST менять категорию связанных +возвратов и результаты затронутых месячных сводок, сохраняя их суммы и даты. + +#### Scenario: Частичные возвраты + +- **WHEN** по расходу 1 000 ₽ пользователь последовательно сохраняет возвраты 300 ₽ и 700 ₽ +- **THEN** оба возврата связаны с расходом, увеличивают баланс суммарно на 1 000 ₽ и используют его категорию + +#### Scenario: Выбор покупки прошлого месяца + +- **WHEN** пользователь открывает сентябрьский журнал и вносит возврат по августовской покупке +- **THEN** форма позволяет найти и выбрать исходный августовский расход этой карты без ограничения сентябрьским журналом + +#### Scenario: Исправление полностью возвращённой покупки + +- **WHEN** по расходу 500 ₽ сохранён единственный возврат 500 ₽ и пользователь изменяет только описание возврата +- **THEN** исправление сохраняется: общая возвращённая сумма остаётся 500 ₽, а прежняя и новая версии не складываются как два возврата + +#### Scenario: Превышение возвращаемой суммы + +- **WHEN** при расходе 1 000 ₽ и сохранённом возврате 800 ₽ пользователь пытается сохранить ещё 300 ₽ +- **THEN** система отклоняет новый возврат без изменения существующих записей и сумм + +#### Scenario: Недопустимая связь или дата возврата + +- **WHEN** возврат ссылается на удалённую операцию, операцию другого вида или другой карты либо датирован раньше расхода +- **THEN** система отклоняет возврат без изменения истории, баланса и сводок + +#### Scenario: Изменение категории покупки + +- **WHEN** пользователь меняет категорию расхода с «Покупки» на «Здоровье», а возврат по нему произошёл в другом месяце +- **THEN** расход и возврат учитываются в категории «Здоровье» в своих месяцах с прежними денежными суммами + +### Requirement: Общие для пользователя категории расходов + +Система SHALL предоставлять каждому пользователю собственный общий для всех +его карт список категорий: «Продукты», «Кафе», «Такси», «Транспорт», «Жильё», +«Здоровье», «Покупки», «Развлечения», «Другое». «Другое» является начальной +категорией по умолчанию. Категории MUST быть доступны до первого расхода. +Начальный набор MUST создаваться при успешном добавлении первой карты +пользователя и не дублироваться при добавлении следующих карт. + +Пользователь SHALL иметь возможность добавлять и переименовывать любые свои +категории, включая начальные. Название MUST содержать от 1 до 80 символов +после удаления внешних пробелов и быть уникальным среди категорий пользователя +без учёта регистра. Переименование MUST сохранять идентичность категории: +все старые расходы, возвраты и сводки показывают её текущее название. +Переименование категории по умолчанию MUST сохранять её роль. +Удаление категорий и вложенные подкатегории SHALL NOT предоставляться в V1. + +#### Scenario: Начальный набор и повторный вход + +- **WHEN** пользователь сохраняет первую карту, затем добавляет следующую карту и повторно входит в приложение +- **THEN** после первой карты ему доступен начальный набор категорий до первого расхода, а дальнейшие действия не создают дубликатов + +#### Scenario: Категория доступна на всех своих картах + +- **WHEN** пользователь создаёт категорию «Питомец» +- **THEN** она доступна для расходов по всем его картам и не появляется у другого пользователя + +#### Scenario: Переименование используемой начальной категории + +- **WHEN** пользователь переименовывает категорию «Другое» в «Остальное» +- **THEN** прежние записи отображаются как «Остальное», суммы не меняются, а новые расходы без другого выбора используют ту же категорию «Остальное» + +#### Scenario: Дублирующееся или недопустимое название + +- **WHEN** пользователь пытается добавить « кафе » при существующей «Кафе», сохранить пустое после удаления пробелов название или название длиннее 80 символов +- **THEN** система объясняет ошибку и не изменяет список категорий + +### Requirement: Исправление и удаление операций с согласованным пересчётом + +Пользователь SHALL иметь возможность изменить сумму, дату, описание +и применимые реквизиты сохранённой операции либо удалить её. Вид операции +и связь возврата с исходным расходом MUST быть неизменны после создания. +Все затронутые карты MUST быть активны; каждое исправление MUST соблюдать +те же денежные, календарные и пользовательские ограничения, что и создание. + +Обычный расход или пополнение SHALL быть переносимы на другую свою активную +карту. Расход с не удалёнными возвратами MUST NOT переноситься на другую карту, +удаляться, уменьшаться ниже суммы возвратов или сдвигаться на дату позже +любого связанного возврата. Для такого действия сначала исправляются +или удаляются мешающие возвраты. Возврат остаётся на карте исходного расхода. + +Исправление или удаление MUST пересчитать балансы, историю и все затронутые +месячные сводки; изменение даты переносит влияние между соответствующими +месяцами. Исправление сторон перевода MUST учитывать его прежние и новые +карты. Удаление перевода MUST удалять обе стороны одним действием. +Повторное удаление уже удалённой собственной операции SHALL быть +безрезультатным успешным действием без повторного денежного влияния. + +#### Scenario: Изменение суммы и месяца расхода + +- **WHEN** пользователь меняет расход 100 ₽ за август на 150 ₽ за сентябрь +- **THEN** баланс дополнительно уменьшается на 50 ₽, август перестаёт учитывать 100 ₽, а сентябрь учитывает 150 ₽ + +#### Scenario: Перенос пополнения на другую карту + +- **WHEN** пользователь переносит пополнение 500 ₽ с A на свою активную B с допустимой датой +- **THEN** баланс A уменьшается на 500 ₽, баланс B увеличивается на 500 ₽, а запись относится к B + +#### Scenario: Изменение получателя перевода + +- **WHEN** перевод 200 ₽ из A в B исправляется на перевод 300 ₽ из A в C и все три карты активны +- **THEN** прежнее влияние на B отменяется, A уменьшается дополнительно на 100 ₽, C увеличивается на 300 ₽, а обе стороны исправленного перевода сохраняются согласованно + +#### Scenario: Исправление расхода ограничено возвратами + +- **WHEN** для расхода 1 000 ₽ существует возврат 600 ₽ и пользователь пытается удалить расход, перенести его на другую карту, уменьшить до 500 ₽ или сдвинуть позже даты возврата +- **THEN** система отклоняет действие и объясняет необходимость сначала исправить связанные возвраты + +#### Scenario: Удаление возврата и затем расхода + +- **WHEN** пользователь удаляет все возвраты расхода, а затем сам расход +- **THEN** влияние каждой удалённой записи отменяется в балансе и в её месячной сводке, а ограничение удаления исходного расхода больше не действует + +#### Scenario: Удаление перевода из любой стороны + +- **WHEN** пользователь удаляет перевод из истории получателя и затем повторяет удаление +- **THEN** обе стороны удаляются при первом действии, денежное влияние отменяется один раз, а повторное удаление ничего не меняет + +#### Scenario: Смена вида или исходного расхода + +- **WHEN** пользователь пытается превратить расход в пополнение либо связать существующий возврат с другим расходом +- **THEN** изменение отклоняется без подмены вида операции или её исходной связи + +### Requirement: Исправление начала ручного учёта + +Пользователь SHALL иметь возможность изменить начальный остаток и дату начала +учёта активной карты. Изменение MUST проверять всю затронутую сохранённую +историю, включая стороны переводов и связанные возвраты, и отклоняться +без частичного результата, если после него какая-либо операция нарушает +действующие ограничения. Удалённые операции не ограничивают новую дату. + +Изменение начального остатка SHALL пересчитывать текущий баланс, сохраняя +суммы, даты и категории операций. Оно MUST NOT создавать отдельную операцию +или изменять месячные расходы и возвраты. Пропущенное движение SHALL +исправляться внесением реальной операции с допустимой датой. + +#### Scenario: Исправление начального остатка + +- **WHEN** при начальном остатке 1 000 ₽ и расходах 300 ₽ пользователь меняет начальный остаток на 1 200 ₽ +- **THEN** баланс становится 900 ₽, история операций не пополняется корректировкой, а месячные расходы остаются 300 ₽ + +#### Scenario: Новое начало противоречит истории + +- **WHEN** пользователь переносит дату начала учёта с 1 на 10 сентября, а по карте сохранён расход или перевод от 5 сентября +- **THEN** система отклоняет изменение и сохраняет прежние дату, баланс и всю историю + +### Requirement: Архив карт сохраняет историю + +Пользователь SHALL иметь возможность архивировать свою карту при любом +балансе, включая положительный, нулевой и отрицательный, и вернуть её +из архива. Архивация MUST NOT менять деньги или операции. Обычный список +SHALL показывать активные карты; переключатель архива SHALL открывать +архивные карты и их историю. + +Пока карта архивна, система MUST запрещать создание, редактирование +и удаление затрагивающих её операций, а также изменение начального остатка +и даты начала учёта. Для исправления или новой операции карту сначала +возвращают из архива. Перевод или возврат MUST требовать активного состояния +всех затронутых карт. Изменение описательных сведений карты само по себе +не является финансовой операцией. + +Архивная карта MUST продолжать участвовать в исторических сводках «Все карты». +Безвозвратное удаление карты SHALL NOT предоставляться в первой версии. + +#### Scenario: Архивирование с ненулевым остатком + +- **WHEN** пользователь архивирует карту с балансом −500 ₽ и сохранённой историей +- **THEN** карта скрывается из обычного списка, остаётся доступной через переключатель архива с теми же балансом и историей и сохраняет вклад в сводки всех карт + +#### Scenario: Финансовое действие по архивной карте + +- **WHEN** пользователь пытается внести, исправить или удалить операцию либо изменить начало учёта архивной карты +- **THEN** система отклоняет действие без изменения данных и объясняет необходимость вернуть карту из архива + +#### Scenario: Возврат или перевод затрагивает архив + +- **WHEN** возврат относится к расходу архивной карты либо хотя бы одна прежняя или новая сторона изменяемого перевода архивна +- **THEN** финансовое действие отклоняется до возвращения всех затронутых карт из архива + +#### Scenario: Возвращение карты + +- **WHEN** пользователь возвращает карту из архива +- **THEN** она снова видна в обычном списке с сохранёнными данными и допускает финансовые действия по общим правилам + +### Requirement: Месячный журнал и сводка расходов по категориям + +Система SHALL показывать историю и месячную сводку выбранной карты, а также +месячную сводку всех банковских карт пользователя. По умолчанию выбирается +текущий месяц Europe/Moscow; пользователь SHALL иметь возможность явно выбрать +другой месяц. История карты и её сводка MUST использовать один выбранный месяц. +Операции SHALL отображаться по убыванию календарной даты со стабильным порядком +внутри даты при повторном чтении неизменившихся данных и доступом к следующим +страницам истории. + +Сводка MUST показывать расходы, возвраты и итог после возвратов, а также +соответствующие суммы по категориям. Расход входит в месяц собственной даты, +возврат — в месяц даты возврата, даже если покупка была раньше. +Итог MUST равняться расходам минус возвраты и может быть отрицательным. +Начальные остатки, пополнения и переводы MUST NOT включаться в эти суммы. + +Сводка MUST учитывать все подходящие не удалённые записи, а не только +текущую страницу истории. «Все карты» MUST включать активные и архивные карты +владельца. Выбор месяца и охвата MUST NOT изменять текущие балансы. +При отсутствии расходов и возвратов SHALL показываться известные нули. + +#### Scenario: Месяц по умолчанию и переключение + +- **WHEN** пользователь открывает карту в сентябре по Europe/Moscow и затем выбирает август +- **THEN** сначала история и сводка относятся к сентябрю, после выбора обе относятся к августу, а текущий баланс остаётся прежним + +#### Scenario: Сводка расходов и переводов + +- **WHEN** в месяце сохранены расходы «Кафе» 500 ₽ и «Такси» 300 ₽, пополнение 2 000 ₽ и перевод 1 000 ₽ без возвратов +- **THEN** сводка показывает расходы 800 ₽, возвраты 0 ₽, итог 800 ₽ и категории 500 ₽ и 300 ₽ без включения пополнения или перевода + +#### Scenario: Возврат покупки прошлого месяца + +- **WHEN** расход «Покупки» 1 000 ₽ совершён в августе, возврат 600 ₽ — в сентябре и других операций этих видов нет +- **THEN** август показывает расходы 1 000 ₽, возвраты 0 ₽ и итог 1 000 ₽, а сентябрь — расходы 0 ₽, возвраты 600 ₽ и итог −600 ₽ в той же категории + +#### Scenario: Все карты включают архивные + +- **WHEN** расходы активной A за месяц составили 800 ₽, архивной B — 200 ₽, а между ними ранее был перевод 1 000 ₽ +- **THEN** общая сводка показывает расходы 1 000 ₽ и не добавляет перевод, независимо от переключателя архива в списке карт + +#### Scenario: Несколько страниц и равные даты + +- **WHEN** операции выбранного месяца занимают несколько страниц и часть операций имеет одинаковую дату +- **THEN** пользователь может просмотреть все страницы в стабильном порядке, а сводка и баланс учитывают все соответствующие записи независимо от открытой страницы + +### Requirement: Изоляция пользовательских финансовых данных + +Карты, категории, операции и связи между ними MUST принадлежать локальному +пользователю WealthLedger. Система MUST предоставлять чтение, создание, +изменение, удаление, архив и восстановление только владельцу с действующей +сессией. Идентификатор сам по себе MUST NOT подтверждать доступ. + +Проверка владельца MUST распространяться на каждую упомянутую карту, +категорию и операцию, включая обе стороны перевода, исходный расход возврата +и новые связи при редактировании. Отказ MUST NOT раскрывать чужие финансовые +сведения или менять данные любой стороны. Сводки MUST включать только +данные текущего пользователя. + +#### Scenario: Нет действующей сессии + +- **WHEN** пользователь без действующей сессии пытается прочитать карты, сводку или изменить финансовые данные +- **THEN** система отказывает в доступе без раскрытия и изменения сохранённых данных + +#### Scenario: Чужая карта или категория + +- **WHEN** пользователь обращается к чужой карте или пытается назначить своей операции чужую категорию +- **THEN** система отклоняет действие, не раскрывает чужие сведения и не меняет баланс, историю или категории + +#### Scenario: Чужая связь перевода или возврата + +- **WHEN** пользователь подставляет чужую карту в перевод или чужой расход в возврат при создании либо редактировании +- **THEN** система отклоняет всё действие без сохранения частичного результата и без раскрытия данных другого пользователя + +#### Scenario: Чужие исправления и архив + +- **WHEN** пользователь пытается изменить или удалить чужую операцию, переименовать чужую категорию либо архивировать или восстановить чужую карту +- **THEN** система отказывает и сохраняет все данные владельца без изменений + +### Requirement: Согласованные изменения и защита повторной отправки + +Успешное финансовое изменение MUST согласованно сохранять операции, связи, +денежное влияние и пересчитанные результаты для всех затронутых карт. +Система MUST NOT подтверждать успех несохранённого действия или оставлять +частичный результат. Независимые одновременные операции MUST сохранять +влияние каждой успешной записи, не затирая друг друга. + +Повторная отправка одного намерения создать карту или операцию с тем же +содержанием MUST приводить не более чем к одному соответствующему результату +и однократному учёту начального остатка или денежного влияния операции. +Иное содержание при повторном использовании того же намерения MUST приводить +к конфликту без изменения данных. После исправления или удаления результата +поздняя повторная отправка исходного создания MUST NOT восстанавливать +старые значения, удалённую запись или её денежное влияние. +Ответ на повтор MUST показывать актуальное состояние ранее созданной карты +или операции либо сообщать, что операция удалена, не выдавая старые данные +за текущий результат. Уже сохранённое намерение MUST NOT проверяться +как новая финансовая запись и повторно расходовать доступную сумму возврата. +Архивирование ранее созданной карты MUST NOT превращать повтор её создания +в создание другой карты или возвращение из архива. + +Редактирование или удаление существующей операции и изменение карты, +основанные на её устаревшем состоянии, MUST завершаться понятным конфликтом +без перезаписи более свежих изменений. Повторное удаление уже удалённой +операции не считается перезаписью и остаётся безрезультатным действием. +Проверки суммы возвратов, состояния архива, дат и других ограничений +MUST сохраняться при одновременных действиях. + +#### Scenario: Повтор после потерянного ответа + +- **WHEN** создание расхода 100 ₽ успешно сохранено, ответ потерян и пользователь повторяет то же намерение с тем же содержанием +- **THEN** история содержит один расход, баланс изменён один раз, а повтор не создаёт дубликат + +#### Scenario: Повтор создания карты после архивации + +- **WHEN** карта с начальным остатком 1 000 ₽ создана, ответ потерян, карта затем архивирована и приходит повтор её исходного создания +- **THEN** система показывает актуальную архивную карту, не создаёт вторую карту, не дублирует начальный остаток и не возвращает карту из архива + +#### Scenario: Повтор полного возврата + +- **WHEN** возврат 500 ₽ полностью покрывает расход 500 ₽, успешно сохранён, но ответ потерян и пользователь повторяет то же намерение создания возврата +- **THEN** система возвращает сохранённый возврат без новой записи, повторного изменения баланса или ошибки о превышении суммы покупки + +#### Scenario: Изменённое содержание того же намерения + +- **WHEN** после сохранения расхода 100 ₽ повтор того же намерения содержит сумму 200 ₽ +- **THEN** система сообщает о конфликте и сохраняет исходный результат без дополнительной операции + +#### Scenario: Позднее создание после исправления или удаления + +- **WHEN** сохранённый расход 100 ₽ исправлен на 150 ₽ или удалён, после чего приходит повтор его исходного создания +- **THEN** система показывает актуальный расход 150 ₽ либо сообщает об удалении; прежняя сумма или удалённая запись не восстанавливаются и дополнительного денежного влияния нет + +#### Scenario: Два независимых расхода + +- **WHEN** при балансе 1 000 ₽ расходы 100 ₽ и 200 ₽ успешно сохраняются одновременно как разные намерения +- **THEN** история содержит оба расхода, а баланс составляет 700 ₽ + +#### Scenario: Два возврата превышают покупку вместе + +- **WHEN** для расхода 1 000 ₽ одновременно сохраняются независимые возвраты 600 ₽ и 600 ₽ +- **THEN** система не допускает общей сохранённой суммы 1 200 ₽: конфликтующее действие отклоняется без частичного результата + +#### Scenario: Исправление по устаревшим данным + +- **WHEN** операция или карта изменилась после того, как пользователь начал её редактирование, а он отправляет изменение прежнего состояния +- **THEN** система сообщает о конфликте, сохраняет более свежие данные и позволяет получить актуальное состояние для нового исправления + +#### Scenario: Удаление по устаревшим данным + +- **WHEN** пользователь пытается удалить существующую операцию, которую другой успешный запрос уже изменил +- **THEN** система отклоняет устаревшее удаление без потери более свежего исправления + +#### Scenario: Архивирование одновременно с финансовым изменением + +- **WHEN** архивирование карты пересекается с созданием или исправлением затрагивающей её операции +- **THEN** результаты соответствуют последовательному выполнению допустимых действий и не допускают финансовую запись, выполненную после вступившего в силу архива + +### Requirement: Сохранённый ручной учёт переживает повторный вход и перезапуск + +Успешно сохранённые карты, начальные остатки, даты начала, операции, +категории, архивное состояние и связи возвратов и переводов MUST оставаться +доступными владельцу после обновления страницы, выхода, повторного входа +и обычного перезапуска приложения. Эти действия MUST NOT создавать +дубликаты, восстанавливать удалённые операции или сбрасывать балансы. +Защита повторной отправки сохранённых намерений MUST сохранять действие +после этих переходов. + +#### Scenario: Повторный вход + +- **WHEN** пользователь выходит и снова входит после сохранения карты с начальным остатком 1 000 ₽ и расходом 100 ₽ +- **THEN** он видит ту же карту, дату начала, расход с его текущей категорией и баланс 900 ₽ + +#### Scenario: Перезапуск с переводом, возвратом и архивом + +- **WHEN** приложение перезапускается после сохранения переводов, связанных возвратов, переименования категорий и архивирования карты +- **THEN** владелец получает прежние связи, названия, архивное состояние и рассчитанные суммы без дубликатов или потери истории + +#### Scenario: Повтор создания после перезапуска + +- **WHEN** после перезапуска повторяется намерение создания ранее сохранённой, а затем удалённой операции +- **THEN** удалённая операция и её денежное влияние не восстанавливаются + +### Requirement: Доступные формы и различимые состояния без ложного успеха + +Раздел SHALL различать отсутствие карт, отсутствие операций за месяц, +известный нулевой баланс, загрузку и ошибку получения данных. Ошибка +MUST NOT подменяться пустым списком или нулевой суммой. Пользователь SHALL +иметь возможность повторить неуспешную загрузку. + +Формы карт, операций, исправлений и категорий MUST иметь доступные названия +полей и действий, управление с клавиатуры и понятные ошибки, связанные +с соответствующим полем или действием. Обязательность, направление денег +и ошибки MUST NOT передаваться только цветом. + +Во время сохранения, ошибки и конфликта введённый черновик MUST оставаться +доступным до явного закрытия или успешного завершения. Пока результат +сохранения не подтверждён, система MUST NOT показывать введённую сумму +как подтверждённый новый баланс или выдавать действие за успешное. +При неопределённом результате после потери ответа повтор SHALL использовать +защиту того же намерения создания. После подтверждённого изменения +отображаемые история, балансы и сводки MUST согласовываться с сохранёнными +данными всех затронутых карт. + +#### Scenario: Пустой список + +- **WHEN** раздел успешно загружен и у пользователя ещё нет карт +- **THEN** система показывает штатное пустое состояние с доступным действием добавления карты + +#### Scenario: Нет операций при ненулевом балансе + +- **WHEN** в выбранном месяце нет операций, но текущий баланс карты составляет 10 000 ₽ +- **THEN** история сообщает об отсутствии операций за месяц, сводка содержит нули, а текущий баланс остаётся 10 000 ₽ + +#### Scenario: Ошибка загрузки + +- **WHEN** загрузка карт, истории или сводки завершается ошибкой +- **THEN** система показывает ошибку и действие повтора, не выдавая её за отсутствие данных или нулевой результат + +#### Scenario: Доступный ввод и проверка + +- **WHEN** пользователь управляет формой с клавиатуры или средством чтения экрана и вводит некорректную сумму +- **THEN** названия полей и действий доступны, ошибка суммы определима без цвета, а введённые значения остаются для исправления + +#### Scenario: Ожидание и неопределённый результат + +- **WHEN** пользователь отправляет перевод и ответ ещё не получен либо потерян +- **THEN** черновик сохраняется, неподтверждённые балансы не выдаются за сохранённые, а повтор того же создания не порождает второй перевод + +#### Scenario: Ошибка или конфликт исправления + +- **WHEN** сохранение исправления отклоняется из-за ошибки или устаревшего состояния +- **THEN** пользователь видит причину, сохраняет черновик и может получить актуальные данные для следующего действия без перезаписи чужого успешного изменения diff --git a/openspec/changes/add-manual-bank-cards/tasks.md b/openspec/changes/add-manual-bank-cards/tasks.md new file mode 100644 index 0000000..28c9fb8 --- /dev/null +++ b/openspec/changes/add-manual-bank-cards/tasks.md @@ -0,0 +1,314 @@ +# Реализация ручного учёта банковских карт + +**Статус:** согласованный объём реализован и проверен 12 сентября 2026 года. +Независимое ревью завершено; change остаётся активным, без архивирования +и синхронизации в основные specs. +Основание — согласованные [Design revision 1](design.md), включая полностью +inline UI/UX package revision 1, и [Delta Specs](specs/manual-bank-cards/spec.md). +Внешних визуальных материалов или дополнительных пакетов для начала нет. + +Checkbox закрывается только после выполнения действия и его Verification. +Verification ниже задаёт критерии; наблюдённые результаты отмечаются отдельно. +Перед Node-командами из affected repo выполняются +`source ~/.nvm/nvm.sh && nvm use --silent`; для specs используется frontend +`.nvmrc`. Git-команды выполняются из соответствующего независимого репозитория. + +DB/HTTP-проверки используют уникальную временную SQLite с вымышленными данными +и явно заданной `DATABASE_URL`, без доступа к пользовательской базе или T-Bank. +Общий `npm run test:e2e` с фиксированной `/tmp/wealth-ledger-e2e.db` для них +не используется. Рабочая финансовая база не сбрасывается и не заменяется +ради выполнения проверки. + +Задачи одного раздела выполняются по указанным зависимостям. Задача 1.2 +может идти независимо от 1.1; остальные параллельные работы допустимы только +при закрытых зависимостях и раздельном владении изменяемыми файлами. + +## 1. Основа хранения и точных денег + +- [x] 1.1 [backend] Подтвердить применимость выбранного Solution Assessment: существующие Prisma/SQLite и migration boundary под Node 22; зафиксировать исходную версию схемы/приложения для проверки перехода и отката. + - **Covers:** `N/A` — техническое предусловие нового долговременного хранения из Design, без самостоятельного пользовательского поведения или смены выбранной архитектуры. + - **Verification:** `npx prisma validate`; существующие migrations применяются через `npx prisma migrate deploy` с `DATABASE_URL` каждой уникальной временной базы. Для снятия известного blocker нужны три последовательных успешных fresh-DB применения согласно [правилу проверки](../../../../docs/development/verification.md#известное-ограничение-сквозной-проверки). При ошибке сохранить версию и точный failure, не закрывать задачу и не подменять migration proof через `db push`, reset или ручной обход migrations. + - **Результат 2026-09-12:** Node `22.21.0`, Prisma/client `7.9.1`, engine `e922089b7d7502aff4249d5da3420f6fa55fc6ad`; `prisma validate` — passed. Исходный backend `8ec172f48eefa2237dc0c16919fbb2e726b39a65`, frontend `427ca23a433a20be25b1f4373f32c9e1f6f8c400`; оба working tree были чистыми. Обычный fresh deploy воспроизвёл `Error: Schema engine error:` без деталей, в том числе вне sandbox. С `RUST_LOG=info` штатный `npx prisma migrate deploy` вне sandbox прошёл три раза подряд: `/tmp/wealthledger-bank-cards-migrations-logging.rFEHKw/fresh-{1,2,3}.db`, логи `migrate-{1,2,3}.log` рядом. В каждом применены обе исходные migrations. Это подтверждает путь с указанной переменной окружения; запуск без неё остаётся ограничением. Версии зависимостей, применённые migrations и пользовательская база не менялись. + +- [x] 1.2 [backend] Выделить общего владельца decimal parsing/formatting и MoneyDto, сохранить protobuf-преобразование у T-Bank и добавить точное преобразование допустимого RUB-ввода в копейки. + - **Covers:** `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`. + - **Verification:** существующие decimal и затронутые брокерские calculation tests проходят до и после переноса; targeted Jest проверяет signed-64-bit границы, отрицательный/нулевой начальный остаток, строго положительную сумму операции, запрет лишней точности и точный Money roundtrip. `npm run typecheck:app`; прежний Money wire contract сохраняется. + - **Результат 2026-09-12:** общий owner `src/common/decimal.ts`, `money.dto.ts`, `rub-money.ts`; protobuf-преобразование осталось в T-Bank. До переноса `npm test -- --runInBand src/tbank` — 24 suites / 191 tests passed; после `npm test -- --runInBand 'src/tbank|src/common/rub-money'` — 25 suites / 213 tests passed. Targeted ESLint, `npm run typecheck:app`, `git diff --check` — passed. `npm run test:openapi-contract` до добавления банковских endpoints подтвердил неизменность прежнего контракта, но проверка обнаружила безусловную замену внешнего `DATABASE_URL` в `test/e2e-env.cjs` на фиксированную временную базу. Этот запуск не принимается как доказательство требуемой изоляции; после исправления test setup сравнение повторяется на уникальной базе. Пользовательская база не использовалась. Новый banking API требует отдельного финального сравнения. + +- [x] 1.3 [backend] После 1.1 добавить BankCard, ExpenseCategory и BankCardOperation, additive migration, связи, ограничения уникальности, индексы, версии, tombstones и fingerprints из Design. + - **Covers:** `manual-bank-cards` — `Сохранённый ручной учёт переживает повторный вход и перезапуск`; `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`. + - **Verification:** `npx prisma validate`, `npx prisma generate`, `npm run typecheck:app`; migration на изолированной базе с representative User/Session/TbankConnection сохраняет их данные. Реальная DB-проверка подтверждает FK/unique constraints и точное хранение копеек больше `2^53` и обеих граничных величин; применённые migrations не переписаны. + +Если для устранения подтверждённого migration blocker потребуется другой +подход или изменение согласованного стека, это возвращает решение в Design. +Успех unit/Swagger-проверки не разрешает закрыть 1.1 или 1.3 без DB-доказательства. + +Результат 1.3: additive migration `20260912090000_add_manual_bank_cards` +(существующие migrations неизменны), `prisma validate/generate` и app typecheck +— passed. Реальный adapter/Prisma roundtrip подтвердил `9007199254740993n` +и обе signed64 границы; configured DB-тесты подтвердили FK/unique. +Переход с исходной схемы сохранил representative User/Session/TbankConnection +в проверке сохранности 3.4. + +## 2. Backend: пользовательский учёт и денежные изменения + +- [x] 2.1 [backend] После 1.2–1.3 создать BankCardsModule и сервис сценариев: добавление карт с защитой повторов, пользовательские категории, начальный набор и переименование с сохранением идентичности и default role. + - **Covers:** `manual-bank-cards` — `Ручное добавление независимых банковских карт`; `manual-bank-cards` — `Общие для пользователя категории расходов`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`. + - **Verification:** targeted service/DB cases проверяют ограничения сведений карты, собственника, atomic card+seed creation, concurrent first-card creation без дублей категорий, регистронезависимую уникальность, rename default category и same-intent create карты с тем же/другим содержанием. Чтение не создаёт данных и не обращается к T-Bank. + +- [x] 2.2 [backend] После 1.2 реализовать чистые правила календарных дат и точные расчёты баланса и месячных расходов из первичных записей, сохранив O(N) подход Design. + - **Covers:** `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Баланс определяется начальным остатком и внесёнными операциями`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`. + - **Verification:** targeted Jest проверяет `1 − 0,10 − 0,20 = 0,70`, отрицательный баланс, итог выше предела отдельного ввода, московскую границу дня/месяца, реальные календарные даты, возврат прошлого месяца и исключение переводов/пополнений из расходов. Расчёт не зависит от страницы журнала; комментарий `ponytail:` фиксирует предел O(N) и условие оптимизации. + +- [x] 2.3 [backend] После 2.1–2.2 реализовать создание расходов и пополнений с проверкой карты/категории/дат, транзакцией и разрешением сохранённого намерения до повторной проверки изменяемых условий. + - **Covers:** `manual-bank-cards` — `Ручное внесение расходов и пополнений`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`. + - **Verification:** service/DB cases проверяют default category, сохранение при отрицательном остатке, отказ чужим ссылкам и недопустимому вводу без частичной записи; same UUID не создаёт дубликат, другой payload даёт конфликт, две независимые успешные траты сохраняются обе. + +- [x] 2.4 [backend] После 2.3 реализовать перевод одной записью с проверкой обеих карт и дат начала учёта, двумя проекциями истории и общим денежным эффектом. + - **Covers:** `manual-bank-cards` — `Перевод между своими картами как одно действие`; `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`. + - **Verification:** реальная DB-проверка подтверждает обе стороны перевода, допустимый отрицательный остаток источника и отсутствие частичного результата при ошибке; отклоняются одинаковые, чужие и архивные карты и дата до начала учёта любой стороны. Сумма не включается в расходы одной или всех карт. + +- [x] 2.5 [backend] После 2.3 реализовать связанные частичные и повторные возвраты, наследование категории покупки, общий лимит и чтение доступных исходных расходов за любой период. + - **Covers:** `manual-bank-cards` — `Возврат связан с исходным расходом`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`. + - **Verification:** DB cases проверяют несколько возвратов, исходный расход прошлого месяца, чужую/удалённую покупку, дату до покупки и два конкурирующих возврата с превышением общего лимита. Повтор успешно сохранённого полного возврата после потерянного ответа возвращает существующий результат без ошибки нового лимита и без дубликата. + +- [x] 2.6 [backend] После 2.4–2.5 реализовать version-protected исправление и логическое удаление всех видов операций с проверкой прежних и новых связей, сохранением creation fingerprint и запретом изменения вида/исходной покупки возврата. + - **Covers:** `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Возврат связан с исходным расходом`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`. + - **Verification:** DB cases покрывают перенос расхода/пополнения между картами, смену получателя и удаление целого перевода, смену месяца/категории, ограничения исходного расхода с возвратами, edit refund с исключением собственной прежней суммы, stale edit/delete и повтор delete. Late create retry после edit/delete не возвращает старые суммы и не воскрешает запись; гонка refund с уменьшением покупки сохраняет общий лимит. + +- [x] 2.7 [backend] После 2.6 реализовать изменение сведений и начала учёта карты, архив и возврат из архива с проверкой всей затронутой истории и version conflict. + - **Covers:** `manual-bank-cards` — `Ручное добавление независимых банковских карт`; `manual-bank-cards` — `Исправление начала ручного учёта`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`. + - **Verification:** DB cases проверяют opening balance без новой операции/изменения месячных трат, недопустимое новое начало с входящим переводом, архив при любом остатке, запрет create/edit/delete по архиву, unarchive и stale card edit. Гонка архива или смены даты с финансовым изменением даёт последовательный допустимый результат; повтор create архивированной карты возвращает ту же архивную карту. + +Результат раздела 2: `src/bank-cards/bank-card.calculations.spec.ts` — 23 +pure tests passed; полный backend gate — 33 suites / 253 tests passed. +`test/bank-cards.e2e-spec.ts` — 18 configured HTTP/SQLite tests passed +(`/tmp/bank-cards-http.log`): атомарные повторы/переводы, независимые траты, +конкурирующие возвраты и уменьшение покупки, смена начала/архив одновременно +с операцией, версии, foreign ownership, исправления/удаления, категории и +точные денежные границы. Fixtures вымышленные, URL уникальный; setup сохраняет +внешний `DATABASE_URL`, банковский suite запрещает shared/user DB. + +## 3. Backend: чтение, HTTP и доказательство сохранности + +- [x] 3.1 [backend] После раздела 2 подключить расчёты к согласованным чтениям списка/деталей карт, месячного журнала, исходных расходов возврата и сводок одной/всех карт. + - **Covers:** `manual-bank-cards` — `Баланс определяется начальным остатком и внесёнными операциями`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Возврат связан с исходным расходом`. + - **Verification:** integration cases проверяют пагинацию и стабильный порядок равных дат, весь набор для баланса/сводки, отрицательный net, архив в общей сводке, перенос между месяцами и перегруппировку связанных возвратов после изменения категории покупки. Связанные данные одного ответа согласованы; отсутствуют server cache и materialized balance. + +- [x] 3.2 [backend] После 3.1 завершить controller/runtime DTO под `/api/v1/bank-cards`, подключить существующие JWT/filter и сформировать полный runtime OpenAPI для frontend. + - **Covers:** `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`; `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`. + - **Verification:** configured HTTP cases проверяют фактические validation/status/error codes, отсутствие сессии, чужие связи, архив/version/refund conflicts, безопасность ответов и работу без T-Bank. Runtime OpenAPI отражает действующие DTO и Money; сырые Prisma records не выдаются вместо оговорённого контракта, финансовые DTO не попадают в ответы с ошибками. + +- [x] 3.3 [backend] После 3.2 завершить runnable configured HTTP/DB проверки критических сценариев и конкурентных изменений в отдельной временной базе, дополнив уже созданные cases вместо их дублирования. + - **Covers:** `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`; `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** с уникальной `DATABASE_URL` выполнить `NODE_ENV=development NODE_OPTIONS=--experimental-vm-modules npx jest --config ./test/jest-e2e.json --runInBand bank-cards`. Проверяемые исходы: один результат concurrent same-intent create карты/операции, сохранность обеих независимых трат, no partial transfer/refund, stale conflicts, архивные/чужие ссылки, loss-of-response replay и отсутствие ложного успеха. Mock Prisma не заменяет DB cases. + +- [x] 3.4 [backend] После 3.3 доказать долговременность ручных данных, корректный backup/restore и совместимость отката приложения с расширенной схемой без удаления новой истории. + - **Covers:** `manual-bank-cards` — `Сохранённый ручной учёт переживает повторный вход и перезапуск`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`. + - **Verification:** на disposable базах перезапуск сохраняет карты, операции, связи, переименованные категории, архив, tombstones и fingerprints; повтор после перезапуска не создаёт дубликат. Проверяются согласованный SQLite backup/restore, сохранность User/Session/TbankConnection, запуск исходной версии приложения с additive schema и доступность новых записей после возврата новой версии. Старый backup не восстанавливается поверх новой истории как штатный rollback. + +Результат раздела 3: те же 18 HTTP/DB tests подтверждают чтения, пагинацию, +month/category regrouping, validation/status и безопасные ошибки без T-Bank. +Дополнительный процессный сценарий `node /tmp/bank-cards-durability.cjs` — passed: +исходный backend собран из Git archive `8ec172f48eefa2237dc0c16919fbb2e726b39a65` +в `/tmp/wealthledger-bank-baseline.8hcDz3`; миграция сохранила старые таблицы, +перезапуск — категории/архив/возвраты/tombstones/fingerprints, старое приложение +запустилось с расширенной схемой, новое вновь прочитало историю, согласованный +SQLite backup восстановлен и прочитан приложением. Свидетельство: +`/var/folders/j_/h2ft23gd3qq35ps3v5bcftfm0000gn/T/wealthledger-bank-durability-hPvsEO/result.json`. +Runnable версия сохранена в `backend/test/bank-cards-durability.cjs`; ей +передаётся каталог отдельно собранной исходной версии. Проверки создают свои +временные базы; старый backup не записывается поверх новой истории. + +## 4. Контракт и слой данных frontend + +- [x] 4.1 [frontend] После 3.2 обновить committed OpenAPI snapshot из настроенного backend runtime document и сгенерировать frontend types, сохранив backend единственным editable HTTP source. + - **Covers:** `N/A` — производные артефакты двустороннего контракта из Design обеспечивают типизированного потребителя, но не вводят отдельного пользовательского поведения. + - **Verification:** `npm run openapi:generate`, `npm run openapi:check`, `npm run typecheck`; snapshot получен по [существующему workflow](../../../../docs/development/runtime-openapi-verification.md), generated types не исправлялись вручную, существующие инвестиционные Money-контракты сохранены. + +- [x] 4.2 [frontend] После 4.1 добавить banking queries/mutations у domain owner: session-aware transport, стабильный create UUID, version inputs, безопасные retries и invalidation всех затронутых банковских GET-представлений. + - **Covers:** `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`; `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** Vitest/MSW через `npm run test:run -- src/entities/bank-card` проверяют одинаковый UUID при network/auth retry, отсутствие optimistic balance, invalidation обеих карт/всех месяцев/исходных расходов, rename категории, сохранение данных при ошибке, GET после version conflict, logout isolation и независимость от T-Bank connection mutation. Свой refresh loop и второй query cache отсутствуют. + +Результат 4.2: `src/entities/bank-card/api/banking.test.tsx` — 10 MSW tests +passed; typecheck, focused Biome и dependency boundaries — passed. Повторы +сохраняют UUID, подтверждённые mutations инвалидируют только все banking GETs, +конфликты вызывают перечитывание, ошибки не меняют суммы оптимистически. +Дополнены реальные feature tests AuthProvider/TbankConnectionSection: 10 tests +passed доказывают очистку banking cache при logout/refresh failure и его +сохранение без invalidation при connect/replace/delete T-Bank. + +Результат 4.1: snapshot экспортирован из configured runtime Nest/Swagger +(42 paths) на уникальной временной SQLite, включая банковские 409/410. +`npm run openapi:generate`, `npm run openapi:check`, `npm run typecheck` — passed; +generated types не редактировались вручную. После финального добавления +описаний 409/410 snapshot/types повторно сгенерированы, `openapi:check` passed. + +## 5. Экраны и формы из UI/UX package revision 1 + +Все задачи раздела используют существующие Mantine-тему, i18n, container/VM/view, +RHF/Zod и shared UI из Design. Проверки формы и состояний выполняются через +видимый результат и сеть MSW; отдельный тест декларативной обёртки не нужен. + +- [x] 5.1 [frontend] После 4.2 собрать `/cards`: навигацию, список активных карт и архива, месячную сводку всех карт и форму добавления карты с начальным остатком/датой. + - **Covers:** `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Ручное добавление независимых банковских карт`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** `npm run test:run -- src/pages/cards` и targeted route tests проверяют login/returnTo без T-Bank, обязательные/необязательные поля, точный отрицательный остаток, no request при invalid, блокировку pending, сохранение ввода при ошибке и один create UUID при повторе. Loaded/empty/error/skeleton и архивный переключатель соответствуют Design; сводка всех карт включает архив независимо от фильтра списка. + +- [x] 5.2 [frontend] После 5.1 собрать `/cards/:id`: крошки и title, текущий баланс, общий выбор месяца для журнала/сводки, пагинацию и представления четырёх видов операций. + - **Covers:** `manual-bank-cards` — `Баланс определяется начальным остатком и внесёнными операциями`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** `npm run test:run -- src/pages/card` проверяет month/URL и страницу, неизменный текущий баланс, расходы/возвраты/net со знаком, понятные стороны перевода, локальную ошибку сводки, cached snapshot при refetch failure и безопасное состояние отсутствующей карты. Frontend форматирует Money без собственной финансовой арифметики. + +- [x] 5.3 [frontend] После 5.2 добавить формы создания расхода, пополнения и перевода с общими проверками денежного ввода/дат и сохранением одного намерения до определённого результата. + - **Covers:** `manual-bank-cards` — `Ручное внесение расходов и пополнений`; `manual-bank-cards` — `Перевод между своими картами как одно действие`; `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`; `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** RTL/MSW cases проверяют default category, положительную сумму/копейки без потери больших значений, даты обеих карт перевода, отрицательный итог без запрета реальной траты, visible labels, pending/ошибки с сохранённым вводом и повтор одного перевода без дубликата. Новое подтверждённое состояние приходит из банковских queries. + +- [x] 5.4 [frontend] После 5.3 добавить создание и редактирование возврата с выбором исходной покупки за всю историю карты и корректным лимитом текущей записи. + - **Covers:** `manual-bank-cards` — `Возврат связан с исходным расходом`; `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`. + - **Verification:** RTL/MSW journey выбирает покупку прошлого месяца, записывает частичный возврат и видит его в месяце получения; полный возврат позволяет исправить описание, исключая собственную старую сумму из лимита. Проверяются превышение, stale source, immutable purchase link, архив, network retry полного возврата и сохранение формы без ложного успеха. + +- [x] 5.5 [frontend] После 5.3–5.4 добавить остальные исправления/удаления операций, изменение сведений и начала учёта карты, архив и возврат из архива с понятными конфликтами. + - **Covers:** `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Исправление начала ручного учёта`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** RTL/MSW проверяет edit с version и последующим GET при конфликте, сохранение черновика, перенос между картами/месяцами, подтверждение удаления перевода с обеими картами, запреты связанного расхода и архива, opening date error и обратимый архив. После успешного действия все затронутые видимые суммы соответствуют новым серверным данным. + +- [x] 5.6 [frontend] После 5.3 добавить управление начальными и своими категориями и создание категории из формы расхода без потери введённой операции. + - **Covers:** `manual-bank-cards` — `Общие для пользователя категории расходов`; `manual-bank-cards` — `Ручное внесение расходов и пополнений`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`. + - **Verification:** RTL/MSW проверяет add/rename, уникальность без регистра и длину, переименование default category без утраты роли, обновление названий в истории/сводке и неизменный черновик расхода при добавлении категории. Удаление и вложенные подкатегории отсутствуют согласно scope. + +Результат 5.1–5.6: реализованы страницы, четыре вида операций, исправления, +удаление, архив и категории. RTL/MSW покрывают точный ввод и UUID после ошибки, +month/page/returnTo, неизменный текущий баланс, полный возврат с собственным +лимитом, stale version с явным принятием новой версии, opening-date conflict, +архив, обе стороны удаления перевода и вложенное создание категории. +Финальный frontend gate включает 43 test files / 263 passed tests. + +Браузер обнаружил потерю нового имени категории после reset неконтролируемого +поля: поле переведено на RHF Controller, исправление проверено RTL и настоящим +PATCH/GET. Независимое ревью обнаружило скрытые ошибки вспомогательных GET +карт/категорий: добавлены локальные сообщения и retry, две regression-проверки +и browser recovery passed. Существующие authentication/инвестиционные tests +прошли; returnTo сохраняет карточку и query выбранного месяца. + +## 6. Проверка пользовательского результата и документация + +- [x] 6.1 [frontend] После 3.3–3.4 и разделов 4–5 проверить UI package в браузере с изолированным backend и подготовить Storybook-примеры новых собственных представлений на вымышленных данных. + - **Covers:** `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`; `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Сохранённый ручной учёт переживает повторный вход и перезапуск`. + - **Verification:** реальный UI journey без T-Bank: две карты → расход/пополнение → перевод → возврат прошлой покупки → исправление/удаление → архив/unarchive → reload/login. Проверить широкий экран и 320–375 px, пять пунктов навигации, длинные суммы/названия, keyboard/focus return, reduced motion и loading/empty/error/refetch recovery. Сохранить краткие результаты и локальные визуальные свидетельства; Storybook build не подменяет браузерную проверку. + +- [x] 6.2 [docs] После подтверждения реализованного поведения обновить документацию: область банковских карт, глоссарий, обзор продукта и тематическую навигацию, хранимые данные, денежные правила и безопасное восстановление. + - **Covers:** `N/A` — as-built объяснение поставленного поведения и восстановления из Delivery Map; новая норма не вводится и не копируется параллельно Delta Specs. + - **Verification:** пройти путь README → банковские карты → понятия/расчёты/исправления и восстановление; сверить текст с поставленными flows и ограничениями, включая ручной источник баланса, отрицательные итоги, архив, повторы и сохранение новой истории при rollback. Ссылки ведут на существующие targets; исторические материалы не переписываются, обещания неподтверждённой миграции не добавляются. + +Результат 6.1: реальный браузер работал с отдельным BrowserContext и backend +на уникальной временной SQLite, с вымышленным пользователем без T-Bank. +Через UI выполнены две карты (1 000 ₽ и 0 ₽), августовский расход 100 ₽, +сентябрьские пополнение 200 ₽, перевод 150 ₽ и возврат 60 ₽: баланс A 1 010 ₽, +B 150 ₽, сентябрьский net −60 ₽. Возврат исправлен до полного 100 ₽, +его описание изменено, затем сумма возвращена к 60 ₽. Исправление покупки +100 → 150 ₽ дало A 960 ₽; удаление перевода с подтверждением обеих карт — +A 1 110 ₽ и B 0 ₽. Архив сохранил историю и общий net; unarchive вернул действия. + +Добавление категории из расхода сохранило сумму 12,34 ₽ и описание черновика; +переименование «Другое» сохранило default role. Проверены Escape и возврат +фокуса к действию, visible labels, initial loading/empty, ошибки GET и retry, +сохранённый net при неудачном refetch, reduced motion. Logout → protected URL → +login вернул ту же карту и август; reload сохранил баланс 1 110 ₽ и расход 150 ₽. +Экран 1440 px и 320/375 px проверен визуально: пять пунктов навигации, +длинное название и точная предельная сумма доступны без горизонтального overflow. + +Локальные screenshots итогового UI: +`/tmp/wealthledger-bank-cards-final-desktop.png`, +`/tmp/wealthledger-bank-cards-final-mobile.png`, +`/tmp/wealthledger-bank-cards-final-mobile-long.png`, +`/tmp/wealthledger-bank-card-final-mobile.png`; +состояние cached error — `/tmp/wealthledger-bank-cards-cached-error.png`. +Добавлены две изолированные story groups и 9 примеров на вымышленных данных; +storybook dependency isolation и build passed. + +Результат 6.2: обновлены README/overview, glossary, область cards, описание +хранения, локальная миграция/восстановление и UI-каталог. Маршруты README → cards → +Money/восстановление проверены; объяснения сверены с Delta Specs, реализацией +и наблюдёнными flows. Новых Mermaid-диаграмм и изменений исторических ADR нет. + +## 7. Финальные gates и состояние SDD-артефактов + +Backend `npm run verify` — passed (`/tmp/bank-cards-verify.log`): lint без +warnings, app/test TypeScript, 33 suites / 253 tests, Prisma generate и build. +Последующий итоговый HTTP-прогон и independent review описаны ниже; +успех gates не является заменой semantic review. + +После независимого review исправлен P2 в DTO: явный `null` для Money, +архива и non-null полей отклоняется; nullable реквизиты очищаются по-прежнему. +Configured HTTP/SQLite — 19/19 passed, включая 400 без изменения данных/версий; +OpenAPI parity на уникальной базе — passed, wire shape не изменился. +Итоговый `npm run verify` повторно passed +(`/tmp/bank-cards-final-backend-verify.log`). Сохранённый runnable durability +сценарий повторно passed на итоговом backend: +`/var/folders/j_/h2ft23gd3qq35ps3v5bcftfm0000gn/T/wealthledger-bank-durability-dXSTmz/result.json`. +Reviewer перепроверил исправление и не нашёл других backend findings. + +Gate применяется к итоговому состоянию соответствующего repo. Уже прошедшие +проверки не повторяются без новых изменений, сбоев или оставшегося риска. +Если финальная правка меняет проверенную границу, повторяется её нужный gate. + +- [x] 7.1 [backend] После разделов 1–3 выполнить canonical backend gate и сохранить результаты DB/HTTP/durability проверок для того же состояния кода. + - **Covers:** `N/A` — обязательная проверка backend delivery из Design, дополняющая уже покрытые поведенческие и DB cases без новой возможности. + - **Verification:** `npm run verify`; отдельные результаты 3.3–3.4 остаются применимы к итоговому коду. Type/lint/unit/build не объявляются доказательством транзакций или миграции, если DB-проверка не запускалась успешно. + +- [x] 7.2 [frontend] После 6.1 выполнить canonical frontend gate для итоговых API types, UI, i18n, stories и dependency boundaries. + - **Covers:** `N/A` — обязательный frontend delivery gate из Design, без добавления самостоятельного пользовательского поведения. + - **Verification:** `npm run verify` включает OpenAPI drift, typecheck, lint, coverage, Knip, dependency boundaries и обе сборки. Отдельный результат браузерной проверки 6.1 сохраняет применимость; процент покрытия и Storybook build не заменяют его. + +- [x] 7.3 [workspace] После 4.1, 7.1 и 7.2 подтвердить двусторонний runtime OpenAPI contract по итоговому backend и frontend snapshot/types. + - **Covers:** `N/A` — межрепозиторная verification из Contract Gate Decision, без владения кодом приложений или отдельным поведением. + - **Verification:** из backend выполнить `npm run test:openapi-contract`; использовать актуальный frontend `openapi:check` из 7.2, не повторяя его при неизменном snapshot/types. Проверка не требует T-Bank и не изменяет snapshot автоматически. + +- [x] 7.4 [docs] После 6.2 выполнить canonical docs gate, ручную смысловую сверку и проверку затронутых маршрутов чтения. + - **Covers:** `N/A` — проверка связности as-built документации из Delivery Map, без самостоятельного нормативного поведения. + - **Verification:** из docs выполнить `npm run verify`; проверить новые Markdown-файлы и затронутые внешние относительно docs ссылки. Если изменены Mermaid-диаграммы, отдельно проверить доступным локальным renderer их вид либо явно сохранить ограничение; новый стек для этого не устанавливать. + +- [x] 7.5 [specs] После всех implementation-owned задач сверить поставленный результат со всеми Delta Requirements и обновить этот Tasks только по выполненным действиям и наблюдённым проверкам, затем выполнить canonical validation. + - **Covers:** `N/A` — поддержание существующих SDD-артефактов и traceability реализации из Delivery Map; новые требования, архитектура и отдельный журнал доказательств не создаются. + - **Verification:** каждое требование `manual-bank-cards` связано с выполненной задачей и подходящим доказательством; все оставшиеся ограничения названы. Выполнить `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs` и `git diff --check`, отдельно просмотрев новые файлы и ссылки. Approval и завершение реализации не выводятся только из наличия файлов. + +Независимая проверка результата и завершение lifecycle выполняются после +implementation-owned задач; они не маскируются checkbox, который исполнитель +не может закрыть сам. Этот план не включает преждевременную синхронизацию +нового capability в основные specs, архивирование или изменение истории. +Такие действия относятся к последующему завершению выбранного SDD lifecycle. + +Итоговые результаты 7.2–7.4: frontend `npm run verify` — passed +(`/tmp/bank-cards-final-frontend-verify.log`): OpenAPI drift, TypeScript, +Biome, 43 files / 263 tests с coverage, Knip, обе dependency проверки, +app и Storybook builds. Backend `npm run test:openapi-contract` — passed +на уникальной базе (`/tmp/bank-cards-contract.log`); последний DTO fix +не меняет wire shape, frontend snapshot/types после этого не изменялись. +Docs `npm run verify` — passed, 54 Markdown-файла, 0 issues. + +Независимое ревью backend/frontend/docs завершено: найденные null-validation, +category-form и auxiliary-query recovery дефекты исправлены и перепроверены; +открытых actionable findings нет. Обычный Prisma fresh migrate deploy без +`RUST_LOG=info` по-прежнему имеет указанное в 1.1 ограничение; причина +не установлена. Подтверждён только штатный путь с переменной окружения. +Предупреждение Storybook/Vite о размере chunks не препятствует сборке. + +Результат 7.5: все 17 Delta Requirements сверены с реализацией и Covers/проверками +выше; 29 implementation-owned задач завершены. `openspec validate +add-manual-bank-cards --type change --strict --store wealth-ledger-specs` — valid; +`openspec validate --specs --strict --store wealth-ledger-specs` — 25 passed, +0 failed. Итоговый routing повторён по modified/untracked файлам четырёх repo; +новые файлы и ссылки просмотрены, `git diff --check` — passed в каждом repo. +Proposal, Delta Specs, Design, основные specs и архивы не изменялись. + +Последующие проверки перед commit: по запросу пользователя все четыре нативных +поля дат/месяца заменены на Mantine DateInput/MonthPickerInput. Сохранение +календарных дат, month/URL и календарь на 320 px проверены в браузере +на новой временной базе; будущие даты недоступны. Полный frontend +`npm run verify` повторно passed: 43 files / 263 tests и обе сборки +(`/tmp/bank-mantine-verify.log`). + +После отдельного разрешения пользователя миграция применена к рабочей SQLite +через `RUST_LOG=info npx prisma migrate deploy`. До применения создан +согласованный SQLite backup; после — проверены integrity/FK и неизменность +User/Session/TbankConnection. Это обновление рабочей среды, не тестовые записи +в пользовательской базе. Backup исключён из Git; ограничение обычного запуска +Prisma без `RUST_LOG=info` остаётся прежним.