Урок 4. Платежи в Telegram

В этом уроке мы максимально подробно разберем платежи в Telegram

Урок будет достаточно объемным, полный листинг программы как и всегда доступен на GitHub

Как принимать платежи?

Итак, мы собрались принимать платежи в Telegram. Для этого необходимо перейти в @BotFather, выбрать своего тестового бота и подключить к нему тестовые платежи. Я подключил платежи сервиса Яндекс.Касса. После этого переходим обратно к BotFather и получаем токен для Тестовых платежей, выглядит он примерно так: 012345678:TEST:1234.

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

Создаем файлы config.py и messages.py.

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

После стандартных импортов делаем кое-что новое, а именно:

  • Получаем текущий event loop: loop = asyncio.get_event_loop(). Предположим, что для полноценной работы нашего абстрактного интернет магазина в loop нужно будет ещё запустить работу жизненно важных процессов.
  • Устанавливаем стандартный для бота Parse Mode: bot = Bot(BOT_TOKEN, parse_mode=types.ParseMode.MARKDOWN). На этом остановлюсь поподробнее: предположим, что наши стандартные сообщения размечены при помощи одного типа разметки. И поэтому при отправке сообщений нам придется каждый раз явно указывать тип этой самой разметки. Однако библиотека aiogram позволяет нам установить стандартную разметку для всех отправляемых сообщений. Это можно сделать как при создании инстанса бота, так и в любой момент в коде обычным присваиванием: bot.parse_mode = 'HTML', а при необходимости удалить обычным del bot.parse_mode. При этом мы всё ещё можем указывать тип разметки явно, и это будет иметь преимущество. Таким образом, при стандартной разметке 'MARKDOWN' при вызове метода send_message, передав в параметр parse_mode значение 'HTML', сообщение будет отправлено с HTML разметкой.
  • Ну и создаем инстанс диспетчера с новым для нас параметром: dp = Dispatcher(bot, loop=loop).

Теперь переходим к объявлению цены. Так как в минимальном примере она у нас будет одна, записываем следующее:

PRICE = types.LabeledPrice(label='Настоящая Машина Времени', amount=4200000)

У объекта LabeledPrice всего 2 параметра - это label и amount. И если первый говорит сам за себя, то второй требует больше внимания. В него нужно передавать целочисленное значение в минимально возможных единицах валюты. То есть если мы говорим о рублях, то передавать нужно копейки (123 ₽ как 12300). В подавляющем большинстве валют это 2 знака (как копейки у рубля и центы у доллара), однако, если вам пришлось работать со специфичной валютой, можно обратиться к файлу currencies.json и найти параметр exp. К примеру, те же йены или Исландские кроны не имеют "копеек".

Далее создаем обычный хэндлер команд:

@dp.message_handler(commands=['terms'])
async def process_terms_command(message: types.Message):
    await message.reply(MESSAGES['terms'], reply=False)

И переходим к написанию самой логики платежей.

Отправляем счёт для оплаты

Распознаем команду, по которой будем присылать квитанцию:

@dp.message_handler(commands=['buy'])
async def process_buy_command(message: types.Message):
    if PAYMENTS_PROVIDER_TOKEN.split(':')[1] == 'TEST':
        await bot.send_message(message.chat.id, MESSAGES['pre_buy_demo_alert'])

И чтобы не запутать ни себя, ни пользователя, проверяем, работаем ли мы сейчас с тестовой оплатой. Узнать это очень просто - по слову TEST в середине токена для платежей.

И затем в этом же хэндлере высылаем сам счет:

await bot.send_invoice(
    message.chat.id,
    title=MESSAGES['tm_title'],
    description=MESSAGES['tm_description'],
    provider_token=PAYMENTS_PROVIDER_TOKEN,
    currency='rub',
    photo_url=TIME_MACHINE_IMAGE_URL,
    photo_height=512,  # !=0/None, иначе изображение не покажется
    photo_width=512,
    photo_size=512,
    is_flexible=False,  # True если конечная цена зависит от способа доставки
    prices=[PRICE],
    start_parameter='time-machine-example',
    payload='some-invoice-payload-for-our-internal-use'
)

Что же там произошло? Объясняю (до параметра currency всё и так понятно):

  • В currency необходимо передать трехзначный код валюты в формате ISO 4217. Полный список валют можно найти здесь.
  • Параметры photo_height и photo_width нужно указать, если вы передаете ссылку на изображение в photo_url, иначе изображение либо вообще не отобразится, либо отобразится некорректно. Следом можно добавить параметр photo_size, у меня отображается и без него, но лишним точно не будет.
  • Параметр is_flexible отвечает за то, что если финальная цена зависит от способа доставки (когда передаем True), то:
    • будет добавлено поле выбора адреса доставки, даже если не передан параметр need_shipping_address (как в этом примере)
    • после выбора пользователем адреса доставки будет отправлена ShippingQuery, на которую ботапи будет ждать ответ (об этом в более подробном примере ниже в этом уроке)
  • Поле prices принимает массив из цен, поэтому передаем туда [PRICE]
  • Параметр start_parameter является обязательным, документация утверждает, что можно использовать уникальный диплинкинг параметр, чтобы создать эту квитанцию, однако как этим воспользоваться, мне узнать не удалось. Разве что проверить этот параметр, если пользователь ответит на сообщение с квитанцией, но это уж очень большие заморочки, как по мне
  • Ну и параметр payload. Объем 1-128 байт. Рекомендуется использовать его для того, чтобы идентифицировать инвойсы - он не показывается пользователю, но мы можем его прочитать при получении успешной оплаты (об этом далее)

Ну что ж! С самым сложным разобрались! Запрашиваем квитанцию, заходим со смартфона (на десктопе платежи до сих пор не поддерживаются) и нажимаем на единственную кнопку под сообщением (она добавляется автоматически, если мы не передаем никакую клавиатуру к сообщению, в ином случае мы обязаны сделать первую кнопку "платежной", а остальные как обычно) и переходим к оплате:

Как мы видим, телеграм сам ставит разделитель копеек.

Нажимаем Payment Method и вводим данные карточки. Номер, как нам уже подсказал бот, это повторяющиеся 42, срок действия нужно указать валидный (больше текущей даты). Затем набираем трехзначный CVC и нажимаем "Заплатить" / "Pay".

Как ни странно, в этот момент оплата ещё совершена не будет, мы вернемся к предыдущему экрану, куда добавится информация о нашей платежной карточке.

Для того, чтобы обработать оплату в данном примере, нам понадобится ещё два хэндлера:

@dp.pre_checkout_query_handler(func=lambda query: True)
async def process_pre_checkout_query(pre_checkout_query: types.PreCheckoutQuery):
    await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True)

Когда пользователь нажимает на кнопку "Оплатить" / "Pay", Телеграм отправляет апдейт типа PreCheckoutQuery, на который нам необходимо ответить в течение десяти секунд. Подразумевается, что получив данный апдейт мы обработаем содержащуюся в нем информацию и отправим положительный, либо отрицательный ответ. Так как этот пример максимально простой, здесь мы всегда будем отвечать утвердительно (а ниже разберем пример с отказом), поэтому передаем ok=True в answer_pre_checkout_query. После получения сервером Телеграм положительного ответа происходит оплата (взаимодействие с провайдером оплаты, тот передает информацию об успешной оплате), и мы получаем апдейт (сообщение) с ContentType SUCCESSFUL_PAYMENT, который можем обработать (и будет правильно так сделать):

@dp.message_handler(content_types=ContentType.SUCCESSFUL_PAYMENT)
async def process_successful_payment(message: types.Message):
    print('successful_payment:')
    pmnt = message.successful_payment.to_python()
    for key, val in pmnt.items():
        print(f'{key} = {val}')

    await bot.send_message(
        message.chat.id,
        MESSAGES['successful_payment'].format(
            total_amount=message.successful_payment.total_amount // 100,
            currency=message.successful_payment.currency
        )
    )

Для наглядности на строчках 58-61 я сделал печать интересующих нас параметров в формате ключ = значение:

# successful_payment:
# currency = RUB
# total_amount = 4200000
# invoice_payload = some-invoice-payload-for-our-internal-use
# telegram_payment_charge_id = _
# provider_payment_charge_id = 123456789_3214567_654321

Для того, чтобы определить, что за платеж прошел, мы можем обратиться к полю provider_payment_charge_id. Получив сообщение с информацией об успешной оплате, нам остается проинформировать пользователя о том, что оплата прошла успешно, для этого собираем нужные данные и отправляем ответ:

Только что мы рассмотрели самый простой пример приема платежей в Telegram

Полный листинг этого примера доступен по ссылке

Дополнительные информационные поля, валидация данных перед платежом

Только что мы разобрали принцип получения платежей в Telegram. Теперь перейдем к более сложной части. Этот пример проведем в отдельном файле.

Начнем с изменения первой же цены. Запишем:

PRICES = [
    types.LabeledPrice(label='Настоящая Машина Времени', amount=4200000),
    types.LabeledPrice(label='Подарочная упаковка', amount=30000)
]

Теперь в оплату у нас помимо Машины Времени входит ещё и подарочная упаковка, которую нельзя убрать.

Затем на строках 29-45 объявляем возможные способы доставки.

TELEPORTER_SHIPPING_OPTION = types.ShippingOption(
    id='teleporter',
    title='Всемирный* телепорт'
).add(types.LabeledPrice('Телепорт', 1000000))

RUSSIAN_POST_SHIPPING_OPTION = types.ShippingOption(
    id='ru_post', title='Почтой России')
RUSSIAN_POST_SHIPPING_OPTION.add(
    types.LabeledPrice(
        'Деревянный ящик с амортизирующей подвеской внутри', 100000)
)
RUSSIAN_POST_SHIPPING_OPTION.add(
    types.LabeledPrice('Срочное отправление (5-10 дней)', 500000)
)

PICKUP_SHIPPING_OPTION = types.ShippingOption(id='pickup', title='Самовывоз')
PICKUP_SHIPPING_OPTION.add(types.LabeledPrice('Самовывоз в Москве', 50000))

Об их применении чуть позже, сейчас добавим стандартные хэндлеры:

@dp.message_handler(commands=['start'])
async def process_start_command(message: types.Message):
    await message.reply(MESSAGES['start'])

@dp.message_handler(commands=['help'])
async def process_help_command(message: types.Message):
    await message.reply(MESSAGES['help'])

@dp.message_handler(commands=['terms'])
async def process_terms_command(message: types.Message):
    await message.reply(MESSAGES['terms'], reply=False)

И отправляем счёт по команде /buy:

@dp.message_handler(commands=['buy'])
async def process_buy_command(message: types.Message):
    if PAYMENTS_PROVIDER_TOKEN.split(':')[1] == 'TEST':
        await bot.send_message(message.chat.id, MESSAGES['pre_buy_demo_alert'])

    await bot.send_invoice(message.chat.id,
                           title=MESSAGES['tm_title'],
                           description=MESSAGES['tm_description'],
                           provider_token=PAYMENTS_PROVIDER_TOKEN,
                           currency='rub',
                           photo_url=TIME_MACHINE_IMAGE_URL,
                           photo_height=512,  # !=0/None or picture won't be shown
                           photo_width=512,
                           photo_size=512,
                           need_email=True,
                           need_phone_number=True,
                           # need_shipping_address=True,
                           is_flexible=True,  # True If you need to set up Shipping Fee
                           prices=PRICES,
                           start_parameter='time-machine-example',
                           payload='some-invoice-payload-for-our-internal-use')

Разберем добавившиеся параметры:

  • need_email - будет запрошена электронная почта при заполнении информации для доставки
  • need_phone_number - аналогично, но номер телефона
  • need_shipping_address - этот параметр нам указывать не нужно, так как дальше мы задаем is_flexible=True. Но если бы мы одновременно хотели получить адрес доставки и не менять цену в зависимости от адреса, можно было бы воспользоваться этим параметром.

В документации приведена ещё пара необзательных параметров, касающихся платежей:

  • send_phone_number_to_provider - передать провайдеру платежей телефонный номер пользователя
  • send_email_to_provider - аналогично, но адрес электронной почты

Ну что ж. Мы получили инвойс, открываем его и заполняем.

При сохранении адреса будет отправлен апдейт, ловим его и обрабатываем:

@dp.shipping_query_handler(func=lambda query: True)
async def process_shipping_query(shipping_query: types.ShippingQuery):
    print('shipping_query.shipping_address')
    print(shipping_query.shipping_address)

    if shipping_query.shipping_address.country_code == 'AU':
        return await bot.answer_shipping_query(
            shipping_query.id,
            ok=False,
            error_message=MESSAGES['AU_error']
        )

    shipping_options = [TELEPORTER_SHIPPING_OPTION]

    if shipping_query.shipping_address.country_code == 'RU':
        shipping_options.append(RUSSIAN_POST_SHIPPING_OPTION)

        if shipping_query.shipping_address.city == 'Москва':
            shipping_options.append(PICKUP_SHIPPING_OPTION)

    await bot.answer_shipping_query(
        shipping_query.id,
        ok=True,
        shipping_options=shipping_options
    )

Раз уж мы решили, что от адреса доставки изменяется конечная цена, обработаем этот самый адрес. Для начала укажем страну Австралию. Предположим, что туда мы не можем доставить наш товар, для этого проверяем country_code (код страны) у объекта ShippingAddress. Это код в формате ISO 3166-1 alpha-2. Коды всех стран доступны тут. Убедившись, что выбрана страна Австралия, отвечаем пользователю, что такой вариант нам не подходит:

Ещё у shipping_query есть параметр id, который мы задавали в начале. Для варианта Почта России это 'ru_post'. Так что проверить можно ещё и по айди.

Ладно, выберем, например, Зимбабве, а ещё заполним поля, требующие номер телефона и почту:

Тут мы видим, что это не Россия, поэтому передаем только один вариант доставки - телепортом.

Выглянув в окно и убедившись, что мы всё таки не в Зимбабве, выставляем страну Россия, ну и укажем город Москва. Программа проверит это и добавит нужные опции в массив. Таким образом мы получим уже три варианта доставки:

Смотрите ещё что. В вариант отправки Почтой России мы добавили два обязательных параметра: это Деревянный ящик с амортизирующей подвеской внутри и Срочное отправление (5-10 дней), стоимость этих пунктов добавляется в квитанцию.

Нажимаем кнопку "Pay", но оплата у нас не пройдет: не забыли, что нам необходимо ещё подтвердить платеж? Создаем следующий хэндлер:

@dp.pre_checkout_query_handler(func=lambda query: True)
async def process_pre_checkout_query(pre_checkout_query: types.PreCheckoutQuery):
    print('order_info')
    print(pre_checkout_query.order_info)

    if hasattr(pre_checkout_query.order_info, 'email') and (pre_checkout_query.order_info.email == '[email protected]'):
        return await bot.answer_pre_checkout_query(
            pre_checkout_query.id,
            ok=False,
            error_message=MESSAGES['wrong_email'])

    await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True)

Допустим, мы всегда запрашиваем у пользователя почту, а затем проверяем её валидность. В данном примере мы запрещаем пользователю использовать почту [email protected], поэтому при совпадении возвращаем ошибку.

Теперь для интереса (чтобы вариант "Самовывоз" пропал) сменим адрес на не Москву

Вот так.

Ну и, исправив почту, мы таки выполняем платеж:

И ещё: при каждом выборе адреса доставки мы печатали этот самый адрес. А при попытке оплатить печатали информацию по заказу. Взглянем же на распечатанное:

# shipping_query.shipping_address
# {"country_code":"RU","state":"НеМосква","city":"НеМосква","street_line1":"Улица Академика Зелинского","street_line2":"дом 6, квартира 84","post_code":"119334"}
# order_info
# {"phone_number":"79991239876","email":"[email protected]","shipping_address":{"country_code":"RU","state":"НеМосква","city":"НеМосква","street_line1":"Улица Академика Зелинского","street_line2":"дом 6, квартира 84","post_code":"119334"}}

Эти данны мы можем использовать для дальнейшей обработки заказа, но это уже история совсем не про Telegram Bot API.

На этом урок по платежам подошел к концу

В качестве домашнего задания попробуйте добавить больше фильтров валидации к своему платежу

results matching ""

    No results matching ""