Leyla покупает в магазине книги и хочет «Prowid Shop Plus»: бесплатная доставка за 9.90 AZN в месяц. Нажимать «Оплатить» каждый месяц она не станет: деньги должны списываться сами, пока она не откажется. Это подписка, и у неё три сложности: регулярные списания, смена и отмена тарифа и карты, которые иногда не проходят. Всё это берёт на себя Stripe Billing, а нам остаётся хранить у себя состояние доступа.

Объекты подписки

Product — что продаём («Prowid Shop Plus»), Price — сколько и как часто (9.90 AZN, recurring.interval: 'month'), Customer — покупатель. Subscription — договор между покупателем и ценой: «списывать 9.90 AZN каждый месяц». Каждый период Stripe выпускает Invoice (счёт) и оплачивает его через PaymentIntent (урок 041). Аналогия: абонемент в спортзал: договор один, а счёт приходит каждый месяц.

Объекты Stripe Billing: Product и Price, Customer, Subscription, Invoice и PaymentIntent, а в вашей базе таблицы User и Subscription, которые обновляют вебхуки
Объекты Stripe Billing: Product и Price, Customer, Subscription, Invoice и PaymentIntent, а в вашей базе таблицы User и Subscription, которые обновляют вебхуки

Stripe хранит свои объекты, а ваша база — копию состояния: кто подписан и до какого числа. Приложение читает свою базу, а не Stripe.

Цена и Checkout в режиме подписки

Product и рекуррентный Price создают один раз, в Dashboard или скриптом:

js
// scripts/create-plus.js  (run once)
import { stripe } from '../src/stripe.js';

const product = await stripe.products.create({ name: 'Prowid Shop Plus' });

const price = await stripe.prices.create({
  product: product.id,
  currency: 'azn',
  unit_amount: 990, // 9.90 AZN every month
  recurring: { interval: 'month' },
});

console.log(price.id); // price_... -> PLUS_PRICE_ID in .env

Id цены положите в .env как PLUS_PRICE_ID. Покупателю нужен Customer: создаём его при первой подписке и запоминаем stripeCustomerId в User. Затем Checkout Session с mode: 'subscription':

js
// src/billing.js
import { stripe } from './stripe.js';
import { prisma } from './db.js';
import { handleOnce } from './idempotent.js';

export async function startSubscription(req, res) {
  const user = req.user; // from your auth middleware, never from the body
  let customerId = user.stripeCustomerId;

  if (!customerId) {
    const customer = await stripe.customers.create(
      { email: user.email, metadata: { userId: String(user.id) } },
      { idempotencyKey: `user-${user.id}-customer` },
    );
    customerId = customer.id;
    await prisma.user.update({
      where: { id: user.id },
      data: { stripeCustomerId: customerId },
    });
  }

  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    customer: customerId,
    line_items: [{ price: process.env.PLUS_PRICE_ID, quantity: 1 }],
    success_url: `${process.env.APP_URL}/plus/thanks`,
    cancel_url: `${process.env.APP_URL}/plus`,
  });
  res.json({ url: session.url });
}