389 lines
45 KiB
Markdown
389 lines
45 KiB
Markdown
---
|
||
name: budget-current-plan
|
||
description: Актуальный бизнес-процесс фичи «Бюджет» — версия после полного сброса проекта (см. ниже "Второй урок"); не путать со старым бюджет_проект_план.md (2006 год, источник идей, не требований)
|
||
metadata:
|
||
type: project
|
||
---
|
||
|
||
Старый файл `бюджет_проект_план.md` — заметки 2006 года, источник идей, не требований. Этот файл —
|
||
то, что реально подтверждено в разговоре. **Актуальна только эта версия** — предыдущая версия этого
|
||
же файла (обсуждение копилок/целей ДО того, как весь проект снесли и начали заново) частично устарела,
|
||
см. "Второй урок" ниже про то, почему.
|
||
|
||
## Продукт
|
||
|
||
Личный (не мульти-пользовательский) трекер бюджета — один человек ведёт свой бюджет. `users` всё же
|
||
нужна (не совсем однопользовательский в смысле схемы БД) — счета/категории/транзакции ссылаются на
|
||
`user_id`, без этого пришлось бы возвращаться к вопросу при первом же втором пользователе.
|
||
|
||
## Users
|
||
|
||
`id`, `login`, `password`, `name`, `email`, `date_add`, `last_login`. Пока достаточно.
|
||
|
||
## Мультипользовательский режим — согласованный дизайн (гипотетика, НЕ в текущем приоритете, не кодировать)
|
||
|
||
Обсуждали отдельно от основного скоупа, пользователь попросил зафиксировать. Два независимых механизма,
|
||
оба дёшевы и не требуют объединять/сверять чужие бюджеты между собой:
|
||
|
||
1. **Изолированные бюджеты (например «я» и «сын»)** — ничего доделывать не нужно, уже работает: каждый
|
||
`users.id` — это и есть владелец полностью независимого бюджета, `accounts`/`categories`/
|
||
`transactions`/`budgets`/`goals` уже фильтруются по своему `user_id` (см. "Продукт" выше — заложено
|
||
с самого начала на этот случай). Один бюджет никак не видит другой, ничего сверять/делить не нужно
|
||
(в отличие от гипотезы про «общие расходы между раздельными бюджетами», которую обсуждали и
|
||
отбросили — пользователь явно сказал не думать про дележ ЖКХ/общих счетов).
|
||
|
||
2. **Viewer/editor доступ к ЧУЖОМУ бюджету (например жена смотрит бюджет мужа)** — новая маленькая
|
||
таблица-разрешение, не полноценная система приглашений:
|
||
```sql
|
||
CREATE TABLE budget_access (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
owner_user_id INT UNSIGNED NOT NULL, -- чей бюджет (users.id)
|
||
viewer_login VARCHAR(64) NOT NULL, -- логин из Services\Auth — кого пускаем
|
||
role ENUM('viewer','editor') NOT NULL DEFAULT 'viewer',
|
||
FOREIGN KEY (owner_user_id) REFERENCES users (id)
|
||
);
|
||
```
|
||
Логика: человек логинится через `Services\Auth` (уже поддерживает несколько логинов). Приложение
|
||
смотрит — есть ли у него свой `users.id` (владеет своим бюджетом), и есть ли записи в `budget_access`
|
||
по его `login` (доступ к чужим). Оба факта независимы и совместимы одновременно: можно и владеть
|
||
своим бюджетом, и иметь viewer/editor-доступ к чужому сразу — это НЕ взаимоисключающие случаи.
|
||
|
||
**Следствие для UI**: если у залогиненного человека доступ больше чем к одному бюджету (свой + чужой
|
||
через `budget_access`), нужен переключатель «чей бюджет сейчас смотрим».
|
||
|
||
**Точечная видимость внутри доступа (например скрыть «Подарки», чтобы не спалить сюрприз жене)** —
|
||
блок-лист поверх `budget_access`, не allow-лист (по умолчанию viewer видит всё разрешённое, точечно
|
||
скрываем конкретные счета/категории, а не наоборот — перечислять вручную всё видимое было бы утомительно):
|
||
```sql
|
||
CREATE TABLE budget_access_restrictions (
|
||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||
budget_access_id INT UNSIGNED NOT NULL, -- к какому разрешению (budget_access) относится
|
||
entity_type ENUM('account', 'category') NOT NULL,
|
||
entity_id INT UNSIGNED NOT NULL, -- id скрываемого счёта или категории
|
||
FOREIGN KEY (budget_access_id) REFERENCES budget_access (id)
|
||
);
|
||
```
|
||
Скрытие категории должно прятать и связанные с ней транзакции/план-факт в отчётах viewer'а (не
|
||
спроектировано, как именно — фильтровать на уровне запроса или на уровне рендера).
|
||
|
||
**Решено, с примером.** У viewer'а те же две цифры, что у владельца («Доступно сейчас» и «Всего по
|
||
всем счетам»), но пересчитанные только по счетам, которые ему разрешены (не заблокированы через
|
||
`budget_access_restrictions`). Два независимых уровня фильтрации, не путать:
|
||
1. **`include_in_total`** — решает владелец, на каждом своём счёте: считать ли его в СВОЁ «Доступно
|
||
сейчас» (кошелёк/карта — да, копилка — обычно нет).
|
||
2. **Доступ viewer'а** (`budget_access_restrictions`) — более грубый фильтр: какие счета viewer вообще
|
||
видит. Скрытый от viewer'а счёт не участвует ни в одной из его двух сумм, независимо от того, какая
|
||
у этого счёта галочка `include_in_total` у владельца.
|
||
|
||
Пример: Лена смотрит бюджет Егора, но копилка «Остаток» от неё скрыта. У Лены «Доступно сейчас» =
|
||
кошелёк+карта (то же, что и у Егора, минус «Остаток», которого она вообще не видит), «Всего по всем
|
||
счетам» — тоже без «Остатка». Скрытый счёт просто не участвует в подсчёте — не до конца проговорено,
|
||
нужно ли отдельно прятать сам факт его существования (не только сумму), и блок-лист это или allow-лист
|
||
по ощущению пользователя — переспросить при реализации.
|
||
|
||
**Побочный эффект для схемы**: при таком дизайне `login`/`password` в таблице `users` (бюджетной)
|
||
становятся не нужны вообще — реальная авторизация целиком на `Services\Auth`, а `users` вырождается
|
||
в «чей это бюджет» (по сути `id` + `name`). Не менять `users` прямо сейчас — это следствие, которое
|
||
всплывёт, только если реально начнём кодировать мультипользовательский режим.
|
||
|
||
**Побочный эффект для схемы**: при таком дизайне `login`/`password` в таблице `users` (бюджетной)
|
||
становятся не нужны вообще — реальная авторизация целиком на `Services\Auth`, а `users` вырождается
|
||
в «чей это бюджет» (по сути `id` + `name`). Не менять `users` прямо сейчас — это следствие, которое
|
||
всплывёт, только если реально начнём кодировать мультипользовательский режим.
|
||
|
||
## Accounts (счета)
|
||
|
||
**Один пользователь приложения может вести несколько реальных счетов, включая счета, номинально
|
||
оформленные на других членов семьи** (пример: «мой счёт», «счёт жены») — это не мультипользовательская
|
||
функция и не требует отдельного `user_id` на счёт: все они просто разные строки `accounts` с разными
|
||
`title`, привязанные к одному `user_id` (см. "Продукт" выше). Различать их — по `title`, не по схеме.
|
||
|
||
**Будущие типы счёта под инвестиции (не сейчас, см. "Приоритет фич" → Инвестиции)** — уточнено, это
|
||
два разных счёта, не один:
|
||
- **Брокерский** — акции, облигации, фонды, фьючерсы.
|
||
- **ЦФА** (цифровые финансовые активы) — долговые, ноты, картины (NFT-подобное), крипто.
|
||
|
||
Оба не подходят ни под `cash` (не наличные), ни под `bank` (не просто расчётный счёт), ни под `savings`
|
||
(не вклад под процент, а портфель с рыночной переоценкой, не фиксированной ставкой). **Как именно с ними
|
||
работать (отдельные позиции/тикеры внутри счёта, переоценка, дивиденды/купоны, transfer при
|
||
покупке/продаже) — пользователь явно попросил отложить обсуждение, не проектировать сейчас вообще**,
|
||
даже на уровне схемы `accounts`/`type_acc`. Сейчас — только основной бюджет (cash/bank/savings).
|
||
|
||
Три типа `type_acc` (актуальные, реализовано):
|
||
- **`cash`** — кошелёк (наличные).
|
||
- **`bank`** — счёт в банке; карта просто привязана к нему, сама по себе денег не хранит (это
|
||
сознательное упрощение — раньше в модели были отдельные "дебетовая карта"/"кредитная карта"/
|
||
"е-money" и т.п., это НЕ подтверждено заново после сброса, убрано).
|
||
- **`savings`** — копилка: тот же счёт, но с процентом на остаток (история ставок — отдельная таблица,
|
||
привязанная к `account_id`). Опционально может иметь цель (см. `goals` ниже) — если записи в `goals`
|
||
нет, это просто копилка "без цели", копишь под процент без конкретной суммы/даты.
|
||
|
||
Поля: `id`, `user_id`, `type_acc`, `title`, `summa` (баланс, вводится как есть при создании счёта —
|
||
любая сумма и своя валюта, не обязательно с нуля), `currency` (при создании счёта, вместе с суммой),
|
||
`description` (необязательное свободное поле), `is_delete` (архивация вместо удаления), `order`,
|
||
**`include_in_total`** (bool) — счёт учитывается в общей сумме на дашборде или нет; по умолчанию `true`
|
||
для `cash`/`bank`, `false` для `savings` — но это **чисто ручной переключатель пользователя, не
|
||
системное правило по типу/валюте счёта**. Подтверждено явно: то, что валютный счёт «Евро» был
|
||
исключён из «Доступно сейчас» на макете — не потому что валютные счета по умолчанию исключаются, а
|
||
потому что пользователь лично выбрал не включать именно этот счёт. Дефолт (true для cash/bank, false
|
||
для savings) — просто стартовое значение при создании счёта, дальше пользователь сам решает по каждому.
|
||
|
||
**Счёт = title + summa + currency (+ description)** — минимальный набор полей, ничего структурного
|
||
сверху (например «принадлежит члену семьи X») не нужно — если понадобится пометить «это счёт жены»,
|
||
это просто текст в `title`/`description`, не отдельное поле.
|
||
|
||
**Уход в минус — невозможен для `cash`/`bank`/`savings`.** Овердрафт не подключается и не планируется;
|
||
наличным взяться неоткуда, если их нет. Если пользователь тратит больше, чем есть на счету — это
|
||
концептуально уже кредит (транзакция «с кредита»), а не отрицательный баланс обычного счёта. Кредиты
|
||
по-прежнему вне скоупа (см. "Приоритет фич"), но когда до них дойдём — они не про минус на обычном
|
||
счёте, а про отдельный тип счёта/операции.
|
||
|
||
**Копилка/цель — намеренно тот же счёт, не отдельная сущность.** Пользователь явно подтвердил принцип
|
||
«не плодить сущности»: `savings` — это `accounts` с типом + история ставок (`account_rates`) + опционально
|
||
`goals` (цель) сверху, а не отдельная параллельная таблица «копилки». Это уже так и спроектировано —
|
||
просто явное подтверждение архитектурного выбора.
|
||
|
||
**Курс валюты — автоматически, из внешнего источника (ЦБ РФ)**, не вручную (в отличие от процентной
|
||
ставки копилки — там осознанно вручную, см. `Account_rates` ниже; для курса валют внешний источник
|
||
есть и он надёжный, незачем дублировать руками). Не спроектировано пока: какой конкретно endpoint ЦБ РФ
|
||
(daily XML/JSON), куда класть клиент — скорее всего новый `Services\CurrencyRate` (по аналогии с
|
||
`Services\Elasticsearch`/`Services\Mongo` — точка входа + REST/HTTP-клиент), поверх уже существующего
|
||
`System\Classes\HTTP\Client\Curl`. Дашборд показывает курс для валют, которые реально встречаются среди
|
||
счетов пользователя (не весь список валют ЦБ РФ огулом). **Для дашборда курс — просто «сегодняшнее
|
||
значение» с кэшем на сутки, историю хранить для этого не нужно** (история нужна в другом месте, см. ниже).
|
||
|
||
**Общая сумма на дашборде** — не сумма вообще всех счетов, а сумма счетов с `include_in_total = true`,
|
||
сконвертированная в рубли по текущему курсу для не-рублёвых. Рубль — базовая валюта для агрегации
|
||
(остальные конвертируются в неё для складывания в одну сумму).
|
||
|
||
**Курс валюты на операции — фиксируется прямо в транзакции, не отдельной историей.** Возникло из
|
||
вопроса «отчёт по Отпуску, если траты в разных валютах» — решено, что в момент создания расходной
|
||
операции по валютному счёту курс ЦБ РФ на этот день записывается прямо в саму транзакцию и остаётся
|
||
неизменным навсегда (так же, как в реальных банковских FX-операциях — курс фиксируется в моменте, а не
|
||
ищется задним числом). Отдельная таблица `currency_rates` с историей по датам **не нужна** — это было
|
||
более сложное первое решение, отменено в пользу этого:
|
||
```sql
|
||
ALTER TABLE transactions ADD COLUMN currency CHAR(3) NULL; -- валюта операции (обычно = валюте счёта списания)
|
||
ALTER TABLE transactions ADD COLUMN rate_to_rub DECIMAL(10, 4) NULL; -- курс ЦБ РФ на момент операции; NULL для рублёвых
|
||
```
|
||
Отчёт за период (например «Отпуск» за произвольные даты, не обязательно календарный месяц) суммирует
|
||
`summa * COALESCE(rate_to_rub, 1)` по нужным транзакциям — без join'ов и без логики «на эту дату курса
|
||
нет, берём ближайший». Период отчёта по категории — отдельная фича поверх `transactions`, не привязана
|
||
к месячной сетке `budgets` (та остаётся по месяцам, это для другого — план/факт, не разовые отчёты).
|
||
|
||
## Categories (статьи) — с подкатегориями
|
||
|
||
`id`, `user_id`, `parent_id` (null — категория верхнего уровня; иначе — id родителя), `title`, `type`
|
||
(`income`/`expense`), `order`, **`is_delete`** (архивация вместо удаления — тот же приём, что уже
|
||
есть у `accounts`; подтверждено: если по статье есть история транзакций, просто скрывать её из выбора
|
||
при вводе новой операции, не удалять физически).
|
||
|
||
Пример: «Продукты» — родительская категория, «Алкоголь» — подкатегория внутри неё. Транзакция
|
||
привязывается к конкретной (под)категории. В отчёте можно смотреть по родителю целиком (сумма всех
|
||
подкатегорий) или провалиться в конкретную подкатегорию отдельно.
|
||
|
||
**Защита от дублирования статей — подтверждено, нужно.** Два сценария:
|
||
1. Заархивировали статью, забыли, завели новую с тем же названием — история разъезжается на две части.
|
||
2. Опечатка/невнимательность — завели одну и ту же статью дважды по рассеянности.
|
||
|
||
Решение для обоих — на уровне приложения, не БД-constraint: при создании/переименовании статьи
|
||
проверять, нет ли уже **активной** (`is_delete = 0`) статьи с таким же `title` у **этого же
|
||
`user_id`** (в рамках типа доход/расход и той же родительской группы) — если есть, не давать создать
|
||
копию. Если находится **архивная** статья с тем же именем — предлагать восстановить её
|
||
(`is_delete = 0`) вместо создания новой.
|
||
|
||
**Важно: проверка скоуплена по `user_id`, не глобально.** Если у двух разных (изолированных)
|
||
пользователей — например Егора и сына — у каждого своя статья «Питание», это два независимых ряда в
|
||
`categories` с одинаковым текстом в `title` и это нормально, конфликта нет — проверка сработала бы
|
||
только если ОДИН И ТОТ ЖЕ пользователь попытался завести такую статью у себя дважды. Изоляция
|
||
пользователей (см. раздел про мультипользовательский режим) и так гарантирует, что один вообще не
|
||
видит статьи другого — здесь ничего дополнительно обеспечивать не нужно.
|
||
|
||
**Группировка — целиком в руках пользователя, через `parent_id`, никак не трогает историю.**
|
||
Пользователь явно подтвердил это как требование: перекинул статью в другую группу (сменил `parent_id`)
|
||
— старые `transactions` не меняются (они ссылаются на `categorie_id` листа, не на группу), просто
|
||
отчёты/дашборд на лету пересчитываются по текущей группировке при следующем открытии. Схему менять не
|
||
пришлось — `parent_id` уже это поддерживает.
|
||
|
||
**Реальный список статей пользователя** (из его старой таблицы, не придумано мной) —
|
||
Доход: Зарплата Егор, Зарплата Лена, Квартира, Другие доходы.
|
||
Расход: 10%, К, Питание, Хозтовары, ЖКХ, ЖКХ Люберцы, Авто+Бензин, Проезд, Связь+Инет, Школа, Садик,
|
||
Дети, Медицина, Одежда, Личные расходы, Развлечение, Подарки, Парик/Красота, Отпуск, Хобби,
|
||
Образование, Спорт, Дом, Непредв. расходы, Непонятные расходы.
|
||
|
||
Расшифровка непрозрачных названий:
|
||
- **«К»** — Кирилл (сын). Инвестиция на его счёт, вносится один раз в начале года всей суммой
|
||
(сложный процент выгоднее, чем вносить помесячно), но в плане (`budgets.plan_summ`) размазывается
|
||
по 3 000 ₽/мес. Следствие: факт в январе резко подскочит (реальная транзакция ~36 000 ₽), с февраля
|
||
по декабрь факт будет 0 — это ожидаемо, не баг.
|
||
- **«10%»** — отчисление 10% от зарплаты в копилку «Магнит», позже инвестируется. Та же природа, что
|
||
и «К» — плановая регулярная строка поверх нерегулярных реальных движений денег.
|
||
- **«ЖКХ» / «ЖКХ Люберцы»** — коммуналка по двум разным квартирам (не первая — родитель, вторая —
|
||
подкатегория; это две равноправные отдельные статьи, обе можно сгруппировать вместе, если нужно).
|
||
|
||
Группировка (пример, обсуждали при показе макета категорий) — пользователь дал 5 групп-примеров:
|
||
«Основные» (ЖКХ, Питание, Связь), «Подписки» (Клод, ЧатGPT — новые примеры, не было в старом списке),
|
||
«Дети» (Биба, Боба — тоже новые имена, не факт что совпадают с «Дети»/«Школа»/«Садик»/«К»), «Транспорт»
|
||
(Бензин, Штрафы, Авто, Платные дороги), «Медицина» (Аптека, Лечение). Остальные статьи из старого
|
||
списка пользователь осознанно оставил без группы — сам разберёт в приложении, не наша забота гадать.
|
||
|
||
## Transactions
|
||
|
||
`id`, `user_id`, `categorie_id`, `account_f_id` (откуда), `account_in_id` (куда), `date`, `summa`,
|
||
`type` (`income`/`expense`/`transfer`). Для дохода — только `account_in_id`, для расхода — только
|
||
`account_f_id`, для перевода — оба.
|
||
|
||
**Все transfer — без категории, без исключений**, в том числе пополнение savings-счёта/копилки/цели:
|
||
переложил деньги в копилку «Отпуск» — это просто перемещение между своими счетами, не потрачено и не
|
||
заработано, `categorie_id = null`. **Решено** (закрывает предыдущий открытый вопрос про задвоение):
|
||
реальный расход считается только тогда, когда деньги действительно потрачены — гостиница, билеты и
|
||
т.п. оформляются обычными `expense`-транзакциями с `account_f_id` = счёт, с которого реально платили
|
||
(в т.ч. со счёта-копилки, если платили прямо с него), и категорией «Отпуск» (точнее — под-категорией
|
||
типа «Билеты»/«Гостиница»). Задвоения нет, потому что откладывание в копилку категорией не помечается
|
||
вовсе — план/факт по категории «Отпуск» видит только реальные траты.
|
||
|
||
**Баланс счёта (`accounts.summa`) — решено, «как в банках».** Не пересчитывать всю историю на каждый
|
||
показ, но и не хранить как самостоятельное число без контроля. Гибрид: `summa` — хранимый кэш, который
|
||
обновляется **атомарно, в той же БД-транзакции**, что и сама операция создания/правки/удаления
|
||
`transactions` (`PdoConnection::transaction()` уже есть для этого) — источник истины всё равно лента
|
||
операций, баланс — материализованный кэш поверх неё для скорости. Дополнительно нужен служебный метод
|
||
«пересчитать баланс с нуля из истории» — не для обычной работы, а как инструмент сверки/восстановления
|
||
при подозрении на расхождение (аналог банковской реконсиляции).
|
||
|
||
**Разбивка одной операции на несколько статей — подтверждено, нужно.** Один чек/покупка → несколько
|
||
пар (статья, сумма) в рамках одной операции (пример: поход в магазин — 500 ₽ Питание + 300 ₽
|
||
Хозтовары). Это меняет схему `transactions`: `categorie_id`/`summa` на самой строке `transactions`
|
||
достаточно только для НЕразбитых операций; для разбитых нужна отдельная дочерняя таблица (условно
|
||
`transaction_items`: `id`, `transaction_id`, `categorie_id`, `summa`) — шапка `transactions` держит
|
||
дату/счета/общую сумму/тип, детали разбивки — в дочерних строках. Не спроектировано окончательно:
|
||
нужна ли разбивка для `income`/`transfer` тоже, или только для `expense` (вероятно только `expense` —
|
||
transfer и так без категории, доход обычно один источник). **Два способа ввода:**
|
||
1. **Голосовой** — пользователь надиктовывает через AI API (например «Питание 500 рублей, Хозтовары
|
||
300 рублей»), система распознаёт речь и разбирает текст на пары статья/сумма автоматически.
|
||
2. **Ручной** — то же самое, но вводится руками, пара за парой.
|
||
Не спроектировано: какой конкретно AI API для голоса (Claude API уже упоминался в старом
|
||
`бюджет_проект_план.md` как ИИ-помощник — возможно, тот же), как обрабатывать ошибки распознавания.
|
||
|
||
**Лог правок/удалений — отдельная таблица в БД** (не общий файловый `System\Classes\Log` — тот
|
||
подходит для аудита запросов вообще, но не для точечных «покажи все правки операции №123»). Правка и
|
||
удаление разрешены всегда, без ограничений по давности — но каждое изменение логируется: кто, когда,
|
||
что именно поменялось. Не спроектировано: точная структура (`transaction_history`: id, transaction_id,
|
||
user_id, changed_at, action, old_values/new_values — набросок, не финал).
|
||
|
||
## Budgets (план/факт)
|
||
|
||
`id`, `categorie_id`, `plan_summ`, `year`, `month`, `type`. Факт — не хранится, считается на лету как
|
||
сумма `transactions` по этой категории за месяц (с учётом открытого вопроса выше — возможно, включая
|
||
помеченные transfer на копилки).
|
||
|
||
**Рабочий процесс ввода плана — не помесячно по ходу года, а весь год сразу.** План составляется один
|
||
раз, в конце года, сразу на все 12 месяцев следующего года по каждой статье (пример: в конце 2026-го
|
||
вводится план по «Проезду» на весь 2027-й — 12 чисел за один заход). Влияет на форму ввода — нужна не
|
||
«план на текущий месяц», а «план на год» с 12 полями (по одному на месяц) на каждую статью.
|
||
|
||
**Открыто: формульные статьи бюджета.** Пользователь пояснил про «10%»: план = ЗП × 0,1, факт =
|
||
факт ЗП × 0,1 — то есть не фиксированное число и не сумма собственных транзакций, а процент от
|
||
другой категории (дохода). Концепция обсуждалась, не спроектирована окончательно: скорее всего
|
||
`budgets` получит необязательные `formula_source_categorie_id` + `formula_pct`, и для таких строк
|
||
план/факт вычисляются на лету от категории-источника, а не читаются напрямую. Не решено: один
|
||
источник или сумма нескольких (у пользователя два дохода — Зарплата Егор/Зарплата Лена), что
|
||
происходит при смене процента задним числом. Не проектировать/не кодировать, пока не обсудим детали.
|
||
|
||
## Регулярные/автоматические операции — РЕШЕНО НЕ ДЕЛАТЬ
|
||
|
||
Спрашивал явно — нужна ли фича «повторяющаяся операция», которая сама заводит транзакцию по расписанию
|
||
(для подписок типа Клод/ЧатGPT, коммуналки и т.п.). **Ответ: всё вручную**, автозаведения не нужно.
|
||
Обоснование пользователя: у подписок сумма почти всегда одна и та же, но может повыситься (автоматика
|
||
завела бы неверную сумму молча), а у коммуналки сумма всегда разная (автоматике там вообще нечего
|
||
угадывать) — в обоих случаях ручной ввод надёжнее, чем шаблон "повторить прошлый месяц".
|
||
|
||
## Остаток / Резерв — РЕШЕНО НЕ ДЕЛАТЬ (обсуждали и отменили)
|
||
|
||
Пользователь прислал реальный список категорий из своей старой таблицы, где внизу были строки
|
||
«Остаток», «Ост. с прошлого мес.», «Резерв» — сквозные метрики уровня всего бюджета (не по счёту,
|
||
не по категории). Формула, которую успели согласовать: Остаток(M) = Доход(M) − Расход(M); Резерв(M) =
|
||
Ост. с прошлого мес.(M) + Остаток(M); Ост. с прошлого мес.(M) = Резерв(M−1) — то есть running total
|
||
«Доход минус Расход» с начала учёта.
|
||
|
||
**Но при уточнении пользователь сам решил, что это не нужно** — это был костыль конкретно таблицы,
|
||
у которой не было реальных остатков на счетах: приходилось вручную вычислять «сколько у меня реально
|
||
осталось» через накопительный доход-минус-расход. У нас есть настоящие `accounts.summa` (см. «Доступно
|
||
сейчас» на дашборде — сумма счетов с `include_in_total = true`) — это и есть тот самый ответ на вопрос
|
||
«сколько у меня есть», причём точнее (не зависит от того, залогирована ли вообще каждая транзакция).
|
||
А сравнение план/факт — уже отдельно покрыто таблицей `budgets` по категориям. Резерв как отдельная
|
||
сущность дублировал бы то, что уже есть в двух других местах — **не реализовывать**.
|
||
|
||
## Account_rates (история ставок)
|
||
|
||
`id`, `account_id`, `rate_percent`, `valid_from`, `valid_to` (null = текущая).
|
||
|
||
**История (не единственное поле на `accounts`) нужна ради конкретной фичи — узнать, сколько процентов
|
||
начислилось за произвольный выбранный период** (например «сколько капнуло за март»). Если ставка
|
||
внутри периода менялась, расчёт должен пройти по каждому отрезку `account_rates`, пересекающему
|
||
период, со своей ставкой — без истории это невозможно восстановить задним числом. Это и есть
|
||
причина оставить таблицу, а не одно поле `rate_percent` на `accounts` (более ранняя версия этого
|
||
раздела ошибочно связывала историю только с будущим кредитом — кредит по-прежнему вне скоупа, но
|
||
раз есть более близкий, реальный повод, обоснование хранения истории теперь этот, не кредитный).
|
||
Сам расчёт начисленных процентов за период (простой или сложный, как разбивать по датам) — ещё не
|
||
спроектирован, отдельная задача на будущее.
|
||
|
||
**Ставка вводится вручную, без интеграции с банком** (это личный трекер, не агрегатор счетов) —
|
||
план, ЕЩЁ НЕ РЕАЛИЗОВАНО, возвращаемся к нему, когда дойдём до формы счёта:
|
||
|
||
- **При создании копилки** (форма счёта, `type_acc = savings`) — доп. поле «Ставка, % годовых».
|
||
Создаёт первую строку `account_rates`: `valid_from` = дата создания счёта (или явно введённая
|
||
пользователем), `valid_to = null`.
|
||
- **При изменении ставки** — отдельное действие на странице счёта (не правка задним числом старой
|
||
строки — иначе теряется история для прошедших периодов). Пользователь вводит **и новый процент, и
|
||
дату, с которой он начинает действовать** (`valid_from` — не обязательно «сегодня», банк мог
|
||
прислать уведомление заранее/задним числом). Логика: найти текущую строку через
|
||
`AccountRateRepository::getCurrentRate($account_id)`, закрыть её (`valid_to` = день перед новым
|
||
`valid_from`), вставить новую (`valid_from` = введённая дата, `valid_to = null`, новый
|
||
`rate_percent`) — одной транзакцией (`$repo->transaction(...)`).
|
||
- Метод под это — `AccountRateRepository::setRate(int $account_id, float $rate_percent, string $valid_from)`.
|
||
Сейчас в `AccountRateRepository` есть только `getCurrentRate()` (чтение, уже реализовано и
|
||
протестировано на реальной БД) — `setRate()` ещё не написан.
|
||
- Прогноз «надо ≈X ₽/мес» у цели — открытый вопрос: линейно (без процента, проще) или честной
|
||
формулой аннуитета со сложным процентом (то, что буквально написано в `Goals` ниже — «по текущей
|
||
ставке») — не решено, обсудить перед реализацией расчёта.
|
||
|
||
## Goals (цель для копилки)
|
||
|
||
`id`, `account_id`, `target_summa`, `deadline`. Необязательная надстройка над `savings`-счётом.
|
||
Прогноз «сколько класть в месяц» считается на лету по ТЕКУЩЕЙ ставке (не хранится), в предположении,
|
||
что она не изменится до дедлайна; пересчитывается при просмотре/смене ставки.
|
||
|
||
**Проценты, начисленные на копилку** — `income`-транзакция на счёт-копилку с одной системной
|
||
категорией «Проценты по вкладам/целям» (одна на все копилки, не по одной на каждую) — заводится
|
||
пользователю автоматически при первом использовании.
|
||
|
||
## Быстрый ввод операции — замена идее Telegram-бота
|
||
|
||
В старом `бюджет_проект_план.md` (заметки ДО сброса, источник идей) был пункт «Telegram-бот для
|
||
быстрого ввода трат». **Переиграно**: вместо внешнего бота — страница быстрого добавления операции
|
||
внутри самого Bicycle (тот же стек, без бот-токена/вебхука/парсинга свободного текста от Telegram API).
|
||
**Не решено** — закреплять ли её как PWA-иконку на экране телефона (`View::setManifest()` уже
|
||
поддержан в проекте, см. CLAUDE.md) — пользователь пока не уверен, обсудить отдельно перед реализацией.
|
||
|
||
## Приоритет фич
|
||
|
||
1. **Цели (план/факт) + анализ бюджета** — сейчас.
|
||
2. **Инвестиции** (вклады/акции/облигации) — следующий слой, схему делать с запасом.
|
||
3. **Кредиты** — отложены, но `account_rates` намеренно универсальна для переиспользования позже.
|
||
|
||
## Второй урок (почему эта версия файла переписана)
|
||
|
||
После того как весь код/схема домена бюджета были снесены "с нуля" по прямому требованию пользователя,
|
||
я в следующем же заходе молча притащил старые выводы из ПРЕДЫДУЩЕЙ версии этого файла (users, accounts,
|
||
categories, transactions, budgets) как будто они уже согласованы — хотя после сброса заново обсуждались
|
||
только копилки/цели. Пользователь: «про пользователя мы ничего не обсуждали, про бюджет мы ничего не
|
||
обсуждали... ОТКУДА БЛЯДЬ???». **Урок: "сохранено в памяти" ≠ "можно молча переиспользовать после
|
||
полного сброса"** — если проект/фичу explicitly снесли и просили начать заново, каждое решение
|
||
переподтверждается в текущем разговоре заново, даже если оно дословно совпадёт с тем, что уже было
|
||
записано. Ссылаться на память можно только явно и открыто ("вот что записано, актуально ли?"), не
|
||
молча вставлять в предложение как решённое.
|
||
|
||
**How to apply**: перед тем как писать `schema.sql`/контроллеры/репозитории — свериться с этим файлом
|
||
**и убедиться, что каждый пункт был реально произнесён в текущем контексте разговора**, а не просто
|
||
существует здесь с прошлого раза. Новые уточнения — дописывать сюда сразу.
|