AI-бот с подпиской в MAX: GPT-4o, ЮKassa и 54-ФЗ за 10 дней
Подписочный SaaS внутри MAX: GPT-4o, веб-поиск, ЮKassa с 54-ФЗ, авто-продление, админка и cron-логика жизненного цикла.
- вручную → авто + emailФискализация чеков 54-ФЗ
- 4 Cron-задачи жизненного цикла подписки
- 71 Автотест бизнес-логики
Проблема
Запрос: не MVP-прототип, а полноценный SaaS внутри мессенджера MAX — с платной подпиской, налоговыми чеками по 54-ФЗ, авто-продлением и админкой. Всё, что есть у взрослых продуктов — но в закрытой экосистеме без привычных инструментов.
Задача
Клиент пришёл с запросом построить полноценный подписочный AI-бот в российском мессенджере MAX. Не MVP, не «тест идеи», а production-продукт: оплата картой или СБП, налоговые чеки по 54-ФЗ на email, авто-продление через 30 дней, напоминания об окончании, админка с метриками. Всё, что есть у взрослых SaaS — но внутри закрытой экосистемы, где нет привычных инструментов и обширной документации.
Сложность: MAX — относительно новая платформа, long polling вместо вебхуков, слабая публичная документация Bot API, нет готовых библиотек для интеграции с ЮKassa под MAX-контекст. Нужно было собрать production-стек с нуля и за разумные сроки — без права на «потом допилим».
Решение
1. MAX Bot API через long polling
Бот работает через long polling — постоянный блокирующий GET-запрос к MAX API. Выбор в пользу polling, а не вебхука: не нужно разворачивать публичный endpoint для бота, проще локальная отладка, Express используется только для вебхуков ЮKassa и статики.
Все сообщения проходят через единый обработчик message_created → троттлинг 3 секунды → сохранение в сессию → вызов LLM → ответ. Команды (/start, /new, /history, /status, /subscribe, /cancel, /admin) обрабатываются отдельным роутером.
2. GPT-4o через OpenRouter с веб-поиском
Запросы к LLM идут через OpenRouter — OpenAI-совместимый шлюз, который поддерживает десятки моделей. По умолчанию работает openai/gpt-4o, но модель можно менять через переменные окружения без деплоя. Включён встроенный веб-поиск через openrouter:web_search: когда пользователь спрашивает про актуальные события, бот подтягивает свежие данные с источниками.
Контекст передаётся последними 100 парами сообщений — достаточно для связного диалога, не взрывает токены.
3. Память диалогов и лимиты
Каждая сессия — отдельная запись в PostgreSQL с массивом сообщений в формате JSON. Пользователь может:
- Продолжать текущий диалог (память сохраняется между сообщениями).
- Начать новый
/new— старая сессия остаётся в истории. /history— последние 10 непустых диалогов с возможностью вернуться к любому.
Лимиты считаются в символах (input + output): 4 000 символов/день на бесплатном тарифе, 150 000 на платном. Сброс каждый день в 00:00 МСК через cron. Учёт и input, и output — чтобы нельзя было обойти лимит короткими промптами и длинными ответами.
4. ЮKassa, 54-ФЗ и авто-продление
Оплата через ЮKassa — карты, СБП, банковские переводы. Стоимость подписки — 70 ₽/мес, срок — 30 дней.
Фискализация 54-ФЗ. Перед оплатой бот запрашивает email пользователя. В метаданные платежа передаётся состав чека (наименование услуги, сумма, НДС, email получателя) — ЮKassa формирует фискальный чек и сама отправляет его на почту. Никаких сторонних сервисов чеков.
Вебхук с двойной проверкой. POST /api/yookassa/webhook валидирует IP источника (allowlist ЮKassa из SDK), затем перезапрашивает статус платежа напрямую в API ЮKassa — чтобы исключить подделку. Идемпотентность обеспечена уникальным yookassaId в таблице транзакций.
Авто-продление. Если у способа оплаты есть сохранённый payment_method_id (карта, не СБП), cron в 00:01 МСК списывает следующий период за 24 часа до истечения текущей подписки. Если платёж не прошёл — подписка деактивируется в 00:10, пользователь получает уведомление.
5. Cron-оркестрация жизненного цикла
Четыре задачи, все в таймзоне Europe/Moscow:
| Время | Задача |
|---|---|
| 00:00 | Сброс дневных лимитов всем пользователям |
| 00:01 | Авто-продление подписок, истекающих в ±24ч |
| 00:10 | Деактивация истёкших подписок |
| 09:00 | Напоминания об окончании (за 3 дня, не чаще раза в 2 дня) |
Порядок важен: сброс лимитов → попытка продлить → деактивация неоплаченных → утренние напоминания. Авто-продление идемпотентно, повторный запуск не создаёт дубликаты транзакций.
6. Админ-панель прямо в боте
Команда /admin (доступна только пользователям с флагом isAdmin) открывает мини-панель внутри MAX:
- Статистика — всего пользователей, активных подписок, оборот.
- Список пользователей — пагинация по 10, сортировка по дате.
- Карточка пользователя — email, тариф, последние 10 транзакций, общая сумма платежей.
Первые админы задаются через переменную ADMIN_MAX_USER_IDS в .env — список maxUserId через запятую. Флаг обновляется при каждом взаимодействии (upsert), так что новых админов можно добавлять без перезапуска.
7. Деплой и надёжность
Весь бот — один Node.js-процесс: Express + MAX long polling + cron + Prisma pool. Монолит оправдан: cron требует единственного инстанса, горизонтальное масштабирование усложнило бы архитектуру без выигрыша.
Graceful shutdown: SIGTERM → остановка cron → остановка бота → закрытие HTTP → дисконнект Prisma. Никаких брошенных платежей и оборванных диалогов.
Docker Compose: два контейнера (bot + postgres), volume для БД, рестарт unless-stopped. Nginx с Let’s Encrypt на хосте, SSL обновляется certbot автоматически. 71 автотест бизнес-логики через Vitest с моками Prisma — тесты не требуют реальной БД.
Результат
Production-бот на iiibt.ru: пользователь пишет /start, за 30 секунд оформляет подписку, получает фискальный чек на email. Через 30 дней подписка продлевается автоматически — или деактивируется, если оплата не прошла. Админ в том же MAX видит метрики, пользователей и транзакции.
10 дней от первого коммита до production. Полноценный SaaS внутри закрытой экосистемы, без компромиссов в архитектуре или соответствии российскому законодательству.
Production-продукт на iiibt.ru: от /start до оплаченной подписки — 30 секунд, email-чек, авто-продление через 30 дней, админ видит метрики и транзакции в MAX.