У SaaS недостаточно сделать экран «Тариф активен». В сервисе для обработки заявок отдела эксплуатации должны согласованно работать аккаунт пользователя, организация-клиент, роль, подписка и разрешение на конкретную функцию. Даже если интерфейс выглядит законченным, он ничего не доказывает о списании денег или доступе к данным.
Четыре понятия, которые легко смешать
Представим компанию с четырьмя сотрудниками. У каждого свой user_id; все принадлежат к tenant_id одной организации. Подписка относится к организации, а не к последнему вошедшему сотруднику. subscription_status сообщает состояние отношений с биллингом. entitlement отвечает на вопрос, разрешена ли конкретная возможность — например, экспорт CSV или пять активных операторов. Роль (owner, manager, viewer) отвечает, кто из пользователей может пригласить коллегу, менять настройки или просматривать обращения.
Это четыре уровня правил, а не одно поле isPremium:
| Слой | Учебный пример | Проверка при запросе |
|---|---|---|
| Пользователь | Сотрудник вошёл в систему | Валидна ли серверная сессия? |
| Tenant | Организация north-office |
К какому tenant принадлежит пользователь? |
| Роль | manager может менять очередь |
Разрешено ли действие для этой роли? |
| Entitlement | В тарифе разрешён экспорт | Включена ли эта функция для tenant и не истекла ли подписка? |
Ни tenant_id, присланный из браузера, ни скрытая кнопка тарифа не являются полномочием. Сервер должен извлечь пользователя из подтверждённой сессии, найти его членство в tenant и проверять разрешение на каждом запросе к данным/действию.
Состояния, а не один переключатель
Для демонстрации используй такой жизненный цикл: trialing → active → past_due → canceled. Подпиши, что именно меняется в каждом переходе: после успешного подтверждения первой оплаты активируется нужный entitlement; при неудаче оплаты появляется состояние для обработки ошибки; при отмене будущая возможность может остаться доступной до оплаченного периода — если именно так устроена политика продукта. Не выдумывай для этого универсальное правило: сверяй бизнес-политику с провайдером и условиями своего предложения.
Если выбран Stripe, его руководство по webhooks для подписок перечисляет события вроде invoice.paid, invoice.payment_failed и изменения статуса подписки; в примере провайдера доступ можно выдавать после invoice.paid и проверки subscription.status == active. Документация отдельно предупреждает, что события приходят не обязательно по порядку и могут дублироваться. Поэтому обработчик должен проверять подпись по сырому телу запроса, сохранять уже обработанные event IDs и выдерживать повторную доставку. См. также проверку Stripe webhook-событий. Документация Stripe Entitlements описывает entitlement как условие выдачи или отзыва доступа к функциям продукта; конкретное соответствие тарифов возможностям задаётся в приложении. Это пример интеграции со Stripe, а не общая спецификация всех платежных систем.
Checkout завершён в браузере — или оплата подтверждена?
Страница возврата ?success=true означает лишь, что браузер вернулся по URL. Она не удостоверяет транзакцию: такой URL можно открыть вручную, а сетевой callback может задержаться. Для реального Stripe-подключения сервер принимает подписанный webhook и сопоставляет customer/subscription с аккаунтом вашего приложения. Только после проверки ожидаемого статуса, суммы/товара и организации обновляет запись биллинга. Сами секреты webhook не должны попадать в frontend.
Для учебного прототипа безопасно ограничиться тестовым режимом и явно подписанным demo-планом, который не принимает реальных платежей. Переход в production требует настройки настоящего платежного аккаунта, правил возврата/налогов, серверного endpoint, мониторинга, обработки повторов и проверки ошибок — одной кнопки Checkout недостаточно.
Проверка на примере организации
Создай два tenant (north-office, south-office) и пользователей с разными ролями. Затем проходи матрицу не только через интерфейс:
- Войти
managerиз north-office и открыть собственную заявку — разрешено. - Поменять ID заявки на известный ID south-office в прямом API-запросе — отказ без раскрытия содержимого.
- Открыть экспорт, которого нет в entitlement тарифа — серверный отказ, даже если вручную вызвать endpoint.
- Отправить success-редирект checkout без webhook — право не появляется.
- Передать webhook с некорректной подписью — событие не меняет биллинг.
- Повторно доставить один корректный event ID — второй раз не выдаётся новый период.
- Провести renewal/payment failure/cancellation в тестовой среде и сравнить серверную запись с данными провайдера.
Это проверочный план, а не заявленный результат испытания. В таблице хранится источник истины: provider customer/subscription IDs, текущий статус, период действия, tenant и дата последней обработанной версии события. Для каждой сверки сохраняй audit event без токенов и платёжных реквизитов.
Сначала попроси ИИ построить схему сущностей и переходов, потом отдельно endpoint оплаты и policy-check. Попроси перечислить негативные проверки до генерации кода. При моделировании серверных таблиц пригодится руководство о схеме БД; детали аутентификации и прав отдельно разобраны в статье про роли и доступ.
