Skip to main content
Быстрый старт оставляет вас с работающей установкой и учётной записью оператора. Каталога в ней нет, поэтому обе клиентские поверхности показывают пустой список тарифов и купить нельзя ничего. Эта страница закрывает пробел: один тариф, одна неизменяемая версия с ценой, один платёжный провайдер, подключённый бот и настоящая покупка, прослеженная от нажатия клиента до пользователя Remnawave, которого она создаёт. Проходите по порядку. Каждый шаг проверяем сам по себе, и это важно, потому что проверки, решающие, продаётся тариф или нет, размазаны по четырём таблицам.
Покупка сейчас не может завершиться расчётом на свежемигрированной базе данных. Расчёт вставляет задание на исполнение заказа в той же транзакции, что и деньги, а ни одна закоммиченная миграция не создаёт собственных таблиц очереди River, поэтому вставка падает и вся транзакция расчёта откатывается. Всё вплоть до передачи управления провайдеру включительно работает, и настроить это стоит; шаг, на котором всё останавливается, описан в разделе Где сегодня останавливается тестовая покупка.

Сначала определитесь с валютой расчётов

APP_DEFAULT_CURRENCY (по умолчанию RUB) — это валюта, в которой обе клиентские поверхности сравнивают каталог; при старте она приводится к верхнему регистру и сверяется с ^[A-Z]{3}$. Здесь она важна из-за одного правила, на котором спотыкается каждая первая установка: Тариф без цены в валюте расчётов полностью исключается из клиентского каталога. Он не показывается как недоступный. Строка, с которой ничего нельзя сделать, — это лишь вопрос, на который клиент не может ответить, поэтому она вообще не отрисовывается. Если вы собираетесь продавать только через Telegram Stars, задайте APP_DEFAULT_CURRENCY=XTR и назначайте версиям цену в XTR. Stars не рассчитываются ни в какой другой валюте.

Создайте тариф

Тариф — это устойчивый продукт, обращённый к клиенту: код в нижнем регистре, тип, русский и английский тексты, флаг видимости и порядок сортировки. Он не несёт ни цены, ни длительности — они принадлежат его версиям, и именно это позволяет переименовывать и переупорядочивать продукт, не трогая то, что кто-то уже купил. Панель оператора не умеет создавать тариф. /admin/catalog публикует версии, переключает видимость и сохраняет дополнения, но создание тарифа существует только в API с bearer-токеном — об этом пробеле стоит знать до того, как вы пойдёте искать кнопку.
Один вызов создаёт тариф, обе локализации, первую версию и её цены в одной сериализуемой транзакции, поэтому тариф никогда не оказывается видимым без текста и цены, которые делают его продаваемым. Отказы, которых стоит ждать: неизвестное поле JSON — это 400 invalid_request, потому что декодер отвергает неизвестные поля, а не молча выбрасывает опечатку; отсутствующая длительность или пустой список цен — 422 plan_version_rejected с duration and prices are required; неверный bearer-токен — 401 unauthorized.
Повторный вызов с тем же code не завершится ошибкой. Вставка выполняет upsert по code, поэтому вы получите обратно существующий тариф — а createVersion затем добавит версию 2. Смените код или воспользуйтесь панелью, если публиковать ещё одну версию вы не собирались.

Опубликуйте версию с ценой из панели

Любое последующее изменение цены — это новая версия. Маршрута обновления версии нет намеренно: версия тарифа неизменяема, как только на неё ссылается заказ, а редактор, способный её изменить, молча переоценил бы историю. Заказ дополнительно хранит JSON-снимок периода, длительности, трафика, лимита устройств, сквадов и политик жизненного цикла версии на момент покупки, поэтому даже подделанная строка версии не смогла бы изменить, сколько стоил существующий заказ. Откройте /admin/catalog (право доступа catalog.read; для элементов управления нужно catalog.write), найдите строку тарифа и нажмите Новая версия. Редактор раскроется под строкой, предзаполненный из текущей версии, потому что изменение цены обычно и есть единственное отличие. Кнопка Опубликовать версию остаётся неактивной, пока длительность не станет больше нуля, пока не разберётся хотя бы одна цена и пока не введена причина. Карточка называется «Опубликовать следующую версию» и объясняет почему: версия тарифа неизменяема, как только на неё ссылается заказ, поэтому здесь публикуется новая, а не изменяется текущая. Публикация берёт рекомендательную блокировку на время выделения номера, поэтому два оператора не могут выпустить версию 4 дважды. Редактор версии сам называет последствие: существующие заказы сохраняют версию, по которой были куплены.
Два выпадающих списка в этом редакторе отправляют значения, которые база данных отвергает. Выбор сквадов отправляет automatic, customer_choice или fixed, тогда как ограничение колонки допускает automatic, optional и required. Пробный период отправляет new_customer, never или any_customer, тогда как колонка допускает new_customer, never_trialled и any. Всё, кроме Назначаются автоматически и Только новым клиентам, проходит собственную валидацию панели и затем нарушает ограничение, что проявляется как 500 operation_failed. Чтобы предложить выбор локаций клиентом или правило пробного периода, отличное от значения по умолчанию, вызывайте POST /v1/panel/catalog/plans/{planID}/versions напрямую со значениями, которые понимает база.
Версия, опубликованная из панели, не предлагает дополнений. Предлагаемый набор заменяется ровно теми addonIds, что пришли в запросе, а редактор панели это поле не отправляет. Чтобы его заполнить, нужен прямой вызов API — см. Дополнения и конфигуратор.
Что происходит с версией дальше, определяют ещё два элемента управления, оба доступны только через API: POST /v1/panel/catalog/plans/{planID}/archive прекращает новые покупки тарифа целиком и принудительно ставит visible = false, а POST /v1/panel/catalog/plan-versions/{planVersionID}/retire прекращает предложение конкретной версии в каталоге. Ни то, ни другое не трогает существующее право на услугу: клиент сохраняет то, что купил.

Локализуйте его

Оба запроса клиентского каталога соединяют локализацию для запрошенной локали через join, а не через left join. Тариф без текста в какой-то локали не отрисовывается пустым — он исчезает для клиентов, пользующихся этим языком. Именно поэтому вызов создания выше отправляет и ru, и en. Чтобы изменить текст потом:
Этому маршруту нужна сессия панели с правом catalog.write и CSRF-токен, а не bearer-токен оператора, и он отвечает 204. Любая локаль, кроме ru или en, — это 422. Редактора локализаций в панели нет.

Проверьте готовность тарифа к продаже

Прежде чем вовлекать платёжного провайдера, посмотрите на каталог так, как это делает экран клиента. Публичный маршрут не требует учётных данных:
Запись появляется, только когда выполняется всё перечисленное:
  1. plans.visible истинно, а archived_at пуст.
  2. Существует строка plan_localizations для запрошенной локали.
  3. У тарифа есть хотя бы одна не выведенная из обращения версия — побеждает наибольшая из таких.
  4. У этой версии есть хотя бы одна строка цены.
Клиентские поверхности добавляют ещё одно условие, которого нет у этого маршрута: цена должна быть в валюте расчётов. Этот эндпоинт возвращает по одной записи на строку цены, поэтому тариф с ценами в двух валютах появляется здесь дважды, а в боте — один раз. Если список пуст, идите по этому списку проверок, а не гадайте. /admin/catalog показывает тариф, номер его последней версии и то, сколько строк заказов уже на него ссылаются.

Настройте платёжного провайдера

Платёжные адаптеры собираются из переменных окружения при старте. manual и telegram_stars регистрируются всегда; два других появляются, только когда есть их учётные данные, а пустые учётные данные оставляют адаптер незарегистрированным, вместо того чтобы зарегистрировать сломанный. Для первой продажи самый быстрый работающий канал — обычно CryptoBot или Telegram Stars, потому что ни одному из них не нужен публичный URL:
APP_CRYPTOBOT_TESTNET по умолчанию равна true. Счета создаются на тестовом хосте, пока вы не поставите false, — ровно то, что нужно для тестовой покупки, и ровно то, что нужно не забыть изменить, прежде чем принимать настоящие деньги.
YooKassa дополнительно требует APP_PUBLIC_URL, потому что размещённому у провайдера оформлению нужна абсолютная ссылка возврата; бот отказывается стартовать, когда shop ID настроен, а публичного URL нет. Направьте вебхук провайдера на POST /v1/payments/webhooks/{provider} на хосте API. Этот маршрут неаутентифицирован по необходимости: подлинность обеспечивает проверка подписи адаптером по точным полученным байтам, и именно поэтому прокси, перекодирующий тела JSON, ломает вебхуки всех провайдеров.
На /admin/settings есть карточка Платёжные провайдеры, которая хранит флаг включённости, порядок отображения и запечатанные учётные данные по каждому мерчанту. Эти строки не настраивают ни один адаптер: реестр строится только из переменных окружения. Проверка соединения в этой карточке к тому же записывает значение статуса, которое ограничение самой таблицы отвергает. Настраивайте провайдеров в .env и перезапускайте, а всю картину читайте в Платёжных провайдерах.
Поскольку manual регистрируется всегда, он предлагается каждому клиенту на обеих поверхностях, и переключателя, чтобы его скрыть, нет. На установке, которая не принимает банковские переводы, ожидайте увидеть его и объяснять его наличие.

Подключите бота

Перезапустите сервис bot. Без настроенного webhook-URL бот работает на длинных опросах, которым не нужны ни входящий порт, ни TLS — по этой причине они и выбраны по умолчанию для первой установки. При старте он публикует свой список команд в Telegram: девять команд для локали по умолчанию и восемь для русской. Бот отвечает только в личных чатах. Каждый экран несёт состояние подписки, а групповой чат — это аудитория, поэтому /start, любая команда, любой callback и операторская панель /ops молча ничего не возвращают, когда чат не является личным. Отправьте /start. Вы должны получить главное меню; /plans должен показать только что опубликованный тариф блоком с периодом, трафиком, устройствами и ценой и кнопкой с подписью Pro · 499 RUB. Если список пуст, пустое состояние называет валюту, которую оно искало, — вернитесь к разделу Проверьте готовность тарифа к продаже. О режиме webhook, домене Mini App и группе операторов см. Telegram.

Совершите тестовую покупку

Возьмите Telegram-аккаунт, который никогда не общался с этим ботом, чтобы увидеть то, что видит клиент в первый раз. Первый посетитель может смотреть и покупать до того, как появится хоть один пользователь Remnawave: канонический клиентский профиль Omniflow создаётся на /start, а пользователь Remnawave создаётся выдачей доступа после оплаты заказа.
1

Откройте тариф и выберите действие

/plans → кнопка тарифа. Экран тарифа показывает описание, период, трафик, устройства, цену, политики повышения, понижения и отмены словами и льготный период, когда он ненулевой. Политика forbid убирает действие целиком, а не проваливается после нажатия клиента, поэтому кнопки, которые вы видите, — ровно то, что бэкенд принял бы: 🛒 Buy для клиента, у которого нет ничего активного, ♻️ Extend this plan для той же версии или кнопка перехода для повышения либо понижения, которое разрешает политика.
2

Выберите способ оплаты

Перечисляются только настроенные адаптеры, способные рассчитаться в валюте этого тарифа, и каждый подписан ценой, которую он списал бы, потому что один и тот же тариф через разных провайдеров может стоить разное число. Именно выбор способа фиксирует валюту заказа. Если не перечислено ничего, ни один адаптер не поддерживает валюту, в которой у тарифа есть цена.
3

Проверьте сводку

Экран оформления заказа — это в точности та разбивка, из которой будет создан заказ: подытог, скидка, дополнения, кошелёк и итого к оплате. Отсюда клиент может ввести промокод, включить или выключить списание с кошелька, добавить предлагаемые дополнения, сохранить корзину на потом или сменить способ оплаты.
4

Оплатите

💳 Pay создаёт заказ и сразу начинает оплату. Его охраняют два ключа идемпотентности: собственный ключ оформления переиспользуется для каждой попытки создать заказ, поэтому двойное нажатие сходится в один заказ, а ключ платежа привязан и к заказу, и к провайдеру, поэтому клиент, бросивший один способ и вернувшийся с другим, получает второй платёж, а не сбивающее с толку переиспользование первого. Действие подтверждения к тому же защищено от повторов и ограничено шестью вызовами в минуту.Для размещённого у провайдера способа экран статуса предлагает 💳 Open payment page. Для Telegram Stars он предлагает ⭐ Pay in Telegram, что отправляет настоящий счёт Telegram; бот одобряет предварительный вопрос об оплате только тогда, когда заказ принадлежит этому клиенту, валюта совпадает, итог в точности равен непогашенной сумме, а заказ по-прежнему находится в состоянии pending или draft.
Клиентский веб-кабинет — это тот же поток с теми же записями: /account/store → тариф → /account/checkout → Подтвердить. Оформление, открытое на одной поверхности, — это то же самое оформление, которое показывает другая.

Где сегодня останавливается тестовая покупка

Когда провайдер сообщает об успехе, вебхук или обновление Stars доходит до RecordProviderPayment, который открывает одну транзакцию и делает в ней всё сразу: записывает событие платежа, переводит заказ в paid, тратит кредит кошелька через книгу проводок с двойной записью, создаёт право на услугу, пишет операцию исполнения, вставляет outbox-событие order.paid, начисляет реферальное вознаграждение и ставит в очередь задание исполнения. Проблема — последняя вставка. Задание уходит в собственные таблицы River, ни одна закоммиченная миграция их не создаёт, и ничто в стеке не запускает мигратор River, — поэтому вставка падает и вся транзакция откатывается. Что вы наблюдаете:
  • Деньги у провайдера.
  • Заказ остаётся в pending. Ни права на услугу, ни движения по книге проводок, ни пользователя Remnawave.
  • Вебхук записан на /admin/system → Вебхуки со статусом failed.
  • worker тоже не работает: он завершается при старте с записью start River.
То же самое относится к заказу, полностью покрытому кредитом кошелька, — он рассчитывается в момент создания тем же путём, — и к бесплатному или пробному тарифу, который никому ничего не должен вовне и потому рассчитывается сразу. Всё, что предшествует расчёту, — настоящее и стоит проверки: каталог, право на покупку, расчёт цены, промокод, арифметика кошелька, заказ и передача управления провайдеру работают и записываются. Следите за этой страницей и за Заданиями и очередями в ожидании миграции, которая это закроет.

Куда попадает завершённая продажа

Когда очередь появится, вот какой след оставит одна покупка, и стоит знать, какой экран отвечает на какой вопрос. Воркер забирает задание из очереди critical и отправляет желаемое состояние в Remnawave. Имя пользователя в Remnawave выводится из UUID подписки (omniflow_ плюс шестнадцать шестнадцатеричных символов), и воркер ищет его перед тем, как что-либо создавать, поэтому повторный create после потерянного ответа подхватывает существующего пользователя, а не создаёт второго. При успехе более ранние права на услугу этой подписки помечаются вытесненными, и заказ переходит из paid в fulfilled. Отказы провайдера повторяются с задержкой от пяти секунд, удваивающейся до потолка в 1280 секунд, а после трёх неудачных попыток оператору ставится в очередь уведомление — по одному на операцию, сколько бы раз она ни повторялась. Любое действие оператора над подпиской после этого идёт через тот же конвейер, а не пишется в Remnawave напрямую, и поэтому те кнопки сообщают поставлено в очередь, а не «сделано»: нажатие в панели, которое писало бы в Remnawave само, было бы тем самым изменением, которое сверка не смогла бы объяснить.

Дальнейшие шаги

Каталог и промоакции

Пробные периоды, промокоды, персональные предложения и правила, решающие, по какой версии оценивается клиент.

Платёжные провайдеры

Настройка каждого провайдера, проверка вебхуков, возвраты и то, чего не умеет каждый адаптер.

Исполнение заказов

Операции, ключи идемпотентности, повторы, обнаружение расхождений и сверка по расписанию.

Заказы и платежи

Машина состояний, классификации платежей и что каждая из них означает для оператора.

Telegram

Режим webhook, домен Mini App и группа операторских уведомлений.

Операции администратора

Регламенты для возвратов, зависших платежей и всего остального, что следует за продажей.