Вебхуки и повторы: как не потерять деньги на продлениях
Общие правила работы с платным API одинаковы везде: свой идентификатор операции, повтор вместо паники при обрыве связи, проверка подписи на входящих событиях. Про них отдельно написано в статье про идемпотентность и вебхуки. Здесь про то, чего у разовых товаров нет вовсе: подписка после выдачи продолжает жить, и ошибки в этой части стоят денег не один раз, а каждый месяц.
Чем подписка отличается от заказа
Заказ проходит путь от создания до доставки и на этом заканчивается. Подписка после выдачи меняет состояние сама: срок идёт, дата окончания приближается, клиент может сменить тариф, докупить трафик, заморозить доступ на время отпуска. Каждое из этих событий меняет то, что вы показываете человеку в кабинете, и то, за что вы с него берёте деньги.
Отсюда простое следствие. Состояние подписки нужно читать у платформы, а не считать самому от даты покупки. Своя арифметика сломается на первой же заморозке и на первом продлении, которое сделал не ваш код.
Ловушка первая: двойное продление
Клиент жмёт «Продлить», страница задумывается на пару секунд, он жмёт ещё раз. Если идентификатор операции у обоих запросов один, второй вернёт результат первого и денег не спишет. Если разный, вы продлили подписку дважды и списали с депозита две суммы.
Правило простое: идентификатор операции генерируется один раз в момент, когда человек нажал кнопку, и сохраняется у вас вместе с заказом до того, как ушёл запрос. Не в момент отправки, не в цикле повторов, а один раз на одно намерение пользователя. Повтор из-за таймаута сети берёт тот же идентификатор и потому безопасен.
Ловушка вторая: гонка с автопродлением
Если у подписки включено автопродление на стороне платформы, а вы дополнительно продлеваете её своим кодом, оба продления однажды случатся в одну минуту. Формально ошибки нет, оба списания законные. Клиент при этом заплатил один раз, а месяцев получил два, и разницу оплатили вы.
Решать это надо не блокировками, а выбором: либо продлением управляет платформа, либо ваш код. Смешивать два источника продления на одной подписке не стоит. Если продлеваете сами, автопродление держите выключенным и реагируйте на события об окончании срока.
Ловушка третья: истёкший срок
Продление всегда идёт от даты окончания, если она в будущем, и от текущего момента, если срок уже вышел. Это правильно с точки зрения денег, но неожиданно для интерфейса: человек, который продлил через неделю после окончания, получит новый срок с сегодняшнего дня, а не задним числом. Показывать ему предполагаемую дату стоит до оплаты, а не после.
Отдельно про заморозку: пока подписка заморожена, дни не идут. Значит, любые ваши расчёты вида «куплено 2 сентября, значит кончится 2 октября» будут врать. Дату окончания берите из ответа API.
Приём события: три обязательных шага
Обработчик события у вас должен делать ровно три вещи, и в таком порядке:
- проверить подпись по сырому телу запроса, до разбора JSON. Подпись считается по байтам, и пересобранный из объекта JSON даст другую строку;
- проверить, не обрабатывали ли вы уже это событие. Повторы приходят штатно, если ваш сервер ответил медленно или не ответил вовсе;
- быстро ответить кодом 2xx, а тяжёлую работу сделать после. Письма, начисления и походы в другие сервисы внутри обработчика превращают повтор в лавину.
Когда вебхук молчит
Вебхук это ускорение, а не источник правды. Ваш сервер мог лежать, домен мог не отвечать, повторы могли закончиться. Поэтому раз в сутки полезно сверять состояние подписок, у которых срок кончается в ближайшие дни, обычным запросом статуса. Это дешёвая страховка, которая ловит именно те случаи, когда клиент остался без доступа и пошёл жаловаться.
Проверить сам приём удобно тестовой отправкой: платформа шлёт на ваш адрес событие по запросу, и сразу видно, доходит оно и как ваш код на него отвечает.
Депозит кончился посреди месяца
Отдельный сценарий, который забывают заложить. Депозит опустел, покупка отбивается отказом, а у вас на этот случай нет ни письма себе, ни понятного текста покупателю. Человек видит непонятную ошибку, вы узнаёте о проблеме из его сообщения.
Минимум, который стоит сделать: отдельная ветка на код отказа по балансу, свой текст покупателю в духе «оплата не прошла, деньги не списаны, попробуйте позже» и уведомление вам. Остаток депозита виден отдельным запросом, и следить за ним лучше заранее, а не в момент отказа.
Чек-лист перед первым платящим клиентом
- идентификатор операции создаётся один раз на действие и сохраняется до запроса;
- повтор запроса после таймаута не списывает деньги второй раз;
- подпись события проверяется по сырому телу, а не по пересобранному JSON;
- повторное событие с тем же идентификатором не начисляет ничего дважды;
- дата окончания берётся из ответа API, а не считается у вас;
- автопродление и своё продление не работают на одной подписке одновременно;
- отказ по депозиту виден вам и понятен покупателю.
Ключ выдаётся в кабинете партнёра
Выдавайте и продлевайте VPN-подписки из своего сервиса. Оплата с депозита по вашей закупочной цене, розницу назначаете вы.