Files
family_budget/pdf2json.md
Anton fccde4259d feat(analytics): account commission and investment transfers
Handle cashback commission imports, include commissions in analytics with separate investment metrics, and expose commission/version details in the UI.

Made-with: Cursor
2026-04-14 16:15:05 +03:00

13 KiB
Raw Permalink Blame History

Инструкция агента: конвертация банковской выписки ВТБ (PDF → JSON)

Цель

Преобразовать PDF-выписку банка ВТБ в JSON-файл строго по схеме 1.0.
На выходе — валидный JSON-файл без потерь операций, с правильными суммами в копейках и корректной проверкой баланса.


Схема JSON (schema 1.0)

{
  "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.pdffile-18.json
  • Кодировка: UTF-8
  • Формат: JSON с отступами (indent = 2)
  • ensure_ascii = false — кириллица записывается как есть, не экранируется

Пример корректной транзакции

{
  "operationAt": "2026-03-29T20:38:01+03:00",
  "amountSigned": -500000,
  "commission": 0,
  "description": "Оплата товаров и услуг. IP KOVTUN L.B.. по карте *9058"
}

Расшифровка: списание 5 000.00 ₽ = -500 000 копеек, комиссии нет.


Пример корректного поступления

{
  "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)

{
  "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, кириллица не экранирована