Мы разрабатываем системы подписок для интернет-магазинов с 2018 года. Подписка меняет экономику: покупатель принимает решение о покупке один раз — это даёт магазину предсказуемый денежный поток и снижает стоимость повторной продажи на 30–40%. Технически это одна из сложнейших задач в e-commerce: рекуррентные платежи, управление расписанием, обработка сбоев оплаты, управление паузами и отменами. Неверно спроектированная система приводит к утечке клиентов на этапе dunning. Мы закладываем в архитектуру отказоустойчивость с первого дня.
Наш опыт показывает: правильно настроенная подписка увеличивает LTV в 2–3 раза. Мы гарантируем, что система будет работать без сбоев при любой нагрузке, благодаря test-driven development и code review. В этой статье разберём, как построить систему подписок с нуля: от схемы данных до личного кабинета.
Архитектура системы
Система подписок состоит из трёх независимых слоёв:
- Subscription core — хранение планов, подписок, периодов
- Billing engine — рекуррентные платежи, retry-логика, dunning
- Fulfillment — генерация заказов и управление отгрузкой
Разделение принципиально: сбой биллинга не должен блокировать выполнение уже оплаченных подписок, а изменение плана не должно ломать текущие биллинг-циклы.
Схема данных
CREATE TABLE subscription_plans ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, slug VARCHAR(100) NOT NULL UNIQUE, interval_type VARCHAR(20) NOT NULL, -- day | week | month | year interval_count INTEGER NOT NULL DEFAULT 1, trial_days INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT true ); CREATE TABLE subscriptions ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), plan_id BIGINT NOT NULL REFERENCES subscription_plans(id), status VARCHAR(50) NOT NULL DEFAULT 'active', -- trial | active | paused | past_due | cancelled | expired payment_method_id VARCHAR(255) NOT NULL, current_period_start TIMESTAMP NOT NULL, current_period_end TIMESTAMP NOT NULL, trial_ends_at TIMESTAMP, paused_at TIMESTAMP, resumes_at TIMESTAMP, cancelled_at TIMESTAMP, cancel_reason TEXT, created_at TIMESTAMP NOT NULL DEFAULT NOW() ); Рекуррентные платежи
Ключевой вопрос — какой платёжный шлюз поддерживает токенизацию карты и серверные списания без участия пользователя. Stripe рекомендует использовать SetupIntents для рекуррентных платежей (использование PaymentIntents + SetupIntents — стандарт индустрии).
| Шлюз | Рекуррент | Особенности |
|---|---|---|
| Stripe | PaymentIntents + SetupIntents | Лучшее API, SCA-ready |
| ЮKassa | recurring_object | Актуально для РФ |
| CloudPayments | token-платежи | Подходит для СНГ |
| Robokassa | Recurring | Базовый функционал |
Пример с CloudPayments — типичный для Беларуси и России:
class RecurringBillingService { public function chargeSubscription(Subscription $subscription): BillingAttempt { $amount = $this->calculateAmount($subscription); $attempt = BillingAttempt::create([ 'subscription_id' => $subscription->id, 'amount' => $amount, 'status' => 'pending', ]); try { $result = $this->cloudpayments->chargeToken([ 'Token' => $subscription->payment_method_id, 'Amount' => $amount, 'Currency' => 'RUB', 'AccountId' => $subscription->user_id, 'Description' => "Подписка #{$subscription->id}", ]); $attempt->update(['status' => 'success', 'payment_id' => $result->TransactionId]); $this->advancePeriod($subscription); } catch (PaymentDeclinedException $e) { $attempt->update([ 'status' => 'failed', 'failure_code' => $e->getCode(), 'failure_message' => $e->getMessage(), 'next_retry_at' => $this->calculateRetryTime($attempt), ]); $this->handleFailedPayment($subscription, $attempt); } return $attempt; } } Как обрабатываются сбои платежей?
Dunning — это процесс повторных попыток списания при неудаче. Стандартная схема:
- День 0: списание не прошло → статус past_due, попытка 1
- День 3: retry попытка 2 + email "Проблема с оплатой"
- День 7: retry попытка 3 + email с кнопкой обновить карту
- День 14: retry попытка 4 + SMS
- День 21: подписка отменяется автоматически
Каждый шаг — джоб в очереди:
// Планируется при неудачном списании RetrySubscriptionPayment::dispatch($subscription) ->delay(now()->addDays(3)); // Планируется при второй неудаче SendDunningEmail::dispatch($subscription, 'update_card') ->delay(now()->addDays(7)); Важно: при неудаче сервис всё равно отгружает текущий период — подписчик не должен страдать за технический сбой. Подписка переходит в past_due, но заказ создаётся.
Почему пауза выгоднее отмены?
Пауза — конкурентное преимущество перед полной отменой. Подписчик указывает дату возобновления. Максимальный срок паузы конфигурируется (обычно 3 месяца). Паузы в середине периода: неиспользованное время не сгорает — следующий период начинается от даты возобновления.
Подписчик должен управлять своей подпиской самостоятельно: изменить состав, объём, интервал, пропустить период, поменять адрес доставки или обновить карту. Изменение тарифного плана mid-cycle требует пересчёта суммы с пропорциональным кредитованием или доначислением.
Генерация заказов
В день отгрузки создаётся реальный заказ. Наша реализация использует Repository pattern и Transactional outbox для гарантии создания заказа даже при сбое.
Личный кабинет подписчика
Минимальный UI для управления подпиской:
- История поставок с возможностью просмотра каждого заказа
- Ближайшая дата следующей отгрузки
- кнопки: «Пропустить следующую», «Пауза», «Отменить»
- Управление составом и объёмом
- История платежей с возможностью скачать чек
Что входит в работу
- Документация архитектуры и API
- Исходный код с покрытием тестами (>80%)
- Интеграция с платёжным шлюзом
- Личный кабинет подписчика
- Инструкция по эксплуатации и обучение команды
- Гарантия 3 месяца после запуска
Мы — команда инженеров с 5+ лет опыта в e-commerce. Реализовали 50+ проектов, включая системы подписок для магазинов с оборотом до 1 млн $ в месяц. Наше кастомное решение гибче готовых плагинов типа ReCharge в несколько раз по настройке тарифов и правил отмены.
Сроки реализации
- Базовые подписки и рекуррентный биллинг (один шлюз): 7–10 дней
- Dunning и retry-логика с уведомлениями: 3–4 дня
- Управление паузами, пропусками, апгрейдом плана: 3–4 дня
- Личный кабинет подписчика: 3–5 дней
- Аналитика (MRR, churn, LTV): +2–3 дня
Полная система: 3–5 недель в зависимости от платёжного шлюза и сложности тарифной сетки. Свяжитесь с нами для оценки вашего проекта — мы подготовим коммерческое предложение за 1–2 дня.







