From bb4932a1a23160b73440b05d96d0ed04613046c8 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 28 Jun 2026 16:29:06 +0600 Subject: [PATCH 1/2] [2026-06-28] eagle: personal/projects/budget-app/index.md --- personal/projects/budget-app/index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/personal/projects/budget-app/index.md b/personal/projects/budget-app/index.md index a070dcbf..d28a8791 100644 --- a/personal/projects/budget-app/index.md +++ b/personal/projects/budget-app/index.md @@ -501,6 +501,18 @@ UPDATE account SET type = 'credit' WHERE name ILIKE '%кредитка%'; - `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,6 +532,7 @@ UPDATE account SET type = 'credit' WHERE name ILIKE '%кредитка%'; | Налоговый учёт | ✅ | Кит | | DateTime в транзакциях (date → DateTime, datetime-local на фронте, миграция) | ✅ | Кит | | Символ валюты вместо колонки (Transactions, Accounts, TaxRecords) | ✅ | Кит | +| Исправлен тип кредиток (debit → credit) | ✅ | Кит | | Страница быстрого ввода наличных / обмена валюты (CashInput.vue) | ✅ | Кит | From dce88bbf2d05990f5f7ee45560f564320c851557 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 28 Jun 2026 16:44:19 +0600 Subject: [PATCH 2/2] [2026-06-28] eagle: personal/projects/budget-app/bank-statement-import.md personal/projects/budget-app/index.md --- .../budget-app/bank-statement-import.md | 39 +++++++++++++++++++ personal/projects/budget-app/index.md | 39 +++++++++---------- 2 files changed, 58 insertions(+), 20 deletions(-) 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 d28a8791..2ce78890 100644 --- a/personal/projects/budget-app/index.md +++ b/personal/projects/budget-app/index.md @@ -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"`** — все `