Партнёрская программа
Партнёрская программа — независимый от обычных рефералов одноуровневый канал привлечения. Одобренный партнёр получает отдельные Telegram/Web App ссылки, комиссию с каждого подходящего успешного внешнего платежа своих клиентов и раздельный баланс по каждой валюте. Выплаты остаются ручными: партнёр создаёт заявку, администратор проверяет реквизиты и фиксирует результат.
Функция по умолчанию выключена. До включения можно применить миграции, настроить методы выплаты и проверить состояние без ретроактивных начислений.
Безопасное первое включение
Заголовок раздела «Безопасное первое включение»-
Сделайте резервную копию и обновите
backend,workerиfrontendдо одной версии, не включая программу. -
Создайте секрет командой
openssl rand -base64 32 | tr '+/' '-_', задайтеPARTNER_REQUISITES_ENCRYPTION_KEYи уникальныйPARTNER_REQUISITES_KEY_ID, затем пересоздайтеbackendиworker. -
В админке откройте Настройки → Маркетинговые программы → Партнёрская программа. Проверьте, что диагностика шифрования зелёная.
-
Настройте валюты, исключённые типы продаж, ставку, hold и хотя бы один способ выплаты. Для включённой банковской карты обязательно поле
card_number, для СБП —phone, для криптовалюты —addressи хотя бы одна сеть. -
Оставляя программу выключенной, выполните отчёт сверки:
-
Включите программу, одобрите тестового партнёра, проведите два внешних платежа и один полный цикл выплаты. Оплату балансом включайте отдельно после проверки работающего
worker.
Где находятся разделы и как применяется включение
Заголовок раздела «Где находятся разделы и как применяется включение»- У пользователя раздел Партнёрство появляется в навигации Mini App только при включённом
PARTNER_PROGRAM_ENABLED. Прямая ссылка/partnerпри выключенной программе возвращает на главную. До одобрения раздел показывает форму или состояние заявки; active-партнёр видит ссылки, клиентов, комиссии, раздельные балансы и выплаты. - У администратора операционный раздел Партнёры остаётся доступным и после выключения программы, чтобы завершать ранее созданные выплаты и проверять историю. Счётчик внимания обновляется каждые 30 секунд и учитывает ожидающие заявки и открытые выплаты.
- Конфигурация находится в Настройки → Маркетинговые программы → Партнёрская программа. Сохранённые через админку параметры записываются как overrides в БД и применяются работающим backend без перезапуска. В той же открытой админской сессии публичные данные Mini App перечитываются сразу, поэтому пункт Партнёрство появляется или исчезает без полной перезагрузки страницы. Другие уже открытые клиентские сессии увидят изменение при следующей загрузке данных.
- Переключатель
PARTNER_AUTO_ENROLLMENT_ENABLEDвключает режим без заявок. При его подтверждении админка одновременно включаетPARTNER_PROGRAM_ENABLED, а backend в одной транзакции с настройкой создаёт active-профили всем существующим неблокированным пользователям. Новые пользователи получают профиль в первом Telegram bot/Web App flow, поэтому форма заявки им не показывается. Для профиля фиксируется действующая на момент подключения ставка по умолчанию. - Автоматическое подключение идемпотентно и не отменяет модерацию: active-профиль не дублируется, paused/closed-профиль не реактивируется, индивидуальная ставка не заменяется. Существующая pending-заявка пользователя с новым active-профилем закрывается как approved без массовой рассылки уведомлений; rejected-заявки остаются историей. Заблокированные пользователи не подключаются, но после разблокировки смогут получить профиль в обычном пользовательском flow. Выключение режима останавливает только будущие автоматические подключения: уже созданные профили, ссылки, балансы и обязательства сохраняются.
- Опциональный
PARTNER_REFERRAL_PROGRAM_DISABLEDпо умолчанию выключен. Если включить его вместе с партнёрской программой, обычная реферальная программа остаётся доступна всем пользователям без партнёрского профиля, а у партнёров блокируются реферальные действия. В Mini App реферальная карточка остаётся на месте в приглушённом виде с поясняющей плашкой поверх неё; поле промокода продолжает работать. В Telegram скрывается реферальная кнопка, а старые callback и inline-запросы отклоняются с пояснением. PARTNER_REQUISITES_ENCRYPTION_KEYиPARTNER_REQUISITES_KEY_IDостаются только в окружении. Их изменение требует штатного перезапуска процессов и, для уже сохранённых реквизитов, ротации по инструкции ниже.
Заявки, ссылки и атрибуция
Заголовок раздела «Заявки, ссылки и атрибуция»В ручном режиме пользователь подаёт заявку в разделе Партнёрство. Одновременно может
существовать только одна pending-заявка; повтор сетевого запроса с тем же текстом возвращает её же.
Администратор может одобрить заявку с индивидуальной ставкой, отклонить с сообщением, позже
разрешить повторную подачу, приостановить профиль, изменить ставку или создать профиль вручную.
При включённом PARTNER_AUTO_ENROLLMENT_ENABLED endpoint заявок отклоняет новые заявки как
ненужные, а обзор партнёрской программы сначала идемпотентно материализует active-профиль.
Новая заявка отправляет служебное сообщение в настроенный LOG_CHAT_ID/LOG_THREAD_ID. При
одобрении или отклонении пользователь получает локализованное Telegram-уведомление; одобрение и
прямое создание партнёрского профиля дополнительно фиксируются в лог-чате. Изменения статуса
профиля (active, paused, closed) также уведомляют пользователя. Повторный HTTP-запрос,
вернувший уже существующую pending-заявку, новое сообщение не создаёт.
Партнёрские ссылки имеют собственный код и не используют обычный referral payload. Атрибуция
first-touch создаётся только для нового пользователя: открытие второй ссылки, ссылка самого
партнёра, paused-профиль или уже существующая учётная запись не меняют владельца клиента.
REGISTRATION_INVITE_ONLY_ENABLED принимает валидную партнёрскую ссылку как приглашение, но
обычная реферальная связь при этом остаётся отдельной.
При включённом PARTNER_REFERRAL_PROGRAM_DISABLED ранее выданные обычные реферальные ссылки
active-партнёра не пропадают: при регистрации нового пользователя они разрешаются через текущий
партнёрский профиль и создают такую же партнёрскую first-touch атрибуцию, как его отдельные
партнёрские ссылки. Денежная комиссия начисляется партнёру, а бонусные дни клиенту зависят от
PARTNER_CLIENT_WELCOME_BONUS_ENABLED, PARTNER_CLIENT_PAYMENT_BONUS_ENABLED и
PARTNER_ONE_BONUS_PER_CLIENT; обычные дни пригласившему не начисляются. Ссылка paused/closed
партнёра не создаёт ни обычную реферальную связь, ни партнёрскую атрибуцию. У пользователей без
партнёрского профиля обычные реферальные ссылки продолжают работать без изменений.
Переключение не преобразует историю задним числом: существующий referred_by_id, уже выданные
бонусы и ранее созданная партнёрская атрибуция сохраняются. Существующая учётная запись также не
перепривязывается при открытии такой ссылки. Если выключить партнёрскую программу целиком,
ограничение не действует и обычная реферальная программа снова доступна партнёрским профилям.
Администратор может ротировать код ссылки: старые ссылки перестают создавать новые атрибуции, но уже закреплённые клиенты и финансовая история не меняются. Партнёр может быть прямым клиентом другого партнёра, однако программа одноуровневая: комиссии с его собственных клиентов выше по цепочке не передаются.
В настройках программы есть два независимых, выключенных по умолчанию клиентских бонуса:
PARTNER_CLIENT_WELCOME_BONUS_ENABLEDразрешает один приветственный бонус только пользователю, который был создан по active Telegram/Web App ссылке партнёра, пока переключатель уже был включён. Размер и тариф берутся из обычных настроек реферального welcome-бонуса. Право фиксируется при регистрации, поэтому последующее включение не раздаёт бонус старым клиентам; привязанный позже Telegram позволяет забрать уже зафиксированное право. Общий пользовательский claimed-маркер и наличие любой исторической подписки исключают повторное получение и комбинацию обычного и партнёрского welcome-бонусов.PARTNER_CLIENT_PAYMENT_BONUS_ENABLEDначисляет закреплённому клиенту только колонку бонуса приглашённому из матрицы его period-тарифа. Партнёр не получает бонусные дни приглашавшего — его вознаграждение остаётся денежной комиссией. НезависимыйPARTNER_ONE_BONUS_PER_CLIENTопределяет, бонусна только первая успешная оплата или каждая успешная оплата клиента; по умолчанию действует только первая. Повторный webhook не начисляет дни ещё раз, а конкурентные оплаты одного клиента сериализуются блокировкой пользователя до проверки истории и фиксации бонуса.
Клиентские бонусы не начисляются задним числом. Для PARTNER_ONE_BONUS_PER_CLIENT=True первым
считается именно первый успешный платёж пользователя в истории, а не первое фактическое начисление
бонусных дней: платёж до атрибуции, платёж при выключенном бонусе или период без настроенного
значения в матрице всё равно делает последующие оплаты не первыми. Чтобы бонусировать каждую
будущую подходящую оплату, выключите этот ограничитель.
Если у пользователя уже есть обычный referred_by_id, обычная реферальная связь имеет приоритет:
партнёрская версия бонусных дней поверх неё не добавляется. Этот приоритет относится только к
бонусным дням клиента и пригласившего; денежная комиссия продолжает определяться отдельной
партнёрской атрибуцией. Поэтому после импорта обычных рефералов партнёр может получать комиссию, а
дни по-прежнему рассчитываются правилами обычной реферальной программы. Paused/closed партнёр или
выключенная программа блокируют новые партнёрские клиентские бонусы. Импорт и ручная атрибуция не
создают право на welcome-бонус. Их будущие оплаты получают партнёрскую клиентскую часть матрицы
только при отсутствии обычного referred_by_id и с учётом ограничения первого платежа выше.
Когда обычная реферальная программа выключена, раздел Партнёры предлагает массово преобразовать
существующие связи referred_by_id в партнёрские клиентские атрибуции. Preview заранее показывает
число партнёров и найденных связей, доступных клиентов, уже импортированные записи, конфликты и
количество исторических платежей. Перед execute администратор отдельно подтверждает, что эти
платежи не будут пересчитаны: право на комиссию начинается только с момента импорта.
Операция идемпотентна и повторно не закрепляет клиента. Связь с тем же партнёром считается уже импортированной; клиент другого партнёра и self-referral считаются конфликтами и не перепривязываются. Для каждого партнёра с новыми клиентами создаётся audit event. Если обычная реферальная программа включена, preview и execute отклоняются, чтобы два механизма не меняли одну историю одновременно.
Комиссии и баланс
Заголовок раздела «Комиссии и баланс»Каждый успешный платеж закреплённого клиента получает ровно одно решение. В решении сохраняются
фактически списанная сумма после промокода, валюта и её scale, ставка, провайдер и тип продажи.
Внутренняя оплата партнёрским балансом, неподходящая валюта и исключённый тип продажи получают
решение excluded, но не запись дохода.
Суммы хранятся в целых minor units, ставка — в basis points; округление выполняется HALF_UP.
Ledger добавочный: существующие финансовые строки не исправляются UPDATE/DELETE. До конца hold
комиссия pending, затем worker переводит её в доступный баланс. Refund до hold аннулирует credit,
после hold добавляет отрицательный reversal; поэтому баланс может стать отрицательным, и новые
выводы/оплата балансом блокируются до погашения.
Ручная корректировка администратора поддерживает add, debit и set и всегда создаёт отдельную ledger-запись с причиной и audit event. В отчётах нельзя складывать разные валюты без явной конвертации.
Вывод средств
Заголовок раздела «Вывод средств»Партнёр выбирает настроенный способ, сумму или MAX и отправляет заявку с idempotency key. Сервер
повторно проверяет актуальный minimum, валюту, сеть, число активных заявок и доступный баланс под
блокировкой профиля. Создание заявки сразу резервирует сумму. Партнёр может отменить только статус
requested; администратор переводит requested в processing, rejected или failed, а
processing — в paid, rejected или failed. Состояние failed не возвращает резерв
автоматически: администратор может повторить обработку (processing) либо завершить её отказом
(rejected) с возвратом. Резерв освобождается ровно один раз только при canceled или rejected.
Каждая новая заявка на вывод отправляется администратору в тот же Telegram-чат логов с безопасным
профилем пользователя, суммой, валютой и внутренним номером — без полных реквизитов. Пользователь
получает сообщения о переходах в processing, paid, rejected, failed и canceled. Для
failed уведомление отдельно поясняет, что сумма остаётся зарезервированной. Идемпотентный повтор
создания заявки и повтор перехода в уже установленный статус уведомления не дублируют.
Способ выплаты, scale валюты, сеть и маска реквизитов сохраняются снимком в момент заявки. Поэтому последующее изменение или отключение способа влияет только на новые заявки и не мешает завершить уже созданную.
Полные реквизиты шифруются AES-GCM с привязкой к партнёру и idempotency key. Пользовательские,
списочные и событийные контракты содержат только маску. Администратор раскрывает реквизиты только
в открытой карточке; каждое раскрытие записывается в audit. После срока
PARTNER_REQUISITES_RETENTION_DAYS worker удаляет ciphertext завершённых заявок, сохраняя маску и
финансовую историю.
Оплата из баланса
Заголовок раздела «Оплата из баланса»Active-партнёр может включить использование баланса при покупке периода, пакета трафика, дополнительных устройств или платной смене тарифа. Баланс применяется после промокода. Если его не хватает, внешний провайдер получает остаток; при этом сумма внешнего счёта не опускается ниже минимума провайдера. Если баланс покрывает весь серверный quote, покупка завершается внутренне без создания внешнего счёта. Telegram Stars и методы с ценой, управляемой провайдером, не смешиваются с балансом.
Payment и debit создаются одной транзакцией, а ключи ledger привязаны к payment_id. При отмене,
истечении, ошибке создания/активации или refund компенсирующий credit возвращает средства ровно
один раз; worker повторяет пропущенные возвраты. Если после возврата приходит запоздалый успешный
webhook, credit переводится в void, поэтому исходное списание снова действует и деньги не
дублируются. Последующий refund может повторно активировать тот же credit. Полностью внутренняя
покупка имеет internal funding source и не увеличивает cash revenue; в смешанном Payment внешней
выручкой считается только остаток, реально оплаченный провайдеру.
Ежедневная сверка и диагностика
Заголовок раздела «Ежедневная сверка и диагностика»Read-only отчёт ничего не изменяет и не выводит персональные данные:
Поле ok должно быть true. issues проверяет отсутствующие решения для успешных платежей,
несогласованные commission/withdrawal ledger, повторяющиеся operational references, пустые
idempotency keys и ссылки на удалённые сущности. balance_rows пересчитывает каждый баланс только
из posted ledger, а negative_balances отдельно показывает допустимые долги после reversal.
Отрицательный баланс сам по себе не делает отчёт ошибочным.
Worker каждую минуту идемпотентно:
- создаёт пропущенные решения и reversals;
- выпускает комиссии после hold;
- освобождает зависшие internal spends, включая полностью покрытые checkout между списанием и активацией;
- возвращает partner-balance reservations для терминальных checkout;
- применяет сроки хранения аудита и реквизитов.
Сначала устраните причину расхождения. Исправления выполняйте штатным reconciler, reversal или ручной корректировкой; не редактируйте ledger напрямую.
Ротация ключа реквизитов
Заголовок раздела «Ротация ключа реквизитов»Ротация атомарно расшифровывает все ещё хранящиеся реквизиты старым ключом и шифрует новым. До
успешного завершения не заменяйте текущий ключ в .env.
-
Выключите создание новых выплат, остановите
backendиworker, сделайте backup БД. -
Оставьте в окружении старые
PARTNER_REQUISITES_ENCRYPTION_KEYиPARTNER_REQUISITES_KEY_ID. Добавьте временныеPARTNER_REQUISITES_NEW_ENCRYPTION_KEYиPARTNER_REQUISITES_NEW_KEY_ID. -
Запустите dry-run; он проверит расшифровку каждой строки и ничего не запишет:
-
Повторите с
APPLY=1. Все строки меняются одной транзакцией, а в audit попадают только ids ключей и количество строк: -
Только после успеха перенесите новый ключ/id в основные переменные, удалите временные
PARTNER_REQUISITES_NEW_*, запустите процессы и проверьте раскрытие одной тестовой заявки.
Если dry-run или apply завершился ошибкой, оставьте старый ключ: транзакция не коммитится. После успешной ротации откат требует обратной ротации на сохранённый старый ключ либо восстановления backup; поэтому старый секрет храните по принятой политике восстановления, а не в репозитории.
Rollback и приватность
Заголовок раздела «Rollback и приватность»Для функционального rollback выключите программу, новые выводы и оплату балансом. Не откатывайте append-only миграцию и не удаляйте таблицы: админская история должна оставаться доступной для уже созданных долгов и выплат. Worker можно остановить отдельно после обработки ожидающих операций.
Обычный backup PostgreSQL включает все партнёрские таблицы. Храните backup вместе с подходящей версией encryption key: без неё реквизиты восстановить нельзя. Удаление учётной записи закрывает и анонимизирует профиль/заявки/атрибуцию, но сохраняет обезличенный ledger и обязательства. Клиенты и партнёры видят публичные snapshot-метки, а не email, Telegram ID, provider payment id или данные подписки. Перед включением разместите собственные условия, налоговые/KYC требования и privacy notice для вашей юрисдикции: Core предоставляет технические механизмы, но не заявляет юридическую совместимость автоматически.
Связанные разделы: платежи, админ-панель, бэкапы, переменные окружения.