Интеграция оплаты Юкасса в telegramm для самозанятых

Привет тем кто хочет опробовать себя в качестве бизнесмена! Недавно в голову пришла идея, получить некоторый опыт предпринимательства. В качестве продукта выступает доступ к некоторой цифровой услуге, а контроль за оплатой этой услуги ложиться на плечи телеграмм бота. В ходе поисков системы оплаты была найдена Юкасса, одна из немногих систем (если вообще не единственная), которая работает с самозанятыми.

На сайте подробно описана интеграция оплаты в telegramm бота. Однако на этапе подписания документов выясняется что интеграция недоступна для самозанятых.

bc94639306a362c310de230c2733ff74.png

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

Интеграция для самозанятых

Реализовать оплату услуг самозанятого можно через сайт юкассы. Я это реализовал следующим образом: клиент запрашивает в боте услугу, в ответ ему приходит ссылка на оплату, клиент переходит и оплачивает товар, после чего его перенаправляет обратно в бот. Обработка же платежа работает следующим образом: как только создается ссылка на оплату, бот запрашивает статус платежа, до тех пор пока статус является «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)

Пример работы бота, с интеграцией оплаты, можно посмотреть здесь

© Habrahabr.ru