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

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