docs: refactor project docs and agents tasks

This commit is contained in:
vakabunga
2026-03-02 00:30:56 +03:00
parent 9551b93a09
commit 9d12702688
14 changed files with 1146 additions and 87 deletions

View File

@@ -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
View 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 | Семантическая ошибка валидации (некорректные данные, ошибка при вставке) |

View 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
View 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`.

View File

@@ -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)`.

View File

@@ -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.

View File

@@ -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`;