Skip to main content
Две вещи позволяют клиенту настроить подписку сверх выбора тарифа: выбор сквадов, которые она использует, и покупка дополнительной ёмкости посреди периода. Обе задаются на версии тарифа, обе проверяются на сервере, а не в интерфейсе, и обе в итоге сворачиваются в одно право на услугу, чтобы Remnawave получал одно желаемое состояние, а не тариф с последующей поправкой.

Наборы сквадов на уровне тарифа

Версия тарифа несёт две коллекции сквадов:
  • remnawave_squad_ids — назначаются каждой подписке на этом тарифе всегда;
  • plan_version_squads — выбираемый набор, где каждая строка несёт русскую и английскую подпись и порядок сортировки.
squad_selection решает, что клиенту позволено: Пустое значение ведёт себя как automatic, а любое значение вне этих трёх отклоняется сразу, а не додумывается. Итоговый набор сквадов для права на услугу — это включённые сквады плюс принятый выбор, дедуплицированные и отсортированные. Сортировка важна: два клиента, выбравшие одни и те же сквады в разном порядке, обязаны дать побайтово идентичное состояние права на услугу, иначе обнаружение расхождений сообщило бы о несовпадении, которого нет. Выбор проверяется на сервере по тарифу. Данные коллбэка, называющие сквад, которого тариф не предлагает, отклоняются с squad_not_offered, поэтому подделанный коллбэк не может прикрепить сквад, за который клиент не платил. Конфигуратор появляется только тогда, когда тариф действительно предлагает выбор. Тариф с выбором automatic идёт с экрана тарифа прямо к оплате, ровно как и раньше.

Полный список отказов

Все пять оборачивают одну доменную ошибку и проявляются как HTTP 422 с причиной в поле type по RFC 9457, которое обе клиентские поверхности переводят в собственные формулировки. База данных подкрепляет границы: min_selectable_squads не меньше 0, max_selectable_squads больше 0, если задан, именованная межколоночная проверка требует, чтобы максимум был не меньше минимума, а подписи — от 1 до 80 символов на каждом языке.

Выбор сквадов при оформлении заказа

Веб. На /account/checkout группа полей Серверы появляется только тогда, когда тариф сообщает, что выбор настраиваемый. Каждый чекбокс отправляет PATCH /v1/account/checkout со всем массивом squadIds, а сервер пересчитывает цену и возвращает оформление заказа целиком. Границы, которые показывает страница, — только подсказка: настоящая проверка выполняется при создании заказа, потому что вторая реализация того же правила подсчёта на React — это ровно то, что рано или поздно пропустило бы набор, который заказ затем отклонит. Telegram. Выбор способа оплаты сначала открывает конфигуратор сквадов, когда тариф настраиваемый, и каждый коллбэк squad: переключает один сквад. GET /v1/account/plans/{planVersionID} возвращает всю картину в поле squads с selection, minimum, maximum, configurable и списком предложенных сквадов. Подтверждённый набор хранится в заказе в selected_squad_ids; если он пуст, при расчёте используется собственный набор версии тарифа.
Выбираемый набор сквадов сейчас нельзя настроить из /admin. Редактор версий тарифа проверяет squadSelection по значениям automatic, customer_choice и fixed, тогда как CHECK-ограничение plan_versions допускает automatic, optional и required — единственные три значения, которые реализует домен. Публикация чего-либо, кроме automatic, из панели падает на базе данных с HTTP 500 operation_failed, а optional или required, отправленные в панель, отклоняются с 422 ещё раньше.Пока эти два списка не согласованы, публикуйте такую версию прямым вызовом POST /v1/panel/catalog/plans/{planID}/versions со значениями базы данных. Полный справочник по редактору версий — на странице Каталог и акции.

Дополнения

Дополнение добавляет подписке ёмкость. Купить его можно двумя способами:
  • Вместе с тарифом, при оформлении заказа или в сохранённой корзине. Оно списывается тем же заказом, поэтому одно подтверждение — это один платёж, и покрывает весь первый период по каталожной цене.
  • В середине периода, против живого права на услугу. Оно списывается отдельным заказом с operation = 'addon' и оценивается по правилу пропорционального расчёта своей версии относительно оставшегося времени периода.
Существует три типа: Дополнение предлагается только для версий тарифа, которые перечисляют его в plan_version_addons. Покупка дополнения для тарифа, который его не предлагает, отклоняется с addon_unavailable. Эта связь — не только курирование: идентификатор дополнения приходит в данных коллбэка или в пути URL, которые контролирует клиент, поэтому именно она останавливает попытку прикрепить дополнение к тарифу, для которого оно никогда не оценивалось. Идентификатор, который даже не является UUID, обрабатывается так же, а не как ошибка разбора.

Версионированные цены

Дополнения версионируются точно так же, как тарифы. Строка дополнения несёт представление и изменяема; версия несёт то, что получает клиент, и то, сколько это стоит, и неизменяема. addon_versions неизменяема, как только на неё ссылается заказ, а order_addon_lines хранит снимок ёмкости, цены и правила пропорционального расчёта версии на момент покупки. Изменение цены дополнения создаёт новую версию; оно никогда не переписывает исторический заказ.

Создание дополнения

1

Откройте каталог

Перейдите в /admin/catalog и переключитесь на вкладку Дополнения. В списке показаны код, метка типа, текущая версия в виде v{n} и метка «Видно» или «Скрыто». Список только для чтения — в строке нет элементов редактирования или вывода из обращения.
2

Опишите его

Нажмите Новое дополнение. Заполните Код, Тип, Название (EN), Название (RU) и зависящее от типа содержимое: Трафик в ГБ, Слоты устройств или список ID сквадов через запятую. Трафик переводится в байты в браузере до отправки.
3

Назначьте цену и границы

Задайте «Максимум в заказе», правило цены в середине периода и Цены в том же виде RUB=19900, USD=299, который использует редактор тарифов. Включите Видимость и введите причину.
4

Сохраните

Отправка вызывает PUT /v1/panel/catalog/addons (catalog.write) и возвращает ID дополнения. Аудит panel.addon.saved.
Сохранение существующего кода обновляет строку дополнения через upsert — только видимость и порядок сортировки — и каждый раз публикует новую версию. Редактирования на месте нет по той же причине, по какой его нет у тарифов: исторический заказ обязан по-прежнему стоить столько, сколько стоил. Что сохранение отклоняет, всё с 422:
  • Отсутствующий код или код, не соответствующий ^[a-z][a-z0-9_-]{1,63}$;
  • Правило пропорционального расчёта вне трёх известных;
  • Максимум в заказе меньше 1;
  • Полное отсутствие цен;
  • Дополнение traffic без трафика, дополнение devices без слотов, дополнение squads без сквадов или любой другой тип;
  • Отсутствие названия на одном из языков. Отсутствующий перевод отклоняется, а не сохраняется пустым, потому что дополнение без названия на одном языке — это пустая строка на экране клиента.
Вывод версии из обращения — это POST /v1/panel/catalog/addon-versions/{addonVersionID}/retire (catalog.write, аудит panel.addon_version.retired). Вывод уже выведенной версии не находит ни одной строки и отвечает 404. Элемента управления для вывода из обращения в панели тоже нет.
Набор предлагаемых дополнений на версии тарифа заполняется только массивом addonIds в POST /v1/panel/catalog/plans/{planID}/versions, и публикация заменяет этот набор, а не дополняет его. Редактор версий в панели это поле не отправляет, поэтому каждая версия тарифа, опубликованная из /admin/catalog, не предлагает никаких дополнений вовсе. Заполнить набор можно только прямым вызовом эндпоинта версий.
Проверка предложения сопоставляет по дополнению, а не по версии дополнения, поэтому любая живая версия предложенного дополнения подходит, как только само дополнение перечислено.

Покупка дополнений вместе с тарифом

Веб. /account/checkout рисует раздел Дополнения, когда версия тарифа предлагает хотя бы одно. Каждая запись — кнопка-переключатель; нажатие вызывает POST /v1/account/checkout/addons/{addonVersionID}, который переключает строку с количеством один и возвращает всё оформление заказа с пересчитанной ценой. Разбивка расчёта после этого сообщает addonMinor отдельно от subtotalMinor и discountMinor, потому что клиент, которому назвали только итог к оплате, не может отличить акцию от дополнения, а последствия ошибки в каждом из них совершенно разные. Telegram. На экране оформления заказа появляется кнопка дополнений, когда тариф их предлагает; в списке одно нажатие переключает одну запись. Переключение дополнения, которого тариф не предлагает, отвечает 404 plan_unavailable в вебе и «это дополнение больше недоступно» в боте. Архивированное дополнение, выведенная из обращения версия или версия без цены в валюте заказа отклоняются с addon_unavailable.
Промокод даёт скидку только на цену тарифа. Промежуточная сумма — это цена тарифа плюс сумма дополнений, но скидка считается только от цены тарифа, поэтому процент скидки никогда не съедает ёмкость дополнений.

Покупка дополнений в середине периода

Telegram. Подписки → выберите одну → Дополнения → нажмите на дополнение. Покупка становится собственным заказом с количеством один; кошелёк оплачивает его, когда баланса хватает, а иначе появляется обычный экран оплаты. Веб. Недоступно. Под /v1/account нет маршрута для покупки дополнения к живой подписке. Клиентская веб-панель может выбирать дополнения внутри оформления заказа и больше нигде. Покупка в середине периода отклоняется с: Режим обслуживания блокирует покупку целиком, как и любой другой заказ. Повторное использование ключа идемпотентности с другими параметрами дополнения отклоняется, а не разрешается молча в прежний заказ. Одно и то же дополнение дважды в одном заказе отклоняется сразу.

Правила пропорционального расчёта

Правило пропорционального расчёта — это свойство версии дополнения, и оно всегда задано явно. Omniflow никогда не выводит частичную цену из оставшегося времени, если дополнение этого не говорит. Правило применяется только к покупке в середине периода; дополнение, купленное вместе с тарифом, покрывает полный период и списывается по каталожной цене, что бы ни говорило его правило. Три свойства выполняются для любого правила, и у каждого есть собственный модульный тест:
  1. Пропорциональное списание никогда не превышает каталожную цену.
  2. Округление всегда вверх, поэтому частичный период никогда не округляется до нуля.
  3. Покупка, сделанная до начала периода или против периода, в котором не осталось времени, откатывается к полной цене. Списывать ноль за ёмкость, которая всё равно будет предоставлена, значило бы отдать её даром.
Промежуточное умножение выполняется в 128 битах. Длинный период и большая цена переполнили бы 64-битное произведение и дали бы бессмысленную сумму.

Применение дополнения

При расчёте купленная ёмкость сворачивается в само право на услугу до того, как вычисляется желаемое состояние выдачи доступа, поэтому Remnawave получает один совокупный результат, а не тариф с последующей поправкой:
Право на услугу с безлимитом остаётся безлимитным: обновление оставляет null-лимит трафика или null-лимит устройств как есть, потому что добавлять ёмкость к «без ограничения» бессмысленно.При этом ничто на клиентских поверхностях не запрещает такую покупку и не предупреждает о ней. Дополнение с трафиком, предложенное на безлимитном тарифе, показывается, оценивается, списывается и записывается — и не даёт ничего. Не предлагайте дополнения с ёмкостью на версиях тарифа, у которых соответствующий лимит безлимитен.
Сквады — это множество, и они никогда не умножаются на количество. Покупка двух одинаковых дополнений со сквадами списывает деньги дважды и выдаёт те же сквады один раз, поэтому max_quantity у дополнения со сквадами должно быть равно 1. Каждая оценённая строка снимается в order_addon_lines с кодом, типом, версией, трафиком, слотами, сквадами, правилом пропорционального расчёта и концом периода, чтобы заказ объяснял себя сам спустя годы, без обращения к каталогу.

Идемпотентное исполнение

entitlement_addons уникальна по (order_id, addon_version_id). Повторный расчёт находит строку уже на месте, пропускает её и оставляет право на услугу нетронутым. В итоги входят только те строки, которые были фактически вставлены, поэтому частичный повтор не может посчитать дважды. Заказ на дополнение в середине периода ставит в очередь собственную операцию исполнения с ключом order:<order id>:addon, несущую операцию set_limits для совокупного результата, и заказ переходит в fulfilled. Дополнению, купленному вместе с тарифом, отдельная операция не нужна: оно уже часть права на услугу, которое выдаёт собственная операция create тарифа. Что происходит с этой операцией дальше, описано в разделе Исполнение заказов.

Когда прикреплять не к чему

Если к моменту расчёта у целевой подписки нет живого права на услугу, деньги не теряются и ничто не откатывается. Заказ остаётся в paid, пишется событие аудита addon.no_active_entitlement в категории financial с результатом «сбой», и в очередь ставится операторское уведомление типа fulfillment_failure, дедуплицированное по заказу. Откат записанного платежа был бы хуже, чем оставить оператору понятную задачу, — ничто здесь не вправе откатывать уже принятые деньги.

Справочник API по этой области

Маршруты сохранения и вывода дополнения из обращения, а также маршрут версий тарифа реализованы, но отсутствуют в api/openapi.yaml, поэтому их нет ни в сгенерированном клиенте, ни в опубликованном справочнике API. Как их всё-таки вызывать, объясняет страница Интеграция с API.

Смежные страницы

Каталог и акции

Тарифы, неизменяемые версии, цены, пробные периоды и правила акций, которым подчиняется скидка.

Одновременные подписки

Право на услугу, к которому прикрепляется дополнение, и что происходит, когда его период заканчивается.

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

Как совокупное желаемое состояние доходит до Remnawave и как выглядит неудавшаяся операция.

Пополнение кошелька и сохранённые корзины

Дополнения внутри сохранённой корзины и почему корзина всегда переоценивается перед списанием.