/admin/marketing, хотя форма там есть только у реферальной
программы; определения лояльности публикуются через /v1/panel.
Настройка реферальной программы
referral_programs — это строка-синглтон: первичный ключ здесь булев и должен
быть истинным. Вторая программа означала бы два ответа на вопрос «сколько
приносит приглашение?», а клиенту уже назвали один из них.
Откройте /admin/marketing и прокрутите до нижней карточки. Для редактирования
нужно settings.write, а не маркетинговое право: изменение того, сколько
платит приглашение, — коммерческое решение со счётом на руках, поэтому оно стоит
рядом с прочими вещами, которые стоят установке денег.
Установка, где программу никогда не настраивали, читает выключенные значения по
умолчанию —
RUB, first_paid_order, 90 дней действия, без ограничения, — а не
ошибку об отсутствующей строке.
Под формой стоит история программы на текущий момент: атрибутировано,
засчитано, отклонено и сколько денег выдано. Это размещено там, чтобы изменение
делалось на основании фактов, а не догадки.
Что API отклоняет — всё с 422
qualificationвнеfirst_paid_order. Это закрытый набор, потому что поле со свободным текстом было бы обещанием, которое ничем не обеспечено.- Валюта, состоящая не ровно из трёх заглавных букв.
- Отрицательное вознаграждение с любой из сторон.
- Включённая программа с нулевыми вознаграждениями с обеих сторон. Это была бы схема, которая что-то обещает клиентам и не платит ничего, что хуже, чем отсутствие схемы: приглашения всё равно расходятся. Панель показывает красное предупреждение «ничего не платит» и блокирует сохранение, чтобы оператор узнал об этом до нажатия кнопки, а не после.
attributionValidityDaysноль или меньше, а также — если они вообще заданы —inviterRewardCapилиrewardExpiryDaysноль или меньше.
referral.program.saved в категории маркетинга.
Что на самом деле засчитывает реферала
Сначала атрибуция, и только один раз
Реферальный код клиента — это десять символов base-32, соответствующих^[A-Z0-9]{10}$, выпускаемых лениво при первом открытии реферального экрана.
Один и тот же генератор работает и в боте, и в веб-панели, поэтому код,
выпущенный на одной поверхности, атрибутируется на другой.
referral_attributions ключуется на приглашённом клиенте и вставляется с
ON CONFLICT DO NOTHING. Поэтому атрибуция неизменяема и выигрывает первая
запись: кто пригласил клиента, решается в момент его прихода, и никакая
последующая ссылка /start этого не перепишет. Самореферал невозможен по
построению — в таблице есть CHECK, что два клиента различны, а вставка ещё и
отклоняет код, принадлежащий тому же клиенту, который его использует.
Засчитывание происходит внутри транзакции расчёта
Засчитывание и выдача происходят в той же транзакции базы данных, которая рассчитывает засчитываемый заказ. Именно это делает их ровно-однократными: повторный вебхук, воспроизведённое обновление Stars или ручное повторное подтверждение не могут зачислить деньги в кошелёк дважды. Должно выполняться всё перечисленное:- Программа включена, и её
qualification—first_paid_order. - Заказ покупает время подписки:
purchase,extension,renewal,upgradeилиdowngrade. Пополнение кошелька, покупка в магазине, подарок для кого-то другого и код доступа, оплаченный дистрибьютором, не рассматриваются — они не квалифицируют реферала и, рассчитавшись раньше, не мешают более позднему заказу подписки его квалифицировать. - Заказ дошёл до
paidилиfulfilled. - Платёжный провайдер действительно провёл деньги —
paid_minorза вычетомwallet_minorположителен, — поэтому заказ, полностью оплаченный кошельком, не создаёт из реферала новую стоимость. В кошельке может лежать вознаграждение приглашённого по этому же рефералу; квалифицируют только деньги извне. - Это первый рассчитанный заказ подписки клиента. Ничего более раннего у
него с одной из этих операций не лежит в
paid,fulfilled,partially_refundedилиrefunded. - Атрибуция всё ещё укладывается в
attributionValidityDays. - Пригласивший не исчерпал
inviterRewardCap, считаемый по вознаграждениям, которые у него уже есть в роли пригласившего. - Эта роль по данной паре ещё не была выдана.
Выдача один раз и что она записывает
referral_rewards уникальна по (referred_user_id, role), где роль — inviter
или invitee. Каждая выдача пишет в одной транзакции:
- сбалансированную транзакцию двойной записи в книге проводок — кредит
customer_walletпротив дебетаplatform_clearing— с ключом идемпотентностиreferral:<invited customer>:<role>; - строку
referral_rewards, вставленную сON CONFLICT DO NOTHING; - необязательный
expires_at, выведенный изrewardExpiryDays; - событие аудита
referral.reward_granted; qualified_atиqualifying_order_id, проставляемые на атрибуции один раз.
Проверка и отмена реферала
Решение, что кто-то злоупотребил программой, и изъятие его вознаграждения — это неблагоприятное для клиента решение, поэтому маршруты проверки стоят за правами risk, а не за маркетинговыми, — за теми же, что управляют списком блокировок.customerID — это приглашённый клиент, потому что именно он является ключом
атрибуции. Тело — {state, note}, где состояние — clear, held или
rejected.
Экрана в панели нет ни у одного из этих маршрутов. Проверка выполняется
запросами к
/v1/panel, и сейчас нет ничего, что поднимало бы сигнал
автоматически — состояние held и фильтр signalled=true достижимы только
тогда, когда их выставит оператор.Сигналы зовут человека, но никогда не наказывают
Сигнал придерживает реферальную пару для проверки и переводит её изclear в
held. Он никогда не может сам перевести пару в rejected.
В этом вся политика. Автоматическая система, которая молча наказывает клиентов, —
это система, которую никто не может объяснить наказанному клиенту, поэтому
единственная власть эвристики — позвать человека. Сигнал записывает увиденное в
терминах, которые оператор может проверить: количества и идентификаторы, но
никогда отпечаток устройства или IP-адрес, потому что поверхность для проверки —
не повод расширять то, что хранится.
Отклонение отматывает деньги через книгу проводок
Отклонение пары обходит все её вознаграждения, которые ещё не отменены, и пишет компенсирующую транзакциюcorrection в книге проводок — дебет кошелька против
клиринга платформы — с ключом referral-reversal:<rewardID>. Затем оно
проставляет на строке вознаграждения reversed_at, reversed_by,
reversal_reason и идентификатор транзакции отмены в книге проводок. Ограничение
CHECK гарантирует, что вознаграждение не может читаться как отменённое без
того, чтобы деньги действительно вернулись.
Оригинал не удаляется и не правится в ноль. Книга проводок доступна только на
дозапись, а «это было выдано, а потом изъято таким-то оператором по такой-то
причине» — факт иной, чем «этого никогда не выдавали»: клиент видел баланс в
любом случае, и запись обязана это сказать.
Отклонение требует непустой заметки, потому что оно забирает у клиента
деньги, а причина — это то, что оператор ему процитирует.
Отмена также возвращает пригласившему слот в лимите, потому что счётчик, который
этим лимитом управляет, видит только живые вознаграждения. Решение аудируется как
panel.referral.reviewed в категории риска, вместе с состоянием и заметкой.
Лояльность
Определения версионируются и неизменяемы
Строкиloyalty_programs версионируются, и маршрута обновления нет вовсе.
Опубликовать изменение — значит опубликовать следующую версию; старая продолжает
объяснять сделанные под ней назначения. Одновременно включена может быть не более
чем одна программа, что обеспечивается частичным уникальным индексом, а
публикация с enable: true выключает все остальные программы в том же запросе.
Клиент, дошедший до gold при одном наборе порогов, не должен незаметно из него
выпасть из-за того, что кто-то поменял числа. Позиция хранит метрику, по которой
было принято решение, и действовавшую версию, поэтому у вопроса «почему у меня
silver?» есть ответ, не зависящий от того, каковы пороги сегодня.
Публикация возможна только через API:
POST
требует marketing.write; GET /v1/panel/loyalty/programs требует
marketing.read.
По чему измеряется уровень
Все три — факты, которые Omniflow и так записывает. Это сделано намеренно: модель
лояльности, которой нужно новое отслеживание, — это модель лояльности, которой
нужно новое согласие.
Правила публикации, которые домен отклоняет
Два из них ограничениеCHECK выразить не может, и все они дают 422:
- Уровней нет вовсе.
- Порог самого нижнего уровня не равен нулю, из-за чего часть клиентов не попадала бы ни на один уровень. Уровень должен быть у каждого клиента.
- Уровень выше стоимостью меньше, чем уровень ниже, — представимо и наверняка является ошибкой.
- Неподдерживаемая метрика или неположительное окно для оконной метрики.
discountBps от 0 до 10000, коды и пороги
уровней уникальны внутри программы, graceDays ноль или больше, windowDays
больше нуля, валюта — три заглавные буквы.
Оценка воспроизводима, а понижение медленнее повышения
Evaluate принимает текущее время аргументом и не обращается к базе данных, поэтому одни
и те же входные данные всегда дают один и тот же уровень. Решение о лояльности,
которое никто не может воспроизвести, — это решение, которое никто не сможет
защитить, когда клиент его оспорит.
Правила по порядку:
- Текущего уровня нет — берётся заработанный уровень.
- Удерживаемый уровень принадлежит устаревшей версии — клиент переставляется по действующему определению.
- Заработанный порог не ниже удерживаемого — берётся заработанный уровень и сбрасывается льготный отсчёт; клиента никто не тащит на себе.
- Ниже удерживаемого уровня при нулевом льготном периоде — понижение сразу. Это законная конфигурация, а не ошибка.
- Ниже, отсчёт не запущен — уровень сохраняется, а отсчёт запускается до
now + graceDays. В историю ничего не пишется, потому что ничего не изменилось. - Ниже, отсчёт идёт и ещё не истёк — уровень сохраняется.
- Ниже, отсчёт истёк — понижение.
loyalty_standing_history дописывается только тогда, когда
позиция действительно изменилась, поэтому оператор, нажавший кнопку дважды,
получит одну запись, а не две. Записи истории пишутся с причиной evaluation.
Что видит клиент
/account/referrals отображает лестницу, ступень клиента, метрику, по которой
принято решение, расстояние до следующей ступени и процент пройденной текущей
полосы. Он читает тот уровень, который назван в позиции, а не пересчитывает
его, потому что клиент внутри льготного периода удерживает уровень, который его
текущая метрика уже не заработала, и пересчёт при загрузке страницы понизил бы
его ровно в момент её открытия.
Ответ содержит enabled: false, когда программа не опубликована или в программе
нет уровней, и enabled: true, evaluated: false, когда позиция клиента
принадлежит устаревшей версии: честный ответ здесь — что размещение ждёт
переоценки, а не что уровня у клиента нет.
Реферальная сводка на том же экране показывает количества приглашённых,
засчитанных, ожидающих и отклонённых, взятые из строк, а не выведенные
вычитанием, потому что атрибуция может быть засчитана и позже отклонена при
проверке, и вычитание учло бы такую пару дважды. Заработанные и отменённые деньги
показываются раздельно, чтобы клиент, у которого упал итог, видел причину, а не
гадал.
Ничто в этой проекции не несёт идентификатора приглашённого клиента.
Пригласивший вправе знать, что приглашение принесло выплату, но не получать
список того, кто его друзья.
Смежные страницы
Кошелёк и книга проводок
Куда попадает реферальный кредит и как читается отмена.
Сегменты аудитории и кампании
Фильтр
loyaltyTier и всё остальное, что делает /admin/marketing.Средства контроля рисков
Другие решения, которыми управляют
risk.read и risk.write.Веб-панель клиента
/account/referrals со стороны клиента.