Files

603 lines
43 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.
# Budget App
Персональный бюджет в виде веб-приложения. Замена Excel-таблицы `~/Downloads/Budget.xlsx`, в которой ведутся транзакции 2020–2024+ по нескольким счетам в разных валютах.
**Solo на старте** (только Alex), но код пишем generic — с прицелом на public SaaS позже (multi-tenant, конфиги отделены).
## Источник: текущая Excel-таблица
Файл: `~/Downloads/Budget.xlsx` (3.6 MB, 13 листов).
### Листы
| Лист | Назначение |
|------|------------|
| `транзакции` | **Главное хранилище**. ~34 462 строки, 28 колонок (A–AB). Все дебеты/кредиты по всем счетам подряд. |
| `курсы` | Справочник курсов: `Currency`, `Rate`, `Date`, computed-курс к выбранной валюте отчёта. До 9 467 строк (история по датам). |
| `категории` | Иерархия категорий: подкатегория → родитель с эмодзи (например `Аренда``🏢 Аренда`). |
| `счета` | Справочник счетов: имя, валюта, активный (bool), льготный период, месячный платёж, курс к выбранной, `name in form`. |
| `2020 отчет``2024 отчет` | Годовые сводки: план/факт по доходам, расходам, инвестициям, разнице, накопленному капиталу. |
| `налоги 22-24` | Учёт ИП-патентов, НДФЛ и пр. |
| `сводная` | Сводная таблица по годам. |
| `Наличные (форма)` | Hidden — форма быстрого ввода. |
| `test` | Песочница. |
### Схема листа «транзакции»
Колонки (расшифрованы из shared strings + формул):
| Кол | Имя | Тип | Описание |
|-----|-----|-----|----------|
| A | дата | date | Дата операции. |
| B | сумма | number | Сумма в валюте счёта-источника. |
| C | дебет | ref→счета | Счёт, **с которого** ушло (источник). Пусто = доход извне. |
| D | кредит | ref→счета | Счёт, **на который** пришло (получатель). Пусто = расход вовне. |
| E | категория | ref→категории | Подкатегория (см. лист `категории`). |
| F | комментарий | text | Свободный комментарий. |
| G | курс | number | Кросс-курс дебет→кредит, если перевод между валютами. |
| H | входящ деб | number | Остаток на дебете до операции. |
| I | остаток деб | number | Остаток на дебете после. |
| J | входящ кред | formula | `=IF(D<>"", B*IF(C<>"", G, 1), "")` — сумма в валюте кредита. |
| K | остаток кред | number | Остаток на кредите до. |
| L | остаток кред | formula | `=IF(D<>"", K+J, "")` — остаток на кредите после. |
| M | вал. счёта | formula | `VLOOKUP(C, счета, 2)` — валюта дебета. |
| N | деб: курс к тек | number | Курс валюты дебета к выбранной валюте отчёта. |
| O | сумма в тек вал | number | Сумма в валюте отчёта (по курсу дебета). |
| P | — | — | Скрытая. |
| Q | крд: курс к тек | number | Курс валюты кредита к выбранной валюте отчёта. |
| R | сумма в тек | number | Сумма в валюте отчёта (по курсу кредита). |
| S | Налог | number | Сумма налога по этой операции. |
| T | Курс | number | Курс на дату операции. |
| U–AB | — | — | Резерв / служебные. |
**Семантика двойной записи:** одна строка = одно перемещение средств. Если `C` (дебет) пуст — это вход денег извне (доход). Если `D` (кредит) пуст — это выход вовне (расход). Если оба заполнены — внутренний перевод (между своими счетами, возможно с конверсией валют через `G`).
### Категории (примеры родительских групп)
`💰 Salary`, `💸 Комиссии`, `📟 Квартплата/Связь`, `🧸 Дети`, `🏢 Аренда`, `⛽️ Транспорт/АЗС`, `💊 Медицина`, `🛒 Grocery`, `🍣 Рестораны`, `🧱 Стройка`, `💷 Кредиты`, `🏖️ Отдых`, `💸 Налоги`, `🛠️ Быт/Техника`, `🚙 Авто`, `👚 Одежда`, `💰 Дивиденды`, `💰 Депозиты`.
Подкатегории — конкретные траты (`Кафе и рестораны`, `АЗС`, `Кровля материалы`, `Salary 88 05.11`, ...).
### Счета (примеры)
`Demir KGS` (Сом), `Нал USD` (Доллар), `Альфа` (Рубль), `Нал RUB` (Рубль), `Альфа Кредитка`, `Сбер`, `Demir ИП USD`, `Нал EUR` (Евро), и т.д.
Валюты в обращении: **RUB, USD, EUR, KGS, KZT, UZS, AED, CNY**.
## Что таблица умеет уже сейчас
1. **Учёт транзакций** в любой валюте, с автопересчётом в выбранную валюту отчёта.
2. **Остатки по счетам** в реальном времени (через `остаток деб` / `остаток кред`).
3. **Категоризация** в иерархии (родитель/подкатегория) с эмодзи.
4. **Годовые отчёты**: план vs факт, доход/расход/инвестиции, % отложено, накопленный капитал, долг.
5. **Курсы валют с историей** (за каждую дату — свой курс).
6. **Налоговый учёт** (отдельный лист `налоги 22-24`).
7. **Сводная** — динамика по годам.
## Болевые точки таблицы
- Excel на 34k+ строк уже тормозит на пересчёте формул (`VLOOKUP` на массивы, `IF`-цепочки в каждой строке).
- Ввод транзакций — ручной, через скрытую форму «Наличные», нет автоимпорта банковских выписок.
- Категоризация — ручная.
- Невозможно нормально работать с мобильного.
- Анализ ограничен сводными в Excel: нет нормальных графиков по подкатегориям, кросс-фильтрации, прогнозов.
- Нет ничего про Freedom Gap / инвестиции: нет трекинга портфеля, нормы сбережений, проекции "когда аренда + проекты покроют расходы".
- История курсов хранится в строках листа `курсы` — это не нормализованная таблица.
- Нет API: нельзя интегрировать с банковскими экспортами, нельзя автоматически тянуть в дашборд.
## Концепция веб-приложения
### Цель
Заменить Excel приложением, которое:
1. Принимает все те же транзакции (multi-currency, multi-account, double-entry).
2. Считает аналитику и сводки **на лету** через SQL, а не через `VLOOKUP`.
3. Импортирует банковские выписки (Альфа, Сбер, Demir, …) автоматически + AI-резолвер для неоднозначных случаев.
4. Даёт мобильный UI для быстрого ввода трат на ходу.
5. **Freedom Gap** — разница между расходами и пассивным/полупассивным доходом (аренда, дивиденды, проекты). График прогресса к нулевому gap. What-if сценарии: "если проект даёт X, аренда Y — через N лет свобода".
6. Прогнозирует траты на N месяцев вперёд по сезонной модели (категории `🧱 Стройка`, `🏖️ Отдых` цикличны).
### Доменная модель (первая итерация)
```
User (id, email, ...) -- для будущего multi-tenant SaaS
Settings (user_id, base_currency_id, ...) -- base currency выбирается в настройках
Currency (code, name, symbol)
Account (id, user_id, name, currency_id, type {cash, debit, credit, deposit, brokerage}, active, credit_limit, grace_period_days, monthly_payment)
CategoryGroup (id, user_id, name, emoji) -- родитель
Category (id, user_id, name, group_id) -- подкатегория
ExchangeRate (currency_id, date, rate_to_base, source {nbkr, manual, ...}) -- история по датам
Transaction (
id, user_id, date, amount,
source_account_id NULL, -- дебет (откуда); NULL = доход извне
dest_account_id NULL, -- кредит (куда); NULL = расход вовне
cross_rate NULL, -- курс при internal transfer
category_id, comment, tax_amount NULL,
import_id NULL -- ссылка на BankImport
)
TaxRecord (year, month, type {patent, ндфл, …}, amount, currency_id, paid_date NULL)
BankImport (id, user_id, account_id, source {alfa, sber, demir, ...}, fetched_at, raw_blob, status {pending, parsed, resolved, manual})
Alert (id, user_id, type {manual_resolve_needed, parse_failed, ...}, payload, sent_at, ack_at NULL) -- TG-уведомления
FireGoal (id, user_id, target_amount, target_currency_id, expected_return_rate, target_date NULL)
InvestmentSnapshot (date, account_id, value, currency_id)
```
Base currency не хардкодится — выбирается в `Settings`, и все пересчёты (остатки, отчёты, графики) автоматически приводятся к ней по `ExchangeRate.rate_to_base` на дату.
### Архитектура
- **Backend**: Python + FastAPI + SQLAlchemy + Postgres.
- **Frontend**: Vue 3 + Vite + Pinia. Адаптив для мобильного (PWA).
- **БД**: отдельная БД `budget_app` в существующем кластере Postgres на Eagle (тот же инстанс, что и `personal_os` — не отдельный контейнер).
- **Деплой**: Docker Compose на Mac (Eagle). Конфиги в `~/docker/budget-app/` (по конвенции остальных сервисов). Код проекта — `~/Developer/budget-app/` с git.
- **Внешний доступ**: `budget.qentra.top` через Cloudflare Tunnel (или существующий nginx-reverse-proxy). Auth — login/password + долгоживущий JWT-токен в localStorage/cookie. Один пользователь на старте, но схема готова под multi-tenant.
- **Импорт Excel**: однократный скрипт миграции (`scripts/import-xlsx.py`), читает `Budget.xlsx`, заливает в БД.
- **Курсы валют**: cron-джоб тянет курсы с НБ Кыргызстана (`nbkr.kg` XML feed) по расписанию (раз в день). Если на какую-то дату курса нет — автоматически дозаполняет (back-fill ближайшего рабочего дня). Источник по умолчанию для KGS-базы; для других валют — ручной override или дополнительные источники (ЦБ РФ, ECB).
- **Импорт банков**: автоматический fetch выписок (где есть API/HTML-парсинг) + парсеры CSV/XLS на Python (Альфа, Сбер, Demir — у каждого свой формат). После парсинга — прогон через AI-агента (LLM) для резолва неоднозначных строк: категоризация, merge дубликатов, идентификация контрагентов.
- **Telegram-бот для алертов**: оповещение, когда нужно ручное вмешательство (AI не смог однозначно категоризировать, парсер сломался на новом формате выписки, обнаружен дубль). Подтверждение/правка через инлайн-кнопки в TG → апдейт в БД.
- **HTTP MCP-интеграция**: приложение экспонирует MCP-сервер (HTTP transport), чтобы Eagle/Hermes мог запрашивать сводки, добавлять транзакции голосом, дёргать прогнозы из чата.
- **Аналитика**: SQL-агрегаты + Apache ECharts на фронте.
- **FIRE / Инвестпортфели**: отдельная фаза после того, как бюджетирование готово. Не в MVP.
### Этапы
1. **Phase 0 — Discovery & schema**. Полностью разобрать Excel, утвердить доменную модель. *(в процессе — этот док)*
2. **Phase 1 — Skeleton + import**. Создать `~/Developer/budget-app/` (git init), FastAPI + Vue в Docker Compose в `~/docker/budget-app/` с подключением к существующему Postgres-кластеру на хосте (новая БД `budget_app`). Миграция xlsx → БД. Read-only viewer транзакций + остатков + годовые отчёты. Внешний доступ через `budget.qentra.top` с auth (FastAPI Users + JWT).
3. **Phase 2 — Полноценный Viewer + CRUD**. Довести до уровня Excel по функциональности:
- Валюта у счетов (символ)
- Категории с иерархией (группа → подкатегория)
- Свёртка по годам/месяцам (drill-down как в Excel)
- Dynamic scrolling (infinite scroll вместо кнопок пагинации)
- CRUD транзакций: добавление, редактирование, удаление
- CRUD категорий
- CRUD счетов
- Ручная правка курсов
- Дашборды: расход по категориям, динамика, бёрндаун по бюджету
- Прогнозы (сезонная модель по категориям)
- Multi-currency: отображение балансов в валюте счёта + в base currency
- Сводная таблица по годам (как лист `сводная` в Excel)
- Налоговый учёт (как лист `налоги 22-24` в Excel)
4. **Phase 3 — Импорт банков + AI-резолвер**. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений.
5. **Phase 4 — Mobile / PWA**. Быстрый ввод с телефона.
6. **Phase 5 — MCP HTTP**. Экспонировать MCP-эндпоинт для Hermes.
7. **Phase 6 — Freedom / Инвестиции**. Замена классического FIRE на Freedom Gap — разница между расходами и пассивным доходом.
- P0: Savings Rate Dashboard (норма сбережений из транзакций)
- P0: Multi-Currency Net Worth (общий капитал USD/KGS)
- P1: Investment Snapshots (ручной ввод раз в месяц, таблица + график)
- P1: Freedom Gap Dashboard (доход-расход-аренда-проект = gap)
- P1: Passive Income Tracker (аренда + дивиденды + депозиты)
- P2: Freedom Goal with Projection Engine (what-if сценарии)
- P3: What-if Simulator (слайдеры: курс, аренда, проект, норма сбережений)
- Новые таблицы: `investment_snapshot`, `freedom_goal`
- Новые страницы: `/freedom`, `/finances`
- *Не делать:* Monte Carlo, Withdrawal Strategy Planner, Roth Conversion Ladder — не применимы
### Multi-tenant readiness (для будущего public SaaS)
Даже в solo-режиме на старте:
- Все таблицы доменной модели имеют `user_id` (на solo — захардкожен на одного юзера).
- Конфиги (домен, секреты, базовая валюта) — в `.env` / config-файле, не в коде.
- AI-промпты, парсеры банков, источники курсов — pluggable, через интерфейсы/registry.
- Никаких личных данных Alex в коде (имена счетов, категорий, комментарии) — всё через миграцию данных в БД.
- Лицензия — выбрать перед публичным релизом (AGPL/MIT/коммерческая).
## Открытые вопросы
Phase 0 закрыт — ключевые решения зафиксированы выше (auth, БД, raw blobs, импорт, multi-tenant readiness). Вопросы Phase 4 (AI-провайдер для резолвера, fallback на локальную LLM) — отложены до старта той фазы.
## Phase 1 — Skeleton + import (детальный план)
### Цель фазы
К концу Phase 1:
- FastAPI + Vue запущены в Docker Compose на Eagle.
- Все ~34 462 транзакции из `Budget.xlsx` залиты в БД `budget_app` (в существующем кластере Postgres на хосте).
- По адресу `https://budget.qentra.top` (Cloudflare Tunnel) — login → read-only список транзакций, остатки по счетам, годовые отчёты.
- Auth: FastAPI Users + JWT, один пользователь (Alex), долгоживущий токен (90 дней).
- Никакого CRUD пока — только просмотр. CRUD это Phase 3.
### Структура репозитория `~/Developer/budget-app/`
```
budget-app/
├── .env.example # шаблон конфига
├── .gitignore
├── README.md
├── docker-compose.yml # симлинк → ~/docker/budget-app/
├── backend/
│ ├── pyproject.toml # uv + ruff
│ ├── alembic.ini
│ ├── alembic/versions/ # миграции
│ ├── src/budget/
│ │ ├── __init__.py
│ │ ├── main.py # FastAPI app entry
│ │ ├── config.py # pydantic-settings из .env
│ │ ├── db.py # SQLAlchemy async engine
│ │ ├── auth/ # FastAPI Users
│ │ │ ├── models.py
│ │ │ ├── schemas.py
│ │ │ └── routes.py
│ │ ├── domain/ # SQLAlchemy модели
│ │ │ ├── user.py
│ │ │ ├── settings.py
│ │ │ ├── currency.py
│ │ │ ├── account.py
│ │ │ ├── category.py
│ │ │ ├── exchange_rate.py
│ │ │ └── transaction.py
│ │ ├── api/ # read-only роуты
│ │ │ ├── transactions.py
│ │ │ ├── accounts.py
│ │ │ └── reports.py
│ │ └── importers/
│ │ └── xlsx_import.py
│ └── tests/ # pytest
├── frontend/
│ ├── package.json # pnpm + Vue 3 + Vite + Pinia
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── index.html
│ └── src/
│ ├── main.ts
│ ├── App.vue
│ ├── router.ts
│ ├── stores/auth.ts
│ ├── api/client.ts # axios + JWT interceptor
│ ├── pages/
│ │ ├── Login.vue
│ │ ├── Transactions.vue
│ │ ├── Accounts.vue
│ │ └── Reports.vue
│ └── components/
└── scripts/
├── import-xlsx.py # one-shot миграция
└── create-admin.sh # bootstrap первого юзера
```
### Docker Compose (`~/docker/budget-app/docker-compose.yml`)
Три сервиса:
- `backend` — FastAPI на uvicorn, порт 8400 (внутренний). Подключается к Postgres хоста через `host.docker.internal:5432`.
- `frontend` — nginx со статикой собранного Vite-билда, порт 8401. Проксирует `/api/*` на `backend:8400`.
- `tunnel``cloudflare/cloudflared`, exposes `budget.qentra.top``frontend:80`.
Volumes:
- `./data/imports/``/data/imports/` (для будущих банковских raw blobs).
- `~/Downloads/Budget.xlsx` (read-only) → `/data/import/Budget.xlsx` (для миграции).
### .env (через `.env.example` в репо)
```
# DB — подключение к существующему кластеру Postgres на хосте
DB_HOST=host.docker.internal
DB_PORT=5432
DB_NAME=budget_app
DB_USER=budget_app
DB_PASSWORD=...
# Auth
JWT_SECRET=... # openssl rand -hex 32
JWT_LIFETIME_SECONDS=7776000 # 90 дней (долгоживущий)
ADMIN_EMAIL=alex@...
ADMIN_PASSWORD=... # для первого юзера через create-admin.sh
# Settings
BASE_CURRENCY=KGS # дефолт, меняется в UI
# Cloudflare Tunnel
CLOUDFLARE_TUNNEL_TOKEN=...
```
### Доменные модели для Phase 1 (минимум, чтобы залить xlsx и показать read-only)
| Таблица | Поля для MVP |
|---------|--------------|
| `user` | id, email, hashed_password, is_active, is_superuser (FastAPI Users) |
| `settings` | id, user_id, base_currency_code |
| `currency` | code (PK), name, symbol |
| `account` | id, user_id, name, currency_code, type, active, credit_limit?, grace_period_days?, monthly_payment? |
| `category_group` | id, user_id, name, emoji |
| `category` | id, user_id, name, group_id |
| `exchange_rate` | currency_code, date, rate_to_base, source — PK по (currency_code, date, source) |
| `transaction` | id, user_id, date, amount, source_account_id?, dest_account_id?, cross_rate?, category_id?, comment, tax_amount?, import_id? (NULL для xlsx) |
Опускаем в Phase 1: `tax_record`, `bank_import`, `alert`, `fire_goal`, `investment_snapshot` — приедут в следующих фазах.
### Шаги Phase 1 (порядок исполнения)
1. **Init репо**: `~/Developer/budget-app/` + git init, `.gitignore` (python/node/docker/.env), README со ссылкой на этот док в vault.
2. **Backend skeleton**: `uv init`, FastAPI app, конфиг через pydantic-settings, healthcheck `/api/health`.
3. **Postgres bootstrap** на Eagle: создать БД и юзера в существующем кластере —
```sql
CREATE DATABASE budget_app;
CREATE USER budget_app WITH PASSWORD '...';
GRANT ALL ON DATABASE budget_app TO budget_app;
```
Проверить, что кластер слушает `host.docker.internal` (правка `postgresql.conf` `listen_addresses` + `pg_hba.conf` если нужно).
4. **Alembic init + initial миграция**: SQLAlchemy модели → таблицы.
5. **FastAPI Users**: подключить, роуты `/api/auth/jwt/login`, `/api/auth/register` (после bootstrap закрываем), `/api/users/me`.
6. **Bootstrap первого юзера**: `scripts/create-admin.sh` создаёт Alex из `.env`.
7. **Импортёр xlsx** (`scripts/import-xlsx.py`):
- Читает `~/Downloads/Budget.xlsx` через `openpyxl` (fallback на `lxml` если timeout'ы).
- Парсит листы: `курсы` → `exchange_rate`, `категории` → `category_group`/`category`, `счета` → `account`, `транзакции` → `transaction`.
- Все записи привязываются к `user_id` Alex.
- Идемпотентно: повторный запуск с тем же файлом не дублирует (hash по `date+amount+source+dest+comment`).
8. **Read-only API**:
- `GET /api/transactions?from=&to=&category=&account=&page=` — пагинация.
- `GET /api/accounts` — счета с балансами (в валюте счёта + в base currency).
- `GET /api/reports/yearly?year=` — годовая сводка (план/факт по группам).
9. **Frontend skeleton**: `pnpm create vite`, Vue 3 + TS + Pinia, layout (header/sidebar/main), Login страница, JWT в localStorage.
10. **Read-only страницы**:
- **Transactions**: таблица с пагинацией, фильтры по дате/категории/счёту, поиск по комментарию.
- **Accounts**: список с текущими балансами в валюте счёта + в base currency.
- **Reports**: годовые сводки 2020–2024 по группам категорий, % сбережений.
11. **Docker Compose**: написать в `~/docker/budget-app/`, билдить backend/frontend, прокинуть `host.docker.internal` для Postgres.
12. **Cloudflare Tunnel**: создать DNS-запись `budget.qentra.top` → tunnel ID, прописать ingress в cloudflared конфиге.
13. **Smoke-тест**:
- `https://budget.qentra.top` → login → видишь все ~34k транзакций.
- `GET /api/accounts` возвращает все счета с балансами, совпадающими с Excel (±погрешность округления).
- `GET /api/reports/yearly?year=2024` совпадает с листом `2024 отчет`.
### Зависимости backend
- `fastapi`, `uvicorn[standard]`, `pydantic-settings`
- `sqlalchemy[asyncio]`, `asyncpg`, `alembic`
- `fastapi-users[sqlalchemy]`
- `openpyxl` (xlsx-импорт)
- `httpx` (заготовка для cron курсов в Phase 2)
- `python-multipart`, `passlib[bcrypt]`
- dev: `pytest`, `pytest-asyncio`, `ruff`
### Зависимости frontend
- `vue`, `vue-router`, `pinia`
- `axios` (JWT interceptor)
- `@vueuse/core`
- `echarts` (скаффолд под Phase 5, не используем активно)
- dev: `vite`, `typescript`, `eslint`, `prettier`
### Правила работы
1. **Тесты — обязательны** для каждого нового API-роута или изменения. Если код не покрыт тестом — он не готов.
2. **Обновление доку** — после каждой завершённой задачи обновлять таблицу прогресса и Acceptance criteria в этом доке.
3. **Комит** — после каждой логически завершённой задачи (не раз в 10 шагов).
### Acceptance criteria Phase 1
- [ ] `https://budget.qentra.top` открывается, login работает.
- [ ] Все ~34 462 строки транзакций видны в UI с пагинацией.
- [ ] Балансы счетов совпадают с Excel (по последней транзакции каждого счёта).
- [ ] Годовые отчёты 2020–2024 совпадают с Excel-листами.
- [ ] Код в `~/Developer/budget-app/`, БД `budget_app` в существующем Postgres, Compose в `~/docker/budget-app/`.
- [ ] Никаких хардкод-значений (имена счетов, категорий, комментарии) в коде — всё через xlsx-миграцию.
### Риски и нюансы
- **openpyxl на 34k строк**: может быть медленно (>30 сек). Acceptable — миграция одноразовая. Fallback: прямой парс XML через `lxml`.
- **Cloudflare Tunnel**: первый раз нужен ручной шаг в Cloudflare dashboard (создать tunnel, получить токен).
- **Долгоживущий JWT (90 дней)**: обычно не рекомендуется, но это явное требование. Документируем в README.
- **`host.docker.internal` + Postgres**: на macOS Postgres из brew обычно слушает только `localhost`. Нужно проверить и поправить `listen_addresses = '*'` в `postgresql.conf` + добавить запись в `pg_hba.conf` для подсети Docker (`172.17.0.0/16` / `172.18.0.0/16`).
- **Сетка Docker → DDG VPN**: budget-app не нуждается в доступе к DDG-сетям, обычный bridge ок.
## Следующий шаг
Старт Phase 1 — шаг 1 (init репо `~/Developer/budget-app/`).
---
**Создано:** 2026-06-21
**Статус:** Phase 1 — в работе
## Phase 1 progress
| Шаг | Статус | Кем |
| -------------------------------------- | ------ | ---- |
| Init репо | ✅ | Орёл |
| Backend skeleton | ✅ | Орёл |
| Postgres bootstrap — роль + БД | ✅ | Кит |
| Alembic initial migration | ✅ | Кит |
| FastAPI Users + auth routes | ✅ | Кит |
| Bootstrap первого юзера | ✅ | Кит |
| XLSX импортёр (6 045 транзакций) | ✅ | Кит |
| Read-only API (12 тестов) | ✅ | Кит |
| Frontend skeleton + read-only страницы | ✅ | Кит |
| Docker Compose (2 сервиса, работает) | ✅ | Кит |
| Cloudflare Tunnel budget.qentra.top | ✅ | Alex |
| Сверка данных | ✅ | Кит |
| | | |
## Disaster recovery: CASCADE data loss
**Сценарий:** Удалён пользователь (A-click → user delete). Из-за `ON DELETE CASCADE` на `transaction_user_id_fkey` все транзакции этого пользователя удалены мгновенно (6 045 строк). Балансы обнулены.
### Recovery шаги (на будущее)
```bash
# 1. Пересоздать пользователя с тем же email (admin123)
curl -XPOST .../api/auth/register -H... -d'{"email":"alex@qentra.top","password":"admin123"}'
# 2. Переимпортировать транзакции из xlsx
cd ~/Developer/budget-app
uv run python src/budget/importers/__init__.py
# 3. Проверить балансы — все 18 счетов должны совпасть с excel-balances.md
# 4. Пересобрать и передеплоить backend (дата формат) + frontend (любые изменения)
docker-compose build backend && docker-compose up -d backend
cd frontend && npm run build && cd .. && docker-compose build --no-cache frontend && docker-compose up -d frontend
```
### Формат даты на фронте (актуальный)
API возвращает `t.date.isoformat()` → `2026-05-29T17:00:00`.
Фронт режет: `{{ t.date.slice(0, 10) }} {{ t.date.slice(11, 16) }}` → `2026-05-29 17:00`.
Если время = `00:00` — в Excel не было времени для этой транзакции. Это корректно.
### Символы валют вместо колонки
Колонка "Валюта" убрана из таблиц Transactions, Accounts, TaxRecords.
Вместо неё символ валюты показывается непосредственно перед суммой (Transactions, TaxRecords) или в ячейке (Accounts).
Маппинг на фронте (постоянный, не из БД):
| Код | Символ | Валюта |
|-----|--------|--------|
| USD | $ | Доллар |
| EUR | € | Евро |
| RUB | ₽ | Рубль |
| KGS | С̲ | Сом (с с нижней чертой) |
| KZT | ₸ | Тенге (уже есть в Unicode) |
Файлы: `frontend/src/pages/Transactions.vue`, `Accounts.vue`, `TaxRecords.vue` — каждая содержит `CURRENCY_SYMBOLS` маппинг и функцию `getCurrencySymbol`/`currencySymbol`.
### БД: исправление типа кредиток
Четыре кредитных счета были ошибочно импортированы как `type='debit'` вместо `type='credit'`. Исправлено вручную:
```sql
UPDATE account SET type = 'credit' WHERE name ILIKE '%кредитка%';
```
Затронуты: Альфа Кредитка, Сбер Кредитка, ВТБ Кредитка, Тинькофф Кредитка.
Модель `AccountType` поддерживает `credit` с самого начала — это была неточность импорта из Excel.
### Страница быстрого ввода наличных — CashInput (2026-06-23)
**Файл:** `frontend/src/pages/CashInput.vue`
**Маршрут:** `/cash`
**Хедер:** скрыт на этой странице (App.vue проверяет `route.name === 'CashInput'`)
Одна страница с двумя режимами, переключаемыми сегмент-кнопкой.
**Режим «Расход» (по умолчанию):**
- **Выбор валюты** — крупные кнопки с символами ($ ₽ € С̲ ₸). Последняя выбранная валюта сохраняется в localStorage (`cash_last_currency`).
- **Выбор счёта** — после выбора валюты показываются все счета в этой валюте (не только cash). При смене валюты — авто-выбирается первый наличный (type=cash) счёт. После добавления транзакции — сбрасывается обратно на первый наличный.
- Сумма (крупное поле)
- Чипы топ-категорий (15 штук)
- Комментарий (опционально)
- **is_placeholder**: если выбранный счёт type=cash → обычная транзакция (is_placeholder=false). Если debit/credit → is_placeholder=true (предварительная, будет заменена при импорте выписки).
**Режим «Обмен валюты»:**
- Выбор валюты-источника → счёт-источник
- Выбор валюты-получателя → счёт-получатель
- Три поля с двусторонним пересчётом (сумма, курс, получу)
- Комментарий (предзаполнен)
### Поле 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
| Шаг | Статус | Кем |
| ---------------------------------------------------------------------------- | ------ | -------- |
| Валюта у счетов (символ) | ✅ | Кит |
| Категории с иерархией (API + фронт) | ✅ | Кит |
| Фронт: формы CRUD (транзакции, категории, счета) | ✅ | Кит |
| Свёртка по годам/месяцам (drill-down) | ✅ | Кит |
| Dynamic scrolling (infinite scroll) | ✅ | Кит |
| CRUD транзакций (API + тесты) | ✅ | Кит |
| CRUD категорий (API + тесты) | ✅ | Кит |
| CRUD счетов (API + тесты) | ✅ | Кит |
| Ручная правка курсов + API | ✅ | Кит |
| Дашборды (расход по категориям, динамика, сводная) | ✅ | Кит |
| Multi-currency отображение (баланс в валюте счёта + base currency) | ✅ | Кит |
| Сводная по годам | ✅ | Кит |
| Налоговый учёт | ✅ | Кит |
| DateTime в транзакциях (date → DateTime, datetime-local на фронте, миграция) | ✅ | Кит |
| Символ валюты вместо колонки (Transactions, Accounts, TaxRecords) | ✅ | Кит |
| Исправлен тип кредиток (debit → credit) | ✅ | Кит |
| Страница быстрого ввода наличных / обмена валюты (CashInput.vue) | ✅ | Кит |
| is_placeholder в Transaction (placeholder для не-cash расходов) | ✅ | Кит |
| CashInput: выбор по валюте, is_placeholder для debit/credit | ✅ | Кит |
### Phase 2 — что сделано (подробно)
**API (новые эндпоинты):**
- `GET /api/reports/monthly?year=` — помесячная разбивка доходов/расходов
- `GET /api/reports/summary` — сводная по годам
- `GET /api/reports/category-breakdown?year=&month=` — расходы по группам категорий (данные для дашборда)
- `GET/POST/PUT/DELETE /api/exchange-rates` — CRUD курсов валют
- `GET /api/exchange-rates/summary` — группировка по типу налога
- `GET/POST/PUT/DELETE /api/tax-records` — CRUD налоговых записей
- Accounts API теперь возвращает `balance_in_base` и `base_currency` (мультивалютность)
**БД:**
- Новая таблица `tax_record` (alembic migration)
**Фронтенд (новые страницы):**
- `/dashboard` — дашборд с помесячной динамикой (CSS-chart), расходами по категориям, сводной по годам
- `/exchange-rates` — таблица курсов с фильтрами, CRUD через модалку
- `/tax-records` — таблица налогов с фильтрами и сводкой по типам
- `/cash` — **CashInput.vue** — мобильная страница быстрого ввода наличных расходов и обмена валюты (два режима, чипы категорий, сегмент-кнопка, двусторонний пересчёт курса/суммы)
**Фронтенд (доработки):**
- `/transactions` — infinite scroll вместо пагинации (scroll-based)
- `/accounts` — отображение баланса в валюте счёта + в базовой валюте
- Навигация обновлена — добавлены ссылки на Дашборд, Курсы, Налоги, Наличные
## Тесты
Запуск всех тестов одной командой (из `backend/`):
```bash
cd backend && uv run pytest
```
Verbose: `cd backend && uv run pytest -v`
### Фикстуры
Общий `conftest.py` в `tests/` предоставляет:
- `engine` — Postgres test DB (`budget_app_test`) с `create_all`/`drop_all` на каждый тест + seed валют
- `client` — ASGI клиент с зарегистрированным тестовым юзером
- `auth_headers` — JWT Bearer token
Все тесты используют **Postgres** (не sqlite). Настройка через `settings.test_database_url`.
### Покрытие
**37 тестов + 1 skipped**:
| Файл | Тестов | Что проверяет |
|------|--------|---------------|
| `test_health.py` | 1 | Health endpoint |
| `test_models.py` | 3 | Импорт моделей, метаданные, create_all в sqlite |
| `test_auth.py` | 2 | Auth flow (register→login→me), unauthorized |
| `test_api.py` | 6 | Транзакции (list, filter, search), accounts, reports, unauthorized |
| `test_crud.py` | 6 | CRUD транзакций |
| `test_categories.py` | 8 | CRUD групп и категорий |
| `test_accounts.py` | 6 | CRUD счетов, удаление с транзакциями |
| `test_account_balances.py` | 6 | **Баланс: доход+расход, переводы, cross_rate, initial_balance** |
### Вычисление баланса
`balance = initial_balance + incoming - outgoing`
- `incoming` = SUM(amount) if cross_rate IS NULL, SUM(amount * cross_rate) если перевод между валютами (для dest_account)
- `outgoing` = SUM(amount) — всегда в валюте источника