Handle cashback commission imports, include commissions in analytics with separate investment metrics, and expose commission/version details in the UI. Made-with: Cursor
232 lines
13 KiB
Markdown
232 lines
13 KiB
Markdown
# Инструкция агента: конвертация банковской выписки ВТБ (PDF → JSON)
|
||
|
||
## Цель
|
||
|
||
Преобразовать PDF-выписку банка ВТБ в JSON-файл строго по схеме 1.0.
|
||
На выходе — валидный JSON-файл без потерь операций, с правильными суммами в копейках и корректной проверкой баланса.
|
||
|
||
---
|
||
|
||
## Схема JSON (schema 1.0)
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": "1.0",
|
||
"bank": "VTB",
|
||
"statement": {
|
||
"accountNumber": "string",
|
||
"currency": "RUB",
|
||
"openingBalance": integer,
|
||
"closingBalance": integer,
|
||
"exportedAt": "ISO 8601 string"
|
||
},
|
||
"transactions": [
|
||
{
|
||
"operationAt": "ISO 8601 string",
|
||
"amountSigned": integer,
|
||
"commission": integer,
|
||
"description": "string"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Шаг 1. Извлечение данных из PDF
|
||
|
||
### 1.1 Шапка выписки
|
||
|
||
Извлечь из заголовочного блока:
|
||
|
||
| Поле PDF | Поле JSON | Примечание |
|
||
|---------------------------|----------------------------------|-----------------------------------|
|
||
| Номер счёта | `statement.accountNumber` | строка, без пробелов |
|
||
| Баланс на начало периода | `statement.openingBalance` | перевести в копейки (× 100) |
|
||
| Баланс на конец периода | `statement.closingBalance` | перевести в копейки (× 100) |
|
||
| Поступления | только для валидации | не записывать в JSON |
|
||
| Расходные операции | только для валидации | не записывать в JSON |
|
||
| Период выписки | используется для валидации | формат ДД.ММ.ГГГГ – ДД.ММ.ГГГГ |
|
||
|
||
### 1.2 Поле `exportedAt`
|
||
|
||
Взять дату и время **первой строки таблицы операций** (самая поздняя по дате операции).
|
||
Формат: ISO 8601 с московским часовым поясом: `YYYY-MM-DDTHH:MM:SS+03:00`
|
||
|
||
---
|
||
|
||
## Шаг 2. Извлечение операций
|
||
|
||
### 2.1 Столбцы таблицы в PDF
|
||
|
||
Каждая строка операции содержит:
|
||
|
||
| Столбец PDF | Описание |
|
||
|-------------------------------------|------------------------------------------------|
|
||
| Дата и время операции | дата + время совершения операции |
|
||
| Дата обработки банком | дата, когда банк обработал операцию |
|
||
| Сумма операции в валюте операции | знаковая сумма (`-` = расход, без знака = приход) |
|
||
| Приход (в валюте счёта) | сумма, если поступление |
|
||
| Расход (в валюте счёта) | сумма, если списание |
|
||
| Комиссия | комиссия банка |
|
||
| Описание операции | текстовое описание |
|
||
|
||
### 2.2 Правила маппинга
|
||
|
||
**`operationAt`** — из столбца «Дата и время операции», формат ISO 8601 + московский TZ:
|
||
```
|
||
29.03.2026 20:38:01 → 2026-03-29T20:38:01+03:00
|
||
```
|
||
|
||
**`amountSigned`** — сумма в **копейках** (целое число):
|
||
- Если операция — **приход**: значение **положительное**
|
||
- Если операция — **расход**: значение **отрицательное**
|
||
- Допустим специальный кейс: `amountSigned = 0`, **только если** `commission > 0` и в `description` есть подстрока **«Зачисление»** (без учёта регистра)
|
||
- Определять знак по столбцу «Приход»/«Расход», а не по знаку в PDF (там `-` стоит у расходов, но надёжнее смотреть, в какой колонке стоит сумма)
|
||
- Перевод в копейки: умножить на 100 и округлить до целого (`round(amount * 100)`)
|
||
- Разделитель дробной части в PDF — точка или запятая — оба варианта обрабатывать
|
||
|
||
**`commission`** — комиссия в **копейках** (целое число):
|
||
- Если в PDF стоит `0.00` → записать `0`
|
||
- Если указана ненулевая комиссия → перевести в копейки аналогично `amountSigned`
|
||
- Комиссия всегда **неотрицательная**
|
||
|
||
**`description`** — полный текст описания операции, строка:
|
||
- Убрать лишние пробелы и переносы строк (привести к одной строке)
|
||
- Не обрезать и не сокращать
|
||
- Сохранить кириллицу и спецсимволы как есть
|
||
|
||
### 2.3 Порядок операций
|
||
|
||
Операции записывать **в том же порядке, что в PDF** (от новых к старым, сверху вниз).
|
||
|
||
---
|
||
|
||
## Шаг 3. Валидация перед записью
|
||
|
||
### 3.1 Проверка баланса
|
||
|
||
Вычислить:
|
||
```
|
||
income_sum = сумма всех amountSigned > 0 (в копейках)
|
||
expense_sum = сумма всех amountSigned < 0 (в копейках, отрицательная)
|
||
```
|
||
|
||
Проверить, что:
|
||
```
|
||
openingBalance + income_sum + expense_sum == closingBalance
|
||
```
|
||
|
||
**Если не совпадает** — это нормально и объясняется поведением ВТБ:
|
||
|
||
> ВТБ формирует итоги в шапке по **дате обработки банком**, а не по дате операции.
|
||
> Операции с датой обработки **вне периода выписки** включаются в список транзакций, но **не учитываются в строках «Поступления» и «Расходные операции»** в шапке.
|
||
|
||
Правильная проверка:
|
||
1. Определить границы периода из шапки PDF (`дата_начала`, `дата_конца`)
|
||
2. Из `Поступления` и `Расходные операции` вычислить ожидаемый net:
|
||
`expected_net = openingBalance_kopecks + income_pdf_kopecks - expense_pdf_kopecks`
|
||
3. Убедиться, что `expected_net == closingBalance` (это всегда верно если данные из PDF прочитаны правильно)
|
||
4. Разница между суммой всех транзакций и `(closingBalance - openingBalance)` — это сумма операций, обработанных банком вне периода. Это **не ошибка**.
|
||
|
||
### 3.2 Проверка полноты
|
||
|
||
- Подсчитать количество извлечённых транзакций
|
||
- Пройтись по всем страницам PDF и убедиться, что ни одна строка таблицы не пропущена
|
||
- Особое внимание: операции в конце страницы и в начале следующей (частые точки потери данных)
|
||
|
||
### 3.3 Проверка типов
|
||
|
||
Убедиться, что:
|
||
- `openingBalance`, `closingBalance` — целые числа (`int`)
|
||
- `amountSigned`, `commission` — целые числа (`int`)
|
||
- `operationAt`, `exportedAt` — строки в формате ISO 8601 с `+03:00`
|
||
- `description` — строка (не `null`, не пустая)
|
||
|
||
---
|
||
|
||
## Шаг 4. Частые ошибки — предотвращение
|
||
|
||
| Ошибка | Причина | Как избежать |
|
||
|--------|---------|--------------|
|
||
| Сумма в рублях вместо копеек | Забыли умножить на 100 | Всегда применять `round(x * 100)` |
|
||
| Дробные числа вместо int | Потеря точности float | Использовать `round()`, результат кастовать в `int` |
|
||
| Неверный знак `amountSigned` | Ориентировались на знак в PDF | Определять знак по столбцу «Приход»/«Расход» |
|
||
| Потеря операций на стыке страниц | Парсинг постраничный | Читать PDF целиком, не разбивать по страницам |
|
||
| Обрезанное описание | Перенос строки в ячейке PDF | Объединять строки одной ячейки в одну строку |
|
||
| Комиссия со знаком минус | Перепутали знак | Комиссия всегда `>= 0` |
|
||
| `amountSigned = 0` без условия | Импорт отклонит файл | Разрешать `amountSigned = 0` только при `commission > 0` и наличии «Зачисление» в `description` |
|
||
| Неверный `exportedAt` | Взяли дату начала периода | Брать дату и время **первой (самой новой) операции** в таблице |
|
||
| Расхождение баланса | Не учли операции вне периода | Это не ошибка — см. Шаг 3.1 |
|
||
| Лишние пробелы в описании | PDF добавляет пробелы при переносе | Применять `.strip()` и нормализацию пробелов |
|
||
|
||
---
|
||
|
||
## Шаг 5. Запись файла
|
||
|
||
- Имя файла: то же, что у исходного PDF, с заменой расширения `.pdf` → `.json`
|
||
Пример: `file-18.pdf` → `file-18.json`
|
||
- Кодировка: **UTF-8**
|
||
- Формат: JSON с отступами (indent = 2)
|
||
- `ensure_ascii = false` — кириллица записывается как есть, не экранируется
|
||
|
||
---
|
||
|
||
## Пример корректной транзакции
|
||
|
||
```json
|
||
{
|
||
"operationAt": "2026-03-29T20:38:01+03:00",
|
||
"amountSigned": -500000,
|
||
"commission": 0,
|
||
"description": "Оплата товаров и услуг. IP KOVTUN L.B.. по карте *9058"
|
||
}
|
||
```
|
||
|
||
Расшифровка: списание 5 000.00 ₽ = -500 000 копеек, комиссии нет.
|
||
|
||
---
|
||
|
||
## Пример корректного поступления
|
||
|
||
```json
|
||
{
|
||
"operationAt": "2026-03-24T15:09:41+03:00",
|
||
"amountSigned": 9537522,
|
||
"commission": 0,
|
||
"description": "Поступление заработной платы. 0726 НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ УНИВЕРСИТЕТ \"ВЫСША Поступление заработной платы/иных выплат Salary по реестру Z_0000005862_20260323_001_01 от 23.03.2026. Без НДС.."
|
||
}
|
||
```
|
||
|
||
Расшифровка: поступление 95 375.22 ₽ = +9 537 522 копейки.
|
||
|
||
---
|
||
|
||
## Пример корректного кэшбэка (сумма в commission)
|
||
|
||
```json
|
||
{
|
||
"operationAt": "2026-04-07T19:56:59+03:00",
|
||
"amountSigned": 0,
|
||
"commission": 592000,
|
||
"description": "Зачисление кешбэка по программе лояльности."
|
||
}
|
||
```
|
||
|
||
Расшифровка: для такого кейса `amountSigned = 0` допустим, потому что есть `commission > 0` и маркер «Зачисление» в описании.
|
||
|
||
---
|
||
|
||
## Итоговый чеклист перед отдачей файла
|
||
|
||
- [ ] `schemaVersion` = `"1.0"`
|
||
- [ ] `bank` = `"VTB"`
|
||
- [ ] `accountNumber` совпадает с PDF
|
||
- [ ] `openingBalance` и `closingBalance` в копейках, совпадают с PDF
|
||
- [ ] `exportedAt` = дата и время первой операции в таблице, формат ISO 8601 +03:00
|
||
- [ ] Количество транзакций совпадает с количеством строк в PDF
|
||
- [ ] Все `amountSigned` и `commission` — целые числа
|
||
- [ ] Если `amountSigned = 0`, то `commission > 0` и в `description` есть «Зачисление» (без учёта регистра)
|
||
- [ ] Поступления и баланс проверены (Шаг 3.1)
|
||
- [ ] Файл сохранён в UTF-8, кириллица не экранирована
|