diff --git a/personal/projects/budget-app/bank-statement-import.md b/personal/projects/budget-app/bank-statement-import.md index 27bb37a2..ac55b08b 100644 --- a/personal/projects/budget-app/bank-statement-import.md +++ b/personal/projects/budget-app/bank-statement-import.md @@ -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 = '', category_id = , comment = WHERE 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. diff --git a/personal/projects/budget-app/index.md b/personal/projects/budget-app/index.md index a070dcbf..2ce78890 100644 --- a/personal/projects/budget-app/index.md +++ b/personal/projects/budget-app/index.md @@ -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"`** — все `