Реализация подписочной модели оплаты на сайте
Подписочная модель — один из самых предсказуемых источников дохода для онлайн-сервисов. Но техническая реализация сложнее разовых платежей: нужно управлять жизненным циклом подписки, обрабатывать сбои при повторных списаниях, обеспечивать grace period и корректно работать с апгрейдами/даунгрейдами тарифа.
Модель данных
plans (
id, name, billing_period: monthly | yearly | weekly,
price, currency,
trial_days, features (jsonb),
is_active
)
subscriptions (
id, user_id, plan_id,
status: trialing | active | past_due | canceled | expired,
current_period_start, current_period_end,
trial_ends_at,
cancel_at_period_end (boolean),
canceled_at,
payment_method_id,
external_subscription_id (id в платёжной системе)
)
subscription_invoices (
id, subscription_id, amount, currency,
status: draft | open | paid | failed | void,
attempt_count, next_attempt_at,
paid_at, payment_id
)
Выбор платёжного провайдера
Stripe Billing — лучший вариант для международного SaaS. Встроенное управление подписками, Smart Retries, Customer Portal из коробки. Stripe сам управляет retry-логикой при неудачных списаниях.
ЮKassa + собственная логика — для российского рынка. ЮKassa поддерживает автоплатежи через сохранённые карты (recurring payments). Логику управления подпиской придётся строить самостоятельно.
CloudPayments — хороший вариант для РФ, есть встроенные подписки с Webhooks.
Жизненный цикл подписки
Регистрация
↓
Trial (7/14/30 дней) → Auto-convert to Active
↓
Active → списание в конце периода → новый период
↓ ↓
Cancel Payment Failed
↓ ↓
Cancel at Past Due (grace period 3-7 дней)
period end ↓
↓ Retry (1, 3, 7 дней)
Expired ↓
Expired (после N неудачных попыток)
Автоматические списания
При использовании Stripe: подписка создаётся один раз, Stripe сам управляет renewals. При использовании ЮKassa — нужен собственный scheduler:
// Scheduled Job: каждый час
$dueSubscriptions = Subscription::where('status', 'active')
->where('current_period_end', '<=', now())
->get();
foreach ($dueSubscriptions as $subscription) {
dispatch(new RenewSubscriptionJob($subscription));
}
RenewSubscriptionJob пытается провести списание через сохранённый payment method. При успехе — обновляет current_period_end. При неудаче — переводит в past_due и планирует повторные попытки.
Grace Period и умные повторные попытки
После первого неудачного списания подписка переходит в past_due — пользователь сохраняет доступ, но получает уведомления. Повторные попытки:
- +1 день: первый retry
- +3 дня: второй retry с email-напоминанием
- +7 дней: финальная попытка, предупреждение об отключении
- +10 дней: статус
expired, доступ отозван
Stripe Dunning Management делает это автоматически с ML-выбором времени retry.
Апгрейд и даунгрейд тарифа
Смена тарифа — нетривиальная задача. Пересчёт суммы выполняется пропорционально остатку периода:
- Апгрейд (на более дорогой тариф): немедленно, доплата за оставшееся время периода
- Даунгрейд (на более дешёвый): вступает в силу с начала следующего периода, кредит учитывается
$unusedDays = $subscription->daysRemainingInPeriod();
$creditAmount = $unusedDays * ($currentPlan->dailyPrice());
$chargeAmount = $unusedDays * ($newPlan->dailyPrice()) - $creditAmount;
Клиентский портал и управление подпиской
Пользователь должен иметь возможность:
- Просматривать текущий тариф и дату следующего списания
- Менять тариф (с расчётом пропорции)
- Обновлять платёжный метод (ввод новой карты через hosted fields)
- Отменять подписку с пояснением причины (exit survey)
- Скачивать инвойсы
Stripe предоставляет готовый Customer Portal — hosted-страница для управления. Для кастомного UI на ЮKassa нужно строить своё.
Предотвращение churn
Технические решения для снижения отказа от подписок:
- Email за 3 и 7 дней до списания с напоминанием суммы
- Уведомление об истечении карты за 30 дней
- Возможность поставить подписку на паузу (вместо отмены)
- Оффер при отмене: скидка 20% на следующий месяц
Аналитика подписок
Ключевые метрики: MRR (Monthly Recurring Revenue), Churn Rate, LTV, Trial-to-Paid Conversion, Average Revenue Per User. Для расчёта нужны специализированные запросы по историческим данным подписок — не достаточно просто суммировать платежи.
Срок разработки: 4–6 недель для полной системы с управлением lifecycle, retry-логикой, клиентским порталом и базовой аналитикой.







