Integration of Telegram bot with YuKassa

It seems that YuKassa has a good one documentation about setting up payments through a TG bot, there are several on the Internet articles on this topic, but still in practice you come across many non-obvious nuances…

I will describe step by step the process of connecting payments for a Python bot on aiogram 3, provided that its owner has already registered self-employment.

Test mode

So, let's start a dialogue with BotFatherselect your bot and click the Payments button.

BotFather interface when viewing a bot

BotFather interface when viewing a bot

We select YuKassa from the list of providers, and in the dialog that appears – Connect YuKassa Test.

Two modes - test and real

Two modes – test and real

We get the settings for testing – standard identifiers and test card data.

Test settings

Test settings

We return to BotFather and find that something has changed there:

Test payment token

Test payment token

Let's copy this token to a file .env our bot. It’s more convenient for me to store two tokens – a test one and a real one for better interchangeability. So far they are the same, because… We don’t have a real one yet.

PROVIDER_TOKEN = "381764678:TEST:100037"
TEST_PROVIDER_TOKEN = "381764678:TEST:100037"

You should immediately add there the currency of future payments in the format ISO-4217 and the amount of payment, must be in kopecks, not rubles.

CURRENCY = "RUB"
PRICE = "9900"

Next, these values ​​need to be loaded. I have a dataclass for this purpose.

import json
from environs import Env 
from dataclasses import dataclass

@dataclass
class Config:
    __instance = None

    def __new__(cls):
      if cls.__instance is None:
        env: Env = Env()
        env.read_env()
        cls.__instance = super(Config, cls).__new__(cls)
        …
        cls.__instance.provider_token = env('PROVIDER_TOKEN')
        cls.__instance.currency = env('CURRENCY')
        cls.__instance.price = env.int('PRICE')
        provider_data = {
          "receipt": {
            "items": [
              {
                "description": "Подписка на месяц",
                "quantity": "1.00",
                "amount": {
                  "value": f"{cls.__instance.price / 100:.2f}",
                  "currency": cls.__instance.currency
                },
                "vat_code": 1
              }
            ]
          }
        }
        cls.__instance.provider_data = json.dumps(provider_data)
        return cls.__instance

config = Config()

Everything is obvious here, except provider_data. In accordance with Federal Law 54, a receipt must be issued for each payment. I delegated this task to YuKassa, so I had to prepare data for generating checks:

  • items – list of goods in the order, for self-employed – no more than 6;

  • description – product description up to 128 characters long;

  • quantity – quantity, for the self-employed – necessarily a whole number (and I really wanted to sell only a third of the subscription))));

  • value – product price in rubles. But we store the price in kopecks, so we divide it by 100 and be sure to indicate the format specifier (.2f), so as not to lose a substantial amount of 00 kopecks;

  • currency – currency code;

  • vat_code – VAT rate, for self-employed we write 1.

The main thing is not to confuse where the price is in rubles and where in kopecks. But why such a discrepancy? It goes back to the Telegram Bot API, where object LabeledPricecontaining the price of the product, stores it in the minimum units of currency – cents, kopecks, etc. If you indicate the value in the check env.int('PRICE')then the payment simply will not go through.

Now let's create a command handler /buywhich will be used for purchases in our bot.

@router.message(Command(commands=['buy']))
async def buy_subscription(message: Message, state: FSMContext):
    try:
            # Проверка состояния и его очистка
            current_state = await state.get_state()
            if current_state is not None:
                await state.clear()  # чтобы свободно перейти сюда из любого другого состояния

            from config import config
            if config.provider_token.split(':')[1] == 'TEST':
                await message.reply("Для оплаты используйте данные тестовой карты: 1111 1111 1111 1026, 12/22, CVC 000.")

            prices = [LabeledPrice(label="Оплата заказа", amount=config.price)]
            await state.set_state(FSMPrompt.buying)
            await bot.send_invoice(
                chat_id=message.chat.id,
                title="Покупка,
                description="Оплата бота',
                payload='bot_paid',
                provider_token=config.provider_token,
                currency=config.currency,
                prices=prices,
                need_phone_number=True,
                send_phone_number_to_provider=True,
                provider_data=config.provider_data
            )
    except Exception as e:
        logging.error(f"Ошибка при выполнении команды /buy: {e}")
        await message.answer("Произошла ошибка при обработке команды!")
        current_state = await state.get_state()
        if current_state is not None:
            await state.clear()

Here I'm using aiogram state machine to track if the bot is in payment mode. The state is cleared upon entry and upon emergency exit from the handler.

To ensure that the test card data is always at hand, I send it to the user if a test payment token is specified in the bot settings.

Next we create an array of objects LabeledPrice for transfer to invoice sending methodi.e. bills to pay. In addition, values ​​previously saved in the bot settings, as well as a required parameter, are passed to this method payload (a string that the API forces us to use for our internal processes, without even wondering if we need it at all).

It is worth special mentioning the parameters need_phone_number And send_phone_number_to_provider. They are needed to send the above-mentioned electronic receipts to customers.

If you have set up fiscalization through YuKassa, then you have two ways to obtain user contacts:

  • request an email/phone number in advance and pass this value to provider_data.receipt.email / provider_data.receipt.phone;

  • set parameters need_phone_number/need_email And send_phone_number_to_provider/ send_email_to_provider value True. Then YuKassa will request the corresponding value when paying.

My code uses the second method.

The next method we need to implement will be universal. This is the standard code for processing an update like PreCheckoutQueryto which we need to respond within 10 seconds.

@router.pre_checkout_query()
async def process_pre_checkout_query(pre_checkout_query: PreCheckoutQuery):
    try:
        await bot.answer_pre_checkout_query(pre_checkout_query.id, ok=True)  # всегда отвечаем утвердительно
    except Exception as e:
        logging.error(f"Ошибка при обработке апдейта типа PreCheckoutQuery: {e}")

Myself PreCheckoutQuery – This objectcontaining information about the incoming pre-check request and containing the parameters we are familiar with currency, total_amount and again mandatory payload.

Now let's process the successful payment. To catch him, we need a magic filter F.successful_payment.

@router.message(F.successful_payment)
async def process_successful_payment(message: Message, state: FSMContext, db: Database):
        await message.reply(f"Платеж на сумму {message.successful_payment.total_amount // 100} "
                            f"{message.successful_payment.currency} прошел успешно!")
        await db.update_payment(message.from_user.id)
        logging.info(f"Получен платеж от {message.from_user.id}")
        current_state = await state.get_state()
        if current_state is not None:
            await state.clear()  # чтобы свободно перейти сюда из любого другого состояния

For greater accuracy, data on the amount paid (in kopecks) and the payment currency are taken from the service message about a successful payment. We report all this to the user, save the payment information somewhere in the database (no wonder he paid?) and clear the state.

But if the successful payment filter did not work, and the bot is in the purchasing state, then some kind of error has occurred, about which the user must be notified. That's why I needed a state machine.

@router.message(StateFilter(FSMPrompt.buying))
async def process_unsuccessful_payment(message: Message, state: FSMContext):
        await message.reply("Не удалось выполнить платеж!")
        current_state = await state.get_state()
        if current_state is not None:
            await state.clear()  # чтобы свободно перейти сюда из любого другого состояния

The filter here is essentially default, so this handler should be the very last one in the entire code fragment related to accepting payments.

The final touch is to check that polling starts without skipping unread updates that have accumulated by the time of launch.

await dp.start_polling(bot, skip_updates=False)

This has been the case for working with payments since the days of aigram 2.

Now you can launch the bot and send it a command /buy and check that the test payment is successful.

By the end of the article, I realized that I need to use blur in the screenshot editor)

By the end of the article, I realized that I need to use blur in the screenshot editor)

Full mode

To set up real payments, you need to register with YuKassaadd information about your organization and your store. True, we have a bot, not a store, but managers still ask for some kind of interface where they can see a list of products with prices. I sent a screenshot of the help that my bot issued on command /help.

The result of all these formalities will be your own ShopIDwhich you will see in your personal account.

Let's now return to our dialogue with BotFather.

Here we received a test payment token, remember?

Here we received a test payment token, remember?

Let's select YuKassa and Connect YuKassa Live again. The bot will ask for shopId and shopArticleId (which it advises sending simply 0). Having sent them, we will return to BotFather, where a real payment token of the form will now appear x:LIVE:y. All that remains is to write it down in PROVIDER_TOKEN in the file .env And…

And nothing works!

When there is such a screen, the meme is no longer needed

When there is such a screen, the meme is no longer needed

The fact is that to accept payments in Telegram you need to switch the store to the email protocol. To do this, write an email to ecommerce@yoomoney.ru indicating your ShopID. YuKassa is accustomed to such requests and promptly fulfills them.

Everything is fine now!

You can like this)

You can like this)

Resume

In this article, I tried to put together all the information about setting up integration with YuKassa that I managed to collect on the Internet, through correspondence with technical support and from personal experience. And this experience shows that it is enough to miss the slightest nuance, and YuKassa receives a payment error message without details. I hope that my article will improve this situation a little.

My bot, where it works to accept payments using this system

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *