[2026-06-28] eagle: personal/projects/budget-app/bank-statement-import.md personal/projects/budget-app/index.md
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -473,29 +473,26 @@ 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 счетов), которые позже будут заменены реальными данными из банковской выписки (матчинг по сумме, дате, счёту).
|
||||
|
||||
### Пароль
|
||||
|
||||
@@ -534,6 +531,8 @@ UPDATE account SET type = 'credit' WHERE name ILIKE '%кредитка%';
|
||||
| Символ валюты вместо колонки (Transactions, Accounts, TaxRecords) | ✅ | Кит |
|
||||
| Исправлен тип кредиток (debit → credit) | ✅ | Кит |
|
||||
| Страница быстрого ввода наличных / обмена валюты (CashInput.vue) | ✅ | Кит |
|
||||
| is_placeholder в Transaction (placeholder для не-cash расходов) | ✅ | Кит |
|
||||
| CashInput: выбор по валюте, is_placeholder для debit/credit | ✅ | Кит |
|
||||
|
||||
|
||||
### Phase 2 — что сделано (подробно)
|
||||
|
||||
Reference in New Issue
Block a user