# Инструкция агента: конвертация банковской выписки ВТБ (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, кириллица не экранирована