Реализация продажи подписок на цифровой контент на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация продажи подписок на цифровой контент на сайте
Сложная
~1-2 недели
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация продажи подписок на цифровой контент на сайте

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

Ключевые концепции

Plan — тарифный план с ценой, периодом и набором прав доступа (features).

Subscription — экземпляр подписки конкретного пользователя на конкретный план.

Billing period — расчётный период (месяц, год, квартал).

Grace period — льготный период после неуспешного списания до блокировки доступа.

Trial — пробный период без оплаты.

Схема данных

Schema::create('subscription_plans', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('slug')->unique();
    $table->decimal('price_monthly', 10, 2)->nullable();
    $table->decimal('price_yearly', 10, 2)->nullable();
    $table->integer('trial_days')->default(0);
    $table->jsonb('features'); // {"downloads_per_month": 50, "max_quality": "4k", ...}
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

Schema::create('subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained();
    $table->foreignId('plan_id')->constrained('subscription_plans');
    $table->enum('billing_period', ['monthly', 'yearly']);
    $table->enum('status', ['trialing', 'active', 'past_due', 'cancelled', 'expired']);
    $table->string('payment_provider'); // stripe, yookassa, paddle
    $table->string('provider_subscription_id')->nullable(); // ID в платёжной системе
    $table->string('provider_customer_id')->nullable();
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('current_period_start');
    $table->timestamp('current_period_end');
    $table->timestamp('grace_period_ends_at')->nullable();
    $table->timestamp('cancelled_at')->nullable();
    $table->timestamp('ends_at')->nullable(); // null = до отмены
    $table->timestamps();

    $table->index(['user_id', 'status']);
    $table->index('current_period_end');
});

Создание подписки

class CreateSubscriptionAction
{
    public function execute(User $user, Plan $plan, string $billingPeriod, string $paymentMethodId): Subscription
    {
        $provider = PaymentProviderFactory::make(config('subscriptions.provider'));

        // Создаём или получаем клиента в платёжной системе
        $customerId = $user->payment_customer_id
            ?? $this->createCustomer($provider, $user);

        // Создаём подписку в платёжной системе
        $providerSub = $provider->createSubscription([
            'customer'       => $customerId,
            'payment_method' => $paymentMethodId,
            'price_id'       => $plan->getProviderPriceId($billingPeriod),
            'trial_end'      => $plan->trial_days > 0
                ? now()->addDays($plan->trial_days)->timestamp
                : 'now',
        ]);

        return DB::transaction(function () use ($user, $plan, $billingPeriod, $customerId, $providerSub) {
            $subscription = Subscription::create([
                'user_id'               => $user->id,
                'plan_id'               => $plan->id,
                'billing_period'        => $billingPeriod,
                'status'                => $providerSub->status, // trialing или active
                'payment_provider'      => config('subscriptions.provider'),
                'provider_subscription_id' => $providerSub->id,
                'provider_customer_id'  => $customerId,
                'trial_ends_at'         => $plan->trial_days > 0
                    ? now()->addDays($plan->trial_days)
                    : null,
                'current_period_start'  => now(),
                'current_period_end'    => $this->calcPeriodEnd($billingPeriod),
            ]);

            event(new SubscriptionCreatedEvent($subscription));

            return $subscription;
        });
    }
}

Webhook от платёжной системы

Все изменения статуса подписки приходят через webhook:

class StripeWebhookHandler
{
    public function handle(array $payload): void
    {
        match($payload['type']) {
            'invoice.paid'                     => $this->onInvoicePaid($payload),
            'invoice.payment_failed'           => $this->onPaymentFailed($payload),
            'customer.subscription.updated'    => $this->onSubscriptionUpdated($payload),
            'customer.subscription.deleted'    => $this->onSubscriptionDeleted($payload),
            default => null,
        };
    }

    private function onInvoicePaid(array $payload): void
    {
        $providerSubId = $payload['data']['object']['subscription'];
        $sub = Subscription::where('provider_subscription_id', $providerSubId)->firstOrFail();

        $sub->update([
            'status'              => 'active',
            'grace_period_ends_at' => null,
            'current_period_start' => Carbon::createFromTimestamp($payload['data']['object']['period_start']),
            'current_period_end'   => Carbon::createFromTimestamp($payload['data']['object']['period_end']),
        ]);

        event(new SubscriptionRenewedEvent($sub));
    }

    private function onPaymentFailed(array $payload): void
    {
        $providerSubId = $payload['data']['object']['subscription'];
        $sub = Subscription::where('provider_subscription_id', $providerSubId)->firstOrFail();

        $sub->update([
            'status'              => 'past_due',
            'grace_period_ends_at' => now()->addDays(config('subscriptions.grace_period_days', 3)),
        ]);

        // Уведомляем пользователя
        $sub->user->notify(new PaymentFailedNotification($sub));

        event(new SubscriptionPaymentFailedEvent($sub));
    }
}

Контроль доступа к контенту

class SubscriptionAccessGuard
{
    public function canAccess(User $user, string $feature): bool
    {
        $subscription = $user->activeSubscription();

        if (!$subscription) {
            return false;
        }

        // active или в grace period
        if (!in_array($subscription->status, ['active', 'trialing', 'past_due'])) {
            return false;
        }

        // В grace period — доступ сохраняется
        if ($subscription->status === 'past_due') {
            if ($subscription->grace_period_ends_at?->isPast()) {
                return false;
            }
        }

        // Проверяем feature в тарифном плане
        $features = $subscription->plan->features;
        return isset($features[$feature]) && $features[$feature] !== false;
    }

    public function getFeatureValue(User $user, string $feature): mixed
    {
        $subscription = $user->activeSubscription();
        return $subscription?->plan->features[$feature] ?? null;
    }
}

Апгрейд и даунгрейд тарифа

class ChangePlanAction
{
    public function execute(Subscription $sub, Plan $newPlan, string $billingPeriod): void
    {
        $provider = PaymentProviderFactory::make($sub->payment_provider);

        // Stripe поддерживает немедленный апгрейд с пропорциональным расчётом
        $provider->updateSubscription($sub->provider_subscription_id, [
            'items' => [[
                'id'    => $sub->provider_item_id,
                'price' => $newPlan->getProviderPriceId($billingPeriod),
            ]],
            'proration_behavior' => 'always_invoice', // сразу выставить счёт за разницу
        ]);

        $sub->update([
            'plan_id'        => $newPlan->id,
            'billing_period' => $billingPeriod,
        ]);

        event(new SubscriptionPlanChangedEvent($sub, $newPlan));
    }
}

Отмена подписки

Два варианта отмены:

  1. Немедленная — доступ прекращается сразу, возможен возврат
  2. В конце периода — доступ до current_period_end, без возврата
public function cancel(Subscription $sub, bool $immediately = false): void
{
    $provider = PaymentProviderFactory::make($sub->payment_provider);

    if ($immediately) {
        $provider->cancelSubscription($sub->provider_subscription_id);
        $sub->update(['status' => 'cancelled', 'ends_at' => now(), 'cancelled_at' => now()]);
    } else {
        $provider->cancelSubscriptionAtPeriodEnd($sub->provider_subscription_id);
        $sub->update(['cancelled_at' => now(), 'ends_at' => $sub->current_period_end]);
    }
}

Лимиты в рамках периода

Если тариф ограничивает количество скачиваний в месяц:

class MonthlyDownloadLimiter
{
    public function canDownload(User $user): bool
    {
        $subscription = $user->activeSubscription();
        $limit = $subscription?->plan->features['downloads_per_month'] ?? 0;

        if ($limit === -1) return true; // безлимит

        $used = DownloadEvent::where('user_id', $user->id)
            ->where('downloaded_at', '>=', $subscription->current_period_start)
            ->count();

        return $used < $limit;
    }
}

Сроки реализации

Компонент Срок
Схема данных, модели, базовые CRUD 2 дня
Создание подписки + интеграция Stripe/ЮKassa 3 дня
Webhook-обработчик (оплата, сбой, отмена) 2 дня
Контроль доступа к контенту 1 день
Апгрейд / даунгрейд тарифа 2 дня
Личный кабинет (управление подпиской) 2 дня
Уведомления (продление, сбой, истечение) 1 день
Тестирование всех сценариев 3 дня

Итого: 16–20 рабочих дней для полнофункциональной подписочной системы.