spec: finalize wealth-ledger SDD migration
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
This commit is contained in:
@@ -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. Этот документ не разрешает менять пользовательскую
|
||||
базу или начинать реализацию до согласования подготовленного решения.
|
||||
Reference in New Issue
Block a user