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,2 @@
|
||||
schema: wealth-ledger-sdd
|
||||
created: 2026-09-12
|
||||
@@ -0,0 +1,40 @@
|
||||
# Completion Evidence: add-manual-bank-cards
|
||||
|
||||
## Finalization Authority
|
||||
|
||||
Change Owner authorized Finalization, canonical sync and archive after a current
|
||||
independent read-only Validation PASS. The PASS found no Critical or Important
|
||||
findings after the monthly-summary presentation correction.
|
||||
|
||||
## Delivery
|
||||
|
||||
| Area | Result | Delivery commit |
|
||||
| --- | --- | --- |
|
||||
| frontend | Manual card screens and summaries; Finalization correction shows exact expenses, refunds and net totals with a regression test. | `ff67a76`, `d3ac8c2`, `734c5bd` |
|
||||
| backend | Bank-card persistence, calculations, API and isolated verification. | `e6f58ef` |
|
||||
| docs | Cards overview states the three displayed summary totals. | `3a27268` |
|
||||
| specs | Approved legacy Proposal, Delta Specs, Design, completed Tasks and Finalization evidence; native sync creates canonical behavior spec. | `834f2d1` plus finalization/archive commit |
|
||||
|
||||
## Observed Verification
|
||||
|
||||
| Area | Command or review | Result |
|
||||
| --- | --- | --- |
|
||||
| frontend | `npm run test:run -- src/entities/bank-card/ui/BankSummary/BankSummary.view.test.tsx` | PASS: 1 file, 1 test |
|
||||
| frontend | `npm run typecheck` | PASS |
|
||||
| docs | `npm run verify` | PASS |
|
||||
| specs | `openspec schema validate wealth-ledger-sdd --verbose` | PASS |
|
||||
| specs | `openspec schema validate wealth-ledger-sdd-v2 --verbose` | PASS |
|
||||
| specs | `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs` | PASS |
|
||||
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 25 specs, 0 failed |
|
||||
| all affected areas | `git diff --check` | PASS |
|
||||
|
||||
## Independent Validation
|
||||
|
||||
PASS. The review compared Proposal, all 17 Delta Requirements, Design and
|
||||
completed Tasks with current backend calculations/DTOs, shared frontend summary
|
||||
rendering and cards documentation. It verified that `/cards` and `/cards/:id`
|
||||
share the corrected summary view and found no Critical or Important findings.
|
||||
|
||||
Known non-blocking limitation: a plain Prisma fresh migration deploy still needs
|
||||
`RUST_LOG=info`; the verified migration path and its evidence remain recorded in
|
||||
the change Tasks.
|
||||
@@ -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 этого изменения.
|
||||
+688
@@ -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