Приоритеты и прогнозные даты нескольких целей: что я бы не стал строить второй раз

Входная задача звучит вроде просто: У человека стабильная зарплата и несколько целей: подушка, ремонт, телефон, отпуск. Нужно для каждой цели показать дату, к которой она соберется, и пересчитывать даты после каждой получки или распределения накоплений. Я полгода делал такую модель для своего приложения и несколько раз переписывал. Ниже мой опыт, который может пригодится тем, кто считает даты из денежного потока.
Входные данные
Доход по датам выплат: аванс и зарплата, а не «доход в месяц». Период планирования идет от получки до получки
Обязательные платежи с датами, кредиты с датой окончания.
Кошелек на мелочи и непредвиденные риски.
Подушка: целевая сумма в месяцах обязательных расходов.
Цели: сумма, уже накоплено, желаемая дата, приоритет.
Премии: разовые и регулярные, например раз в квартал.
Свободные деньги периода = доход − обязательные − кошелек. Из них часть уходит в подушку, остальное в цели.
Два режима распределения
Пример: на цели остается 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% времени разработки. А пользователи показали, что каждый день им нужны две вещи: понятная цифра на сегодня и дата подушки. Сложная модель осталась внутри, моим "опытом", в общем как всегда начинать нужно было с кастдева :)
ПыСы тк делал продукт для решения своей боли и поиска точки где ИИ не справится с разработкой, в итоге считаю, что время потрачено не в пустую.
Приложение, Кубыш внутри которого все это выросло
Если считали похожие даты в своих продуктах, расскажите в комментариях, на каких граничных случаях споткнулись вы
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.