10 KiB
Импорт выписки (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.
{
"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— непустая строка.
Ответ при ошибке:
{
"error": "BAD_REQUEST",
"message": "Invalid schema: missing field 'statement.accountNumber'"
}
2) Семантическая валидация (422 Unprocessable Entity)
Проверяется логическая корректность данных. При ошибке — 422, весь файл откатывается.
Проверки:
statement.currencyсоответствует допустимому коду валюты (MVP:"RUB").operationAtу всех транзакций — валидная дата (парсится без ошибок).- Отсутствуют дубликаты fingerprint внутри одного файла.
Ответ при ошибке:
{
"error": "VALIDATION_ERROR",
"message": "Duplicate fingerprint found within file at transaction index 5"
}
Логика обработки
Счёт (account)
- По комбинации
bank+statement.accountNumberвыполняется поиск в таблицеaccounts. - Если счёт найден — используется его
id. - Если счёт не найден — создаётся новая запись:
bank= значение из JSON;account_number=statement.accountNumber;currency=statement.currency;alias=NULL.
- Флаг
isNewAccountв ответе отражает, был ли счёт создан при этом импорте.
Fingerprint
Для каждой транзакции вычисляется SHA-256 от полей, соединённых разделителем |:
accountNumber|operationAt|amountSigned|commission|normalizedDescription
normalizedDescription—descriptionпослеtrim.- Суммы подставляются в том виде, в котором пришли в JSON (числовое представление).
- Разделитель
|исключает коллизии при склейке полей разной длины.
Результат хэширования хранится в поле transactions.fingerprint с префиксом sha256:.
Direction
Направление операции определяется по следующим правилам:
- Проверка ключевых фраз в
description(регистронезависимо):- Если содержит одну из фраз:
"Перевод между своими счетами","перевод средств на счет","Внутри ВТБ"—direction = "transfer".
- Если содержит одну из фраз:
- Если ключевые фразы не сработали:
amountSigned > 0→direction = "income";amountSigned < 0→direction = "expense".
Список ключевых фраз для "transfer" может расширяться; в MVP используется фиксированный набор.
Импорт транзакций
Для каждой транзакции из массива transactions:
- Суммы
amountSignedиcommissionзаписываются как есть — в JSON 1.0 они уже в копейках (BIGINT). - Вычисляется
fingerprint. - Определяется
direction. - Выполняется вставка в таблицу
transactionsсо значениями:account_id— ID найденного/созданного счёта;operation_at— изoperationAt;amount_signed— в копейках;commission— в копейках;description— как есть из JSON;direction— вычисленное значение;fingerprint— вычисленный хэш;category_id = NULL;is_category_confirmed = FALSE.
- При срабатывании уникального ограничения
(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)
{
"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 | Семантическая ошибка валидации (некорректные данные, ошибка при вставке) |