docs: record completed manual bank card change
Track the approved proposal, delta specification and design with all 29 verified implementation tasks. Keep the change active without archiving or syncing main specifications.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd
|
||||
created: 2026-09-12
|
||||
@@ -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. Этот документ не разрешает менять пользовательскую
|
||||
базу или начинать реализацию до согласования подготовленного решения.
|
||||
@@ -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 этого изменения.
|
||||
@@ -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** пользователь видит причину, сохраняет черновик и может получить актуальные данные для следующего действия без перезаписи чужого успешного изменения
|
||||
@@ -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` остаётся прежним.
|
||||
Reference in New Issue
Block a user