Реализация биллинга и тарифных планов для SaaS-приложения

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация биллинга и тарифных планов для SaaS-приложения
Сложная
~2-4 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

SaaS биллинг: подписные планы

Биллинг — критическая часть SaaS. Stripe — стандарт: Products, Prices, Subscriptions, Webhooks. Задача разработчика — связать бизнес-логику с событиями Stripe и не потерять деньги из-за пропущенных webhook'ов.

Структура данных Stripe

Product "Pro Plan"
  ├── Price (monthly): $29/month recurring
  └── Price (annual):  $290/year recurring

Product "Enterprise Plan"
  ├── Price (monthly): $99/month recurring
  └── Price (annual):  $990/year recurring
// Создание продуктов и цен через Stripe API
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// Создаём продукт
const product = await stripe.products.create({
  name: 'Pro Plan',
  description: 'For growing teams',
  metadata: { plan: 'pro' },
});

// Ежемесячная цена
const monthlyPrice = await stripe.prices.create({
  product: product.id,
  currency: 'usd',
  unit_amount: 2900, // $29.00
  recurring: {
    interval: 'month',
  },
  metadata: { billing_period: 'monthly' },
});

// Годовая цена
const annualPrice = await stripe.prices.create({
  product: product.id,
  currency: 'usd',
  unit_amount: 29000, // $290.00
  recurring: {
    interval: 'year',
  },
  metadata: { billing_period: 'annual' },
});

Схема данных

model Subscription {
  id                   String             @id @default(cuid())
  tenantId             String             @unique
  stripeCustomerId     String             @unique
  stripeSubscriptionId String?            @unique
  stripePriceId        String?
  plan                 Plan               @default(FREE)
  status               SubscriptionStatus @default(ACTIVE)
  currentPeriodStart   DateTime?
  currentPeriodEnd     DateTime?
  cancelAtPeriodEnd    Boolean            @default(false)
  canceledAt           DateTime?
  trialEnd             DateTime?

  tenant Tenant @relation(fields: [tenantId], references: [id])
}

enum Plan {
  FREE
  STARTER
  PRO
  ENTERPRISE
}

enum SubscriptionStatus {
  ACTIVE
  PAST_DUE
  CANCELED
  PAUSED
  TRIALING
}

Checkout: создание подписки

// app/api/billing/checkout/route.ts
export async function POST(request: Request) {
  const session = await auth();
  const { priceId, successUrl, cancelUrl } = await request.json();

  const tenant = await getCurrentTenant();
  const subscription = await db.subscription.findUnique({
    where: { tenantId: tenant!.id }
  });

  // Если нет Stripe Customer — создаём
  let customerId = subscription?.stripeCustomerId;
  if (!customerId) {
    const customer = await stripe.customers.create({
      email: session!.user.email!,
      name: tenant!.name,
      metadata: { tenantId: tenant!.id },
    });
    customerId = customer.id;
  }

  // Stripe Checkout Session
  const checkoutSession = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: 'subscription',
    payment_method_types: ['card'],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${successUrl}?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: cancelUrl,
    subscription_data: {
      trial_period_days: 14,
      metadata: { tenantId: tenant!.id },
    },
    allow_promotion_codes: true,
  });

  return Response.json({ url: checkoutSession.url });
}

Webhook: обработка событий

// app/api/webhooks/stripe/route.ts
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  // Идемпотентность: не обрабатываем дважды
  const processed = await db.stripeEvent.findUnique({
    where: { stripeEventId: event.id }
  });
  if (processed) return Response.json({ received: true });

  await db.stripeEvent.create({ data: { stripeEventId: event.id } });

  switch (event.type) {
    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription;
      const tenantId = subscription.metadata.tenantId;
      const plan = getPlanFromPrice(subscription.items.data[0].price.id);

      await db.subscription.upsert({
        where: { tenantId },
        create: {
          tenantId,
          stripeCustomerId: subscription.customer as string,
          stripeSubscriptionId: subscription.id,
          stripePriceId: subscription.items.data[0].price.id,
          plan,
          status: mapStripeStatus(subscription.status),
          currentPeriodStart: new Date(subscription.current_period_start * 1000),
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
          cancelAtPeriodEnd: subscription.cancel_at_period_end,
          trialEnd: subscription.trial_end
            ? new Date(subscription.trial_end * 1000)
            : null,
        },
        update: {
          plan,
          status: mapStripeStatus(subscription.status),
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
          cancelAtPeriodEnd: subscription.cancel_at_period_end,
        }
      });
      break;
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription;
      await db.subscription.update({
        where: { stripeSubscriptionId: subscription.id },
        data: { status: 'CANCELED', canceledAt: new Date() }
      });
      break;
    }

    case 'invoice.payment_failed': {
      // Уведомляем пользователя
      const invoice = event.data.object as Stripe.Invoice;
      await sendPaymentFailedEmail(invoice.customer_email!);
      break;
    }
  }

  return Response.json({ received: true });
}

Проверка лимитов плана

// lib/plan-limits.ts
const PLAN_LIMITS = {
  FREE:       { projects: 3,   members: 1,  storageGb: 1  },
  STARTER:    { projects: 10,  members: 5,  storageGb: 10 },
  PRO:        { projects: 50,  members: 20, storageGb: 100 },
  ENTERPRISE: { projects: Infinity, members: Infinity, storageGb: 1000 },
} as const;

export async function checkProjectLimit(tenantId: string) {
  const [subscription, projectCount] = await Promise.all([
    db.subscription.findUnique({ where: { tenantId } }),
    db.project.count({ where: { tenantId } }),
  ]);

  const plan = subscription?.plan ?? 'FREE';
  const limit = PLAN_LIMITS[plan].projects;

  if (projectCount >= limit) {
    throw new Error(`Project limit reached (${limit} for ${plan} plan)`);
  }
}

Настройка Stripe биллинга с планами, checkout flow и webhook-обработчиком — 3–5 рабочих дней.