Розробка системи підписки на регулярні поставки для інтернет-магазину

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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

Розробка системи подписки на регулярні поставки для E-Commerce

Подписка змінює економіку інтернет-магазину: покупатель більше не приймає рішення про покупку кожен раз — він його прийняв один раз. Для магазину це передбачувана грошовий потік та зниження вартості повторної продажі. Технічно це одна з найскладніших задач у e-commerce: рекурентні платежі, управління розписанням, обробка збоїв оплати, управління паузами та скасуванням.

Архітектура системи

Система подписок складається з трьох незалежних шарів:

1. Subscription core — зберігання планів, подписок, періодів 2. Billing engine — рекурентні платежі, retry-логіка, dunning 3. 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 subscription_plan_items (
    id          BIGSERIAL PRIMARY KEY,
    plan_id     BIGINT NOT NULL REFERENCES subscription_plans(id),
    variant_id  BIGINT NOT NULL REFERENCES product_variants(id),
    qty         INTEGER NOT NULL DEFAULT 1,
    discount    NUMERIC(5,2) NOT NULL DEFAULT 0 -- скидка подписчика в %
);

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()
);

CREATE TABLE subscription_billing_attempts (
    id              BIGSERIAL PRIMARY KEY,
    subscription_id BIGINT NOT NULL REFERENCES subscriptions(id),
    amount          NUMERIC(12,2) NOT NULL,
    status          VARCHAR(50) NOT NULL, -- success | failed | pending
    payment_id      VARCHAR(255),
    failure_code    VARCHAR(100),
    failure_message TEXT,
    attempted_at    TIMESTAMP NOT NULL DEFAULT NOW(),
    next_retry_at   TIMESTAMP
);

Рекурентні платежи

Ключовий питання — який платіжний шлюз підтримує токенізацію карти та серверні списання без участі користувача:

Шлюз Рекурент Особливості
Stripe PaymentIntents + SetupIntents Найкраще API, SCA-ready
ЮКасса 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 — обробка просрочених платежів

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, але заказ створюється.

Управління паузами

Пауза — конкурентне преимущество перед повною відміною. Подписчик вказує дату возобновлення:

class SubscriptionManager
{
    public function pause(Subscription $subscription, Carbon $resumeAt): void
    {
        if ($resumeAt->lte(now())) {
            throw new InvalidResumeDateException();
        }

        $subscription->update([
            'status'      => 'paused',
            'paused_at'   => now(),
            'resumes_at'  => $resumeAt,
        ]);

        // Отменяємо наступне списання
        $subscription->pendingBillingJobs()->delete();

        // Плануємо возобновлення
        ResumeSubscription::dispatch($subscription)->delay($resumeAt);
    }
}

Максимальний термін паузи — конфігурюємий (зазвичай 3 місяці). Паузи в середині періоду: невикористане час не "сгорає" — наступний період починається від resumes_at.

Кастомізація подписки

Подписчик повинен управляти своєю подпиской самостійно:

  • Змінити склад (додати/убрати товари)
  • Змінити об'єм (×1, ×2 і т.д.)
  • Змінити інтервал (раз на 2 тижня → раз на місяць)
  • Пропустити один період
  • Поменяти адресу доставки
  • Оновити карту

Зміна тарифного плану mid-cycle потребує пересчета суми та можливого кредитування або доначислення:

// Upgrade в середині цикла: доcharge за різницю
$unusedDays = now()->diffInDays($subscription->current_period_end);
$totalDays  = $subscription->current_period_start->diffInDays($subscription->current_period_end);
$prorationFactor = $unusedDays / $totalDays;
$chargeNow = ($newPlan->price - $currentPlan->price) * $prorationFactor;

Генерація заказів

У день відправки створюється реальний заказ:

class SubscriptionOrderFactory
{
    public function createFromSubscription(Subscription $subscription): Order
    {
        return DB::transaction(function () use ($subscription) {
            $order = Order::create([
                'user_id'         => $subscription->user_id,
                'source'          => 'subscription',
                'subscription_id' => $subscription->id,
                'delivery_address_id' => $subscription->delivery_address_id,
            ]);

            foreach ($subscription->plan->items as $planItem) {
                $price = $planItem->variant->price
                    * (1 - $planItem->discount / 100);

                $order->items()->create([
                    'variant_id' => $planItem->variant_id,
                    'qty'        => $planItem->qty,
                    'unit_price' => $price,
                ]);

                $this->stockService->reserve($planItem->variant_id, $planItem->qty);
            }

            return $order;
        });
    }
}

Особистий кабінет подписчика

Мінімальний UI для управління подпиской:

  • Історія поставок з можливістю просмотру кожного заказу
  • Ближайша дата наступної відправки
  • Кнопки: "Пропустити наступну", "Пауза", "Відмовитись"
  • Управління складом та об'ємом
  • Історія платежів з можливістю скачати чек

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

  • Базові подписки + рекурентний біллінг (один шлюз): 7–10 днів
  • Dunning + retry-логіка + сповіщення: 3–4 дня
  • Управління паузами, пропусками, апгрейдом плану: 3–4 дня
  • Особистий кабінет подписчика: 3–5 днів
  • Аналітика (MRR, churn, LTV): +2–3 дня

Повна система: 3–5 тижнів залежно від платіжного шлюзу та складності тарифної сітки.