Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Рекуррентные платежи и подписки

Рекуррентные платежи: модель подписки и повторные списания

Рекуррентные платежи: сохранённый способ оплаты, цикл списания, повторные попытки при отказе, отмена подписки и уведомление пользователя.

Прямой ответ

Рекуррентный платёж — списание по сохранённому способу оплаты без участия пользователя в каждом цикле. Модель строится из трёх сущностей: подписка, сохранённый способ оплаты и платёж цикла.

CREATE TABLE subscriptions (
    id             bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id        bigint NOT NULL,
    payment_method text NOT NULL,
    amount_minor   bigint NOT NULL,
    currency       char(3) NOT NULL,
    period         interval NOT NULL,
    status         text NOT NULL,
    current_period_end timestamptz NOT NULL,
    canceled_at    timestamptz
);

Цикл списания определяется периодом и датой окончания текущего периода. Следующее списание выполняется после окончания периода, а не по календарной дате, поскольку месяцы имеют разную длину.

Состояние подписки Списание Доступ к услуге
Активна Выполняется по периоду Открыт
Просрочена Повторные попытки Ограничен либо открыт по политике
Приостановлена Не выполняется По политике сервиса
Отменена Не выполняется До конца оплаченного периода

Отмена не прекращает доступ немедленно, если период оплачен. Практика — доступ сохраняется до окончания оплаченного периода, а следующее списание не выполняется.

Списание выполняется фоновой задачей по расписанию:

due = db.query("""
    SELECT id, payment_method, amount_minor, currency
    FROM subscriptions
    WHERE status = 'active' AND current_period_end <= now()
    FOR UPDATE SKIP LOCKED
""")

Захват строк с пропуском заблокированных позволяет выполнять списания параллельно без дублей. О состояниях платежа — в статье про жизненный цикл статусов.

Повторные попытки и поведение при отказе

Списание по сохранённому способу оплаты отклоняется по причинам, которые делятся на временные и постоянные. Различие определяет стратегию повторов.

Временные причины. Недостаточно средств, ограничение банка, сбой на стороне платёжной системы. Повтор через интервал имеет смысл, поскольку причина может устраниться.

Постоянные причины. Способ оплаты заблокирован либо истёк, подписка отменена пользователем в банке. Повтор не даст результата и требует запроса нового способа оплаты.

Стратегия повторов включает три элемента.

Число попыток. Ограничивается политикой сервиса, обычно от двух до четырёх попыток.

Интервал между попытками. Растёт с каждой попыткой, поскольку немедленный повтор при недостатке средств неэффективен.

Действие после исчерпания попыток. Подписка переводится в просроченную либо отменённую, а доступ ограничивается.

Практическое следствие: уведомление пользователя о предстоящем списании и о неудачной попытке снижает число отмен. Пользователь, получивший уведомление об отказе, обновляет способ оплаты, тогда как молчаливое списание приводит к обращению в поддержку.

Отдельный вопрос — сохранённый способ оплаты. Данные карты не хранятся в сервисе, используется токен провайдера; о токенизации — в статье про токенизацию банковских карт. Токен имеет срок действия, и его истечение требует обновления способа оплаты.

Расчёт даты следующего периода выполняется с учётом длины месяца:

UPDATE subscriptions
SET current_period_end = current_period_end + period
WHERE id = $1;

Прибавление интервала к дате корректно обрабатывает месяцы разной длины, тогда как прибавление фиксированного числа дней смещает дату списания.

Практика: реализация рекуррентных списаний

1. Храните подписку и платёж раздельно. Подписка определяет условия, платёж фиксирует факт списания, поэтому смешение сущностей усложняет учёт.

2. Захватывайте строки с пропуском заблокированных. Параллельное выполнение списаний без дублей обеспечивается на уровне базы.

3. Различайте временные и постоянные отказы. Повтор выполняется только при временной причине, постоянная требует нового способа оплаты.

4. Ограничивайте число попыток. Бесконечные повторы создают нагрузку и не приводят к списанию.

5. Уведомляйте о предстоящем списании. Уведомление снижает число споров и обращений в поддержку.

6. Сохраняйте доступ до конца оплаченного периода. Отмена прекращает следующее списание, но не прерывает оплаченный период.

7. Ведите историю списаний. История показывает число попыток и причины отказов, что требуется при разборе обращений.

8. Проверяйте токен способа оплаты. Истёкший токен приводит к отказу списания, поэтому проверяется до запроса.

Рекомендации:

  1. Не выполняйте списание без сохранённого подтверждения согласия. Рекуррентные платежи требуют явного согласия пользователя на повторные списания.
  2. Учитывайте изменение тарифа. Изменение суммы применяется с нового периода, а не задним числом.
  3. Проверяйте часовые пояса при расчёте периода. Дата окончания периода хранится с отметкой времени, иначе списание смещается; о работе с датой — в статье про интеграцию шлюза.
  4. Мониторьте долю отказов списания. Рост доли отказов указывает на проблему со способом оплаты либо с платёжной системой.
  5. Документируйте политику повторов. Число попыток и интервалы фиксируются, поскольку влияют на доступ пользователя.

Практический сценарий: двойное списание за один период

Контекст: пользователь получил два списания за один месячный период подписки.

  1. Диагностика. В истории платежей две записи с одинаковой суммой и интервалом в несколько минут. Дата окончания периода в подписке не изменилась между списаниями.
  2. Причина. Задача списания выполнялась двумя процессами одновременно, а захват строк не использовался. Оба процесса выбрали одну и ту же подписку и выполнили списание до обновления даты периода.
  3. Действия. (а) Добавили захват строк с пропуском заблокированных, чтобы параллельные процессы выбирали разные подписки; (б) перенесли обновление даты периода в ту же транзакцию, что и создание платежа; (в) добавили уникальное ограничение на пару «подписка — период», чтобы второй платёж за период отклонялся; (г) выполнили возврат по второму платежу; (д) добавили проверку дублей платежей за период в периодическую сверку.
  4. Результат. Параллельное выполнение задачи не приводит к двойному списанию, поскольку строка захватывается, а дубли отклоняются ограничением.

Вывод: списание по подписке требует захвата строки и обновления периода в одной транзакции с созданием платежа. Уникальное ограничение на пару «подписка — период» исключает дубли на уровне базы, а сверка обнаруживает расхождения, возникшие до внедрения защиты.

FAQ

Нужно ли согласие пользователя на рекуррентные списания? Да. Повторные списания выполняются по явному согласию, а его отсутствие делает списание неправомерным.

Что делать при истечении срока действия карты? Запросить новый способ оплаты. Токен истёкшей карты приводит к отказу списания, поэтому пользователь уведомляется заранее.

Прекращается ли доступ сразу после отмены? Обычно нет. Доступ сохраняется до конца оплаченного периода, а следующее списание не выполняется.

Как часто повторять неудачное списание? По политике сервиса, обычно две-четыре попытки с растущим интервалом. При постоянной причине отказа повтор не выполняется.

Источники: Stripe Billing subscriptions, PayPal Subscriptions API, PostgreSQL date/time functions — ссылки в шапке.