Интеграция оплаты Юкасса в telegramm для самозанятых
Привет тем кто хочет опробовать себя в качестве бизнесмена! Недавно в голову пришла идея, получить некоторый опыт предпринимательства. В качестве продукта выступает доступ к некоторой цифровой услуге, а контроль за оплатой этой услуги ложиться на плечи телеграмм бота. В ходе поисков системы оплаты была найдена Юкасса, одна из немногих систем (если вообще не единственная), которая работает с самозанятыми.
На сайте подробно описана интеграция оплаты в telegramm бота. Однако на этапе подписания документов выясняется что интеграция недоступна для самозанятых.
Обидно, но других вариантов оплаты самозанятому найти не удалось, поэтому решено было попробовать написать собственный вариант оплаты. В ходе беглого гугления, не удалось найти готовых решений по самостоятельной реализации оплаты, чему я был сильно огорчен, так-как это скорее всего означало большую сложность собственноручной реализации оплаты. Но так-как, бот был уже готов, я решил попробовать какие-то ещё варианты. Изначально я не думал делать собственную интеграцию, а хотел выставлять многоразовые счета, а потом по примечанию к платежу отслеживать успешные оплаты. Однако, оказалось что интеграция оплаты реализуется гораздо проще, и по факту, в самом дубовом варианте без веб хуков, состоит всего из двух функций.
Интеграция для самозанятых
Реализовать оплату услуг самозанятого можно через сайт юкассы. Я это реализовал следующим образом: клиент запрашивает в боте услугу, в ответ ему приходит ссылка на оплату, клиент переходит и оплачивает товар, после чего его перенаправляет обратно в бот. Обработка же платежа работает следующим образом: как только создается ссылка на оплату, бот запрашивает статус платежа, до тех пор пока статус является «pending». Как только статус меняется на «succeeded» бот выполняет действие (отправляет товар/оказывает услугу и тд).
Для реализации такой схемы необходим модуль yookassa откуда мы возьмем классы Configuration и Payment. Далее необходимо заполнить два поля класса Configuration: Configuration.account_id и Configuration.secret_key, в первый записываем id магазина, во второй api ключ магазина.
import json
from yookassa import Configuration,Payment
import config
Configuration.account_id = config.SHOP_ID
Configuration.secret_key = config.SHOP_API_TOKEN
Далее с помощью метода Payment.create, необходимо создать объект платежа. При создании этот метод сам отправит данные в юкассу.
import json
from yookassa import Configuration,Payment
import config
Configuration.account_id = config.SHOP_ID
Configuration.secret_key = config.SHOP_API_TOKEN
payment = Payment.create({
"amount": {
"value": сумма платежа,
"currency": "RUB"
},
"payment_method_data": {
"type": "bank_card"
},
"confirmation": {
"type": "redirect",
"return_url": "Ссылка, куда перенаправить после совершения платежа"
},
"capture": True,
"description": description
})
Платеж создан, теперь необходимо получить id операции (понадобиться позже) и ссылку на оплату, которую мы и отправим пользователю. Для этого воспользуемся методом json (), который запросит данные по платежу на сервере юкассы и вернет их в формате json. Для удобства преобразуем json в словарь python:
import json
from yookassa import Configuration,Payment
import config
Configuration.account_id = config.SHOP_ID
Configuration.secret_key = config.SHOP_API_TOKEN
payment = Payment.create({
"amount": {
"value": сумма платежа,
"currency": "RUB"
},
"payment_method_data": {
"type": "bank_card"
},
"confirmation": {
"type": "redirect",
"return_url": "Ссылка, куда перенаправить после совершения платежа"
},
"capture": True,
"description": description
})
payment_data = json.loads(payment.json())
payment_id = payment_data['id']
payment_url = (payment_data['confirmation'])['confirmation_url']
Теперь можно отправить payment_url пользователю, по которому он сможет оплатить товар. Однако мы пока не знаем оплатил пользователь товар или нет. Для получения статуса платежа реализуем метод Payment.find_one (payment_id)).json (), которые найдет платеж по указанному id (который мы получили на прошлом шаге) и пришлет его статус в формате json. Далее мы будем опрашивать этот метод до тех пор пока статус платежа не измениться с pending на успешный / не успешный.
import json
from yookassa import Configuration,Payment
import config
import time
Configuration.account_id = config.SHOP_ID
Configuration.secret_key = config.SHOP_API_TOKEN
payment = Payment.create({
"amount": {
"value": сумма платежа,
"currency": "RUB"
},
"payment_method_data": {
"type": "bank_card"
},
"confirmation": {
"type": "redirect",
"return_url": "Ссылка, куда перенаправить после совершения платежа"
},
"capture": True,
"description": description
})
payment_data = json.loads(payment.json())
payment_id = payment_data['id']
payment_url = (payment_data['confirmation'])['confirmation_url']
payment = json.loads((Payment.find_one(payment_id)).json())
while payment['status'] == 'pending':
payment = json.loads((Payment.find_one(payment_id)).json())
time.sleep(время между опросами)
Теперь при успешной оплате или таймауте операции (что-то около 15 минут), мы выйдем из цикла, однако здесь существует огромная проблема, с тем, что если вызывать эти методы из бота, бот будет заблокирован на весь период оплаты. Чтобы избежать подобного поведения будем использовать асинхронный sleep. А также реализуем логику оплаты в виде двух функций: создания и проверки статуса платежа
import json
from yookassa import Configuration,Payment
import config
import asyncio
Configuration.account_id = config.SHOP_ID
Configuration.secret_key = config.SHOP_API_TOKEN
def payment(value,description):
payment = Payment.create({
"amount": {
"value": value,
"currency": "RUB"
},
"payment_method_data": {
"type": "bank_card"
},
"confirmation": {
"type": "redirect",
"return_url": "урл редиректа"
},
"capture": True,
"description": description
})
return json.loads(payment.json())
async def check_payment(payment_id):
payment = json.loads((Payment.find_one(payment_id)).json())
while payment['status'] == 'pending':
payment = json.loads((Payment.find_one(payment_id)).json())
await asyncio.sleep(3)
if payment['status']=='succeeded':
print("SUCCSESS RETURN")
print(payment)
return True
else:
print("BAD RETURN")
print(payment)
return False
Теперь посмотрим как это может выглядеть в боте (aiogram)
import json
from aiogram import Bot, Dispatcher, executor, types, filters
from yookassa import Configuration,Payment
import payment
import config
API_TOKEN = config.API_TOKEN
BASE=config.BASE
REFBASE=config.REFBASE
CURRENT_ENDPOINT=config.CURRENT_ENDPOINT
#--- buttons ---
btnbuy = types.KeyboardButton("купить")
btnprologue = types.KeyboardButton("продлить")
btnstatus = types.KeyboardButton("статус")
btnsinfo = types.KeyboardButton("инфо")
mainmenu=types.ReplyKeyboardMarkup(resize_keyboard = True).add(btnbuy,btnprologue,btnstatus,btnsinfo)
# Initialize bot and dispatcher
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
@dp.message_handler(commands=['start'])
async def send_welcome(message: types.Message):
await message.answer("Hello", reply_markup = mainmenu)
@dp.message_handler(commands=['info'])
async def info(message: types.Message):
await message.answer("info", disable_web_page_preview=True)
@dp.message_handler(commands=['buy'])
async def BuyVPNforMonth(message: types.Message):
await message.answer(msg.buy_message)
payment_deatils = payment.payment(100,'Купить товар1')
await message.answer( (payment_deatils['confirmation'])['confirmation_url'] )
if await payment.check_payment(payment_deatils['id']):
message.answer("платеж")
else:
message.answer("платеж не прошел")
@dp.message_handler()
async def menu_message(message: types.Message):
if message.text == "купить":
await BuyVPNforMonth(message)
if message.text == "инфо":
await info(message)
if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)
Пример работы бота, с интеграцией оплаты, можно посмотреть здесь