diff --git a/README.md b/README.md index 2d53299..5f14a17 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # WealthLedger: документация системы WealthLedger помогает частному инвестору понимать состав портфеля T-Bank, -исполненные операции, полученный доход и будущие выплаты. Здесь объясняются +исполненные операции, полученный доход и будущие выплаты, а также вручную вести +банковские карты и повседневные расходы. Здесь объясняются продукт, понятия, функции, взаимодействия и причины архитектурных решений. Основной текст можно читать без исходников; технические ссылки нужны для дальнейшей проверки и изменения системы. @@ -28,6 +29,7 @@ WealthLedger помогает частному инвестору понимат | --- | --- | | [Аутентификация](domains/authentication/overview.md) | Регистрация, вход, восстановление и завершение сессии | | [Счета](domains/accounts/overview.md) | Доступные счета, сводки и состояния каждого счёта | +| [Банковские карты](domains/cards/overview.md) | Ручные остатки, операции, категории, возвраты, месячные сводки и исправления | | [Портфель](domains/portfolio/overview.md) | Позиции, остатки, суммы, структура и свежесть | | [Все счета](domains/portfolio/all-accounts.md) | Общий портфель, происхождение строк и сводные разделы | | [Операции](domains/operations/overview.md) | Исполненные операции, фильтры и пагинация | @@ -48,7 +50,7 @@ WealthLedger помогает частному инвестору понимат | Узнать причину архитектурного решения | [Журнал ADR](adr/README.md) | | Изменить или проверить систему | [Сопровождение](development/README.md), затем инструкции нужного репозитория | -Практический tutorial первого финансового сценария пока не подготовлен: +Практический tutorial первого инвестиционного сценария пока не подготовлен: он требует подключения брокера, а запуск на новой базе имеет известное ограничение. Маршрут первого знакомства выше — чтение, не учебный запуск. UI-каталог содержит безопасные вымышленные примеры и поясняет границы их использования. diff --git a/architecture/containers.md b/architecture/containers.md index de17f7b..33a8362 100644 --- a/architecture/containers.md +++ b/architecture/containers.md @@ -38,7 +38,7 @@ SPA выполняется в браузере пользователя. Backend | --- | --- | | React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений | | Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши | -| SQLite | Долговременное хранение локальных пользователей, сессий и зашифрованных подключений | +| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений и ручной банковской истории | | Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности | Frontend организован слоями `app → pages → features → entities → shared`: @@ -66,6 +66,19 @@ SQLite содержит пользователя с нормализованны Пароли хешируются bcrypt; token брокера шифруется AES-256-GCM. Plaintext credentials не являются сохраняемым представлением этих данных. +Ручной банковский учёт хранит карты, пользовательские категории и операции +в отдельных таблицах, связанных с User. Начальный остаток и суммы записаны +целыми bigint-копейками, календарные даты — днями без времени. Перевод хранится +одной операцией с двумя картами; возврат связан с исходной покупкой. Баланс +вычисляется из неудалённой истории и отдельно не сохраняется. + +Банковские запросы проверяют локального владельца каждой связи и не используют +подключение брокера. Транзакции, ограничения уникальности и версии согласуют +переводы, возвраты и конкурирующие исправления. Fingerprint исходного ввода +и отметка удаления защищают от повторного создания после потерянного ответа +или перезапуска. Архив обратим и сохраняет историю. Подробности — +[банковские карты](../domains/cards/overview.md). + Сессии и подключение связаны с владельцем. Access token подтверждает локального пользователя, но `accountId` из URL сам по себе не даёт права читать брокерский счёт. Backend получает текущее подключение пользователя diff --git a/development/local-workspace.md b/development/local-workspace.md index b0c56a9..5189e57 100644 --- a/development/local-workspace.md +++ b/development/local-workspace.md @@ -28,8 +28,10 @@ T-Bank token вводится пользователем через интерф **Известное ограничение:** ранее была зафиксирована ошибка `Schema engine error` на новой временной SQLite-базе до создания схемы. Условия проверки и пересмотра — в [ограничении сквозной проверки](verification.md#известное-ограничение-сквозной-проверки). -Пользовательская база для такого воспроизведения не нужна; полный первый запуск -на чистой базе этим свидетельством не подтверждён. +На Node 22.21.0 / Prisma 7.9.1 подтверждено три последовательных применения +миграций к новым временным базам с `RUST_LOG=info` вне sandbox. Без этой +переменной ошибка воспроизводится. Пользовательская база для воспроизведения +не нужна; успешная миграция сама по себе не подтверждает полный первый запуск. В development доступна [локальная Swagger UI](http://localhost:3000/api/docs) при стандартном порте API. Она показывает контракт, но не доказывает доступность @@ -57,8 +59,36 @@ refresh-cookie получает Secure flag и требует защищённо 4. «Доходы» показывают полученные выплаты, «События» — будущие. 5. При выходе локальная сессия и её клиентский кэш очищаются. -Без T-Bank token доступны регистрация, вход и состояние отсутствующего -подключения. Это не подтверждает работу финансовых сценариев. +Без T-Bank token доступны регистрация, вход, состояние отсутствующего +подключения и [ручной банковский учёт](../domains/cards/overview.md). +Банковский сценарий не подтверждает доступность инвестиционных поставщиков. + +## Сохранение данных и откат + +Перед обновлением сохраняется согласованный SQLite backup; ключ шифрования +T-Bank хранится отдельно. Для работающей базы используется SQLite backup API, +который учитывает WAL. Несогласованное копирование файлов базы и WAL не является +резервной копией. Восстановление сначала проверяется на отдельном файле. + +Банковская миграция добавляет таблицы, сохраняя User, Session и TbankConnection. +При необходимости отката приложения сначала прекращаются новые записи и +сохраняется актуальный снимок. Затем возвращается совместимая предыдущая версия +backend при сохранении расширенной схемы. Старый backup не восстанавливается +поверх новой истории: это удалило бы введённые банковские операции. + +Совместимость проверена с исходной версией backend +`8ec172f48eefa2237dc0c16919fbb2e726b39a65`: она выполняла вход с расширенной +схемой, а новая версия после возврата читала прежние карты и операции. +Отдельно проверены перезапуск, служебные данные защиты повторов и приложение +на восстановленной SQLite-копии. Это подтверждение конкретной пары версий, +а не гарантия совместимости любого будущего изменения схемы. + +Проверка воспроизводится через +[процессный сценарий](../../backend/test/bank-cards-durability.cjs) +с каталогом отдельно собранного baseline: после активации backend `.nvmrc` +выполнить `node test/bank-cards-durability.cjs `. +Сценарий создаёт собственные временные базы и вымышленные данные. Migrations +и HTTP/DB-ограничения описаны в [проверке системы](verification.md). ## Если запуск или сценарий не работает diff --git a/development/ui-catalog.md b/development/ui-catalog.md index 2393efa..60fa2a0 100644 --- a/development/ui-catalog.md +++ b/development/ui-catalog.md @@ -12,7 +12,7 @@ Storybook 10.6.0 показывает реальные представлени ## Компоненты и работа с примерами -Каталог включает 10 групп и 41 пример: +Каталог включает 12 групп и 50 примеров: | Группа | Что показывает | | --- | --- | @@ -26,6 +26,8 @@ Storybook 10.6.0 показывает реальные представлени | Обновление портфеля | Сохранённые суммы при обновлении и ошибке, локальную повторную попытку | | Загрузка страницы счёта | Контур ожидаемых заголовка, сводки и строк | | Структура портфеля | Валюты и типы активов, только деньги, пустую и неполную оценку, неизвестные и отрицательные значения | +| Сводка банковских карт | Расходы и возвраты, отрицательный месяц возврата, известные нули, загрузку, ошибку, сохранённые суммы и длинные точные итоги | +| Поля банковской карты | Независимую рублёвую карту и недоступные финансовые поля архива | Каталог не перечисляет все элементы библиотеки или приложения. Отсутствие представления в витрине не означает его отсутствия в продукте. Основы показывают diff --git a/development/verification.md b/development/verification.md index 10b4f74..c6b13e8 100644 --- a/development/verification.md +++ b/development/verification.md @@ -39,10 +39,22 @@ SQLite-базе завершалось `Schema engine error` до запуска Предыдущее описание локальной проверки от 12 сентября 2026 года также отмечало ошибку на временной базе без обращения к пользовательской базе. -Успех unit-проверок и OpenAPI не закрывает эту границу. Принятое условие пересмотра — +Успех unit-проверок и OpenAPI не закрывает эту границу. Условие проверки — стабильная версия Prisma с тремя последовательными успешными применениями -миграций на новых базах под Node 22. Условия безопасного локального запуска и -сохранения данных — в [локальном окружении](local-workspace.md). +миграций на новых базах под Node 22. + +При реализации ручных карт Prisma 7.9.1 на Node 22.21.0 трижды подряд применил +существующие migrations к новым временным базам командой +`RUST_LOG=info DATABASE_URL=file:<новая-временная-база> npx prisma migrate deploy` +вне sandbox. Без `RUST_LOG=info` пустой `Schema engine error` воспроизводился +и вне sandbox. Поэтому подтверждён именно этот способ запуска; исправление +обычного запуска или причина зависимости от журналирования не установлены. +Он использует штатную историю migrations без `db push`, reset или ручного SQL-обхода. +Результат исходной проверки сохранён в +[Tasks ручных карт](../../specs/openspec/changes/add-manual-bank-cards/tasks.md). +Применение новой миграции, DB-инварианты и HTTP-сценарии требуют своих проверок. +Условия безопасного локального запуска и сохранения данных — +в [локальном окружении](local-workspace.md). ## Где выполнять проверки diff --git a/domains/cards/overview.md b/domains/cards/overview.md new file mode 100644 index 0000000..d561ed8 --- /dev/null +++ b/domains/cards/overview.md @@ -0,0 +1,121 @@ +# Ручной учёт банковских карт + +Банковская карта — самостоятельный ручной учёт дебетовой карты в рублях. +Пользователь задаёт название, дату начала и начальный остаток; банк и последние +четыре цифры необязательны. Даже одинаково названные карты одного банка имеют +разные истории и балансы. Полные номера, PIN, срок действия и код безопасности +не запрашиваются как реквизиты этого учёта. + +Данные принадлежат локальному пользователю. Для них достаточно сессии +WealthLedger; подключение T-Bank не требуется. Банковские суммы не входят +в брокерский портфель, пассивный доход или прогноз выплат, а смена подключения +брокера не изменяет ручную историю. + +## Откуда берётся баланс + +Начальный остаток означает деньги до всех внесённых операций начиная с даты +начала учёта. Баланс равен этому остатку плюс пополнения, входящие переводы +и возвраты, минус расходы и исходящие переводы. Он учитывает всю неудалённую +историю, независимо от выбранного месяца, страницы журнала и архива. + +Например, остаток 1 ₽ и расходы 0,10 ₽ и 0,20 ₽ дают ровно 0,70 ₽. +Отрицательный результат допустим: он не блокирует реальную трату или перевод. +Это баланс по внесённым операциям, а не подтверждение остатка в банковском +приложении. Пропущенные движения могут создавать расхождения. + +Деньги хранятся целыми копейками и рассчитываются без преобразования в +JavaScript Number. Отдельный ввод ограничен signed64: начальный остаток — +от −92 233 720 368 547 758,08 ₽ до 92 233 720 368 547 758,07 ₽; сумма операции +строго положительна и не выше верхней границы. Допускается не больше двух +десятичных знаков, лишняя точность отклоняется без округления. Итог сложения +может превышать границу отдельного ввода. HTTP использует общий +[Money](../../glossary.md#money), frontend только форматирует финансовые итоги. + +## Операции и категории + +| Вид | Смысл и связи | +| --- | --- | +| Расход | Уменьшает баланс и относится к категории | +| Пополнение | Увеличивает баланс, не входит в расходы | +| Перевод | Одной записью уменьшает баланс источника и увеличивает баланс другой своей активной карты; не считается расходом | +| Возврат | Связан с исходным расходом той же карты и наследует его текущую категорию | + +Даты сохраняются как календарные дни без времени. Сегодня определяется +в Europe/Moscow; операция допускается от начала учёта до сегодня включительно. +Для перевода дата должна подходить обеим картам. Описание необязательно. + +При первом добавлении карты создаются категории «Продукты», «Кафе», «Такси», +«Транспорт», «Жильё», «Здоровье», «Покупки», «Развлечения», «Другое». +Категории общие для всех карт одного пользователя. Их можно добавлять и +переименовывать; уникальность названия не зависит от регистра и внешних пробелов. +Переименование сохраняет идентичность, поэтому история показывает новое имя. +«Другое» сохраняет роль категории по умолчанию даже после переименования. +Подкатегории и удаление категорий в этой версии отсутствуют. + +Возврат может относиться к покупке любого месяца. Несколько частичных +возвратов допустимы, если вместе не превышают текущую сумму покупки; возврат +не может быть датирован раньше неё. При исправлении возврата его старая сумма +заменяется новой, а не учитывается второй раз. Доступный лимит для исправления +рассчитывает сервер, поэтому полностью возвращённую покупку можно оставить +без изменения суммы и исправить описание. + +## Месяц и сводка + +Журнал и сводка карты относятся к одному выбранному месяцу. Журнал показывает +операции по убыванию даты, затем времени создания и идентификатора; доступны +следующие страницы. Баланс при выборе прошлого месяца остаётся текущим. +Сводка учитывает все подходящие записи, включая не показанные на странице. + +Сводка одной или всех карт отдельно показывает потраченное, возвращённое +и разницу, в том числе по категориям. Возврат уменьшает расходы месяца своего +получения: августовская покупка 1 000 ₽ и сентябрьский возврат 600 ₽ дают +августовский итог 1 000 ₽ и сентябрьский −600 ₽ при отсутствии других трат. +Начальный остаток, пополнения и переводы в эти показатели не входят. +Сводка всех карт включает архивные карты независимо от фильтра списка. + +## Исправления и архив + +Можно исправить сумму, дату, описание и применимые связи операции. Вид записи +и исходная покупка возврата неизменны. Обычный расход или пополнение можно +перенести на другую активную карту; изменение или удаление перевода затрагивает +обе стороны целиком. Исправление категории покупки перегруппировывает её +возвраты, в том числе в других месяцах. + +Расход с возвратами нельзя удалить, перенести, уменьшить ниже уже возвращённого +или датировать позже возврата. Сначала исправляются мешающие возвраты. +Изменение начала учёта проверяет всю историю, включая входящие переводы; +изменение начального остатка не создаёт отдельной операции и не меняет траты. +Произвольной операции «исправить баланс» нет. + +Архив сохраняет историю при любом балансе. До возвращения карты из архива +недоступны финансовые изменения и изменение начала учёта; описательные +реквизиты менять можно. Для исправления перевода должны быть активны и прежние, +и новые стороны. Безвозвратное удаление карт не предусмотрено. + +## Сохранение и конфликты + +Карты, операции и категории хранятся в SQLite и переживают повторный вход +и перезапуск. Переводы и связанные изменения сохраняются транзакционно. +Проверка владельца распространяется на каждую ссылку, включая категорию, +обе карты перевода и покупку возврата. + +Создание карты или операции получает постоянный идентификатор намерения. +Повтор того же ввода после потерянного ответа возвращает текущую запись; +изменённый ввод с тем же идентификатором получает конфликт. Проверка уже +сохранённого намерения предшествует проверке нового лимита возврата или архива. +Поэтому полный возврат не списывается повторно, а архивная карта не становится +активной от запоздалого повтора создания. + +Удалённая операция оставляет служебную отметку и fingerprint первоначального +ввода: повтор не восстанавливает её. Это не полный журнал всех редакций. +Исправления используют версию записи: устаревшее изменение отклоняется, +а форма сохраняет черновик и позволяет получить актуальную запись. +При ошибке сохранения неподтверждённые суммы не выдаются за новый баланс. + +[Безопасное восстановление](../../development/local-workspace.md#сохранение-данных-и-откат) +сохраняет новую финансовую историю и при откате приложения. Устройство хранения +описано в [контейнерах](../../architecture/containers.md#хранимые-данные-и-границы-доступа). +Согласованный контракт этой поставки находится в +[manual-bank-cards](../../../specs/openspec/changes/add-manual-bank-cards/specs/manual-bank-cards/spec.md). +Автоматическая загрузка банковских данных, кредитные карты, другие валюты, +бюджеты и общий баланс банковских денег с инвестициями в эту версию не входят. diff --git a/glossary.md b/glossary.md index 43b8a8c..b901ee1 100644 --- a/glossary.md +++ b/glossary.md @@ -7,7 +7,8 @@ ## User Локальный владелец данных WealthLedger, идентифицируемый `id` и -нормализованным email. User владеет Session и T-Bank connection и видит только +нормализованным email. User владеет Session, T-Bank connection и своими +ручными банковскими картами, категориями и операциями. Он видит только Brokerage Accounts, доступные через его текущее T-Bank connection. _Не использовать_: клиент T-Bank, инвестор T-Bank. @@ -31,6 +32,30 @@ _Не использовать_: T-Bank сессия, browser session. _Не использовать_: сумма без валюты, транспортное представление как доменный термин. +## Bank Card + +Независимая дебетовая карта в RUB для [ручного учёта](domains/cards/overview.md) +локального пользователя. Хранит начало учёта, начальный остаток и собственную +историю; не является Brokerage Account или общим счётом нескольких карт. + +## Bank Card Balance + +Баланс по внесённым операциям: начальный остаток плюс пополнения, входящие +переводы и возвраты, минус расходы и исходящие переводы. Может быть отрицательным +и не подтверждает текущий остаток в банковском приложении. + +## Expense Category + +Принадлежащая пользователю категория ручных банковских расходов, общая для +его карт. Переименование сохраняет идентичность и роль категории по умолчанию. +Возврат использует текущую категорию своей исходной покупки. + +## Net Card Expenses + +Расходы выбранного месяца за вычетом возвратов, полученных в этом месяце. +Итог может быть отрицательным; начальные остатки, переводы и пополнения +в него не входят. + ## T-Bank connection Единственное принадлежащее User подключение к T-Bank Invest, содержащее diff --git a/overview.md b/overview.md index ea6e5f7..479b29d 100644 --- a/overview.md +++ b/overview.md @@ -4,7 +4,8 @@ WealthLedger — локальное приложение для частного понимать свои инвестиции T-Bank: что находится на счетах, какие операции исполнены, сколько купонов и дивидендов получено и какие выплаты ожидаются. Приложение объединяет эти представления, сохраняя связь суммы с валютой, -инструментом и брокерским счётом. +инструментом и брокерским счётом. Отдельная область ручных банковских карт +помогает учитывать рублёвые остатки и повседневные траты. ## Основные задачи пользователя @@ -12,10 +13,12 @@ WealthLedger — локальное приложение для частного [подключает T-Bank Invest](integrations/tbank.md) в настройках. Локальная учётная запись WealthLedger и подключение к брокеру — разные вещи: вход открывает приложение, а подключение определяет доступные инвестиционные счета. +Для ручного учёта банковских карт подключение брокера не требуется. | Задача | Что показывает WealthLedger | | --- | --- | | Найти свой инвестиционный счёт | [Список счетов](domains/accounts/overview.md), их типы и доступные сводки | +| Вести банковские карты | [Ручной учёт](domains/cards/overview.md): независимые балансы, расходы, пополнения, переводы, возвраты и месячные сводки | | Понять текущий состав инвестиций | [Портфель](domains/portfolio/overview.md): позиции, денежные остатки и итоги по валютам | | Разобрать уже совершённые движения | [Журнал операций](domains/operations/overview.md): исполненные операции с фильтрами и последовательной загрузкой истории | | Посчитать полученные выплаты | [Пассивный доход](domains/income/overview.md): исполненные купоны и дивиденды за календарный период | @@ -31,6 +34,10 @@ WealthLedger — локальное приложение для частного Режим «Все счета» объединяет результаты доступных счетов; отдельного брокерского счёта с таким именем он не создаёт. +Банковские карты имеют собственные операции и общий пользовательский список +категорий. Их текущие балансы независимы; месячная сводка всех карт объединяет +траты и возвраты, включая архив. Банковские деньги не добавляются к инвестициям. + Портфель отвечает на вопрос «что есть сейчас», журнал — «что исполнено», аналитика дохода — «какие выплаты уже получены», будущие события — «что ожидается». Прогноз не подтверждает фактическое зачисление. Полученные купоны @@ -49,8 +56,9 @@ T-Bank поставляет личные брокерские данные и р Инвестиционные возможности предназначены для чтения и анализа. Они не описывают размещение торговых поручений или изменение портфеля у брокера. -Локально сохраняются учётные данные, сессии и зашифрованное подключение; -финансовые снимки служат временными наблюдениями, а не накопленной историей +Локально сохраняются учётные данные, сессии, зашифрованное подключение и ручная +история банковских карт. Инвестиционные финансовые снимки служат временными +наблюдениями, а не накопленной историей портфеля в собственной базе WealthLedger. Данные могут зависеть от доступности поставщика и свежести последнего ответа.