Сначала определитесь с валютой расчётов
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 дважды. Редактор версии
сам называет последствие: существующие заказы сохраняют версию, по которой были куплены.
Версия, опубликованная из панели, не предлагает дополнений. Предлагаемый набор заменяется ровно
теми
addonIds, что пришли в запросе, а редактор панели это поле не отправляет. Чтобы его
заполнить, нужен прямой вызов 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. Редактора
локализаций в панели нет.
Проверьте готовность тарифа к продаже
Прежде чем вовлекать платёжного провайдера, посмотрите на каталог так, как это делает экран клиента. Публичный маршрут не требует учётных данных:plans.visibleистинно, аarchived_atпуст.- Существует строка
plan_localizationsдля запрошенной локали. - У тарифа есть хотя бы одна не выведенная из обращения версия — побеждает наибольшая из таких.
- У этой версии есть хотя бы одна строка цены.
/admin/catalog показывает тариф,
номер его последней версии и то, сколько строк заказов уже на него ссылаются.
Настройте платёжного провайдера
Платёжные адаптеры собираются из переменных окружения при старте.manual и telegram_stars
регистрируются всегда; два других появляются, только когда есть их учётные данные, а пустые
учётные данные оставляют адаптер незарегистрированным, вместо того чтобы зарегистрировать сломанный.
Для первой продажи самый быстрый работающий канал — обычно CryptoBot или Telegram Stars, потому что
ни одному из них не нужен публичный URL:
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 и группа операторских уведомлений.
Операции администратора
Регламенты для возвратов, зависших платежей и всего остального, что следует за продажей.