docs: refactor project docs and agents tasks
This commit is contained in:
@@ -20,8 +20,7 @@
|
||||
- `direction: string` — направления движения, одно или несколько значений через запятую:
|
||||
- `income` — приход;
|
||||
- `expense` — расход;
|
||||
- `transfer` — переводы между счетами;
|
||||
- `internal` — внутренние движения (если будут использоваться).
|
||||
- `transfer` — переводы между счетами.
|
||||
Пример: `direction=income,expense`.
|
||||
- `categoryId: number` — идентификатор категории (`categories.id`).
|
||||
- `search: string` — строка поиска по полю `description` (поиск по подстроке, регистронезависимый).
|
||||
@@ -73,7 +72,7 @@
|
||||
- `amountSigned` — сумма операции в копейках.
|
||||
- `commission` — комиссия по операции в копейках.
|
||||
- `description` — описание операции из банка.
|
||||
- `direction` — направление транзакции (`income` / `expense` / `transfer` / `internal`).
|
||||
- `direction` — направление транзакции (`income` / `expense` / `transfer`).
|
||||
- `categoryId` — идентификатор категории (`categories.id`), может быть `null`.
|
||||
- `categoryName` — имя категории (`categories.name`), может быть `null`.
|
||||
- `isCategoryConfirmed` — признак того, что категория подтверждена пользователем.
|
||||
|
||||
212
docs/backlog/api_import.md
Normal file
212
docs/backlog/api_import.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Импорт выписки (POST /api/import/statement)
|
||||
|
||||
## Назначение
|
||||
|
||||
Эндпоинт принимает банковскую выписку в формате JSON 1.0 (см. `format.md`) и атомарно импортирует транзакции в БД.
|
||||
|
||||
## Метод и URL
|
||||
|
||||
- Метод: `POST`
|
||||
- URL: `/api/import/statement`
|
||||
|
||||
## Требования к авторизации
|
||||
|
||||
- Требуется действующая сессия (cookie `sid`).
|
||||
- При отсутствии или недействительной сессии — `401 Unauthorized`.
|
||||
|
||||
## Тело запроса
|
||||
|
||||
JSON строго по схеме 1.0 (`format.md`). Content-Type: `application/json`.
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Валидация
|
||||
|
||||
Валидация выполняется в два этапа.
|
||||
|
||||
### 1) Структурная валидация (400 Bad Request)
|
||||
|
||||
Проверяется формат входного JSON. При ошибке — `400` без частичного импорта.
|
||||
|
||||
Проверки:
|
||||
|
||||
- Тело запроса является валидным JSON.
|
||||
- Присутствуют все обязательные корневые поля: `schemaVersion`, `bank`, `statement`, `transactions`.
|
||||
- `schemaVersion` строго равен `"1.0"`.
|
||||
- `bank` — непустая строка.
|
||||
- `statement` содержит обязательные поля: `accountNumber`, `currency`, `openingBalance`, `closingBalance`, `exportedAt`.
|
||||
- `statement.accountNumber` — непустая строка.
|
||||
- `statement.currency` — непустая строка.
|
||||
- `statement.openingBalance`, `statement.closingBalance` — числа (integer).
|
||||
- `statement.exportedAt` — строка в формате ISO 8601 с обязательным offset (`+HH:MM` или `Z`).
|
||||
- `transactions` — массив, содержащий хотя бы один элемент.
|
||||
- Каждая транзакция содержит обязательные поля: `operationAt`, `amountSigned`, `commission`, `description`.
|
||||
- `operationAt` — строка в формате ISO 8601 с обязательным offset (`+HH:MM` или `Z`).
|
||||
- `amountSigned` — число (integer), не равное `0`.
|
||||
- `commission` — число (integer), `>= 0`.
|
||||
- `description` — непустая строка.
|
||||
|
||||
Ответ при ошибке:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "BAD_REQUEST",
|
||||
"message": "Invalid schema: missing field 'statement.accountNumber'"
|
||||
}
|
||||
```
|
||||
|
||||
### 2) Семантическая валидация (422 Unprocessable Entity)
|
||||
|
||||
Проверяется логическая корректность данных. При ошибке — `422`, весь файл откатывается.
|
||||
|
||||
Проверки:
|
||||
|
||||
- `statement.currency` соответствует допустимому коду валюты (MVP: `"RUB"`).
|
||||
- `operationAt` у всех транзакций — валидная дата (парсится без ошибок).
|
||||
- Отсутствуют дубликаты fingerprint внутри одного файла.
|
||||
|
||||
Ответ при ошибке:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "VALIDATION_ERROR",
|
||||
"message": "Duplicate fingerprint found within file at transaction index 5"
|
||||
}
|
||||
```
|
||||
|
||||
## Логика обработки
|
||||
|
||||
### Счёт (account)
|
||||
|
||||
1. По комбинации `bank` + `statement.accountNumber` выполняется поиск в таблице `accounts`.
|
||||
2. Если счёт найден — используется его `id`.
|
||||
3. Если счёт не найден — создаётся новая запись:
|
||||
- `bank` = значение из JSON;
|
||||
- `account_number` = `statement.accountNumber`;
|
||||
- `currency` = `statement.currency`;
|
||||
- `alias` = `NULL`.
|
||||
4. Флаг `isNewAccount` в ответе отражает, был ли счёт создан при этом импорте.
|
||||
|
||||
### Fingerprint
|
||||
|
||||
Для каждой транзакции вычисляется SHA-256 от полей, соединённых разделителем `|`:
|
||||
|
||||
```text
|
||||
accountNumber|operationAt|amountSigned|commission|normalizedDescription
|
||||
```
|
||||
|
||||
- `normalizedDescription` — `description` после `trim`.
|
||||
- Суммы подставляются в том виде, в котором пришли в JSON (числовое представление).
|
||||
- Разделитель `|` исключает коллизии при склейке полей разной длины.
|
||||
|
||||
Результат хэширования хранится в поле `transactions.fingerprint` с префиксом `sha256:`.
|
||||
|
||||
### Direction
|
||||
|
||||
Направление операции определяется по следующим правилам:
|
||||
|
||||
1. Проверка ключевых фраз в `description` (регистронезависимо):
|
||||
- Если содержит одну из фраз: `"Перевод между своими счетами"`, `"перевод средств на счет"`, `"Внутри ВТБ"` — `direction = "transfer"`.
|
||||
2. Если ключевые фразы не сработали:
|
||||
- `amountSigned > 0` → `direction = "income"`;
|
||||
- `amountSigned < 0` → `direction = "expense"`.
|
||||
|
||||
Список ключевых фраз для `"transfer"` может расширяться; в MVP используется фиксированный набор.
|
||||
|
||||
### Импорт транзакций
|
||||
|
||||
Для каждой транзакции из массива `transactions`:
|
||||
|
||||
1. Суммы `amountSigned` и `commission` записываются как есть — в JSON 1.0 они уже в копейках (`BIGINT`).
|
||||
2. Вычисляется `fingerprint`.
|
||||
3. Определяется `direction`.
|
||||
4. Выполняется вставка в таблицу `transactions` со значениями:
|
||||
- `account_id` — ID найденного/созданного счёта;
|
||||
- `operation_at` — из `operationAt`;
|
||||
- `amount_signed` — в копейках;
|
||||
- `commission` — в копейках;
|
||||
- `description` — как есть из JSON;
|
||||
- `direction` — вычисленное значение;
|
||||
- `fingerprint` — вычисленный хэш;
|
||||
- `category_id = NULL`;
|
||||
- `is_category_confirmed = FALSE`.
|
||||
5. При срабатывании уникального ограничения `(account_id, fingerprint)` запись считается дубликатом и пропускается (счётчик `duplicatesSkipped` увеличивается).
|
||||
|
||||
### Автокатегоризация
|
||||
|
||||
После успешной вставки всех транзакций backend применяет активные правила из `category_rules` к только что импортированным транзакциям (у которых `category_id IS NULL`):
|
||||
|
||||
- Для каждой транзакции ищутся все подходящие активные правила.
|
||||
- При совпадении нескольких правил выбирается правило с максимальным `priority`.
|
||||
- Устанавливается `category_id` из правила.
|
||||
- Если у правила `requires_confirmation = FALSE` → `is_category_confirmed = TRUE`.
|
||||
- Если у правила `requires_confirmation = TRUE` → `is_category_confirmed = FALSE`.
|
||||
|
||||
### Атомарность
|
||||
|
||||
Весь импорт выполняется в одной транзакции БД:
|
||||
|
||||
- Создание счёта (если нужно) + вставка всех транзакций + автокатегоризация.
|
||||
- При любой ошибке валидации или вставки — откат всей транзакции БД, возвращается `422`.
|
||||
|
||||
## Успешный ответ (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"accountId": 1,
|
||||
"isNewAccount": true,
|
||||
"accountNumberMasked": "408178**********5611",
|
||||
"imported": 28,
|
||||
"duplicatesSkipped": 3,
|
||||
"totalInFile": 31
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `accountId: number` — ID счёта в системе.
|
||||
- `isNewAccount: boolean` — `true`, если счёт был создан при этом импорте.
|
||||
- `accountNumberMasked: string` — маскированный номер счёта (первые 6 и последние 4 символа, остальное заменено на `*`).
|
||||
- `imported: number` — количество успешно импортированных (новых) транзакций.
|
||||
- `duplicatesSkipped: number` — количество пропущенных дубликатов.
|
||||
- `totalInFile: number` — общее количество транзакций в файле (`imported + duplicatesSkipped`).
|
||||
|
||||
## Маскирование номера счёта
|
||||
|
||||
Номер счёта маскируется для отображения в UI:
|
||||
|
||||
- Первые 6 символов остаются открытыми.
|
||||
- Последние 4 символа остаются открытыми.
|
||||
- Промежуточные символы заменяются на `*`.
|
||||
- Пример: `40817810825104025611` → `408178**********5611`.
|
||||
|
||||
Маскирование применяется в ответе этого эндпоинта и в `GET /api/accounts`.
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
| Код | Ситуация |
|
||||
|-----|--------------------------------------------------------------------------|
|
||||
| 200 | Импорт выполнен успешно |
|
||||
| 400 | Невалидный JSON или нарушение структуры схемы 1.0 |
|
||||
| 401 | Нет действующей сессии |
|
||||
| 422 | Семантическая ошибка валидации (некорректные данные, ошибка при вставке) |
|
||||
137
docs/backlog/api_reference_accounts_categories.md
Normal file
137
docs/backlog/api_reference_accounts_categories.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Спецификация справочников: Accounts и Categories (API)
|
||||
|
||||
## Общие требования
|
||||
|
||||
- Все эндпоинты требуют действующей сессии (иначе `401 Unauthorized`).
|
||||
- Формат JSON-ответов использует `camelCase`.
|
||||
|
||||
---
|
||||
|
||||
## 1) GET /api/accounts — список счетов
|
||||
|
||||
Назначение:
|
||||
- Справочник для фильтра по счёту (история и аналитика).
|
||||
- Экран настроек (просмотр и редактирование алиасов).
|
||||
|
||||
Параметры запроса: отсутствуют.
|
||||
|
||||
Ответ (200 OK): массив объектов счетов.
|
||||
|
||||
Поведение сортировки:
|
||||
- По умолчанию backend возвращает счета, отсортированные по `bank`, затем по `id` (ASC), чтобы порядок в UI был стабильным.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"bank": "VTB",
|
||||
"accountNumberMasked": "408178******5611",
|
||||
"currency": "RUB",
|
||||
"alias": "Текущий"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Поля:
|
||||
- `id: number` — `accounts.id`.
|
||||
- `bank: string` — код банка.
|
||||
- `accountNumberMasked: string` — маскированный номер счёта для отображения в UI (полный номер фронтенду не отдаётся).
|
||||
- `currency: string` — валюта счёта.
|
||||
- `alias: string | null` — алиас (может быть `null`, если счёт создан и алиас ещё не задан).
|
||||
|
||||
Ошибки:
|
||||
- `401 Unauthorized` — нет действующей сессии.
|
||||
|
||||
---
|
||||
|
||||
## 2) PUT /api/accounts/{id} — обновление алиаса
|
||||
|
||||
Назначение:
|
||||
- После импорта нового счёта фронтенд предлагает ввести алиас.
|
||||
- На экране настроек пользователь может поменять алиас.
|
||||
|
||||
Параметры:
|
||||
- `id: number` — идентификатор счёта.
|
||||
|
||||
Тело запроса:
|
||||
|
||||
```json
|
||||
{ "alias": "Накопительный" }
|
||||
```
|
||||
|
||||
Валидация:
|
||||
- `alias` обязателен.
|
||||
- `alias` не может быть пустым после `trim`.
|
||||
- Максимальная длина алиаса — 50 символов (MVP).
|
||||
|
||||
Ответ (200 OK): обновлённый объект счёта (как в `GET /api/accounts`).
|
||||
|
||||
Ошибки:
|
||||
- `401 Unauthorized` — нет действующей сессии.
|
||||
- `404 Not Found` — счёт не найден.
|
||||
- `400 Bad Request` — алиас пустой или слишком длинный.
|
||||
|
||||
---
|
||||
|
||||
## 3) GET /api/categories — список категорий
|
||||
|
||||
Назначение:
|
||||
- Выпадающий список категорий при редактировании транзакции.
|
||||
- Фильтр по категории в истории.
|
||||
- Экран настроек (просмотр категорий).
|
||||
|
||||
Параметры запроса (query):
|
||||
- `isActive: boolean` — опционально. Поведение:
|
||||
- Параметр не передан → только активные (`is_active = TRUE`).
|
||||
- `isActive=true` → только активные (`is_active = TRUE`).
|
||||
- `isActive=false` → только неактивные (`is_active = FALSE`).
|
||||
|
||||
Ответ (200 OK): массив объектов категорий.
|
||||
|
||||
```json
|
||||
[
|
||||
{ "id": 1, "name": "Продукты", "type": "expense", "isActive": true },
|
||||
{ "id": 23, "name": "Поступления", "type": "income", "isActive": true }
|
||||
]
|
||||
```
|
||||
|
||||
Поля:
|
||||
- `id: number` — `categories.id`.
|
||||
- `name: string` — отображаемое имя категории.
|
||||
- `type: 'expense' | 'income' | 'transfer'` — тип категории.
|
||||
- `isActive: boolean` — активность категории.
|
||||
|
||||
Ошибки:
|
||||
- `401 Unauthorized` — нет действующей сессии.
|
||||
|
||||
---
|
||||
|
||||
## Решение по типам категорий (MVP)
|
||||
|
||||
Полный список категорий и их `type` (для seed-миграции):
|
||||
|
||||
| Категория | type |
|
||||
|---|---|
|
||||
| Продукты | expense |
|
||||
| Авто | expense |
|
||||
| Здоровье | expense |
|
||||
| Арчи | expense |
|
||||
| ЖКХ | expense |
|
||||
| Дом | expense |
|
||||
| Проезд | expense |
|
||||
| Одежда | expense |
|
||||
| Химия | expense |
|
||||
| Косметика | expense |
|
||||
| Инвестиции | transfer |
|
||||
| Развлечения | expense |
|
||||
| Общепит | expense |
|
||||
| Штрафы | expense |
|
||||
| Налоги | expense |
|
||||
| Подписки | expense |
|
||||
| Перевод | transfer |
|
||||
| Наличные | expense |
|
||||
| Подарки | expense |
|
||||
| Спорт | expense |
|
||||
| Отпуск | expense |
|
||||
| Техника | expense |
|
||||
| Поступления | income |
|
||||
255
docs/backlog/api_rules.md
Normal file
255
docs/backlog/api_rules.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Правила категоризации (CRUD /api/category-rules)
|
||||
|
||||
## Назначение
|
||||
|
||||
Набор эндпоинтов для управления правилами автоматической категоризации транзакций.
|
||||
Правила глобальные — без привязки к конкретному пользователю (userId не используется).
|
||||
|
||||
## Общие требования
|
||||
|
||||
- Все эндпоинты требуют действующей сессии (cookie `sid`), иначе `401 Unauthorized`.
|
||||
- Формат JSON-ответов использует `camelCase`.
|
||||
- В MVP поддерживается только `matchType = "contains"`. Значения `"starts_with"` и `"regex"` зарезервированы для будущих версий.
|
||||
|
||||
---
|
||||
|
||||
## 1) GET /api/category-rules — список правил
|
||||
|
||||
Назначение: отображение списка правил на экране настроек (просмотр, активация/деактивация).
|
||||
|
||||
### Параметры запроса (query)
|
||||
|
||||
Все параметры опциональны.
|
||||
|
||||
- `isActive: boolean` — фильтр по активности:
|
||||
- Параметр не передан → все правила (активные и неактивные).
|
||||
- `isActive=true` → только активные (`is_active = TRUE`).
|
||||
- `isActive=false` → только неактивные (`is_active = FALSE`).
|
||||
- `categoryId: number` — фильтр по категории (`category_rules.category_id`).
|
||||
- `search: string` — поиск по подстроке в поле `pattern` (регистронезависимый, `ILIKE '%search%'`).
|
||||
|
||||
### Поведение сортировки
|
||||
|
||||
По умолчанию backend возвращает правила, отсортированные по `priority DESC`, затем по `created_at DESC`.
|
||||
|
||||
### Ответ (200 OK)
|
||||
|
||||
Массив объектов правил.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"pattern": "PYATEROCHKA",
|
||||
"matchType": "contains",
|
||||
"categoryId": 1,
|
||||
"categoryName": "Продукты",
|
||||
"priority": 100,
|
||||
"requiresConfirmation": false,
|
||||
"isActive": true,
|
||||
"createdAt": "2026-02-20T12:00:00+03:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `id: number` — идентификатор правила.
|
||||
- `pattern: string` — строка-шаблон.
|
||||
- `matchType: string` — тип сопоставления (`"contains"` в MVP).
|
||||
- `categoryId: number` — ID категории.
|
||||
- `categoryName: string` — имя категории (JOIN с `categories`).
|
||||
- `priority: number` — приоритет правила.
|
||||
- `requiresConfirmation: boolean` — требуется ли ручное подтверждение категории.
|
||||
- `isActive: boolean` — активно ли правило.
|
||||
- `createdAt: string` — дата создания в формате ISO 8601.
|
||||
|
||||
### Ошибки
|
||||
|
||||
- `401 Unauthorized` — нет действующей сессии.
|
||||
|
||||
---
|
||||
|
||||
## 2) POST /api/category-rules — создание правила
|
||||
|
||||
Назначение: создание нового правила категоризации (из экрана настроек или при редактировании транзакции).
|
||||
|
||||
### Тело запроса
|
||||
|
||||
```json
|
||||
{
|
||||
"pattern": "PYATEROCHKA",
|
||||
"matchType": "contains",
|
||||
"categoryId": 1,
|
||||
"priority": 100,
|
||||
"requiresConfirmation": false
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `pattern: string` — **обязательное**. Строка-шаблон для сопоставления с `description` транзакций.
|
||||
- `matchType: string` — **опциональное**, по умолчанию `"contains"`. В MVP принимается только `"contains"`.
|
||||
- `categoryId: number` — **обязательное**. ID категории из таблицы `categories`.
|
||||
- `priority: number` — **опциональное**, по умолчанию `100`. Целое число. Дефолт 100 означает, что пользовательские правила по умолчанию приоритетнее общих (seed/системных) правил с низким приоритетом.
|
||||
- `requiresConfirmation: boolean` — **опциональное**, по умолчанию `false`.
|
||||
|
||||
### Валидация
|
||||
|
||||
- `pattern`:
|
||||
- Не может быть пустым после `trim`.
|
||||
- Максимальная длина — 200 символов (после `trim`).
|
||||
- `matchType`:
|
||||
- Если передан, должен быть строго `"contains"` (MVP).
|
||||
- Неизвестные значения → `422`.
|
||||
- `categoryId`:
|
||||
- Должен ссылаться на существующую активную категорию.
|
||||
- Несуществующая или неактивная категория → `422`.
|
||||
- `priority`:
|
||||
- Целое число от `0` до `1000`.
|
||||
|
||||
### Ответ (201 Created)
|
||||
|
||||
Созданный объект правила (структура как в `GET`).
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 5,
|
||||
"pattern": "PYATEROCHKA",
|
||||
"matchType": "contains",
|
||||
"categoryId": 1,
|
||||
"categoryName": "Продукты",
|
||||
"priority": 100,
|
||||
"requiresConfirmation": false,
|
||||
"isActive": true,
|
||||
"createdAt": "2026-02-28T15:30:00+03:00"
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибки
|
||||
|
||||
| Код | Ситуация |
|
||||
|-----|-----------------------------------------------------------------------------|
|
||||
| 400 | Структурно невалидные данные (пустой pattern, отсутствуют обязательные поля) |
|
||||
| 401 | Нет действующей сессии |
|
||||
| 422 | Ошибка валидации значений (неизвестный matchType, категория не найдена и пр.)|
|
||||
|
||||
---
|
||||
|
||||
## 3) PATCH /api/category-rules/{id} — частичное обновление правила
|
||||
|
||||
Назначение: изменение параметров правила, в том числе активация/деактивация.
|
||||
|
||||
### Параметры
|
||||
|
||||
- `id: number` — идентификатор правила (path-параметр).
|
||||
|
||||
### Тело запроса
|
||||
|
||||
Частичное обновление: передаются только изменяемые поля.
|
||||
|
||||
```json
|
||||
{
|
||||
"pattern": "PEREKRESTOK",
|
||||
"categoryId": 1,
|
||||
"priority": 150,
|
||||
"requiresConfirmation": true,
|
||||
"isActive": false
|
||||
}
|
||||
```
|
||||
|
||||
Допустимые поля для обновления:
|
||||
|
||||
- `pattern: string` — новый шаблон (валидация как при создании).
|
||||
- `categoryId: number` — новая категория (должна существовать и быть активной).
|
||||
- `priority: number` — новый приоритет (целое, `0`–`1000`).
|
||||
- `requiresConfirmation: boolean` — обновление флага.
|
||||
- `isActive: boolean` — активация/деактивация правила.
|
||||
|
||||
Поле `matchType` в MVP не обновляется. Если клиент передал `matchType` — возвращается `422` с сообщением `"matchType update is not supported in MVP"`, чтобы фронтенд не считал, что значение было применено.
|
||||
|
||||
### Валидация
|
||||
|
||||
- Правило с указанным `id` должно существовать, иначе `404`.
|
||||
- Если передано поле `matchType` — `422` (обновление не поддерживается в MVP).
|
||||
- К переданным полям применяются те же правила валидации, что и при создании.
|
||||
- Если ни одно допустимое поле не передано — `400`.
|
||||
|
||||
### Ответ (200 OK)
|
||||
|
||||
Обновлённый объект правила (структура как в `GET`).
|
||||
|
||||
### Ошибки
|
||||
|
||||
| Код | Ситуация |
|
||||
|-----|-----------------------------------------------------------------------|
|
||||
| 400 | Структурно невалидные данные или ни одного допустимого поля |
|
||||
| 401 | Нет действующей сессии |
|
||||
| 404 | Правило не найдено |
|
||||
| 422 | Ошибка валидации (категория не найдена, передан matchType и пр.) |
|
||||
|
||||
---
|
||||
|
||||
## 4) POST /api/category-rules/{id}/apply — применение правила к прошлым транзакциям
|
||||
|
||||
Назначение: ретроактивное применение конкретного правила к существующим транзакциям.
|
||||
|
||||
### Параметры
|
||||
|
||||
- `id: number` — идентификатор правила (path-параметр).
|
||||
|
||||
### Тело запроса
|
||||
|
||||
Отсутствует.
|
||||
|
||||
### Логика обработки
|
||||
|
||||
1. Правило загружается из БД. Если не найдено — `404`. Если неактивно (`is_active = FALSE`) — `422`.
|
||||
2. Выполняется поиск транзакций, подходящих под данное правило:
|
||||
- Сопоставление `pattern` с `description` (регистронезависимый `ILIKE '%pattern%'` для `matchType = "contains"`).
|
||||
- Затрагиваются только транзакции, у которых:
|
||||
- `category_id IS NULL`, **или**
|
||||
- `is_category_confirmed = FALSE`.
|
||||
- Транзакции с `is_category_confirmed = TRUE` **не затрагиваются** — пользовательские подтверждения неприкосновенны.
|
||||
3. Для найденных транзакций:
|
||||
- Устанавливается `category_id` из правила.
|
||||
- `is_category_confirmed` устанавливается в `FALSE` (категория считается предварительной, требует подтверждения пользователем).
|
||||
- Обновляется `updated_at`.
|
||||
|
||||
### Ответ (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"applied": 12
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `applied: number` — количество транзакций, к которым было применено правило.
|
||||
|
||||
### Ошибки
|
||||
|
||||
| Код | Ситуация |
|
||||
|-----|-----------------------------------------------|
|
||||
| 401 | Нет действующей сессии |
|
||||
| 404 | Правило не найдено |
|
||||
| 422 | Правило неактивно |
|
||||
|
||||
---
|
||||
|
||||
## Связь с импортом
|
||||
|
||||
При импорте выписки (`POST /api/import/statement`) автокатегоризация выполняется автоматически:
|
||||
|
||||
- Ко всем импортированным транзакциям (с `category_id = NULL`) применяются все активные правила.
|
||||
- Если для транзакции подходит несколько правил, выбирается правило с максимальным `priority`.
|
||||
- `is_category_confirmed` определяется флагом `requires_confirmation` сработавшего правила.
|
||||
|
||||
Подробнее см. `api_import.md`.
|
||||
|
||||
## Связь с редактированием транзакций
|
||||
|
||||
При редактировании транзакции (`PUT /api/transactions/{id}`) с включённой опцией «Создать правило» фронтенд отправляет отдельный запрос `POST /api/category-rules` с заполненными полями `pattern`, `categoryId` и т.д.
|
||||
|
||||
Подробнее см. `edit_and_rules.md`.
|
||||
@@ -4,29 +4,31 @@
|
||||
|
||||
Используются следующие категории для классификации транзакций:
|
||||
|
||||
- Продукты
|
||||
- Авто
|
||||
- Здоровье
|
||||
- Арчи
|
||||
- ЖКХ
|
||||
- Дом
|
||||
- Проезд
|
||||
- Одежда
|
||||
- Химия
|
||||
- Косметика
|
||||
- Инвестиции
|
||||
- Развлечения
|
||||
- Общепит
|
||||
- Штрафы
|
||||
- Налоги
|
||||
- Подписки
|
||||
- Перевод
|
||||
- Наличные
|
||||
- Подарки
|
||||
- Спорт
|
||||
- Отпуск
|
||||
- Техника
|
||||
- Поступления
|
||||
| Категория | type |
|
||||
|---|---|
|
||||
| Продукты | expense |
|
||||
| Авто | expense |
|
||||
| Здоровье | expense |
|
||||
| Арчи | expense |
|
||||
| ЖКХ | expense |
|
||||
| Дом | expense |
|
||||
| Проезд | expense |
|
||||
| Одежда | expense |
|
||||
| Химия | expense |
|
||||
| Косметика | expense |
|
||||
| Инвестиции | transfer |
|
||||
| Развлечения | expense |
|
||||
| Общепит | expense |
|
||||
| Штрафы | expense |
|
||||
| Налоги | expense |
|
||||
| Подписки | expense |
|
||||
| Перевод | transfer |
|
||||
| Наличные | expense |
|
||||
| Подарки | expense |
|
||||
| Спорт | expense |
|
||||
| Отпуск | expense |
|
||||
| Техника | expense |
|
||||
| Поступления | income |
|
||||
|
||||
Особенности:
|
||||
|
||||
@@ -49,8 +51,6 @@
|
||||
- `"income"` — доходная категория;
|
||||
- `"transfer"` — категории для переводов/движений между собственными счетами.
|
||||
- `is_active BOOLEAN NOT NULL DEFAULT TRUE` — используется ли категория.
|
||||
- `is_category_confirmed BOOLEAN NOT NULL DEFAULT FALSE` — подтверждена ли категория пользователем.
|
||||
- `comment TEXT` — пользовательский комментарий (например, по маркетплейсам).
|
||||
|
||||
Рекомендуемый DDL:
|
||||
|
||||
@@ -65,14 +65,10 @@ CREATE TABLE categories (
|
||||
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;
|
||||
```
|
||||
|
||||
Поля `is_category_confirmed` и `comment` относятся к таблице `transactions` (см. `edit_and_rules.md`).
|
||||
|
||||
Привязка к транзакциям:
|
||||
|
||||
- В таблице `transactions` поле `category_id` является внешним ключом на `categories(id)`.
|
||||
|
||||
@@ -55,8 +55,7 @@ CREATE UNIQUE INDEX ux_accounts_bank_number
|
||||
- `direction TEXT NOT NULL` — направление движения:
|
||||
- `"income"` — приход;
|
||||
- `"expense"` — расход;
|
||||
- `"transfer"` — перевод между своими счетами / на другие свои счета;
|
||||
- `"internal"` — служебные/внутренние движения (опционально, по мере необходимости).
|
||||
- `"transfer"` — перевод между своими счетами / на другие свои счета.
|
||||
- `fingerprint TEXT NOT NULL` — вычисляемый хэш для обеспечения идемпотентности импорта.
|
||||
- `category_id BIGINT` — ссылка на таблицу категорий (будет определена позже).
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время создания записи в БД.
|
||||
@@ -90,7 +89,7 @@ CREATE UNIQUE INDEX ux_transactions_account_fingerprint
|
||||
|
||||
ALTER TABLE transactions
|
||||
ADD CONSTRAINT chk_transactions_direction
|
||||
CHECK (direction IN ('income', 'expense', 'transfer', 'internal'));
|
||||
CHECK (direction IN ('income', 'expense', 'transfer'));
|
||||
```
|
||||
|
||||
## Таблица `category_rules`
|
||||
@@ -105,8 +104,13 @@ ALTER TABLE transactions
|
||||
- `"contains"` — простое вхождение подстроки;
|
||||
- `"starts_with"` — строка начинается с шаблона;
|
||||
- `"regex"` — регулярное выражение (формируется и/или проверяется в коде на основе пользовательского ввода).
|
||||
- MVP: при создании правила принимается только `"contains"`. Расширение до `"starts_with"` | `"regex"` запланировано.
|
||||
- `category_id BIGINT NOT NULL` — ссылка на категорию (таблица категорий будет описана отдельно).
|
||||
- `priority INT NOT NULL DEFAULT 0` — приоритет правила; чем выше число, тем раньше правило применяется при конфликте.
|
||||
- `requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE` —
|
||||
если `TRUE`, транзакции, категоризированные этим правилом, получают `is_category_confirmed = FALSE`
|
||||
и требуют ручного подтверждения пользователем. Используется для правил с неоднозначным
|
||||
соответствием (например, маркетплейсы: OZON, WILDBERRIES).
|
||||
- `is_active BOOLEAN NOT NULL DEFAULT TRUE` — активно ли правило.
|
||||
- `created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()` — время создания правила.
|
||||
|
||||
@@ -120,28 +124,37 @@ ALTER TABLE transactions
|
||||
|
||||
```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()
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
pattern TEXT NOT NULL,
|
||||
match_type TEXT NOT NULL,
|
||||
category_id BIGINT NOT NULL,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
## Взаимосвязь JSON → БД при импорте
|
||||
|
||||
1. По полям `bank` и `statement.accountNumber` ищется или создаётся запись в `accounts`.
|
||||
Если счёт создан впервые, `alias = NULL`.
|
||||
2. Для каждой транзакции из `transactions`:
|
||||
- преобразуются суммы в копейки (`amountSigned`, `commission` → `BIGINT`);
|
||||
- вычисляется `fingerprint` на основе комбинации полей (например,
|
||||
`accountNumber + operationAt + amountSigned + commission + normalizedDescription`);
|
||||
- определяется `direction` по знаку суммы и/или шаблонам текста (приход/расход/перевод);
|
||||
- определяется `direction`:
|
||||
- `amountSigned > 0` → `"income"`;
|
||||
- `amountSigned < 0` → `"expense"`;
|
||||
- `"transfer"` — определяется по фиксированным ключевым фразам банка в `description`
|
||||
(например, для ВТБ: "Перевод", "между счетами" и т.п.).
|
||||
Если ключевые фразы не сработали — остаётся `"income"` / `"expense"` по знаку суммы;
|
||||
- выполняется попытка вставки в `transactions`;
|
||||
- при срабатывании уникального ограничения `(account_id, fingerprint)` запись считается дубликатом и пропускается.
|
||||
- При импорте новых транзакций is_category_confirmed всегда = FALSE.
|
||||
- При автокатегоризации обычными правилами, если правило “жёсткое” (не маркетплейс) — TRUE, для маркетплейсов — всегда FALSE.
|
||||
|
||||
3. Категория (`category_id`) пока может оставаться `NULL` и заполняться на следующих этапах
|
||||
(правила, ИИ-агент, ручное редактирование в SPA).
|
||||
- при срабатывании уникального ограничения `(account_id, fingerprint)` запись считается дубликатом и пропускается;
|
||||
- при импорте `is_category_confirmed` всегда = `FALSE`, `category_id = NULL`.
|
||||
3. Импорт атомарный: при ошибке валидации любой транзакции весь файл откатывается (возвращается `422`).
|
||||
4. Категория (`category_id`) заполняется на следующих этапах:
|
||||
- автокатегоризация по `category_rules`:
|
||||
- если у сработавшего правила `requires_confirmation = FALSE` → `is_category_confirmed = TRUE`;
|
||||
- если `requires_confirmation = TRUE` → `is_category_confirmed = FALSE` (требуется ручное подтверждение);
|
||||
- ручное редактирование в SPA.
|
||||
|
||||
@@ -77,6 +77,9 @@ ALTER TABLE transactions
|
||||
- `match_type` — тип (на старте `"contains"`);
|
||||
- `category_id` — выбранная категория;
|
||||
- `priority` — например, по умолчанию 100 для правил, созданных пользователем;
|
||||
- `requires_confirmation` — если `TRUE`, транзакции, категоризированные этим правилом,
|
||||
получают `is_category_confirmed = FALSE` и требуют ручного подтверждения.
|
||||
По умолчанию `FALSE`. Используется для правил с неоднозначным соответствием (маркетплейсы и т.п.);
|
||||
- `is_active = TRUE`.
|
||||
|
||||
Логика приоритета:
|
||||
@@ -84,30 +87,32 @@ ALTER TABLE transactions
|
||||
- Если для одной транзакции подходит несколько правил, выбирается правило
|
||||
с максимальным значением `priority`.
|
||||
|
||||
## Особый случай: маркетплейсы
|
||||
## Правила с обязательным подтверждением (`requires_confirmation`)
|
||||
|
||||
Проблема: по описанию вида "OZON", "WILDBERRIES" и т.п. невозможно
|
||||
однозначно определить категорию (там могут быть любые товары).
|
||||
Проблема: некоторые правила не могут однозначно определить категорию транзакции.
|
||||
Например, по описанию "OZON", "WILDBERRIES" невозможно понять, что именно было куплено
|
||||
(продукты, одежда, техника и т.п.).
|
||||
|
||||
Стратегия (MVP):
|
||||
Механизм:
|
||||
|
||||
- Для маркетплейсов создаются специальные правила, которые:
|
||||
- могут присваивать "предполагаемую" категорию (например, "Дом" или
|
||||
любую другую, выбранную пользователем);
|
||||
- при этом все транзакции, закатегоризированные таким правилом,
|
||||
помечаются как `is_category_confirmed = FALSE`.
|
||||
- В таблице `category_rules` используется флаг `requires_confirmation BOOLEAN NOT NULL DEFAULT FALSE`.
|
||||
- Если `requires_confirmation = TRUE`:
|
||||
- правило присваивает категорию (например, "Дом" или любую другую, выбранную пользователем);
|
||||
- но `is_category_confirmed` у транзакции устанавливается в `FALSE`.
|
||||
- Если `requires_confirmation = FALSE` (по умолчанию):
|
||||
- правило присваивает категорию и `is_category_confirmed = TRUE`.
|
||||
|
||||
Результат:
|
||||
|
||||
- В истории операций такие транзакции визуально подсвечиваются
|
||||
как требующие внимания пользователя.
|
||||
- Транзакции, категоризированные правилами с `requires_confirmation = TRUE`,
|
||||
визуально подсвечиваются в истории как требующие внимания.
|
||||
- Пользователь может:
|
||||
- подтвердить категорию (в этом случае `is_category_confirmed` становится `TRUE`);
|
||||
- подтвердить категорию (`is_category_confirmed` становится `TRUE`);
|
||||
- изменить категорию и при желании создать новое правило.
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- Для маркетплейсов особенно полезно поле `comment`, куда пользователь может
|
||||
- Для таких транзакций (например, маркетплейсы) полезно поле `comment`, куда пользователь может
|
||||
записать, что конкретно было куплено (например, "корм для Арчи", "мебель", "одежда").
|
||||
|
||||
## Применение правил к прошлым транзакциям
|
||||
@@ -119,10 +124,13 @@ ALTER TABLE transactions
|
||||
|
||||
При активации этой опции backend:
|
||||
|
||||
- находит все транзакции, подходящие под данное правило;
|
||||
- находит все транзакции, подходящие под данное правило,
|
||||
у которых `category_id IS NULL` или `is_category_confirmed = FALSE`;
|
||||
- проставляет им `category_id`;
|
||||
- устанавливает `is_category_confirmed = FALSE` (категория считается предварительной).
|
||||
|
||||
Транзакции с `is_category_confirmed = TRUE` не затрагиваются — пользовательские подтверждения неприкосновенны.
|
||||
|
||||
Пользователь может затем:
|
||||
|
||||
- отфильтровать операции по `onlyUnconfirmed = true` через `/api/transactions`;
|
||||
|
||||
Reference in New Issue
Block a user