commit 9551b93a0983e6b653973a70b90495a9b8c51d7a Author: Anton Date: Fri Feb 27 19:08:55 2026 +0300 docs: adds rules and agents specs diff --git a/docs/agents/agent_architect.md b/docs/agents/agent_architect.md new file mode 100644 index 0000000..0bbdc23 --- /dev/null +++ b/docs/agents/agent_architect.md @@ -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-интерфейсы). diff --git a/docs/agents/agent_backend.md b/docs/agents/agent_backend.md new file mode 100644 index 0000000..be92e67 --- /dev/null +++ b/docs/agents/agent_backend.md @@ -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`. +- Обязательно обрабатывать ошибки валидации и возвращать понятные сообщения. diff --git a/docs/agents/agent_frontend.md b/docs/agents/agent_frontend.md new file mode 100644 index 0000000..d18a617 --- /dev/null +++ b/docs/agents/agent_frontend.md @@ -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, описанные в соответствующих файлах. +- Настройка видимых колонок в истории должна быть предусмотрена (хотя бы на уровне структуры состояния). diff --git a/docs/agents/agent_rules_analytics.md b/docs/agents/agent_rules_analytics.md new file mode 100644 index 0000000..cdd9229 --- /dev/null +++ b/docs/agents/agent_rules_analytics.md @@ -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. diff --git a/docs/agents/agent_statement_converter.md b/docs/agents/agent_statement_converter.md new file mode 100644 index 0000000..60ec04d --- /dev/null +++ b/docs/agents/agent_statement_converter.md @@ -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`. +- Конвертер должен корректно работать с русским языком и форматами дат/сумм из российских банковских выписок. diff --git a/docs/backlog/analytics.md b/docs/backlog/analytics.md new file mode 100644 index 0000000..b926d06 --- /dev/null +++ b/docs/backlog/analytics.md @@ -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`). +- Топ-3–5 категорий по расходам (сумма, доля). + +Реализуется через эндпоинт `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` — номер месяца (1–12), используется если `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')); +``` + +Использование в аналитике: + +- Для выбранного периода (например, текущий месяц) по каждой категории можно: + - найти соответствующий бюджет (если задан); + - посчитать фактический расход за период; + - показать: потрачено / лимит / % выполнения / остаток. + +Это позволит в будущем реализовать экран "Бюджеты", а также подсветку категорий, +где расходы близки к лимиту или превышают его. diff --git a/docs/backlog/api_history.md b/docs/backlog/api_history.md new file mode 100644 index 0000000..fb64e9c --- /dev/null +++ b/docs/backlog/api_history.md @@ -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` используется как значение по умолчанию + (последние операции сверху). diff --git a/docs/backlog/auth.md b/docs/backlog/auth.md new file mode 100644 index 0000000..905ab63 --- /dev/null +++ b/docs/backlog/auth.md @@ -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, + управление категориями и правилами) доступны только в рамках действующей сессии. +- Так как ролевой модели нет, дополнительные ограничения доступа не требуются: + оба пользователя имеют одинаковые права. diff --git a/docs/backlog/category.md b/docs/backlog/category.md new file mode 100644 index 0000000..4bd88bc --- /dev/null +++ b/docs/backlog/category.md @@ -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"`) и предложить изменить его позже в настройках. diff --git a/docs/backlog/db.md b/docs/backlog/db.md new file mode 100644 index 0000000..821bdbb --- /dev/null +++ b/docs/backlog/db.md @@ -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). diff --git a/docs/backlog/edit_and_rules.md b/docs/backlog/edit_and_rules.md new file mode 100644 index 0000000..15be29b --- /dev/null +++ b/docs/backlog/edit_and_rules.md @@ -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`. diff --git a/docs/backlog/format.md b/docs/backlog/format.md new file mode 100644 index 0000000..e909d6c --- /dev/null +++ b/docs/backlog/format.md @@ -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" + } + ] +} +```