603 lines
43 KiB
Markdown
603 lines
43 KiB
Markdown
# 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) — всегда в валюте источника
|