Биллинг и подписки: как считать деньги внутри платформы
Биллинг внутри платформы — это не «прикрутить оплату», а отдельная подсистема, которая знает про баланс, тарифы, списания и историю каждого рубля. Расскажу, как мы делали её в Papa BU, платформе перепродажи, и что там пришлось переделывать.
Задача, которая выглядела простой
Papa BU собирает поисковое ядро из Вордстата, пишет объявления и работает с Avito API. У каждого действия внутри есть себестоимость: запрос к внешнему сервису, генерация текста, размещение.
Первая формулировка от заказчика звучала так: «пусть у пользователя будет баланс, и с него списывается за работу». Ровно одна фраза, и в ней спрятаны примерно шесть решений.
Что считается единицей списания. Когда именно списывается. Что происходит, если действие не удалось.
Что видно пользователю. Что происходит при нуле. Кто может править баланс руками.
Мы начали с того, что выписали эти вопросы на бумагу и ответили на каждый до того, как написали строку кода. Это сэкономило больше времени, чем любая библиотека.
Первое решение: баланс — не число, а сумма операций
Соблазн держать баланс отдельным полем и менять его при каждом действии огромен. Он же и самый дорогой.
Поле с балансом ничего не помнит. Когда пользователь пишет «у меня было 4 000, а стало 1 200, почему», по такому полю ответить нечем.
Поэтому баланс у нас не хранится как число. Хранится журнал операций: пополнение, списание за действие, возврат, корректировка администратором. Каждая запись содержит сумму, тип, время, ссылку на объект и автора.
Текущий остаток — это сумма всех операций. Для скорости она кешируется, но источник правды — журнал. Любой вопрос о деньгах превращается в выписку, которую можно показать пользователю целиком.
Второе решение: списывать после результата, а не до
Первая версия списывала деньги в момент запуска действия. Логика казалась честной: пользователь нажал, ресурс потрачен.
На практике внешние сервисы отвечают ошибками. Запрос к API падает по таймауту, размещение отклоняется, генерация возвращает пустоту. Деньги при этом уже ушли, и пользователь справедливо злится.
Мы переделали на двухшаговую схему. При запуске сумма резервируется — она перестаёт быть доступной, но ещё не списана. При успешном завершении резерв превращается в списание. При ошибке резерв снимается, и деньги возвращаются на доступный остаток.
Это на одну сущность больше и на несколько дней работы дороже. Зато исчез целый класс обращений в поддержку, каждое из которых раньше решалось ручной корректировкой.
Третье решение: защита от двойного списания
Одна и та же операция может прийти дважды. Пользователь дважды нажал кнопку, сеть повторила запрос, внешний сервис прислал уведомление о платеже дважды.
Лечится это ключом операции. Каждое списание и каждое пополнение получают идентификатор, который вычисляется из содержания, а не из времени. Повторная запись с тем же ключом не создаёт вторую операцию, а возвращает первую.
Особенно это касается пополнений. Платёжный сервис имеет право прислать уведомление об одном платеже несколько раз, и это нормальное поведение, а не сбой. Система, которая на каждое уведомление начисляет деньги, дарит клиенту столько пополнений, сколько уведомлений пришло.
Ночь, когда остаток разошёлся
Через несколько недель после запуска биллинга сумма в кеше перестала сходиться с журналом у одного из пользователей. Разница была небольшой и легко списывалась на «наверное, где-то округление».
Округления там не было. Была гонка: два действия завершались одновременно и оба пересчитывали кеш из состояния, которое уже устарело. Побеждало то, что записалось вторым, и одно списание пропадало из итога.
Ошибка была не в деньгах — журнал оставался верным. Ошибка была в витрине, и это худший вид расхождения, потому что пользователь видит одно, а система знает другое.
Починили двумя правками. Кеш перестал пересчитываться из старого значения и стал вычисляться запросом по журналу. Плюс появилась ночная сверка: если кеш и журнал разошлись, запись попадает в отдельный список, и его видит администратор.
С тех пор правило простое: любое число про деньги на экране должно уметь разложиться в список операций по нажатию. Если разложить нельзя, число подозрительно.
Тарифы живут в данных, а не в коде
Цену действия хочется написать прямо в коде — один раз и забыть. Через месяц владелец меняет её, и правка цены становится задачей разработчику с выкладкой на сервер.
Мы вынесли тарифы в отдельную таблицу: тип действия, цена, дата начала действия цены. Старые операции продолжают ссылаться на ту цену, которая была в момент списания, поэтому история не переписывается задним числом.
Это же позволило сделать индивидуальные условия для отдельных пользователей без ветвлений в коде. Персональный тариф — просто ещё одна запись с признаком, к кому она относится.
Что показывать пользователю
Экран баланса в итоге состоит из четырёх вещей: доступный остаток, сумма в резерве с расшифровкой, история операций с фильтром по типу и прогноз, на сколько дней хватит при текущем расходе.
Прогноз мы добавили последним и по просьбе владельца. Он оказался самой полезной частью экрана, потому что превращает абстрактную цифру в понятное «до среды».
Права доступа устроены как в остальных наших проектах: оператор видит расход по своим объявлениям, но не видит финансовых настроек и не может корректировать баланс. Корректировка руками доступна одному человеку и всегда оставляет запись с комментарием.
Кому биллинг не нужен
Если у вас десять клиентов и фиксированная абонплата, вся эта конструкция — лишние деньги. Счёт раз в месяц и таблица закрывают задачу полностью, а разработка биллинга по нашим ценам попадает в категорию платформы: от 150 000 ₽ и от двух недель.
Биллинг оправдан, когда цена зависит от объёма, клиентов десятки, а вопросы «за что списали» начинают отнимать рабочее время. До этого момента честнее считать руками. Другие разборы платформенных задач лежат в блоге.
Коротко
Баланс — это журнал операций, а не поле с числом. Списание делается в два шага: резерв при запуске, списание при успехе, возврат при ошибке. Каждая операция получает ключ, чтобы повтор не создавал вторую запись. Любая цифра про деньги на экране обязана раскладываться в список операций, а расхождение витрины с журналом ищется ночной сверкой, а не жалобой пользователя.
Первый разговор — разбор задачи: нужна ли вам система или хватит одной страницы. Без обязательств и без счёта.
Частые вопросы
Можно ли на старте обойтись без биллинга и брать оплату вручную?
Можно и часто нужно. Пока пользователей меньше двадцати, счёт руками и таблица дешевле любой разработки. Биллинг имеет смысл строить, когда ручное выставление начинает занимать больше времени, чем поддержка самого продукта.
Чем подписка отличается от оплаты за использование?
Подписка — фиксированная сумма за период, её легко считать и легко объяснить клиенту. Оплата за использование зависит от количества действий и требует счётчика событий и защиты от двойного счёта. В одной платформе эти модели часто живут вместе: абонплата плюс расход.
Что делать, если у клиента кончились деньги посреди месяца?
Заранее решить, что именно останавливается. Мы отделяем чтение от действий: данные и история остаются доступны, а платные действия перестают запускаться. Полное отключение доступа на нулевом балансе приводит к тому, что клиент не может даже пополнить счёт.
Те, что не помогают, мы переписываем — по этой кнопке и решаем, какие именно.
Ещё по теме
Что малому бизнесу от CRM реально нужно, чем готовая система отличается от своей внутри сайта и по каким признакам выбирать между ними.
тем, кто заказывает CRM купили, а работать в ней не стали: разбор причинШесть ошибок, из-за которых оплаченная CRM превращается в пустую базу, и что делать с каждой, пока подписка ещё действует.
тем, кто заказывает Freedom Gym: клубная система с абонементами и расписаниемИстория одного проекта: как из журнала на ресепшене выросла клубная система с абонементами, расписанием и правами тренеров, и что пошло не так.
тем, кто заказывает