docs: adds rules and agents specs

This commit is contained in:
Anton
2026-02-27 19:08:55 +03:00
commit 9551b93a09
12 changed files with 1151 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
# Агент: Архитектор SPA семейного бюджета
## Контекст
- Это локальное SPA-приложение для семейного бюджета.
- Стек: React + TypeScript (FE), Node.js (BE), PostgreSQL (Synology), Git локально.
- Формальные требования и контракт описаны в файлах:
- `format.md` — формат импорта JSON 1.0
- `db.md` — модель БД (accounts, transactions, category_rules)
- `category.md` — категории и алиасы счетов
- `api_history.md` — API истории операций
- `edit_and_rules.md` — редактирование и правила категорий
- `analytics.md` — аналитика и бюджеты
- `auth.md` — авторизация и сессии
## Цели агента
1. Обеспечить целостную архитектуру проекта (папки, модули, naming, структура API).
2. Следить, чтобы Backend и Frontend строго следовали спецификациям из `positionX_*.md`.
3. Раскладывать работу на этапы: сначала MVP (импорт + история + базовая категоризация), потом аналитика, бюджеты и т.д.
4. Давать чёткие задачи Backend- и Frontend-агентам в виде:
- какие файлы создать/изменить,
- какие эндпоинты/компоненты реализовать,
- какие интерфейсы типов (TypeScript) использовать.
## Обязательные ограничения
- Нельзя менять схемы JSON/БД/API, описанные в `./docs/backlog/*.md`, без явной инструкции пользователя.
- Не использовать SQL-запросы внутри Postgres-ноды n8n (если интеграция с n8n появится позднее).
- Приоритет — надёжность, предсказуемость и отсутствие “магии”.
## Этапы MVP (для планирования задач)
1. Импорт JSON 1.0:
- Эндпоинт для загрузки файла.
- Парсинг JSON, валидация, импорт в Postgres с расчётом fingerprint.
2. История операций:
- `GET /api/transactions` по спецификации.
- FE-таблица с фильтрами/сортировкой/пагинацией.
3. Редактирование транзакций и правил:
- `PUT /api/transactions/{id}`
- CRUD для `category_rules`
4. Аналитика (Summary, by-category, timeseries).
5. Авторизация и сессии (`/api/auth/*`) и защита всех API.
Архитектор должен:
- При запуске нового этапа читать соответствующие `*.md` папке `./docs/backlog`.
- Синхронизировать контракты типов между BE и FE (например, общие TypeScript-интерфейсы).

View File

@@ -0,0 +1,87 @@
# Агент: Backend (Node.js + PostgreSQL)
## Стек и требования
- Node.js (Express или Fastify — на усмотрение, но держать код компактным).
- БД: PostgreSQL (подключение через `pg` или иной официальный драйвер).
- Типизация: TypeScript.
- Миграции БД — через миграционный инструмент (например, Knex/Drizzle/TypeORM) или собственные SQL-файлы.
## Обязательные спецификации (читать целиком)
- `format.md`
- `db.md`
- `category.md`
- `api_history.md`
- `edit_and_rules.md`
- `analytics.md`
- `auth.md`
## Основные задачи MVP
1. Структура проекта
- Создать каркас BE: `src/app.ts`, `src/routes/*`, `src/db/*`, `src/middleware/*`, `src/services/*`.
- Реализовать конфигурацию:
- переменные окружения (в т.ч. логин/пароль для авторизации),
- строки подключения к Postgres.
1. Миграции БД
- Реализовать таблицы и поля строго по `db.md`, `category.md`, `edit_and_rules.md`, `analytics.md`.
- Включить все описанные CHECK/UNIQUE/FOREIGN KEY/дополнительные поля (`is_category_confirmed`, `comment`, `alias` для accounts, `budgets` и т.д.).
1. Авторизация и сессии
- Реализовать эндпоинты:
- `POST /api/auth/login`
- `POST /api/auth/logout`
- `GET /api/auth/me`
- Ввести модель сессий с полями:
- `id`, `created_at`, `last_activity_at`, `is_active`
- Реализовать middleware, которое:
- проверяет наличие и валидность session cookie,
- проверяет 3-часовой таймаут бездействия,
- обнуляет сессию и отдаёт `401` по истечении таймаута.
1. Импорт выписки
- Эндпоинт (например) `POST /api/import/statement`:
- Принимает JSON строго формата 1.0 (см. `format.md`).
- Валидирует структуру и типы.
- Находит или создаёт `accounts` по `bank + accountNumber` и заполняет `alias = NULL`.
- Для каждой транзакции:
- считает `fingerprint`,
- определяет `direction`,
- вставляет в `transactions` с учётом уникального индекса `(account_id, fingerprint)`,
- по умолчанию `is_category_confirmed = FALSE`, `category_id = NULL`.
1. История операций
- Реализовать `GET /api/transactions` по `api_history.md`:
- все фильтры и сортировки,
- пагинация,
- поля `accountAlias`, `categoryName`, `isCategoryConfirmed`, `comment`.
1. Редактирование транзакций и правила категорий
- Эндпоинт `PUT /api/transactions/{id}`:
- Обновляет `category_id`, `comment`, `is_category_confirmed`.
- Эндпоинты для `category_rules`:
- создание правила на основе входных данных (pattern, match_type, category_id, priority),
- обновление/деактивация,
- опционально — применение правила к истории (bulk-обновление транзакций с `is_category_confirmed = FALSE`).
1. Аналитика
- Реализовать:
- `GET /api/analytics/summary`
- `GET /api/analytics/by-category`
- `GET /api/analytics/timeseries`
по агрегатам, описанным в `analytics.md`.
## Ограничения
- Не менять контракты API без явной инструкции пользователя.
- Все суммы — в копейках (`BIGINT`), все даты/время — `TIMESTAMPTZ`.
- Обязательно обрабатывать ошибки валидации и возвращать понятные сообщения.

View File

@@ -0,0 +1,81 @@
# Агент: Frontend (React + TypeScript)
## Стек
- React + TypeScript.
- Router (React Router или аналог) для экранов: Login, История, Аналитика, Настройки/Правила.
- Таблицы и графики — на усмотрение (можно использовать готовые компоненты), но без излишней тяжести.
## Обязательные спецификации
- Чтение контрактов API и моделей из:
- `api_history.md`
- `edit_and_rules.md`
- `analytics.md`
- `auth.md`
- а также модели данных и категорий из `db.md`, `category.md`.
## Основные экраны
1. Логин
- Форма `login/password`, запрос `POST /api/auth/login`.
- При успехе — переход на Историю.
- При `401` на любых защищённых запросах — редирект на Login.
1. История операций
- Таблица с колонками:
- Дата,
- Счёт (alias),
- Сумма,
- Описание,
- Категория,
- (иконка/метка неподтверждённой категории),
- (иконка комментария).
- Фильтры:
- период (from/to),
- быстрые предустановки: неделя/месяц/год с перелистыванием,
- счёт,
- тип движения (приход/расход/перевод),
- категория,
- строка поиска,
- быстрый фильтр по сумме,
- флаг `только неподтверждённые`.
- Пагинация страницами: 10/50/100.
- Редактирование транзакции (модальное окно):
- выбор категории,
- поле комментария,
- галочка “Создать правило для похожих транзакций в будущем” (по умолчанию включена),
- вызов `PUT /api/transactions/{id}` и соответствующего API для правил (если решено вызывать напрямую).
1. Аналитика
- Верхний блок выбора периода: неделя/месяц/год/произвольный + стрелки “←/→” (кроме произвольного).
- Фильтры:
- счёт,
- флаг “только подтверждённые”.
- Блок 1: сводка (`/api/analytics/summary`).
- Блок 2: график времени (`/api/analytics/timeseries`).
- Блок 3: диаграмма/таблица по категориям (`/api/analytics/by-category`).
1. Настройки (минимум)
- Список счетов с алиасами:
- отображение номера счёта и alias,
- возможность поменять alias.
- Список категорий (только просмотр).
- Список правил (базово: просмотр, активность, приоритет, тип; редактирование можно отложить на потом).
## Поведение с авторизацией
- При загрузке приложения:
- запрос `GET /api/auth/me`, определение, авторизован ли пользователь.
- Для всех запросов к `/api/*`:
- при `401`сброс локального состояния и редирект на Login.
## Ограничения
- Не менять контракты API, описанные в соответствующих файлах.
- Настройка видимых колонок в истории должна быть предусмотрена (хотя бы на уровне структуры состояния).

View File

@@ -0,0 +1,29 @@
# Агент: Правила категорий и аналитика
## Задачи
1. Помогать Backend-агенту проектировать и оптимизировать запросы:
- авто-категоризация по `category_rules`,
- агрегаты для `/api/analytics/*`,
- применение правил к прошлым транзакциям.
2. Следить за тем, чтобы:
- все суммы считаются в копейках,
- учитывался флаг `is_category_confirmed`,
- фильтры по периодам/счётам/категориям корректно отражались в SQL.
3. Подготавливать “чистые” интерфейсы для FE:
- структуры ответов уже определены в `api_history.md`, `analytics.md` — не менять.
## Контекст
- Читает:
- `db.md`,
- `api_history.md`,
- `edit_and_rules.md`,
- `analytics.md`.
## Ограничения
- Не менять схемы и API.
- Предлагать решения, которые хорошо работают на локальном PostgreSQL.

View File

@@ -0,0 +1,79 @@
# Агент: Конвертер выписок (PDF/XLSX → JSON 1.0)
## Контекст
Цель: по входному файлу выписки (PDF, в будущем XLSX) сформировать JSON строго
по схеме 1.0, описанной в `docs/backlog/format.md`, для дальнейшего импорта
в SPA через backend-эндпоинт.
Формат JSON 1.0:
- `schemaVersion = "1.0"`
- `bank = "VTB"` (на старте)
- `statement`:
- `accountNumber: string`
- `currency: string`
- `openingBalance: integer` (копейки)
- `closingBalance: integer` (копейки)
- `exportedAt: string` (ISO 8601 + TZ)
- `transactions[]`:
- `operationAt: string` (ISO 8601 + TZ)
- `amountSigned: integer` (копейки, >0 приход, <0 расход)
- `commission: integer` (копейки)
- `description: string`
## Задачи агента
1. Конвертация PDF → сырые строки
- Использовать доступный инструмент (локальный ИИ/LLM API или библиотеку),
чтобы преобразовать PDF-выписку в структурированный текст/табличное представление:
- заголовочная часть (счёт, валюта, баланс на начало/конец, дата выгрузки),
- таблица операций (дата/время, сумма, комиссия, описание и т.п.).
- Корректно обрабатывать переносы строк в описании операции.
1. Маппинг полей в JSON 1.0
- Извлечь из заголовка:
- `accountNumber`,
- `currency`,
- `openingBalance`, `closingBalance`,
- `exportedAt` (если нет — использовать время генерации конвертера как суррогат и явно помечать это в комментариях/логах).
- Для каждой строки операции:
- собрать `operationAt` (дата+время) и добавить таймзону `+03:00`, если она не указана явно;
- преобразовать сумму в `amountSigned` (в копейках, с учётом знака прихода/расхода);
- выделить `commission` (если в выписке отдельная колонка, иначе `0`);
- сформировать `description` как максимально близкое к тексту из выписки поле.
1. Нормализация сумм
- Все суммы, которые приходят в формате `12345.67`, должны быть преобразованы в целое число копеек:
- `12345.67 → 1234567`.
- Важно избегать ошибок округления (использовать работу со строками, а не float).
1. Проверка целостности (по возможности)
- При наличии `openingBalance` и `closingBalance`:
- проверить, что `openingBalance + Σ(amountSigned) == closingBalance` (с допустимой погрешностью, если есть комиссии/особые операции);
- если проверка не проходит, вернуть предупреждение вместе с JSON (или отдельный отчёт).
1. Выходной результат
- Агент должен возвращать один JSON-объект строго по схеме 1.0.
- Дополнительно может возвращаться диагностическая информация (лог), но она должна быть отделена от финального JSON.
## Взаимодействие с локальным ИИ
- Допускается использование API к локальной LLM/модели для:
- структурирования текстовых фрагментов из PDF (определение границ колонок, восстановление строк операций),
- распознавания сложных описаний, если PDF в виде "сырого текста".
- Важно:
- всегда явно проверять и постпроцессить результат LLM, чтобы привести его к строгому формату 1.0;
- не полагаться на LLM для вычислений сумм — только для парсинга структуры.
## Ограничения
- Агент **не взаимодействует** напрямую с БД или HTTP API SPA:
- его задача — чистый конвертер файла → JSON.
- Нельзя менять схему JSON 1.0 без обновления `docs/backlog/format.md`.
- Конвертер должен корректно работать с русским языком и форматами дат/сумм из российских банковских выписок.

181
docs/backlog/analytics.md Normal file
View File

@@ -0,0 +1,181 @@
# Позиция 5: Аналитика расходов и доходов
## 5.1. Периоды и навигация
Поддерживаются следующие режимы выбора периода:
- Быстрый выбор:
- Неделя — от начала текущей недели (с понедельника) до текущей даты.
- Месяц — от 1-го числа текущего месяца до текущей даты.
- Год — от 1 января текущего года до текущей даты.
- Произвольный диапазон:
- Пользователь задаёт `from` и `to` вручную.
Навигация:
- Для режимов Неделя/Месяц/Год доступны кнопки переключения периода:
- "предыдущая" / "следующая" неделя;
- "предыдущий" / "следующий" месяц;
- "предыдущий" / "следующий" год.
- При произвольном диапазоне навигация стрелками не используется (только ручная смена дат).
Фильтры, общие для всех отчётов:
- `accountId` (или несколько) — фильтр по счёту/счетам.
- Флаг "только подтверждённые категории" (`onlyConfirmed`) — учитывать только транзакции с `is_category_confirmed = TRUE`.
## 5.2. Основные аналитические блоки
### 1) Сводка периода (Summary)
Цель: дать быстрый обзор финансов за выбранный период.
Показатели:
- Общий расход (`totalExpense`).
- Общий доход (`totalIncome`).
- Чистый результат (`net = totalIncome - totalExpense`).
- Топ-35 категорий по расходам (сумма, доля).
Реализуется через эндпоинт `GET /api/analytics/summary`.
Параметры:
- `from`, `to` — границы периода;
- `accountId?` — фильтр по счёту (опционально);
- `onlyConfirmed?` — учитывать только подтверждённые категории.
Ответ (идея):
```json
{
"totalExpense": 12345600,
"totalIncome": 20000000,
"net": 7654400,
"topCategories": [
{ "categoryId": 1, "categoryName": "Продукты", "amount": 4500000, "share": 0.36 },
{ "categoryId": 2, "categoryName": "ЖКХ", "amount": 2500000, "share": 0.20 }
]
}
```
### 2) Расходы по категориям
Цель: понять структуру расходов, какие категории занимают основную долю.
Реализуется через эндпоинт `GET /api/analytics/by-category`.
Параметры:
- `from`, `to`;
- `accountId?`;
- `onlyConfirmed?`.
Ответ (идея):
```json
[
{
"categoryId": 1,
"categoryName": "Продукты",
"amount": 4500000,
"txCount": 32,
"share": 0.36
},
{
"categoryId": 2,
"categoryName": "ЖКХ",
"amount": 2500000,
"txCount": 5,
"share": 0.20
}
]
```
Использование на фронтенде:
- круговая диаграмма или bar-chart по категориям;
- таблица под диаграммой с суммами, долями и количеством операций.
### 3) Динамика во времени
Цель: увидеть изменение доходов/расходов по времени.
Реализуется через эндпоинт `GET /api/analytics/timeseries`.
Параметры:
- `from`, `to`;
- `accountId?`;
- `categoryId?` — при необходимости анализировать конкретную категорию;
- `onlyConfirmed?`;
- `granularity=day|week|month` — шаг агрегации.
Ответ (идея):
```json
[
{
"periodStart": "2026-02-01",
"periodEnd": "2026-02-07",
"expenseAmount": 3500000,
"incomeAmount": 0
},
{
"periodStart": "2026-02-08",
"periodEnd": "2026-02-14",
"expenseAmount": 4200000,
"incomeAmount": 0
}
]
```
Использование на фронтенде:
- линейный или столбиковый график: расходы и (опционально) доходы по периодам.
## 5.3. Задел под бюджеты (лимиты)
Для поддержки бюджетов по категориям на будущих этапах вводится сущность `budgets`.
### Таблица `budgets` (идея)
Поля:
- `id BIGSERIAL PRIMARY KEY` — идентификатор бюджета.
- `category_id BIGINT` — ссылка на категорию (`categories.id`) или `NULL` для общего бюджета по всем категориям.
- `period_type TEXT NOT NULL` — тип периода:
- `"month"` — месячный бюджет;
- `"year"` — годовой бюджет.
- `year INT NOT NULL` — год действия бюджета.
- `month INT` — номер месяца (112), используется если `period_type = 'month'`.
- `amount_limit BIGINT NOT NULL` — лимит по расходам в копейках.
- `is_active BOOLEAN NOT NULL DEFAULT TRUE` — активен ли бюджет.
Пример DDL (базовый):
```sql
CREATE TABLE budgets (
id BIGSERIAL PRIMARY KEY,
category_id BIGINT,
period_type TEXT NOT NULL,
year INT NOT NULL,
month INT,
amount_limit BIGINT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
ALTER TABLE budgets
ADD CONSTRAINT chk_budgets_period_type
CHECK (period_type IN ('month', 'year'));
```
Использование в аналитике:
- Для выбранного периода (например, текущий месяц) по каждой категории можно:
- найти соответствующий бюджет (если задан);
- посчитать фактический расход за период;
- показать: потрачено / лимит / % выполнения / остаток.
Это позволит в будущем реализовать экран "Бюджеты", а также подсветку категорий,
где расходы близки к лимиту или превышают его.

View File

@@ -0,0 +1,98 @@
# Позиция 3: API истории операций (GET /api/transactions)
## Назначение
Эндпоинт `/api/transactions` предоставляет список транзакций с учётом фильтров, сортировки и пагинации
для отображения в SPA (таблица "История операций").
## Метод и URL
- Метод: `GET`
- URL: `/api/transactions`
## Параметры запроса (query)
Все параметры опциональны, если не указано иное.
- `accountId: number` — идентификатор счёта (`accounts.id`). Если не передан, выбираются транзакции по всем счетам.
- `from: string` — дата начала периода (включительно) в формате `YYYY-MM-DD`.
- `to: string` — дата конца периода (включительно) в формате `YYYY-MM-DD`.
- `direction: string` — направления движения, одно или несколько значений через запятую:
- `income` — приход;
- `expense` — расход;
- `transfer` — переводы между счетами;
- `internal` — внутренние движения (если будут использоваться).
Пример: `direction=income,expense`.
- `categoryId: number` — идентификатор категории (`categories.id`).
- `search: string` — строка поиска по полю `description` (поиск по подстроке, регистронезависимый).
- `amountMin: number` — минимальная сумма операции в копейках (`BIGINT`).
- `amountMax: number` — максимальная сумма операции в копейках (`BIGINT`).
- `onlyUnconfirmed: boolean` — если `true`, возвращаются только транзакции с неподтверждённой категорией (`is_category_confirmed = FALSE`).
- `sortBy: string` — поле сортировки:
- `date` — сортировка по `operation_at`;
- `amount` — сортировка по `amount_signed`.
- `sortOrder: string` — порядок сортировки: `asc` или `desc`.
- `page: number` — номер страницы, начиная с `1`. По умолчанию `1`.
- `pageSize: number` — размер страницы (количество записей на странице). Допустимые значения: `10`, `50`, `100`. По умолчанию `50`.
## Структура ответа
Ответ содержит массив транзакций и данные пагинации.
```json
{
"items": [
{
"id": 123,
"operationAt": "2026-02-26T14:06:57+03:00",
"accountId": 1,
"accountAlias": "Текущий",
"amountSigned": -50000,
"commission": 0,
"description": "Оплата товаров и услуг. OZON.RU. по карте *2249",
"direction": "expense",
"categoryId": 5,
"categoryName": "Дом",
"isCategoryConfirmed": false,
"comment": "еда"
}
],
"page": 1,
"pageSize": 50,
"totalItems": 874,
"totalPages": 18
}
```
Поля элементов `items`:
- `id` — идентификатор транзакции (`transactions.id`).
- `operationAt` — дата и время операции в формате ISO 8601 (из `transactions.operation_at`).
- `accountId` — идентификатор счёта (`accounts.id`).
- `accountAlias` — алиас счёта (`accounts.alias`). Если алиас не задан, можно возвращать `null` или сгенерированное значение.
- `amountSigned` — сумма операции в копейках.
- `commission` — комиссия по операции в копейках.
- `description` — описание операции из банка.
- `direction` — направление транзакции (`income` / `expense` / `transfer` / `internal`).
- `categoryId` — идентификатор категории (`categories.id`), может быть `null`.
- `categoryName` — имя категории (`categories.name`), может быть `null`.
- `isCategoryConfirmed` — признак того, что категория подтверждена пользователем.
- `comment` — пользовательский комментарий к транзакции.
Поля пагинации:
- `page` — текущая страница.
- `pageSize` — размер страницы.
- `totalItems` — общее количество транзакций, удовлетворяющих фильтрам.
- `totalPages` — общее количество страниц при заданном `pageSize`.
## Замечания по реализации
- Справочные данные (`accountAlias`, `categoryName`) могут возвращаться либо сразу в ответе,
либо SPA может подставлять их по `accountId` и `categoryId` из кэшированных справочников,
полученных через отдельные эндпоинты (`/api/accounts`, `/api/categories`).
- Фильтрация по `search` выполняется по полю `description` (ILIKE `%search%`).
- Фильтры по сумме (`amountMin`, `amountMax`) применяются к `amount_signed`.
- Параметр `onlyUnconfirmed=true` добавляет условие `is_category_confirmed = FALSE`.
- Комбинация `sortBy=date&sortOrder=desc` используется как значение по умолчанию
(последние операции сверху).

79
docs/backlog/auth.md Normal file
View File

@@ -0,0 +1,79 @@
# Позиция 6: Авторизация и сессии
## 6.1. Общая модель доступа
- Приложение предназначено для использования ограниченным числом пользователей (2 человека).
- Регистрации пользователей через интерфейс нет.
- Учётные данные (логин и пароль) задаются через переменные окружения (`ENV`),
например:
- `APP_USER_LOGIN`
- `APP_USER_PASSWORD`
- Ролевой модели нет: все авторизованные пользователи имеют полный доступ
ко всем функциям SPA (просмотр, импорт, редактирование, аналитика, управление правилами).
## 6.2. Авторизация
Рекомендуемая схема: сессионная авторизация с backend-сессиями и cookie.
### Эндпоинты
1. `POST /api/auth/login`
- Вход: JSON с полями `login` и `password`.
- Backend сравнивает полученные значения с `ENV` (`APP_USER_LOGIN`, `APP_USER_PASSWORD`).
- При успешной аутентификации создаётся сессия и клиенту выдаётся session cookie
(например, `sid`), привязанная к записи в БД или in-memory хранилищу.
2. `POST /api/auth/logout`
- Инвалидирует текущую сессию (удаляет/помечает как неактивную) и очищает cookie.
3. `GET /api/auth/me`
- Возвращает информацию о том, авторизован ли пользователь (например, `200` + базовый профиль
или `401`, если сессия недействительна).
Все защищённые эндпоинты (`/api/transactions`, `/api/analytics/*`, импорт выписок,
управление правилами и т.д.) работают **только** при наличии действующей сессии.
## 6.3. Сессии и таймаут по бездействию
Требования:
- Авто-логаут при длительном бездействии (жёсткий таймаут).
- Таймаут бездействия: **3 часа**.
Реализация:
- В backend вводится сущность "сессия" (в БД или в памяти) с полями:
- `id` — идентификатор сессии;
- `created_at` — время создания сессии;
- `last_activity_at` — время последней активности по этой сессии;
- `is_active` — флаг активности.
- При каждом запросе с действительным `sid` backend:
1. Проверяет, не истёк ли таймаут:
- если `now - last_activity_at > 3 часа` — сессия инвалидируется (`is_active = FALSE`),
возвращается `401 Unauthorized` и на фронтенде пользователь переводится на экран логина;
2. Если таймаут не истёк — обновляет `last_activity_at` текущим временем.
- Таймаут **жёсткий**:
- нет "длинных" сессий;
- нет опции "запомнить меня" — после 3 часов полного бездействия требуется повторный логин.
## 6.4. Интеграция с SPA
- При открытии приложения SPA выполняет запрос `GET /api/auth/me`:
- если ответ `200` — пользователь остаётся в приложении;
- если `401` — показывается форма логина.
- При логине (`POST /api/auth/login`):
- при успехе SPA сохраняет состояние "авторизован" и переходит к основному интерфейсу.
- При получении `401` от любого защищённого API:
- SPA сбрасывает локальное состояние пользователя,
- перенаправляет на экран логина.
## 6.5. Связь с другими позициями
- Все ранее описанные эндпоинты (`/api/transactions`, `/api/analytics/*`, импорт JSON,
управление категориями и правилами) доступны только в рамках действующей сессии.
- Так как ролевой модели нет, дополнительные ограничения доступа не требуются:
оба пользователя имеют одинаковые права.

98
docs/backlog/category.md Normal file
View File

@@ -0,0 +1,98 @@
# Категории расходов и доходов (MVP)
## Базовый список категорий
Используются следующие категории для классификации транзакций:
- Продукты
- Авто
- Здоровье
- Арчи
- ЖКХ
- Дом
- Проезд
- Одежда
- Химия
- Косметика
- Инвестиции
- Развлечения
- Общепит
- Штрафы
- Налоги
- Подписки
- Перевод
- Наличные
- Подарки
- Спорт
- Отпуск
- Техника
- Поступления
Особенности:
- Категории на данном этапе одноуровневые (без подкатегорий).
- Категории используются как для фильтрации и аналитики, так и для правил авто-категоризации.
- Категория `Перевод` используется для переводов (в т.ч. между своими счетами и на другие счета).
- Категория `Инвестиции` предназначена для операций, связанных с брокерским счётом/ценными бумагами.
- Категория `Поступления` используется для всех видов доходов (зарплата, кешбэк и др.), пока без детализации.
## Таблица `categories` (PostgreSQL)
Для хранения категорий используется отдельная таблица `categories`.
Рекомендуемая структура:
- `id BIGSERIAL PRIMARY KEY` — идентификатор категории.
- `name TEXT NOT NULL` — отображаемое имя категории (из списка выше).
- `type TEXT NOT NULL` — тип категории:
- `"expense"` — расходная категория;
- `"income"` — доходная категория;
- `"transfer"` — категории для переводов/движений между собственными счетами.
- `is_active BOOLEAN NOT NULL DEFAULT TRUE` — используется ли категория.
- `is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE` — подтверждена ли категория пользователем.
- `comment TEXT` — пользовательский комментарий (например, по маркетплейсам).
Рекомендуемый DDL:
```sql
CREATE TABLE categories (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
ALTER TABLE categories
ADD CONSTRAINT chk_categories_type
CHECK (type IN ('expense', 'income', 'transfer'));
ALTER TABLE transactions
ADD COLUMN is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE transactions
ADD COLUMN comment TEXT;
```
Привязка к транзакциям:
- В таблице `transactions` поле `category_id` является внешним ключом на `categories(id)`.
- При авто- или ручной категоризации транзакции записывается соответствующий `category_id`.
## Алиасы счетов
- При первом появлении нового счёта (новая комбинация `bank + account_number`) после загрузки выписки
SPA предлагает пользователю ввести человекочитаемый алиас: например, `"Текущий"`, `"Накопительный"`, `"Брокерский"`.
- Алиас хранится в таблице `accounts` в отдельном поле, например `alias TEXT`.
- В таблице истории операций на фронтенде вместо номера счёта показывается `alias` (с возможностью при наведении/по клику увидеть полный номер).
Изменённая таблица `accounts` (добавлен алиас):
```sql
ALTER TABLE accounts
ADD COLUMN alias TEXT;
```
При создании нового счёта:
- Если алиас задан пользователем — сохраняем его в `accounts.alias`.
- Если пользователь пропустил ввод алиаса — можно задать временный (`"Счёт 1"`, `"Счёт 2"`) и предложить изменить его позже в настройках.

147
docs/backlog/db.md Normal file
View File

@@ -0,0 +1,147 @@
# Модель БД (PostgreSQL)
## Общие принципы
- Используется PostgreSQL, развёрнутый локально (например, на Synology).
- Основные сущности:
- `accounts` — банковские счета пользователя;
- `transactions` — движения средств по счетам;
- `category_rules` — правила автоматической категоризации транзакций (подготовка к SPA-редактору правил).
- Все суммы хранятся в минорных единицах (копейки) как `BIGINT`.
- Время хранится в `TIMESTAMPTZ` (временная зона сохраняется).
## Таблица `accounts`
Предназначение: хранение информации о счетах, по которым загружаются выписки.
Структура:
- `id BIGSERIAL PRIMARY KEY` — внутренний идентификатор счёта в системе.
- `bank TEXT NOT NULL` — код/имя банка (например, `"VTB"`).
- `account_number TEXT NOT NULL` — номер счёта в банке (как в выписке).
- `currency TEXT NOT NULL` — код валюты счёта (например, `"RUB"`).
Ограничения и индексы:
- Уникальность комбинации `(bank, account_number)` — один и тот же счёт в банке не должен дублироваться.
Рекомендуемый DDL:
```sql
CREATE TABLE accounts (
id BIGSERIAL PRIMARY KEY,
bank TEXT NOT NULL,
account_number TEXT NOT NULL,
currency TEXT NOT NULL
);
CREATE UNIQUE INDEX ux_accounts_bank_number
ON accounts(bank, account_number);
```
## Таблица `transactions`
Предназначение: хранение всех операций по счетам с привязкой к `accounts`.
Структура:
- `id BIGSERIAL PRIMARY KEY` — внутренний идентификатор транзакции.
- `account_id BIGINT NOT NULL` — внешний ключ (FK) на `accounts(id)`;
каждая транзакция жёстко привязана к одному счёту.
- `operation_at TIMESTAMPTZ NOT NULL` — дата и время операции.
- `amount_signed BIGINT NOT NULL` — сумма операции в копейках; знак отражает тип движения (приход/расход).
- `commission BIGINT NOT NULL` — комиссия по операции в копейках.
- `description TEXT NOT NULL` — описание операции из выписки.
- `direction TEXT NOT NULL` — направление движения:
- `"income"` — приход;
- `"expense"` — расход;
- `"transfer"` — перевод между своими счетами / на другие свои счета;
- `"internal"` — служебные/внутренние движения (опционально, по мере необходимости).
- `fingerprint TEXT NOT NULL` — вычисляемый хэш для обеспечения идемпотентности импорта.
- `category_id BIGINT` — ссылка на таблицу категорий (будет определена позже).
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время создания записи в БД.
- `updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время последнего обновления записи.
Ограничения и индексы:
- Внешний ключ `account_id` ссылается на `accounts(id)` и обеспечивает целостность (нельзя создать транзакцию для несуществующего счёта).
- Уникальный индекс `(account_id, fingerprint)` обеспечивает идемпотентность: одна и та же операция не может быть загружена дважды.
- Опционально — CHECK-ограничение на поле `direction`.
Рекомендуемый DDL:
```sql
CREATE TABLE transactions (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
operation_at TIMESTAMPTZ NOT NULL,
amount_signed BIGINT NOT NULL,
commission BIGINT NOT NULL,
description TEXT NOT NULL,
direction TEXT NOT NULL,
fingerprint TEXT NOT NULL,
category_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX ux_transactions_account_fingerprint
ON transactions(account_id, fingerprint);
ALTER TABLE transactions
ADD CONSTRAINT chk_transactions_direction
CHECK (direction IN ('income', 'expense', 'transfer', 'internal'));
```
## Таблица `category_rules`
Предназначение: хранение правил автоматической категоризации транзакций на основе текста описания.
Структура:
- `id BIGSERIAL PRIMARY KEY` — идентификатор правила.
- `pattern TEXT NOT NULL` — строка-шаблон, вводимая пользователем через SPA (в простом виде).
- `match_type TEXT NOT NULL` — тип сопоставления:
- `"contains"` — простое вхождение подстроки;
- `"starts_with"` — строка начинается с шаблона;
- `"regex"` — регулярное выражение (формируется и/или проверяется в коде на основе пользовательского ввода).
- `category_id BIGINT NOT NULL` — ссылка на категорию (таблица категорий будет описана отдельно).
- `priority INT NOT NULL DEFAULT 0` — приоритет правила; чем выше число, тем раньше правило применяется при конфликте.
- `is_active BOOLEAN NOT NULL DEFAULT TRUE` — активно ли правило.
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время создания правила.
Логика приоритета:
- При авто-категоризации для транзакции ищутся все правила, которые ей соответствуют.
- Если совпало несколько правил, выбирается правило с максимальным `priority`.
- Это позволяет задавать общие правила с низким приоритетом и более точные (например, по конкретным мерчантам) с высоким приоритетом.
Рекомендуемый DDL:
```sql
CREATE TABLE category_rules (
id BIGSERIAL PRIMARY KEY,
pattern TEXT NOT NULL,
match_type TEXT NOT NULL,
category_id BIGINT NOT NULL,
priority INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
## Взаимосвязь JSON → БД при импорте
1. По полям `bank` и `statement.accountNumber` ищется или создаётся запись в `accounts`.
2. Для каждой транзакции из `transactions`:
- преобразуются суммы в копейки (`amountSigned`, `commission``BIGINT`);
- вычисляется `fingerprint` на основе комбинации полей (например,
`accountNumber + operationAt + amountSigned + commission + normalizedDescription`);
- определяется `direction` по знаку суммы и/или шаблонам текста (приход/расход/перевод);
- выполняется попытка вставки в `transactions`;
- при срабатывании уникального ограничения `(account_id, fingerprint)` запись считается дубликатом и пропускается.
- При импорте новых транзакций is_category_confirmed всегда = FALSE.
- При автокатегоризации обычными правилами, если правило “жёсткое” (не маркетплейс) — TRUE, для маркетплейсов — всегда FALSE.
3. Категория (`category_id`) пока может оставаться `NULL` и заполняться на следующих этапах
(правила, ИИ-агент, ручное редактирование в SPA).

View File

@@ -0,0 +1,141 @@
# Позиция 4: Редактирование транзакций и правила категорий
## Цели
- Минимизировать ручной труд при категоризации транзакций.
- Обеспечить возможность "дообучения" системы за счёт действий пользователя.
- Отдельно обработать сложные случаи (маркетплейсы), где по описанию нельзя однозначно определить категорию.
## Редактирование транзакций (SPA)
При открытии истории операций пользователь может открыть карточку редактирования транзакции.
Доступные действия для одной транзакции:
- Изменить категорию (выбор из списка категорий `categories`).
- При необходимости добавить/изменить пользовательский комментарий (`comment`).
- Включить/отключить опцию "Создать правило для похожих транзакций в будущем".
Ограничения:
- Описание операции (`description`) **не редактируется** в SPA, чтобы сохранять оригинальные данные из банка.
Поле `comment` используется только для заметок пользователя и не влияет напрямую на
автоматические правила (на старте).
## Поля в таблице `transactions`
К ранее описанной структуре добавляются поля:
- `is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE`
признак того, что текущая категория транзакции подтверждена пользователем
(явно или неявно).
- `comment TEXT` — пользовательский комментарий к транзакции.
Рекомендуемый DDL-дельта:
```sql
ALTER TABLE transactions
ADD COLUMN is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE transactions
ADD COLUMN comment TEXT;
```
## Поведение при редактировании транзакции
При сохранении изменений в SPA backend выполняет:
1. Обновление транзакции:
- устанавливается новое значение `category_id`;
- при изменении комментария сохраняется `comment`;
- поле `is_category_confirmed` устанавливается в `TRUE`.
2. Если опция "Создать правило для похожих транзакций в будущем" включена (по умолчанию включена):
- создаётся новая запись в `category_rules` или обновляется существующая.
## Создание правил из транзакций
В карточке редактирования транзакции отображается блок "Правило для будущих операций":
- Галочка (переключатель):
- по умолчанию **включена**;
- отвечает за то, будет ли на основе текущей транзакции создано/обновлено правило.
- Поле "ключевая строка/шаблон" (pattern):
- заполняется автоматически (например, ключевым словом из `description`),
но пользователь может его скорректировать;
- сохраняется в `category_rules.pattern`.
- Тип совпадения (match_type): на старте можно использовать только `"contains"`.
- В дальнейшем допускается расширение до `"starts_with"` и `"regex"`.
При сохранении:
- создаётся правило в `category_rules` с полями:
- `pattern` — строка из формы;
- `match_type` — тип (на старте `"contains"`);
- `category_id` — выбранная категория;
- `priority` — например, по умолчанию 100 для правил, созданных пользователем;
- `is_active = TRUE`.
Логика приоритета:
- Если для одной транзакции подходит несколько правил, выбирается правило
с максимальным значением `priority`.
## Особый случай: маркетплейсы
Проблема: по описанию вида "OZON", "WILDBERRIES" и т.п. невозможно
однозначно определить категорию (там могут быть любые товары).
Стратегия (MVP):
- Для маркетплейсов создаются специальные правила, которые:
- могут присваивать "предполагаемую" категорию (например, "Дом" или
любую другую, выбранную пользователем);
- при этом все транзакции, закатегоризированные таким правилом,
помечаются как `is_category_confirmed = FALSE`.
Результат:
- В истории операций такие транзакции визуально подсвечиваются
как требующие внимания пользователя.
- Пользователь может:
- подтвердить категорию (в этом случае `is_category_confirmed` становится `TRUE`);
- изменить категорию и при желании создать новое правило.
Дополнительно:
- Для маркетплейсов особенно полезно поле `comment`, куда пользователь может
записать, что конкретно было куплено (например, "корм для Арчи", "мебель", "одежда").
## Применение правил к прошлым транзакциям
В интерфейсе управления правилами (отдельный раздел SPA) для каждого правила
может быть доступна опция:
- "Применить правило к прошлым транзакциям".
При активации этой опции backend:
- находит все транзакции, подходящие под данное правило;
- проставляет им `category_id`;
- устанавливает `is_category_confirmed = FALSE` (категория считается предварительной).
Пользователь может затем:
- отфильтровать операции по `onlyUnconfirmed = true` через `/api/transactions`;
- просмотреть и при необходимости скорректировать категории;
- после ручного подтверждения транзакции становятся `is_category_confirmed = TRUE`.
## Связь с API `/api/transactions`
- Эндпоинт `/api/transactions` возвращает поля `isCategoryConfirmed` и `comment`,
что позволяет:
- отображать неподтверждённые категории отдельно (подсветка в UI);
- предоставлять фильтр "Только неподтверждённые" (`onlyUnconfirmed=true`).
- При редактировании транзакции через отдельный эндпоинт (например, `PUT /api/transactions/{id}`):
- передаются новые значения `categoryId` и/или `comment`;
- флаг `isCategoryConfirmed` устанавливается в `true`.

82
docs/backlog/format.md Normal file
View File

@@ -0,0 +1,82 @@
# Формат импорта выписки (JSON 1.0)
## Общие принципы
- На вход SPA принимает файл в формате JSON фиксированной схемы версии `1.0`.
- Один JSON-файл = выписка по одному счёту одного банка за произвольный период.
- Все суммы хранятся в минорных единицах (копейки) как целые числа.
- Дата и время операции передаются в формате ISO 8601 с таймзоной (например, `2026-02-26T14:06:57+03:00`).
## Структура JSON 1.0
Корневые поля:
- `schemaVersion: "1.0"` — версия схемы обменного формата.
- `bank: string` — идентификатор банка (например, `"VTB"`).
- `statement: object` — заголовок выписки.
- `transactions: array` — массив операций.
### Объект `statement`
Обязательные поля:
- `accountNumber: string` — номер счёта в банке (как в выписке). Допускается полный номер.
- `currency: string` — код валюты счёта (например, `"RUB"`).
- `openingBalance: integer` — баланс на начало периода в копейках.
- `closingBalance: integer` — баланс на конец периода в копейках.
- `exportedAt: string` — дата и время формирования выписки в формате ISO 8601 с таймзоной.
Особенности:
- Период выписки (даты "с" и "по") в JSON не передаётся, так как каждая транзакция содержит свой `operationAt`.
- Балансы `openingBalance` и `closingBalance` используются для первичной и периодической сверки остатков, а также при первичной загрузке выписки в пустую БД.
### Массив `transactions`
Каждый элемент массива описывает одну транзакцию и имеет следующую структуру:
Обязательные поля:
- `operationAt: string` — дата и время операции в формате ISO 8601 с таймзоной.
- `amountSigned: integer` — сумма операции в копейках:
- `> 0` — приход;
- `< 0` — расход.
- `commission: integer` — комиссия по операции в копейках (всегда передаётся, даже если `0`).
- `description: string` — текстовое описание операции из выписки банка (как есть).
Особенности:
- Никакие ID транзакций банка не используются, так как они отсутствуют в выписке.
- Поле `accountNumber` внутри транзакции не дублируется — связь идёт через `statement.accountNumber`.
- Поле `fingerprint` в JSON не передаётся — оно будет вычисляться на стороне backend при импорте и храниться только в БД.
## Идемпотентность импорта
- Для каждой транзакции backend вычисляет `fingerprint` (например, SHA-256 от конкатенации:
`accountNumber + operationAt + amountSigned + commission + normalizedDescription`).
- В БД вводится уникальное ограничение по паре `(account_id, fingerprint)`,
что позволяет безопасно загружать один и тот же файл (или пересекающиеся по периоду файлы) без появления дублей.
## Пример JSON 1.0
```json
{
"schemaVersion": "1.0",
"bank": "VTB",
"statement": {
"accountNumber": "40817810825104025611",
"currency": "RUB",
"openingBalance": 4256167,
"closingBalance": 8845938,
"exportedAt": "2026-02-27T13:23:00+03:00"
},
"transactions": [
{
"operationAt": "2026-02-26T14:06:57+03:00",
"amountSigned": -50000,
"commission": 0,
"description": "Оплата товаров и услуг. PAVELETSKAYA. по карте *8214"
}
]
}
```