Skip to main content

Обзор

Лендинги (Landing Pages) — публичные страницы покупки VPN-подписки, доступные по прямой ссылке без необходимости регистрации в Telegram. Покупатель выбирает тариф, период, способ оплаты, вводит email или Telegram-контакт и оплачивает подписку.

Возможности

  • Публичная страница по уникальному slug: https://cabinet.example.com/quick/{slug}
  • Выбор из разрешённых тарифов и периодов
  • Все подключённые платёжные системы (с возможностью ограничения и sub-options)
  • Покупка для себя или в подарок
  • Мультиязычный интерфейс (заголовки, описания, баджи — JSON с ключами ru, en, uz, uk, es)
  • Система скидок с таймером обратного отсчёта и переопределениями по тарифам
  • Кастомный CSS
  • SEO meta-теги
  • Автосоздание аккаунта в Cabinet с отправкой пароля на email
  • Уведомления администраторов в Telegram о новых покупках

Требования

Для работы лендингов необходимо:
  1. CABINET_ENABLED=true
  2. CABINET_URL заполнен (используется для формирования ссылок в email)
  3. SMTP настроен и работает — для отправки данных подписки и пароля от Cabinet на email покупателя
  4. Хотя бы одна платёжная система подключена и настроена
  5. Настроены тарифы (SALES_MODE=tariffs)
  6. Права на управление лендингами (если включён RBAC)

Почему SMTP обязателен

Когда пользователь покупает подписку через лендинг по email:
  1. Создаётся аккаунт в Cabinet с автосгенерированным паролем
  2. На email отправляется письмо с данными для входа, ссылкой подписки и инструкциями
  3. Без SMTP пользователь не получит ни пароль, ни ссылку подписки
Настройка SMTP: Настройка SMTP
Для улучшения доставляемости email бот автоматически добавляет заголовки Message-ID и Date ко всем исходящим письмам.

Создание лендинга

Через Admin Cabinet

  1. Откройте Cabinet → Админ-панель → Лендинги
  2. Нажмите Создать лендинг
  3. Заполните поля:
ПолеОписаниеОбязательно
SlugURL-путь лендинга (латиница, цифры, дефис). УникальныйДа
ЗаголовокМультиязычный заголовок страницы (JSON)Да
ПодзаголовокМультиязычный подзаголовокНет
ТарифыВыбор из доступных тарифов (макс. 50)Да
ПериодыДоступные периоды по тарифам (по умолчанию — все)Нет
Способы оплатыРазрешённые платёжные методы с sub-optionsДа
ПодаркиВключить покупку в подарокНет
ОсобенностиСписок фич с иконками (промо-блок)Нет
FooterМультиязычный текст внизу страницыНет
CSSКастомный CSS для стилизацииНет
Meta titleSEO заголовок (мультиязычный)Нет
Meta descriptionSEO описание (мультиязычный)Нет
Порядок отображенияЧисловой порядок в списке лендинговНет
  1. Нажмите Сохранить
Лендинг будет доступен по адресу: https://cabinet.example.com/quick/{slug}

Мультиязычные поля

Все текстовые поля поддерживают мультиязычность в формате JSON:
{
  "ru": "Быстрый VPN",
  "en": "Fast VPN",
  "uz": "Tez VPN"
}
Язык выбирается автоматически по языку браузера пользователя. Если перевод не найден — используется русский (ru).

Ограничение периодов

По умолчанию на лендинге доступны все периоды тарифа. Можно ограничить через поле Допустимые периоды:
{
  "15": [30, 90],
  "23": [30, 90, 180]
}
Где ключ — ID тарифа (строка), значение — список доступных периодов в днях. Максимум 50 тарифов, до 20 периодов на каждый.

Способы оплаты и sub-options

Выберите платёжные системы, доступные на конкретном лендинге. Некоторые платёжные системы поддерживают sub-options — отдельные кнопки для разных методов внутри одного провайдера:
ПровайдерSub-options
YooKassaКарта, СБП
PayPalych (Pal24)Карта, СБП
FreekassaКарта, СБП (NSPK)
PlategaДинамические (из PLATEGA_ACTIVE_METHODS)
Telegram Stars, CryptoBot, CloudPayments и др.Нет sub-options
На публичной странице лендинга sub-options отображаются как отдельные кнопки выбора способа оплаты.

Особенности (промо-блок)

Список фич для промо-блока на странице лендинга. Каждая фича содержит:
[
  {
    "icon": "shield",
    "title": {"ru": "Безопасность", "en": "Security"},
    "description": {"ru": "Шифрование трафика", "en": "Traffic encryption"}
  }
]

Система скидок

Лендинги поддерживают временные скидки с таймером обратного отсчёта.

Настройка скидки

В редакторе лендинга заполните блок Скидка:
ПолеОписаниеОграничения
Процент скидкиГлобальная скидка на все тарифы1-99%
Начало скидкиДата и время начала действияUTC, datetime-local с секундами
Окончание скидкиДата и время окончанияUTC, должно быть позже начала
Текст баджаМультиязычный текст на баннереJSON, опционально
ПереопределенияИндивидуальный процент по тарифамJSON, ключи должны быть из списка тарифов
Дата начала должна быть строго раньше даты окончания. Это проверяется на бэкенде и на уровне базы данных (CHECK constraint).

Переопределения по тарифам

Если для тарифа задано индивидуальное значение скидки, оно используется вместо глобального:
{
  "15": 30,
  "23": 50
}
Тариф 15 получит скидку 30%, тариф 23 — 50%, остальные — глобальный процент.
Ключи переопределений должны быть ID тарифов из списка разрешённых тарифов лендинга. Несуществующие ID будут отклонены при сохранении.

Каскадное удаление

При сбросе процента скидки (discount_percent = null) автоматически очищаются все связанные поля:
  • discount_overridesnull
  • discount_starts_atnull
  • discount_ends_atnull
  • discount_badge_textnull

Таймер обратного отсчёта

На публичной странице лендинга отображается анимированный баннер скидки:
  • Текст баджа (или «Скидка N%» по умолчанию)
  • Таймер обратного отсчёта: дни : часы : минуты : секунды
  • Перечёркнутая старая цена и новая цена со скидкой
Когда таймер истекает:
  1. Баннер скидки плавно исчезает (анимация AnimatePresence)
  2. Данные лендинга автоматически перезагружаются с сервера
  3. Цены обновляются до полных
  4. Повторная загрузка подтверждает, что скидка действительно закончилась на сервере

Расчёт цены со скидкой

Формула (одинаковая на бэкенде и фронтенде):
discounted_price = max(1, price - floor(price * discount_percent / 100))
  • Целочисленное деление// в Python, Math.floor() в JavaScript
  • Все расчёты в копейках
  • Минимальная цена — 1 копейка (защита от бесплатных покупок)
  • Переопределение по тарифу имеет приоритет над глобальным процентом

Процесс покупки

Покупка по email (полный lifecycle)

Покупатель                    Лендинг                     API бота                  Remnawave
    │                           │                           │                           │
    │  1. Открывает             │                           │                           │
    │  /quick/{slug}            │                           │                           │
    │──────────────────────────>│                           │                           │
    │                           │  2. GET /landing/         │                           │
    │                           │  config/{slug}            │                           │
    │                           │──────────────────────────>│                           │
    │                           │  {tariffs, prices,        │                           │
    │                           │   discount, payments}     │                           │
    │                           │<──────────────────────────│                           │
    │  3. Выбирает тариф,      │                           │                           │
    │  период, оплату,         │                           │                           │
    │  вводит email            │                           │                           │
    │──────────────────────────>│                           │                           │
    │                           │  4. POST /landing/        │                           │
    │                           │  purchase                 │                           │
    │                           │──────────────────────────>│                           │
    │                           │  {token, payment_url}     │                           │
    │                           │<──────────────────────────│                           │
    │  5. Переход на            │                           │                           │
    │  платёжную систему       │                           │                           │
    │──────────────────────────>│                           │                           │
    │                           │                           │                           │
    │                           │           6. Webhook:     │                           │
    │                           │           оплата успешна  │                           │
    │                           │                           │                           │
    │                           │                           │  7. Создание подписки     │
    │                           │                           │──────────────────────────>│
    │                           │                           │  {subscription_url}       │
    │                           │                           │<──────────────────────────│
    │                           │                           │                           │
    │                           │                           │  8. Создание аккаунта     │
    │                           │                           │  Cabinet + пароль         │
    │                           │                           │                           │
    │  9. Email: пароль,       │                           │                           │
    │  ссылка подписки,        │                           │                           │
    │  ссылка на Cabinet       │                           │                           │
    │<─────────────────────────────────────────────────────│                           │
    │                           │                           │                           │
    │  10. Вход в Cabinet      │                           │                           │
    │  по email + пароль       │                           │                           │
Детали шагов 7-9:
  1. Бот проверяет цену с учётом активной скидки (пересчитывает на момент оплаты)
  2. Создаёт пользователя в Remnawave с указанным тарифом и периодом
  3. Если аккаунт Cabinet с таким email не существует:
    • Создаёт аккаунт с автосгенерированным паролем
    • Генерирует одноразовый auto-login токен
  4. Отправляет email с:
    • Паролем от Cabinet
    • Ссылкой подписки (subscription URL)
    • Ссылкой для автоматического входа в Cabinet
    • Инструкциями по подключению

Покупка по Telegram

  1. Покупатель вводит свой Telegram username (без @)
  2. После оплаты бот отправляет сообщение в Telegram с данными подписки
  3. Если у пользователя нет аккаунта бота — подписка ожидает активации:
    • Пользователь запускает бот через /start
    • Бот автоматически активирует ожидающую подписку

Подарочная покупка

  1. Покупатель включает переключатель «В подарок»
  2. Вводит свои контакты (email или Telegram)
  3. Вводит контакты получателя (email или Telegram)
  4. Опционально добавляет сообщение для получателя
  5. После оплаты подписка активируется на аккаунте получателя

Управление лендингами

Список лендингов

Admin Cabinet → Лендинги — список всех лендингов:
  • Slug и заголовок
  • Статус (активен / неактивен)
  • Количество покупок
  • Дата создания
  • Действия: редактирование, toggle, удаление

Активация / деактивация

Кнопка toggle в списке лендингов. Неактивные лендинги возвращают 404 при попытке открыть публичную страницу.

Удаление

Удаление доступно только для лендингов без покупок (для сохранения истории платежей).

RBAC права

Если включён RBAC, для управления лендингами требуются соответствующие разрешения:
ПравоОписание
Просмотр лендинговСписок и детали лендингов
Создание лендинговСоздание новых лендингов
Редактирование лендинговИзменение настроек, скидок, контента
Удаление лендинговУдаление лендингов без покупок
Настройка прав: RBAC

Уведомления администраторов

При новых покупках через лендинг:
  • Telegram: администраторы получают уведомление в настроенный чат/топик (если ADMIN_NOTIFICATIONS_ENABLED=true)
  • Email покупателю: письмо с данными подписки и паролем от Cabinet
  • Telegram покупателю: сообщение в Telegram (если покупка по username)
Уведомления отправляются в топик, указанный в ADMIN_NOTIFICATIONS_TOPIC_ID. Если топик не задан — в основной чат ADMIN_NOTIFICATIONS_CHAT_ID.

Email-шаблоны

Шаблоны писем для покупок через лендинг можно настроить в Admin Cabinet → Email шаблоны. Доступные шаблоны:
ШаблонОписание
Quick Purchase — результат покупкиПисьмо покупателю после успешной оплаты
Шаблоны поддерживают переменные-плейсхолдеры, которые автоматически подставляются при отправке (ссылка подписки, пароль, имя сервиса и др.). Предварительный просмотр доступен в редакторе шаблонов.

API эндпоинты

GET /landing/config/

Публичный эндпоинт. Возвращает конфигурацию лендинга для отображения. Ответ:
{
  "slug": "promo-2025",
  "title": "Быстрый VPN",
  "subtitle": "Безопасное подключение",
  "features": [...],
  "footer_text": "...",
  "gift_enabled": true,
  "custom_css": null,
  "tariffs": [
    {
      "id": 15,
      "name": "Стандарт",
      "periods": [
        {"days": 30, "price_kopeks": 30000},
        {"days": 90, "price_kopeks": 80000}
      ]
    }
  ],
  "payment_methods": [
    {
      "method_id": "yookassa",
      "display_name": "ЮKassa",
      "sub_options": [
        {"id": "card", "name": "Карта"},
        {"id": "sbp", "name": "СБП"}
      ]
    }
  ],
  "discount": {
    "percent": 20,
    "ends_at": "2025-12-31T23:59:59Z",
    "badge_text": "Новогодняя скидка!"
  }
}
Переопределения скидок по тарифам (discount_overrides) не передаются в публичный API. Скидка уже «вшита» в цены тарифов — каждый тариф получает свою цену с учётом индивидуальной или глобальной скидки.

POST /landing/purchase

Создание покупки. Возвращает токен покупки и URL для оплаты.

GET /landing/purchase//status

Проверка статуса покупки по токену.

Пример .env

# === ОБЯЗАТЕЛЬНЫЕ ДЛЯ ЛЕНДИНГОВ ===
CABINET_ENABLED=true
CABINET_URL=https://cabinet.example.com
SALES_MODE=tariffs

# === SMTP (обязательно для email-покупок) ===
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASSWORD=your_password
SMTP_FROM_EMAIL=noreply@example.com
SMTP_FROM_NAME=My VPN Service

# === Платёжная система (хотя бы одна) ===
YOOKASSA_ENABLED=true
YOOKASSA_SHOP_ID=123456
YOOKASSA_SECRET_KEY=your_secret

# === Уведомления администраторов (рекомендуется) ===
ADMIN_NOTIFICATIONS_ENABLED=true
ADMIN_NOTIFICATIONS_CHAT_ID=-1001234567890
ADMIN_NOTIFICATIONS_TOPIC_ID=123

Устранение проблем

Лендинг возвращает 404

  1. Лендинг активен? Проверьте статус в Admin Cabinet → Лендинги
  2. Slug указан верно в URL? (регистрозависимый)
  3. CABINET_ENABLED=true?

Покупатель не получает email

  1. SMTP настроен? Проверьте Настройка SMTP
  2. Письмо в спаме? Проверьте SPF, DKIM, DMARC записи домена
  3. SMTP_FROM_EMAIL заполнен?
  4. Проверьте логи бота:
    docker logs <bot_container> 2>&1 | grep -i smtp
    

Скидка не отображается

  1. Текущее время сервера между discount_starts_at и discount_ends_at?
  2. discount_percent задан (1-99)?
  3. Даты указаны в UTC (не в локальном времени)?
  4. Проверьте API:
    curl -s https://cabinet.example.com/api/landing/config/{slug} | jq '.discount'
    

Подписка не создаётся после оплаты

  1. Remnawave панель доступна? Проверьте REMNAWAVE_API_URL
  2. Тариф существует в панели? ID тарифа совпадает?
  3. Проверьте логи:
    docker logs <bot_container> 2>&1 | grep -i "guest_purchase"
    

Неверная цена со скидкой

  1. Скидка пересчитывается на момент оплаты — если скидка закончилась между выбором и оплатой, будет полная цена
  2. Формула: max(1, price - floor(price * percent / 100)) — целочисленное деление, не округление
  3. Переопределение по тарифу имеет приоритет над глобальным процентом

Ошибка при сохранении скидки

ОшибкаПричина
discount_starts_at must be before discount_ends_atДата начала >= дата окончания
Невалидные ключи переопределенийID тарифа не из списка разрешённых тарифов лендинга
Процент вне диапазонаЗначение должно быть 1-99 (CHECK constraint в БД)