docs: adds rules and agents specs
This commit is contained in:
181
docs/backlog/analytics.md
Normal file
181
docs/backlog/analytics.md
Normal 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`).
|
||||
- Топ-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'));
|
||||
```
|
||||
|
||||
Использование в аналитике:
|
||||
|
||||
- Для выбранного периода (например, текущий месяц) по каждой категории можно:
|
||||
- найти соответствующий бюджет (если задан);
|
||||
- посчитать фактический расход за период;
|
||||
- показать: потрачено / лимит / % выполнения / остаток.
|
||||
|
||||
Это позволит в будущем реализовать экран "Бюджеты", а также подсветку категорий,
|
||||
где расходы близки к лимиту или превышают его.
|
||||
98
docs/backlog/api_history.md
Normal file
98
docs/backlog/api_history.md
Normal 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
79
docs/backlog/auth.md
Normal 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
98
docs/backlog/category.md
Normal 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
147
docs/backlog/db.md
Normal 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).
|
||||
141
docs/backlog/edit_and_rules.md
Normal file
141
docs/backlog/edit_and_rules.md
Normal 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
82
docs/backlog/format.md
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user