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

28 KiB
Raw Blame History

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 Курс на дату операции.
UAB Резерв / служебные.

Семантика двойной записи: одна строка = одно перемещение средств. Если 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.
  • tunnelcloudflare/cloudflared, exposes budget.qentra.topfrontend: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: создать БД и юзера в существующем кластере —
    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 спланирован