🤖 Настройка кнопок Telegram-бота EastPay

Дата: 2026-02-12
Связанный промт: 5-version-promt.md
Платформа: n8n + NextBot + Supabase


📐 1. Архитектура кнопочного меню

1.1. Обзор структуры

flowchart TD
    START["/start"] --> WELCOME["👋 Приветственное сообщение"]
    WELCOME --> MENU["📋 Главное меню (3 кнопки)"]
    
    MENU --> BTN1["💱 Узнать курс"]
    MENU --> BTN2["📝 Заказать обмен"]
    MENU --> BTN3["📦 Статус обмена"]
    
    BTN1 --> RATES_SUB["Подменю валютных пар"]
    RATES_SUB --> R1["🇹🇭 THB/RUB"]
    RATES_SUB --> R2["🇺🇸 USD/RUB"]
    RATES_SUB --> R3["🇨🇳 CNY/RUB"]
    RATES_SUB --> R4["🇦🇪 AED/RUB"]
    RATES_SUB --> R5["💎 USDT/RUB"]
    RATES_SUB --> R6["📊 Все курсы"]
    
    R1 --> SB_RATES[("Supabase: daily_rates")]
    R6 --> SB_RATES
    
    BTN2 --> CITY_SELECT["Выбор города"]
    CITY_SELECT --> C1["🏝️ Bangkok"]
    CITY_SELECT --> C2["🏝️ Phuket"]
    CITY_SELECT --> C3["🏜️ Dubai"]
    CITY_SELECT --> C4["🕌 Istanbul"]
    CITY_SELECT --> C5["🏙️ Moscow"]
    
    C1 --> PAIR_SELECT["Выбор валюты"]
    PAIR_SELECT --> AMOUNT_INPUT["Ввод суммы (текст)"]
    AMOUNT_INPUT --> SB_CALC[("Supabase: calculate_deal()")]
    SB_CALC --> CONFIRM["✅ Подтвердить / ❌ Отменить"]
    CONFIRM --> SB_ORDER[("Supabase: bot_create_order()")]
    
    BTN3 --> STATUS_CHECK["Проверка статуса"]
    STATUS_CHECK --> SB_ORDERS[("Supabase: orders")]
    SB_ORDERS --> STATUS_DISPLAY["Статус заявки #ID"]

1.2. Типы кнопок в Telegram

Тип кнопки Назначение Когда использовать
ReplyKeyboardMarkup Постоянные кнопки снизу чата (клавиатура) Главное меню «Узнать курс / Заказать обмен / Статус»
InlineKeyboardMarkup Кнопки под сообщением бота Подменю (выбор валюты, города, подтверждение)
ReplyKeyboardRemove Убрать клавиатуру После завершения сценария

🔘 2. Главное меню (Persistent Keyboard)

2.1. Конфигурация Reply Keyboard

После /start или команды показа меню бот отправляет сообщение с ReplyKeyboardMarkup:

{
  "reply_markup": {
    "keyboard": [
      [
        { "text": "💱 Узнать курс" },
        { "text": "📝 Заказать обмен" }
      ],
      [
        { "text": "📦 Статус обмена" }
      ]
    ],
    "resize_keyboard": true,
    "one_time_keyboard": false,
    "is_persistent": true,
    "input_field_placeholder": "Выберите действие или задайте вопрос..."
  }
}

Параметры:

2.2. Настройка в n8n

В n8n ноде Send Message (Telegram):

  1. Text: Приветственное сообщение.
  2. Reply Markup → Reply Keyboard:
    • Добавьте 2 ряда кнопок:
      • Ряд 1: 💱 Узнать курс, 📝 Заказать обмен
      • Ряд 2: 📦 Статус обмена
    • Установите Resize Keyboard = true, One-Time Keyboard = false.

2.3. Настройка в NextBot

В NextBot используйте Сценарий → Событие «Начало диалога»:

  1. Действие: «Отправить сообщение»
  2. Тип: «Кнопки (Reply Keyboard)»
  3. Добавьте 3 кнопки с текстом:
    • 💱 Узнать курс
    • 📝 Заказать обмен
    • 📦 Статус обмена

💱 3. Кнопка «Узнать курс» — Подробная логика

3.1. Триггер

Пользователь нажимает кнопку → Telegram отправляет текст 💱 Узнать курс → n8n Switch ловит это как текстовое сообщение.

3.2. Обработка в n8n Switch

В n8n ноде Switch добавьте новую ветку:

Условие: {{ $json.message.text }} contains "Узнать курс"

3.3. Inline-кнопки с выбором валюты

{
  "text": "📊 Выберите валютную пару:",
  "reply_markup": {
    "inline_keyboard": [
      [
        { "text": "🇹🇭 THB/RUB", "callback_data": "rate_THB/RUB" },
        { "text": "🇺🇸 USD/RUB", "callback_data": "rate_USD/RUB" }
      ],
      [
        { "text": "🇨🇳 CNY/RUB", "callback_data": "rate_CNY/RUB" },
        { "text": "🇦🇪 AED/RUB", "callback_data": "rate_AED/RUB" }
      ],
      [
        { "text": "💎 USDT/RUB", "callback_data": "rate_USDT/RUB" },
        { "text": "🇪🇺 EUR/RUB", "callback_data": "rate_EUR/RUB" }
      ],
      [
        { "text": "📊 Все курсы сразу", "callback_data": "rate_ALL" }
      ]
    ]
  }
}

3.4. Supabase-запрос по callback

При выборе конкретной валюты (rate_THB/RUB):

-- n8n Supabase Node: Read Row from daily_rates
SELECT symbol, rate, updated_at 
FROM daily_rates 
WHERE symbol = 'THB/RUB' AND is_active = true;

При выборе «Все курсы» (rate_ALL):

-- n8n Supabase Node: Read All from daily_rates
SELECT symbol, rate, updated_at 
FROM daily_rates 
WHERE is_active = true 
ORDER BY symbol;

3.5. Формат ответа

Один курс:

📊 Курс THB/RUB

💱 1 THB = 2.85 RUB
📅 Обновлено: 12.02.2026, 16:00 (ICT)

Рассчитать сумму обмена? 👇

Все курсы:

📊 Актуальные курсы EastPay

| Пара | Курс |
|------|------|
| 🇹🇭 THB/RUB | 2.85 |
| 🇺🇸 USD/RUB | 96.00 |
| 🇨🇳 CNY/RUB | 13.40 |
| 🇦🇪 AED/RUB | 26.80 |
| 💎 USDT/RUB | 98.50 |
| 🇪🇺 EUR/RUB | 105.00 |
| 🇹🇷 TRY/RUB | 3.10 |

📅 Обновлено: 12.02.2026

3.6. Альтернатива: AI-агент обрабатывает кнопку

Вместо отдельной ветки Switch можно направить текст 💱 Узнать курс прямо в AI-агента — он сам вызовет get_rates_tool и ответит. Это предпочтительный подход, если AI-агент уже настроен и работает стабильно.


📝 4. Кнопка «Заказать обмен» — Подробная логика

4.1. Триггер

Пользователь нажимает 📝 Заказать обмен → текст уходит в n8n.

4.2. Пошаговый сценарий (Step-by-step)

sequenceDiagram
    participant U as 👤 Клиент
    participant BOT as 🤖 Бот
    participant AI as 🧠 AI Agent
    participant SB as 🗄️ Supabase

    U->>BOT: 📝 Заказать обмен
    BOT->>U: 🏙️ Выберите город (Inline Keyboard)
    U->>BOT: Bangkok (callback)
    BOT->>U: 💱 Выберите валюту (Inline Keyboard)
    U->>BOT: THB/RUB (callback)
    BOT->>U: 💰 Введите сумму в рублях
    U->>BOT: 100000
    BOT->>SB: RPC: calculate_deal('Bangkok', 100000, 'THB/RUB')
    SB-->>BOT: {rate: 2.85, amount_get: 35088, currency_get: THB}
    BOT->>U: 📊 Расчёт + [✅ Подтвердить] [❌ Отменить]
    U->>BOT: ✅ Подтвердить (callback)
    BOT->>SB: RPC: bot_create_order(tg_id, 'Bangkok', 100000, 'THB/RUB')
    SB-->>BOT: {order_id: 42, status: 'new'}
    BOT->>U: ✅ Заявка #42 создана! Менеджер свяжется...

4.3. Шаг 1: Выбор города (Inline Keyboard)

{
  "text": "🏙️ В каком городе хотите совершить обмен?",
  "reply_markup": {
    "inline_keyboard": [
      [
        { "text": "🏝️ Bangkok", "callback_data": "city_Bangkok" },
        { "text": "🏝️ Phuket", "callback_data": "city_Phuket" }
      ],
      [
        { "text": "🏜️ Dubai", "callback_data": "city_Dubai" },
        { "text": "🕌 Istanbul", "callback_data": "city_Istanbul" }
      ],
      [
        { "text": "🏙️ Москва", "callback_data": "city_Москва" }
      ]
    ]
  }
}

Supabase-запрос для динамических городов:

SELECT DISTINCT name, country 
FROM locations 
WHERE is_active = true 
ORDER BY name;

Лучшая практика: Вместо хардкода — загружай список городов из Supabase (get_locations_tool или прямой запрос) и генерируй inline-кнопки динамически в n8n Code Node.

4.4. Шаг 2: Выбор валютной пары

После выбора города бот показывает доступные валютные пары:

{
  "text": "💱 Какую валюту хотите обменять?\n\n📍 Город: Bangkok (🇹🇭 Таиланд)",
  "reply_markup": {
    "inline_keyboard": [
      [
        { "text": "RUB → THB", "callback_data": "pair_THB/RUB" },
        { "text": "USDT → THB", "callback_data": "pair_THB/USDT" }
      ],
      [
        { "text": "RUB → USD", "callback_data": "pair_USD/RUB" },
        { "text": "USDT → RUB", "callback_data": "pair_USDT/RUB" }
      ],
      [
        { "text": "⬅️ Назад к городам", "callback_data": "back_cities" }
      ]
    ]
  }
}

Фильтрация валют по городу из Supabase:

SELECT i.name, dr.symbol, dr.rate
FROM market_rates mr
JOIN instruments i ON i.id = mr.instrument_id
JOIN daily_rates dr ON dr.symbol LIKE '%' || i.base_currency || '%'
WHERE mr.location_id = (SELECT id FROM locations WHERE name = 'Bangkok' LIMIT 1)
  AND i.is_active = true
  AND dr.is_active = true;

4.5. Шаг 3: Ввод суммы

💰 Введите сумму, которую хотите обменять (в рублях):

📍 Bangkok | 💱 RUB → THB

Примеры: 50000, 100000, 500000

Ожидание: Бот ждёт текстовое сообщение с числом. Валидация:

  • Число > 0
  • Число — это число (не текст)
  • Если введено неправильно → «Пожалуйста, введите сумму числом. Например: 100000»

4.6. Шаг 4: Расчёт + Подтверждение

Supabase RPC вызов:

SELECT * FROM calculate_deal('Bangkok', 100000, 'THB/RUB');

Ответ бота:

📊 Расчёт обмена:

📍 Город: Bangkok (Таиланд)
💱 Направление: RUB → THB
💰 Отдаёте: 100 000 ₽
💎 Получаете: ~35 088 ฿
📈 Курс: 2.8512

Подтверждаете обмен?
{
  "reply_markup": {
    "inline_keyboard": [
      [
        { "text": "✅ Подтвердить", "callback_data": "confirm_order" },
        { "text": "❌ Отменить", "callback_data": "cancel_order" }
      ],
      [
        { "text": "🔄 Пересчитать", "callback_data": "recalc_order" }
      ]
    ]
  }
}

4.7. Шаг 5: Создание заявки

При нажатии ✅ Подтвердить:

Supabase RPC вызов:

SELECT * FROM bot_create_order(
  '123456789',       -- p_tg_id (telegram id клиента)
  'Bangkok',         -- p_city_name
  100000,            -- p_amount
  'THB/RUB'          -- p_currency_pair
);

Ответ бота:

✅ Заявка #42 создана!

📋 Детали:
• Город: Bangkok
• Направление: RUB → THB
• Сумма: 100 000 ₽ → ~35 088 ฿
• Статус: Новая

Менеджер свяжется с вами в ближайшее время. Спасибо! 🙏

Что-то ещё? 👇

4.8. Альтернатива: AI-агент ведёт сценарий

Вместо жёстких inline-кнопок AI-агент может вести весь диалог заказа текстом:

Преимущество AI-подхода: Обрабатывает свободный ввод («хочу 100к рублей на баты в бангке» → сразу всё понял).

Рекомендация: Гибридный подход — предлагать кнопки, но также принимать свободный текст через AI-агента.


📦 5. Кнопка «Статус обмена» — Подробная логика

5.1. Триггер

Пользователь нажимает 📦 Статус обмена → текст уходит в n8n.

5.2. Логика проверки

flowchart TD
    BTN["📦 Статус обмена"] --> LOOKUP["Поиск заявок по telegram_id"]
    LOOKUP --> SB[("Supabase: orders + clients")]
    SB --> CHECK{{"Есть заявки?"}}
    CHECK -->|"Да (1 заявка)"| SHOW_ONE["Показать статус"]
    CHECK -->|"Да (несколько)"| SHOW_LIST["Список заявок (Inline)"]
    CHECK -->|"Нет"| NO_ORDERS["«У вас пока нет заявок»"]
    
    SHOW_LIST --> SELECT["Клиент выбирает #ID"]
    SELECT --> SHOW_DETAIL["Детали заявки"]

5.3. Supabase-запрос для поиска заявок

-- Найти все активные заявки клиента по telegram_id
SELECT 
  o.id,
  o.status,
  o.amount_give,
  o.currency_give,
  o.amount_get,
  o.currency_get,
  o.exchange_rate,
  l.name as city,
  o.created_at,
  o.updated_at
FROM orders o
JOIN clients c ON c.id = o.client_id
LEFT JOIN locations l ON l.id = o.location_id
WHERE c.telegram_id = :telegram_id
  AND o.status NOT IN ('done', 'cancelled')
ORDER BY o.created_at DESC
LIMIT 5;

5.4. Новая RPC-функция для Supabase (рекомендация)

CREATE OR REPLACE FUNCTION bot_get_client_orders(p_tg_id TEXT)
RETURNS TABLE (
  order_id BIGINT,
  status order_status,
  amount_give NUMERIC,
  currency_give TEXT,
  amount_get NUMERIC,
  currency_get TEXT,
  exchange_rate NUMERIC,
  city TEXT,
  created_at TIMESTAMPTZ
)
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
  RETURN QUERY
  SELECT 
    o.id,
    o.status,
    o.amount_give,
    o.currency_give,
    o.amount_get,
    o.currency_get,
    o.exchange_rate,
    l.name,
    o.created_at
  FROM orders o
  JOIN clients c ON c.id = o.client_id
  LEFT JOIN locations l ON l.id = o.location_id
  WHERE c.telegram_id = p_tg_id
  ORDER BY o.created_at DESC
  LIMIT 10;
END;
$$;

GRANT EXECUTE ON FUNCTION bot_get_client_orders TO service_role;

5.5. n8n Tool для AI-агента

Добавьте в n8n новый инструмент get_order_status_tool:

Параметр Значение
Имя: get_order_status_tool
Описание: Получить статус заявок клиента по telegram_id
Тип ноды: Supabase RPC
RPC Function: bot_get_client_orders
Параметры: p_tg_id = {{ $json.telegram_id }}

5.6. Маппинг статусов на русский

Статус (DB) На русском Emoji
new Новая 🆕
calculated Рассчитана 📊
payment_pending Ожидание оплаты
processing В обработке 🔄
delivery Доставка 🚚
office В офисе 🏢
done Завершена
cancelled Отменена
dispute Спор ⚠️

5.7. Формат ответа — одна заявка

📦 Статус заявки #42

• 🆕 Статус: Новая
• 📍 Город: Bangkok
• 💱 Направление: RUB → THB
• 💰 Сумма: 100 000 ₽ → ~35 088 ฿
• 📅 Создана: 12.02.2026

Менеджер скоро возьмёт вашу заявку в работу.
Есть вопросы? Пишите! 😊

5.8. Формат ответа — несколько заявок

📦 Ваши активные заявки:
{
  "reply_markup": {
    "inline_keyboard": [
      [{ "text": "#42 — RUB→THB — 🆕 Новая", "callback_data": "order_42" }],
      [{ "text": "#38 — USDT→RUB — 🔄 В обработке", "callback_data": "order_38" }],
      [{ "text": "#35 — RUB→AED — ⏳ Ожидание оплаты", "callback_data": "order_35" }]
    ]
  }
}

5.9. Формат ответа — нет заявок

📦 У вас пока нет активных заявок.

Хотите создать новую? 💱
{
  "reply_markup": {
    "inline_keyboard": [
      [{ "text": "📝 Заказать обмен", "callback_data": "start_order" }],
      [{ "text": "💱 Узнать курс", "callback_data": "show_rates" }]
    ]
  }
}

⚙️ 6. Реализация в n8n Workflow

6.1. Обновлённая архитектура Switch

flowchart LR
    TG["📱 Telegram Trigger"] --> SW{{"Switch"}}
    
    SW -->|"/start"| WELCOME["👋 Welcome + Menu"]
    SW -->|"💱 Узнать курс"| RATES_FLOW["Rates Flow"]
    SW -->|"📝 Заказать обмен"| ORDER_FLOW["Order Flow"]
    SW -->|"📦 Статус обмена"| STATUS_FLOW["Status Flow"]
    SW -->|"callback_query"| CALLBACK_HANDLER["Callback Router"]
    SW -->|"voice"| AUDIO_FLOW["Audio → Transcribe"]
    SW -->|"photo"| PHOTO_FLOW["Photo → Analyze"]
    SW -->|"text (other)"| AI_AGENT["🤖 AI Agent"]
    
    RATES_FLOW --> INLINE_RATES["Inline: Выбор валюты"]
    ORDER_FLOW --> INLINE_CITIES["Inline: Выбор города"]
    STATUS_FLOW --> SB_QUERY["Supabase: bot_get_client_orders"]
    
    CALLBACK_HANDLER --> CB_SWITCH{{"Callback Switch"}}
    CB_SWITCH -->|"rate_*"| SB_RATES["Supabase: daily_rates"]
    CB_SWITCH -->|"city_*"| SHOW_PAIRS["Inline: Валютные пары"]
    CB_SWITCH -->|"pair_*"| ASK_AMOUNT["Запросить сумму"]
    CB_SWITCH -->|"confirm_order"| SB_CREATE["Supabase: bot_create_order"]
    CB_SWITCH -->|"order_*"| SB_ORDER_DETAIL["Supabase: Детали заявки"]

6.2. Обработка Callback Query в n8n

Telegram Trigger должен слушать не только message, но и callback_query:

  1. В ноде Telegram Trigger → Updates: добавьте callback_query.
  2. В Switch ноде добавьте ветку:
    Условие: {{ $json.callback_query }} is not empty
    
  3. Далее Callback Router (ещё один Switch) разбирает callback_query.data:
    "rate_*" → Запрос к daily_rates
    "city_*" → Показ валютных пар
    "pair_*" → Запрос суммы
    "confirm_order" → Создание заявки
    "cancel_order" → Отмена
    "order_*" → Детали заявки
    "back_*" → Возврат на предыдущий шаг
    

6.3. Answer Callback Query

⚠️ Обязательно! После каждого callback нужно отправить answerCallbackQuery, иначе на кнопке будет бесконечный индикатор загрузки.

HTTP Request:
POST https://api.telegram.org/bot{TOKEN}/answerCallbackQuery
Body: { "callback_query_id": "{{ $json.callback_query.id }}" }

6.4. Хранение state между шагами

Между шагами (город → валюта → сумма → подтверждение) нужно хранить промежуточное состояние.

Варианты:

Способ Плюсы Минусы
Callback data Просто. city_Bangkok_pair_THB/RUB Ограничение 64 байта
n8n Static Data Быстро, в памяти n8n Теряется при перезапуске
Supabase sessions Надёжно, персистентно Доп. таблица
AI Agent контекст Естественно, без кода Зависит от memory

Рекомендация: Для кнопочного флоу — кодируй состояние в callback_data:

callback_data: "ord_Bangkok_THBRUB"    → город + пара
callback_data: "conf_Bangkok_THBRUB_100000"  → все данные для заявки

🔗 7. Связь с Supabase — Сводная таблица

Кнопка Supabase таблица/RPC Действие Параметры
💱 Узнать курс daily_rates (SELECT) Получить курс symbol
💱 Все курсы daily_rates (SELECT ALL) Все курсы is_active = true
📝 Выбор города locations (SELECT) Список городов is_active = true
📝 Расчёт calculate_deal() (RPC) Калькулятор city, amount, pair
📝 Создать заявку bot_create_order() (RPC) Новая заявка tg_id, city, amount, pair
📦 Статус bot_get_client_orders() (RPC) * Заявки клиента tg_id
📦 Детали заявки orders (SELECT) Одна заявка order_id

* — Нужно создать новую RPC bot_get_client_orders() (см. раздел 5.4).


📋 8. Чеклист реализации

Фаза 1: Базовые кнопки (🔴 Высокий приоритет)

Фаза 2: Динамические данные (🟡 Средний приоритет)

Фаза 3: AI-гибрид (🟢 Низкий приоритет)


🏗️ 9. Архитектура NextBot + n8n (если используем оба)

flowchart LR
    TG["📱 Telegram"] --> NB["NextBot\n(Кнопки, Сценарии,\nFollow-up, CRM)"]
    TG --> N8N["n8n\n(AI Agent, Tools,\nMedia Processing)"]
    
    NB --> SB[("🗄️ Supabase\n(Единый источник правды)")]
    N8N --> SB
    
    NB -.->|"Webhook/API"| N8N
    N8N -.->|"Webhook/API"| NB

Роли:

Варианты стыковки:

  1. NextBot → n8n: При сложном вопросе NextBot вызывает n8n Webhook для обработки AI-агентом.
  2. n8n → NextBot: После создания заявки n8n вызывает NextBot API для запуска дожим-сценария.
  3. Оба → Supabase: Обе платформы читают/пишут в одну БД.