Перейти к содержимому

Партнёрская программа

Партнёрская программа — независимый от обычных рефералов одноуровневый канал привлечения. Одобренный партнёр получает отдельные Telegram/Web App ссылки, комиссию с каждого подходящего успешного внешнего платежа своих клиентов и раздельный баланс по каждой валюте. Выплаты остаются ручными: партнёр создаёт заявку, администратор проверяет реквизиты и фиксирует результат.

Функция по умолчанию выключена. До включения можно применить миграции, настроить методы выплаты и проверить состояние без ретроактивных начислений.

  1. Сделайте резервную копию и обновите backend, worker и frontend до одной версии, не включая программу.

  2. Создайте секрет командой openssl rand -base64 32 | tr '+/' '-_', задайте PARTNER_REQUISITES_ENCRYPTION_KEY и уникальный PARTNER_REQUISITES_KEY_ID, затем пересоздайте backend и worker.

  3. В админке откройте Настройки → Маркетинговые программы → Партнёрская программа. Проверьте, что диагностика шифрования зелёная.

  4. Настройте валюты, исключённые типы продаж, ставку, hold и хотя бы один способ выплаты. Для включённой банковской карты обязательно поле card_number, для СБП — phone, для криптовалюты — address и хотя бы одна сеть.

  5. Оставляя программу выключенной, выполните отчёт сверки:

    docker compose exec -T backend python -m scripts.partner_reconciliation
  6. Включите программу, одобрите тестового партнёра, проведите два внешних платежа и один полный цикл выплаты. Оплату балансом включайте отдельно после проверки работающего 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 отчёт ничего не изменяет и не выводит персональные данные:

docker compose exec -T backend python -m scripts.partner_reconciliation

Поле 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.

  1. Выключите создание новых выплат, остановите backend и worker, сделайте backup БД.

  2. Оставьте в окружении старые PARTNER_REQUISITES_ENCRYPTION_KEY и PARTNER_REQUISITES_KEY_ID. Добавьте временные PARTNER_REQUISITES_NEW_ENCRYPTION_KEY и PARTNER_REQUISITES_NEW_KEY_ID.

  3. Запустите dry-run; он проверит расшифровку каждой строки и ничего не запишет:

    docker compose run --rm backend python -m scripts.rotate_partner_requisites_key
  4. Повторите с APPLY=1. Все строки меняются одной транзакцией, а в audit попадают только ids ключей и количество строк:

    docker compose run --rm -e APPLY=1 backend python -m scripts.rotate_partner_requisites_key
  5. Только после успеха перенесите новый ключ/id в основные переменные, удалите временные PARTNER_REQUISITES_NEW_*, запустите процессы и проверьте раскрытие одной тестовой заявки.

Если dry-run или apply завершился ошибкой, оставьте старый ключ: транзакция не коммитится. После успешной ротации откат требует обратной ротации на сохранённый старый ключ либо восстановления backup; поэтому старый секрет храните по принятой политике восстановления, а не в репозитории.

Для функционального rollback выключите программу, новые выводы и оплату балансом. Не откатывайте append-only миграцию и не удаляйте таблицы: админская история должна оставаться доступной для уже созданных долгов и выплат. Worker можно остановить отдельно после обработки ожидающих операций.

Обычный backup PostgreSQL включает все партнёрские таблицы. Храните backup вместе с подходящей версией encryption key: без неё реквизиты восстановить нельзя. Удаление учётной записи закрывает и анонимизирует профиль/заявки/атрибуцию, но сохраняет обезличенный ledger и обязательства. Клиенты и партнёры видят публичные snapshot-метки, а не email, Telegram ID, provider payment id или данные подписки. Перед включением разместите собственные условия, налоговые/KYC требования и privacy notice для вашей юрисдикции: Core предоставляет технические механизмы, но не заявляет юридическую совместимость автоматически.

Связанные разделы: платежи, админ-панель, бэкапы, переменные окружения.