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

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація білінгу та тарифних планів для SaaS-застосунку
Складна
~2-4 тижні
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • 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

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
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',
  },
});

const annualPrice = await stripe.prices.create({
  product: product.id,
  currency: 'usd',
  unit_amount: 29000, // $290.00
  recurring: {
    interval: 'year',
  },
});

Схема даних

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: створення підписки

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

  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;
  }

  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: обробка подій

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),
        },
        update: {
          plan,
          status: mapStripeStatus(subscription.status),
          currentPeriodEnd: new Date(subscription.current_period_end * 1000),
        }
      });
      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 });
}

Перевірка лімітів плану

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 },
};

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 робочих днів.