Files
family_budget/docs/backlog/api_import.md
Anton 172246db0b fix: align docs and code with actual DB schema and format spec
- Rewrite db.md as canonical schema: add categories, sessions tables; add alias to accounts, is_category_confirmed/comment to transactions, FK references to categories(id); mark budgets as post-MVP

- Fix account masking to use fixed 6 asterisks (code + docs)

- Remove budgets from MVP requirements in agent_backend.md

- Add explicit 'not in MVP' note to analytics.md budgets section

- Fix test_Statement.json: convert amounts to kopecks (integers), remove fingerprint fields (computed by backend)

Made-with: Cursor
2026-03-02 11:34:00 +03:00

213 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Импорт выписки (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 символа остаются открытыми.
- Промежуточные символы заменяются фиксированным набором из 6 символов `******` (количество звёздочек не раскрывает длину номера).
- Пример: `40817810825104025611``408178******5611`.
Маскирование применяется в ответе этого эндпоинта и в `GET /api/accounts`.
## Коды ошибок
| Код | Ситуация |
|-----|--------------------------------------------------------------------------|
| 200 | Импорт выполнен успешно |
| 400 | Невалидный JSON или нарушение структуры схемы 1.0 |
| 401 | Нет действующей сессии |
| 422 | Семантическая ошибка валидации (некорректные данные, ошибка при вставке) |