[2026-06-28] taiga-vault: personal/projects/budget-app/bank-statement-import.md personal/projects/budget-app/index.md

This commit is contained in:
Taiga
2026-06-28 10:50:08 +00:00
parent 0381fa730b
commit e65267cd2d
2 changed files with 71 additions and 20 deletions
@@ -153,6 +153,45 @@ scripts/bank_scraper/
**Статус:** ✅ base + Demir driver написаны, Playwright установлен. **НО — Demir требует QR-логин через мобильное приложение**, не логин/пароль на сайте.
## Match placeholder transactions
### Проблема
Ручной ввод расходов с debit/credit счетов через CashInput создаёт placeholder-транзакции (`is_placeholder=true`). У них есть категория и комментарий, но нет реального подтверждения из банка. Когда выписка импортируется, те же траты появляются как новые транзакции из банка — без категории и комментария. Нужно сопоставить плейсхолдеры с реальными транзакциями.
### Механика матчинга
При импорте CSV-выписки скрипт (будущий `csv_bank_import.py` или orchestrator) выполняет:
1. Для каждой строки из выписки найти плейсхолдеры в БД с теми же:
- `source_account_id` (тот же счёт)
- `is_placeholder = true`
- Сумма совпадает с точностью ±0.01
- Дата в пределах ±3 дней от даты выписки
2. Если найден ровно 1 плейсхолдер:
- `UPDATE transaction SET is_placeholder = false, import_id = '<import_id>', category_id = <placeholder.category_id>, comment = <placeholder.comment> WHERE id = <placeholder.id>`
- Реальная транзакция **не создаётся** — плейсхолдер становится постоянным с перенесёнными категорией и комментарием
3. Если найдено несколько плейсхолдеров:
- Создать реальную транзакцию как есть
- Пометить плейсхолдеры как "требуют ручного разрешения" (флаг в будущем через TG-бот)
4. Если не найден ни один:
- Создать обычную транзакцию (is_placeholder=false, категория не указана)
### SQL для матчинга (ориентир)
```sql
-- Найти плейсхолдер для строки выписки
SELECT id, category_id, comment
FROM transaction
WHERE user_id = $user_id
AND source_account_id = $source_account_id
AND is_placeholder = true
AND ABS(amount - $amount) < 0.01
AND ABS(EXTRACT(EPOCH FROM (date - $date))) < 259200 -- ±3 дня в секундах
ORDER BY ABS(amount - $amount), ABS(EXTRACT(EPOCH FROM (date - $date)))
LIMIT 1
```
## Реальность Demir IB
Сайт `93.171.215.109``apps.demirbank.kg/ib/`) — **Flutter web SPA** с QR-аутентификацией. Нет формы логина с паролем — нужно сканировать QR мобильным приложением Demir.
+32 -20
View File
@@ -473,34 +473,43 @@ UPDATE account SET type = 'credit' WHERE name ILIKE '%кредитка%';
Одна страница с двумя режимами, переключаемыми сегмент-кнопкой.
**Режим «Расход» (по умолчанию):**
- Выбор наличного счёта (type=cash) — чипами, последний сохраняется в `localStorage` под ключом `cash_last_account_id`
- Поле суммы (крупное, центрированное, автофокус)
- Чипы с топ-категориями наличных расходов (15 штук, по частоте из БД):
🛒 Grocery, 🍣 Рестораны, 🛠️ Быт/Техника, 🧱 Стройка, 🧸 Дети, 🏖️ Отдых, 💊 Медицина, 🚙 Авто, ⛽️ Транспорт/АЗС, 👰‍♀️ Жена, 👚 Одежда, 💷 Кредиты, 📟 Квартплата/Связь, 🏢 Аренда, Прочее
- Без кнопки «Другое» — все уникальные категории уже в списке, других нет в БД
- Опциональный комментарий
- POST /api/transactions (source_account → NULL)
- **Выбор валюты** — крупные кнопки с символами ($ ₽ € С̲ ₸). Последняя выбранная валюта сохраняется в localStorage (`cash_last_currency`).
- **Выбор счёта** — после выбора валюты показываются все счета в этой валюте (не только cash). При смене валюты — авто-выбирается первый наличный (type=cash) счёт. После добавления транзакции — сбрасывается обратно на первый наличный.
- Сумма (крупное поле)
- Чипы топ-категорий (15 штук)
- Комментарий (опционально)
- **is_placeholder**: если выбранный счёт type=cash → обычная транзакция (is_placeholder=false). Если debit/credit → is_placeholder=true (предварительная, будет заменена при импорте выписки).
**Режим «Обмен валюты»:**
- Селекты счёт-источник / счёт-получатель (фильтр: другая валюта)
- Сумма (отдаю), Курс, Получу — три поля с двусторонним пересчётом (через `_updating` флаг для защиты от циклов):
- меняешь сумму → `получу = сумма * курс`
- меняешь курс → `получу = сумма * курс`
- меняешь получу → `курс = получу / сумма`
- Комментарий (предзаполнен «Обмен валюты»)
- POST /api/transactions (source → dest с cross_rate)
- Выбор валюты-источника счёт-источник
- Выбор валюты-получателя → счёт-получатель
- Три поля с двусторонним пересчётом (сумма, курс, получу)
- Комментарий (предзаполнен)
**Технические детали и исправления, которые были сделаны:**
1. **Дата** — `new Date()` форматируется как `YYYY-MM-DDTHH:mm` (локальное время без TZ и миллисекунд). Через `localNow()` helper. Это критично: `new Date().toISOString()` даёт UTC c `Z`, что падает на бэкенде (`can't subtract offset-naive and offset-aware datetimes` — Pydantic парсит как offset-aware, а SQLAlchemy вставляет в `TIMESTAMP WITHOUT TIME ZONE`).
2. **Чипы — `type="button"`** — все `<button>` внутри `<form>` явно имеют `type="button"`, иначе клик по чипу триггерит `submit` формы из-за дефолтного `type="submit"`.
3. **Очистка error** — `error.value` обнуляется при выборе категории, счёта, переключении режима.
4. **Дубликаты категорий** — в БД 34 строки на 17 уникальных категорий (видимо импорт задвоил). `topCats` использует дедупликацию по id через `Set`.
5. **Хедер скрыт** — навигация не показывается на странице `/cash` (App.vue: `<header v-if="!hideHeader">`).
### Поле is_placeholder
Добавлено в модель Transaction:
- `is_placeholder: Boolean` (default false)
- Alembic migration: `add_is_placeholder_to_transaction`
- API: `POST /api/transactions` принимает `is_placeholder`, `GET /api/transactions` фильтрует по `is_placeholder`
- Назначение: помечать предварительные транзакции (ручной ввод расходов с debit/credit счетов), которые позже будут заменены реальными данными из банковской выписки (матчинг по сумме, дате, счёту).
### Пароль
- `admin123` — совпадает с `ADMIN_PASSWORD` в `.env`
### Типы счетов
| Тип | Описание |
|-----|----------|
| `cash` | Наличные (Нал KGS, USD, RUB, EUR, KZT, AED, UZS) |
| `debit` | Дебетовые карты и счета (Альфа, Сбер, Demir и т.д.) |
| `credit` | Кредитные карты (Альфа Кредитка, Сбер Кредитка, ВТБ Кредитка, Тинькофф Кредитка) |
| `deposit` | Депозиты (не используется) |
| `brokerage` | Брокерские счета (не используется) |
В Phase 1 кредитки были ошибочно импортированы как `debit` — исправлено на `credit` прямым SQL UPDATE.
## Phase 2 progress
| Шаг | Статус | Кем |
@@ -520,7 +529,10 @@ UPDATE account SET type = 'credit' WHERE name ILIKE '%кредитка%';
| Налоговый учёт | ✅ | Кит |
| DateTime в транзакциях (date → DateTime, datetime-local на фронте, миграция) | ✅ | Кит |
| Символ валюты вместо колонки (Transactions, Accounts, TaxRecords) | ✅ | Кит |
| Исправлен тип кредиток (debit → credit) | ✅ | Кит |
| Страница быстрого ввода наличных / обмена валюты (CashInput.vue) | ✅ | Кит |
| is_placeholder в Transaction (placeholder для не-cash расходов) | ✅ | Кит |
| CashInput: выбор по валюте, is_placeholder для debit/credit | ✅ | Кит |
### Phase 2 — что сделано (подробно)