Files
obsidian-vault/personal/projects/budget-app/index.md
T

368 lines
28 KiB
Markdown
Raw 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: нет нормальных графиков по подкатегориям, кросс-фильтрации, прогнозов.
- Нет ничего про FIRE: проекций пенсии, моделирования инвестиций, целевых процентов нормы сбережений.
- История курсов хранится в строках листа `курсы` — это не нормализованная таблица.
- Нет API: нельзя интегрировать с банковскими экспортами, нельзя автоматически тянуть в дашборд.
## Концепция веб-приложения
### Цель
Заменить Excel приложением, которое:
1. Принимает все те же транзакции (multi-currency, multi-account, double-entry).
2. Считает аналитику и сводки **на лету** через SQL, а не через `VLOOKUP`.
3. Импортирует банковские выписки (Альфа, Сбер, Demir, …) автоматически + AI-резолвер для неоднозначных случаев.
4. Даёт мобильный UI для быстрого ввода трат на ходу.
5. Моделирует **FIRE-сценарии** (отдельная фаза после готового бюджетирования).
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 — Курсы и настройки**. Cron-джоб для НБ КР с back-fill. Базовая валюта в настройках. Все пересчёты привязаны к ней.
4. **Phase 3 — Ручной ввод**. Форма добавления транзакции, CRUD категорий/счетов, ручная правка курсов.
5. **Phase 4 — Импорт банков + AI-резолвер**. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений.
6. **Phase 5 — Аналитика**. Дашборды: расход по категориям, динамика, бёрндаун по бюджету. Прогнозы.
7. **Phase 6 — Mobile / PWA**. Быстрый ввод с телефона.
8. **Phase 7 — MCP HTTP**. Экспонировать MCP-эндпоинт для Hermes.
9. **Phase 8 — Инвестпортфели + FIRE**. Тикеры/цены/дивы. Калькулятор FIRE. *(отдельное планирование когда бюджетирование готово.)*
### 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`
### 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 0 закрыт, Phase 1 спланирован