docs: refactor project docs and agents tasks
This commit is contained in:
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 | Семантическая ошибка валидации (некорректные данные, ошибка при вставке) |
|
||||
Reference in New Issue
Block a user