Входная задача звучит вроде просто: У человека стабильная зарплата и несколько целей: подушка, ремонт, телефон, отпуск. Нужно для каждой цели показать дату, к которой она соберется, и пересчитывать даты после каждой получки или распределения накоплений. Я полгода делал такую модель для своего приложения и несколько раз переписывал. Ниже мой опыт, который может пригодится тем, кто считает даты из денежного потока.
Входные данные
-
Доход по датам выплат: аванс и зарплата, а не «доход в месяц». Период планирования идет от получки до получки
-
Обязательные платежи с датами, кредиты с датой окончания.
-
Кошелек на мелочи и непредвиденные риски.
-
Подушка: целевая сумма в месяцах обязательных расходов.
-
Цели: сумма, уже накоплено, желаемая дата, приоритет.
-
Премии: разовые и регулярные, например раз в квартал.
Свободные деньги периода = доход − обязательные − кошелек. Из них часть уходит в подушку, остальное в цели.
Два режима распределения
Пример: на цели остается 50 000 в месяц. Цели: ремонт 300 000 (критичная), телефон 80 000, отпуск 180 000.
По очереди. Сортируем по приоритету, затем по желаемой дате, и весь бюджет отдаем первой цели.
-
Ремонт: 300 000 / 50 000 = 6 месяцев.
-
Телефон стартует после ремонта: 80 000 / 50 000 = 1,6, округляем вверх до выплаты, 2 месяца. Итого 8.
-
Отпуск: 180 000 / 50 000 = 3,6, то есть 4 месяца. Итого 12.
Одновременно. Бюджет делим пропорционально остаткам: 53,6%, 14,3% и 32,1%. Все три цели собираются примерно через 11,2 месяца, то есть одновременно.
Обратите внимание: последняя цель в обоих режимах закрывается примерно в одно время, потому что общий остаток один и тот же. Разница в том, когда человек получает первую закрытую цель: через 6 месяцев или через 11. Для удержания это решает все. Параллельный режим выглядит честным, а на деле все цели опаздывают разом.
Подушка: политика или конверт
Я сделал 3 политики: сначала цели, сначала подушка, пополам. Каждая меняет даты всех целей. Пользователю пришлось бы понимать, что он выбирает, а он хотел знать только, когда будет подушка.
Сейчас я бы сделал подушку отдельным конвертом с фиксированным порядком: сначала месяц расходов в подушку, потом цели. Люди, у которых есть подушка, возвращаются чаще: она защищает цели от первой неожиданной траты.
Премии: две даты хуже одной
Премии непредсказуемы, поэтому базовый прогноз считается без них, а детальный считается помесячной симуляцией до 60 месяцев, где в месяц премии бюджет на цели растет. Инвариант: дата с премиями не позже даты без премий.
На практике две даты на разных экранах рождают вопрос «какая правильная?». Лучше одна дата по гарантированному доходу и подсказка «с премией в марте будет на 2 месяца раньше».
Календарь: два бага, которые стоили месяцев
Конец дня. Кредит с окончанием сегодня в полдень утром еще учитывался в обязательных расходах, а вечером уже нет. Тест воспроизвел дату цели 12.04.2027 против 12.12.2026, то есть 4 месяца разницы в течение одного дня. Исправление: нормализовать дату старта к началу календарного дня один раз, на входе, и тот же день класть в ключ кеша. Все экраны считают «сегодня» одинаково.
Индекс поступления. Квартальная премия 5 января при расчете, запущенном 12 октября, попадала в декабрь: номер месяца считался полными месяцами от дня запуска. Правильно считать разницу начал календарных месяцев.
Оба бага не ловятся юнит-тестом «на 30 дней вперед», потому что зависят от времени суток и дня запуска.
Пять движков и один источник правды
Модель росла слоями: базовый прогноз, календарный, детальный с премиями, единый. В какой-то момент экраны онбординга, целей и обзора читали разные движки и показывали разные даты. Лечится только так:
-
Одна функция «снимок финансов → даты всех целей», пакетный расчет за один вызов.
-
Все экраны читают ее результат, своих формул на экранах нет.
-
Независимый оракул: помесячный журнал на нескольких фикстурах, который не вызывает код приложения. У меня это 3 фикстуры:
база 170 000; плюс 50 000 ежемесячно; плюс 50 000 раз в квартал. Допуск: один календарный месяц.
Что я бы не строил второй раз
-
Параллельный режим целей. Все опаздывают разом, а первой победы нет.
-
4 уровня приоритета. Хватит двух: «важно» и «хотелка без даты».
-
3 политики подушки. Подушке хватит конверта с фиксированным порядком.
-
Две даты с премиями и без на разных экранах.
-
Новый движок рядом со старым «на время». Временное живет до релиза, где даты разъедутся.
Чек-лист: 8 вопросов к модели дат до первой строчки кода
-
Что такое «сегодня»: начало календарного дня, в каком часовом поясе, где нормализуем?
-
Планируем по календарным месяцам или по периодам между выплатами?
-
Как индексируем нерегулярные поступления: от начала календарного месяца или от дня запуска?
-
Как округляем дату: вверх до ближайшей выплаты?
-
Какая одна функция отдает даты всем экранам?
-
Что в ключе кеша и меняется ли он внутри дня?
-
Где независимый оракул и какой допуск?
-
Что показываем, если цель недостижима при текущем доходе?
Вместо вывода
По моей оценке, на приоритеты и даты ушло 20–30% времени разработки. А пользователи показали, что каждый день им нужны две вещи: понятная цифра на сегодня и дата подушки. Сложная модель осталась внутри, моим «опытом», в общем как всегда начинать нужно было с кастдева 🙂
ПыСы тк делал продукт для решения своей боли и поиска точки где ИИ не справится с разработкой, в итоге считаю, что время потрачено не в пустую.
Приложение, Кубыш внутри которого все это выросло
Если считали похожие даты в своих продуктах, расскажите в комментариях, на каких граничных случаях споткнулись вы
ссылка на оригинал статьи https://habr.com/ru/articles/1099920/