446 lines
44 KiB
Markdown
446 lines
44 KiB
Markdown
## 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. Этот документ не разрешает менять пользовательскую
|
||
базу или начинать реализацию до согласования подготовленного решения.
|