Права на услугу
Строка права на услугу несёт то, что Omniflow должен, плюс последнее, что он наблюдал на той стороне:observed_state, remnawave_user_id и reconciled_at. Разделение этих двух вещей и позволяет Omniflow отличить то, чего он хочет, от того, что он видел в последний раз.
Право на услугу существует как отдельная запись, потому что состояние платежа, состояние права и состояние Remnawave отказывают независимо. Успешный платёж обязан пережить недоступность Remnawave: деньги записаны, право на услугу записано, а выдача доступа повторяется позже. Остальное обеспечивает схема — UNIQUE (order_id) означает, что один заказ никогда не создаст два права на услугу, а проверка, требующая, чтобы конец был позже начала, делает нулевой или отрицательный период непредставимым.
Право на услугу никто не создаёт вручную. Оно записывается при расчёте по оплаченному заказу или при получении подарка, внутри той же транзакции, которая записывает деньги.
Всё, кроме
pending и superseded, пишется из наблюдаемого статуса Remnawave. superseded пишется локально, после того как замена действительно выдана.
Сохранённый статус права на услугу никогда не становится
expired по таймеру.
Истечение вычисляется для отображения из ends_at, а сохранённый статус
меняется на expired только тогда, когда проход сверки вычитает это из
Remnawave. Экран клиента, показывающий «истекло», и строка в базе данных,
всё ещё читающаяся как active, — это один и тот же факт.Модель подписки
Один клиент может владеть несколькими пользователями Remnawave, и подписка — это стабильная ручка ровно к одному из них. Без промежуточной ручки Omniflow пришлось бы угадывать, о каком удалённом пользователе речь в оповещении, продлении или удалении устройства.
Права на услугу, заказы, доставки уведомлений и настройки автопродления — все несут
subscription_id. Баланс кошелька, право на участие в акциях, атрибуция рефералов и заявки на пробный период намеренно его не несут: это свойства клиента, поэтому открытие второй подписки не даёт ни второго пробного периода, ни второго реферального вознаграждения.
Что относится к подписке, а что к клиенту
Защита от злоупотреблений пробным периодом считает клиентов, а не подписки.
trial_claims ключуется по клиенту, поэтому вторая подписка клиента, который уже использовал пробный период, не может получить ещё один.
Названия
Название обрезается по краям, ограничивается 40 рунами и отклоняется сразу, если оно пустое или содержит управляющие символы, — но никогда не усекается молча, потому что оно показывается клиенту дословно. Клиенты переименовывают подписку черезPATCH /v1/account/subscriptions/{subscriptionID}; маршрут переименования в боте делает то же самое. Оператор переименовывает через PATCH /v1/panel/customers/{customerID}/subscriptions/{subscriptionID} (subscriptions.write). Оба принимают название и ничего больше.
Одновременные подписки
APP_MAX_SUBSCRIPTIONS_PER_CUSTOMER по умолчанию равен 3 и ограничивается снизу единицей. Пока APP_MULTI_SUBSCRIPTION_ENABLED равен false, действующий потолок равен одной подписке, что бы ни говорил максимум.
Тариф может задать собственный, более жёсткий потолок через plans.max_concurrent_per_customer, который сужает лимит установки и никогда не может его расширить. Оба проверяются в момент покупки, поэтому тариф, настроенный выше предела установки, ограничивается им, а не переопределяет его. Где редактируется этот лимит, описано на странице Каталог и акции.
Клиент, упёршийся в любой из потолков, получает стабильную причину отказа, проверяемую в таком порядке:
Лимит применяется там, где деньги, а не в интерфейсе. Подсчёт выполняется под рекомендательной блокировкой на клиента в пределах транзакции, поэтому две одновременные покупки не могут обе увидеть устаревший счёт и обе пройти.
Открыть новую подписку может только покупка. Продление, повышение или понижение тарифа всегда называет существующую. Веб-оформление заказа отказывается угадывать: при включённой многоподписочности и более чем одной подписке открытие любой из этих операций без
subscriptionId отклоняется, а не берётся по умолчанию, потому что случайно сброшенная цель подписки — это ровно то, как продление молча становится второй подпиской.
Строка подписки записывается при создании заказа на неё, до того как двинутся деньги, чтобы платёж мог на неё указывать, а потолок — её учитывать, пока чекаут открыт. Если этот заказ затем закрывается неоплаченным — отменён клиентом или оператором либо истёк по чистке, — строка удаляется, при условии что ею больше ничто никогда не пользовалось: ни право на услугу, ни другой живой заказ, ни пользователь Remnawave, ни настройка автопродления, уведомление, корзина, сессия чекаута или попытка дозвона. Закрытый заказ сначала от неё отвязывается. Отмена оператором делает это в той же транзакции; чистка истёкших заказов — для каждого клиента, чей заказ она закрыла; отмена клиентом на любой поверхности вызывает ту же очистку после коммита. Подписка, которая когда-либо была выдана, этим никогда не удаляется — для неё есть операторская процедура в разделе Как оставить клиенту одну подписку.
Деньги по отвязанному заказу всё ещё могут прийти. Ручной перевод часто подтверждают после окна оплаты, а чекаут, оставленный открытым в другой вкладке, можно оплатить после отмены. Тогда проведение открывает свежую подписку вместо удалённой — под той же рекомендательной блокировкой и тем же потолком параллельности, что и покупка, — записывает её в заказ и создаёт право на услугу там же, внутри проводящей транзакции. Оно никогда не откатывается к другой подписке клиента: вторая подписка на тридцать дней, подтверждённая поздно, раньше попадала на основную и затирала месяцы оставшегося времени. Если потолок не допускает ещё одну подписку, платёж классифицируется как subscription_unavailable и не проводится; решает оператор. По той же причине воркер исполнения никогда не переносит право на услугу на подписку, отличную от его собственной и от подписки его заказа: право, чью подписку нельзя установить, окончательно отказывает с subscription_unresolved, а не выдаётся на основную подписку.
Что меняется для клиента
При включённой многоподписочности:- главное меню открывает переключатель подписок вместо экрана единственной подписки;
- каждая покупка, продление, повышение, понижение тарифа и покупка дополнения явно называют целевую подписку;
- управление устройствами, ротация ссылки и инструкции по подключению действуют на пользователя Remnawave выбранной подписки;
- уведомления об истечении, трафике, продлении, льготном периоде, восстановлении и исполнении называют подписку, о которой идёт речь.
/account перечисляет карточки, /account/subscriptions/{id} показывает одну подписку с индикатором трафика, формой переименования и элементом ротации, а /account/devices показывает устройства с переключателем, когда подписок больше одной. Обе поверхности полностью описаны на страницах Клиентская веб-панель и Telegram-бот.
Изоляция уведомлений
Дедупликация уведомлений ключуется по(user_id, kind, subscription_id, dedupe_key) с nulls not distinct. Без подписки в ключе две подписки, истекающие в один день, столкнулись бы на одном ключе дедупликации, и клиенту сообщили бы только об одной из них. Активная подписка никогда не сможет заглушить уведомление тихой.
Исполнение заказов
Каждая подписка выдаёт доступ собственному пользователю Remnawave, имя которого детерминированно выводится из идентификатора подписки:remnawave_users на уровне клиента продолжает отслеживать основную подписку (слот 1). Именно это оставляет докоммерческие экраны самообслуживания работающими без изменений.
Обнаружение расхождений, сверка и замещение — всё ограничено одной подпиской. Покупка второй подписки никогда не выводит из обращения право на услугу первой. На странице Исполнение заказов описаны конвейер операций, политика повторов и то, как выглядит сбой.
Изменение подписки: арифметика
Операция заказа плюс политики версии тарифа определяют три момента времени: когда право на услугу начинается, когда оно заканчивается и когда должна фактически выполниться выдача доступа. Пусть база — это конец текущего права на услугу, если он ещё в будущем, и текущий момент в противном случае.
Нигде в этой таблице нет пропорционального пересчёта. Повышение тарифа списывает полную каталожную цену нового тарифа;
replace отбрасывает оставшееся время, а extend его сохраняет. Omniflow никогда не угадывает, сколько стоит смена тарифа посреди периода, — именно поэтому выбор является политикой на уровне версии, которую задаёт оператор, а не формулой.
Понижение тарифа с at_expiry — это то, ради чего существует отложенный момент выдачи доступа: задание исполнения засыпает до этого момента, а не занимает воркер, и предыдущее право на услугу замещается только после того, как замена действительно выдана. До этого момента обе поверхности показывают действующий тариф: каждое клиентское чтение предпочитает право на услугу, чей старт уже наступил, тому, что запланировано позже, поэтому отложенное понижение не выглядит как «настраивается» весь остаток периода, который ещё не начался.
extension и renewal выполняют одинаковую арифметику и остаются разными операциями, потому что различается причина — одну попросил клиент, другую породило автоматическое списание, — и поддержка должна уметь их различать.
Запрещённая операция недостижима, а не просто не предлагается. Правило жизненного цикла живёт в internal/commerce и применяется внутри создания заказа к подписке, на которую заказ разрешился, поэтому любой вызывающий — веб-чекаут, бот, API с bearer-токеном, сохранённая корзина — получает один и тот же отказ, HTTP 422 operation_forbidden:
purchaseотклоняется на подписке, чьё право на услугу ещё действует (приостановленное считается действующим), — покупка начиналась бы с текущего момента и выбрасывала бы оставшееся время; если подписка не названа, покупка, которая по умолчанию легла бы на действующую основную, отклоняется, а не угадывается;extensionдолжно быть того тарифа, который держит подписка; продление истёкшей подписки покупает новый период от текущего момента и допускается;upgradeдолжен вести к другому тарифу с ценой не ниже текущего,downgrade— к тарифу дешевле, и обоим нужен действующий тариф, с которого переходить;- политика версии тарифа по-прежнему отклоняет
forbid.
renewal — собственная операция воркера продления, открываемая по постоянному согласию клиента для подписки, которую она называет, и не переклассифицируется. Длительность меньше или равная нулю отклоняется сразу.
Что предлагается клиенту
GET /v1/account/plans возвращает массив operations по каждому тарифу, вычисленный на сервере. Клиенту без подписок предлагается purchase. Клиенту с одной подпиской предлагается purchase, если потолок одновременных подписок это ещё позволяет, плюс extension, плюс upgrade и downgrade везде, где политика не forbid. Панель рисует кнопки; она не решает политику — панель, которая сама решала бы, какие кнопки рисовать, была бы второй реализацией правил, и рано или поздно они разошлись бы в пользу клиента или против него, а и то и другое — ошибка. Массив — это commerce.OfferedOperations, то же правило, которое применяет создание заказа, поэтому всё предложенное принимается.
Отмена
cancellation_policy хранится на версии тарифа, снимается в строку заказа и показывается — и на неё ничто не реагирует. В модели заказа нет операции отмены, нет маршрута отмены и нет элемента отмены ни на одной поверхности. Подписка заканчивается тем, что доходит до своей даты окончания без продления.
Оператор, которому нужно остановить доступ раньше, отключает подписку через конвейер исполнения — POST /v1/panel/customers/{customerID}/subscriptions/{subscriptionID}/operations с disable, причиной и правом subscriptions.write, — а это изменение состояния в Remnawave, а не отмена, и оно ничего не возвращает. Деньги возвращаются через путь возврата, описанный на странице Заказы и платежи.
Фазы: что сообщается клиенту
Одна функция сводит право на услугу вместе с наблюдаемым трафиком Remnawave к единственной фазе, чтобы бот и браузер не могли описывать одну и ту же подписку по-разному:
Окно в 7 дней совпадает с самым ранним напоминанием о продлении, поэтому бот никогда не показывает «активна», уже прося клиента продлить.
Оставшиеся дни вычисляются на сервере и округляются вверх, потому что подписка, у которой осталось восемь часов, с точки зрения клиента имеет ещё один день. Процент трафика ограничивается диапазоном 0–100, чтобы перерасход рисовался полной полосой, а «безлимит» передаётся явно, а не выводится из нулевого лимита: индикатор без потолка — это не индикатор на нуле процентов.
Льготные периоды
plan_versions.grace_period_seconds по умолчанию равен 0, и ноль означает немедленное истечение. Никакого неявного льготного периода нигде нет. Он задаётся на версию тарифа в редакторе версий, вводится в целых днях и существует для того, чтобы текст об истечении не был догадкой: он сохраняет доступ после конца периода, чтобы клиенту можно было сказать точно, сколько времени у него осталось на продление.
Льготный период доводится до клиента как gracePeriodSeconds в данных тарифа, и лестница фаз сообщает grace всё время, пока он длится.
Он доводится и до Remnawave — именно это делает обещание настоящим. ends_at права на услугу остаётся оплаченным концом — его читают арифметика продления, напоминания и отчётность, — но окончание, которое воркер выдачи доступа проталкивает в Remnawave, это ends_at плюс льготный период версии тарифа, для каждой операции: первого create, продления, возобновления после паузы и прохода сверки. Расхождение измеряется относительно той же суммы, поэтому удалённое окончание с учётом льготного периода — это согласие, а не расхождение expiry. Поскольку Remnawave затем держит пользователя живым всё окно, сообщённый статус EXPIRED означает, что окно действительно закончилось, и лестница фаз доверяет ему больше, чем часам: право на услугу, чей удалённый пользователь истёк, — это expired, никогда не grace.
Пауза
Отмена и возврат заканчивают подписку. Ни то ни другое не покрывает случай, о котором на самом деле просят: «меня не будет шесть недель, остановите счётчик». Обходной путь, к которому иначе тянется оператор, — отключить пользователя в Remnawave, а это останавливает доступ и не останавливает счётчик. Клиент возвращается к подписке, которая истекла, пока он не мог ею пользоваться. Поэтому пауза — это два факта, а не один. Доступ прекращается. Пользователь Remnawave отключается через обычный конвейер исполнения, поэтому несёт тот же ключ идемпотентности, ту же политику повторов и ту же историю, что и любое другое изменение. Счётчик останавливается.entitlements.paused_at фиксирует момент, и все
цифры оставшегося времени на обеих клиентских поверхностях считаются от него, а
не от «сейчас». Подписка на паузе, отсчитывающая себя до нуля на собственном
экране клиента, была бы функцией, противоречащей себе ровно там, куда клиент
смотрит.
Возобновление сдвигает ends_at вперёд ровно на длительность паузы и
записывает ту же величину в paused_seconds — поэтому подписка, срок которой
оказался позже оплаченного заказом, объясняется собственными колонками, а не
выглядит ошибкой.
На чём это держится
Возобновление — одна операция исполнения, а не две, и порядок объясняет
почему: новый срок отправляется до включения пользователя, поэтому пользователь
никогда не оказывается ненадолго включённым со сроком, который уже прошёл.
Покупка изменения во время паузы
Приостановленное право на услугу остаётся действующим правом подписки, и изменение, купленное во время паузы — продление, продлевающий подарок или код доступа, повышение или понижение тарифа, — строится на нём и списывает его ровно так же, как строилось бы на работающем. Строится оно на времени, которое пауза сохраняла: база нового права — это сейчас плюсends_at − paused_at, так что
клиент получает обратно ровно те дни, которые дало бы возобновление, а купленный
период добавляется сверху. Выдача доступа по новому праву суперседит
приостановленное и закрывает его паузу той же записью — paused_at очищается, а
прошедшая пауза добавляется в paused_seconds, — поэтому позднейшее
возобновление старой строки отклоняется, и дни не могут быть выданы дважды.
Оплаченное изменение возобновляет подписку: новое право включается в Remnawave,
как любое другое оплаченное изменение.
Понижение at_expiry — единственное изменение, которое не вступает в силу сразу:
оно ждёт окончания текущего периода. Поставленное за приостановленным правом, оно
начиналось бы через замороженный остаток от текущего момента, пока подписка
остаётся отключённой, и сохранённые дни истекли бы, пока у клиента нет доступа.
Поэтому его проведение сначала возобновляет приостановленное право — в той же
транзакции и ровно так, как это сделало бы возобновление оператором: ends_at
сдвигается вперёд на прошедшую паузу, а операция resume с ключом
order:{orderID}:resume снова включает пользователя, — и понижение начинается,
когда заканчивается возобновлённый период.
Автопродление во время паузы никогда не срабатывает: оно выбирает только
активные и ограниченные права. Дополнение во время паузы отклоняется с
no_active_subscription, потому что оно оценивается относительно оставшегося
времени текущего периода, а у остановленных часов оставшегося периода нет.
Сверка знает об этом
Подписка на паузе и отключённая подписка со стороны Remnawave — одно и то же, и разницу знает только Omniflow. Поэтому два случая считаются согласием, а не расхождением:- Удалённый пользователь отключён — так пауза выглядит сразу после применения.
- Удалённый пользователь истёк — так она выглядит через неделю, когда реальное время ушло за замороженный срок. Remnawave ничего не знает о паузе. Считать это истечением подписки значило бы забрать дни, ради сохранения которых пауза и существует, — через неделю после того, как клиенту сказали, что они в безопасности.
Паузы со стороны клиента нет
Намеренно. Пауза превращает подписку с датой в бессрочную, и позволить решать, когда идёт счётчик, тому, кто ею владеет, значило бы сделать «тридцать дней» равным «тридцати дням по моему выбору, навсегда». Пауза, поставленная оператором по просьбе, — это действие поддержки с приложенной причиной, которую фиксирует запись аудита. Что клиент получает — честное состояние: и бот, и панель говорят, что подписка на паузе и оставшиеся дни сохранены, а не «отключена — обратитесь в поддержку», что отправило бы человека в очередь обращений, чтобы услышать, что всё в порядке. Автопродление пропускает подписку на паузе без всякого дополнительного правила: запрос продления и так выбирает толькоactive и limited.
Напоминания, уведомления о льготном периоде и восстановление
Нотификатор бота обходит каждую подписку клиента и отправляет, по каждой подписке:
Истёкшая подписка, о которой никому не сказали, — это потерянный клиент, а восстановимая стоит того, чтобы дать путь назад в одно нажатие. Сообщение о восстановлении несёт кнопку, открывающую покупку той же версии тарифа, плюс ссылки на каталог и меню.
APP_RECOVERY_WINDOW_HOURS задаёт, как долго это предложение остаётся открытым; по умолчанию это 14 дней, которые .env.example поставляет как 336. Маркетинговые ограничения объёма к ним не применяются — уведомления жизненного цикла проходят через собственные настройки уведомлений клиента и тихие часы. См. Уведомления.
Восстановление работает только в Telegram. В клиентской веб-панели эквивалентного напоминания нет.
Клиент, импортированный до появления подписок и имеющий только сопоставление Remnawave на уровне клиента, всё равно получает оповещения: нотификатор откатывается к сводке, несущей лишь удалённый идентификатор. Названия подписок добавляются в оповещения только тогда, когда у клиента их больше одной.
Автоматическое продление
Автопродление — это согласие на списание за следующий период в отсутствие клиента, и оно выключено, пока клиент его не включит. Настраивается оно только в Telegram-боте — ни в клиентской веб-панели, ни в/admin нет ни эндпоинта, ни экрана автопродления.
Страница Регулярные платежи описывает запись о согласии, источники средств, заблаговременность списания, график повторных попыток и текущее состояние пути записи.
Включение и выключение многоподписочности
Включение
Мигрировать ничего не нужно. Миграция, которая ввела подписки, уже создала по одной на каждого существующего клиента, переняв имевшегося у него пользователя Remnawave. ПереключениеAPP_MULTI_SUBSCRIPTION_ENABLED в true меняет только то, что клиенты могут добавлять с этого момента.
Выключение
Возврат вfalse безопасен и неразрушителен:
- существующие дополнительные подписки продолжают работать, продлеваться и сохранять своих пользователей Remnawave;
- ни один клиент не может открыть ещё одну;
- клиент, у которого уже больше одной, по-прежнему видит переключатель, потому что прятать подписки, которыми он владеет, было бы хуже, чем показывать их.
Как оставить клиенту одну подписку
Что оператор может сделать вместо этого из/admin/customers/{customerId}, имея subscriptions.write и причину:
1
Дайте ей истечь или отключите её
Дайте лишней подписке дойти до даты окончания без продления либо поставьте
против неё операцию
disable, чтобы её пользователь Remnawave перестал
работать немедленно.2
Убедитесь, что ничего не висит
Проверьте, что право на услугу подписки находится в
expired или
superseded и что ни одна операция исполнения не остаётся в pending или
retrying в /admin → System → Jobs.3
Оставьте её на месте
Строка подписки остаётся, без живого права на услугу. Она занимает слот
одновременных подписок, поэтому клиенту, упёршемуся в потолок и желающему
другой тариф, нужно поднять потолок, а не удалять старую подписку.
Приостановка клиента до Remnawave тоже не доходит. Приостановка записывает
статус, событие жизненного цикла и строку аудита; ни одна часть конвейера
исполнения не читает статус клиента. Приостановленный клиент сохраняет
работающий доступ до конца своего периода — единственное, что приостановка
действительно останавливает, это автоматическое списание за продление. Если
доступ должен прекратиться сейчас, отключите ещё и подписку.
Смежные страницы
Каталог и акции
Где задаются длительность, льготный период, сквады и политики повышения и понижения тарифа.
Исполнение заказов
Конвейер операций, сверка, расхождения и изменения по запросу оператора.
Дополнения и конфигуратор
Дополнительный трафик, слоты устройств и сквады поверх живого права на услугу.
Регулярные платежи
Согласие на автопродление, источники средств, повторы и разбор неудавшихся списаний.