Всем привет! В этой статье разберём как подключить Payme — одну из самых популярных платёжных систем Узбекистана — к Telegram боту на Python. Для этого мы будем использовать библиотеку aiopayme — async-first решение с роутерами и dependency injection как в aiogram и FastAPI. В конце статьи вы получите полностью рабочую интеграцию: бот принимает команду /pay, пользователь оплачивает через Payme, бот получает уведомление об успешной оплате.

Установка

pip install aiopayme

Что такое aiopayme? aiopayme — это асинхронная Python библиотека для интеграции платёжной системы Payme. Главная особенность — роутерная архитектура, знакомая каждому кто работал с aiogram или FastAPI:

from aiopayme import Router

router = Router()

@router.check_perform_transaction()
async def check_perform(ctx: CheckPerformTransactionCtx, db: AsyncSession): ...

Единственная зависимость — httpx. Библиотека не навязывает ORM, фреймворк или структуру проекта.

Модели

Создадим две модели — заказ и транзакцию Payme.

# models/order.py

import enum
from sqlalchemy import Column, BigInteger, Integer, String
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

class OrderStatus(enum.Enum):
    PENDING = "pending"
    PAID = "paid"
    CANCELED = "canceled"

class Order(Base):
    __tablename__ = "orders"

    id = Column(Integer, primary_key=True)
    telegram_id = Column(BigInteger, nullable=False)
    amount = Column(BigInteger, nullable=False)
    status = Column(String, default=OrderStatus.PENDING.value)
    payme_transaction_id = Column(String, nullable=True)
# models/payme.py

from sqlalchemy import Column, Integer, String, BigInteger
from models.order import Base


class PaymeTransaction(Base):
    __tablename__ = "payme_transactions"

    id = Column(Integer, primary_key=True)
    payme_id = Column(String, unique=True, nullable=False)
    order_id = Column(Integer, nullable=True)
    state = Column(Integer, default=1)
    amount = Column(BigInteger, nullable=False)
    create_time = Column(BigInteger, nullable=False)
    perform_time = Column(BigInteger, default=0)
    cancel_time = Column(BigInteger, default=0)
    reason = Column(Integer, nullable=True)

PaymeService

Вынесем всю бизнес-логику в отдельный сервис. Это сделает хендлеры чистыми и логику легко тестируемой. Создаём services/payme.py:

# services/payme.py

from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from aiopayme.exceptions import Errors
from aiopayme.utils import time_to_payme
from aiopayme.types import (
    CheckPerformTransactionCtx,
    CreateTransactionCtx,
    PerformTransactionCtx,
    CancelTransactionCtx,
    CheckTransactionCtx,
    GetStatementCtx,
)

from models import OrderStatus, Order, PaymeTransaction


class PaymeService:

    def __init__(self, db: AsyncSession):
        self.db = db

    async def get_order(self, order_id) -> Order | None:
        return await self.db.scalar(
            select(Order).where(Order.id == int(order_id))
        )

    async def get_transaction(self, payme_id: str) -> PaymeTransaction | None:
        return await self.db.scalar(
            select(PaymeTransaction).where(PaymeTransaction.payme_id == payme_id)
        )

    async def get_active_transaction(self, order_id: int) -> PaymeTransaction | None:
        return await self.db.scalar(
            select(PaymeTransaction).where(
                PaymeTransaction.order_id == order_id,
                PaymeTransaction.state == 1,
            )
        )

    async def get_transactions(self, from_time: int, to_time: int):
        return (await self.db.scalars(
            select(PaymeTransaction).where(
                PaymeTransaction.create_time >= from_time,
                PaymeTransaction.create_time <= to_time,
            )
        )).all()


    async def check_perform(self, ctx: CheckPerformTransactionCtx):
        order = await self.get_order(ctx.account.order_id)
        if not order:
            raise Errors.invalid_account()
        if order.amount * 100 != ctx.amount:
            raise Errors.invalid_amount()
        return ctx.ok(allow=True)

    async def create_transaction(self, ctx: CreateTransactionCtx):
        tx = await self.get_transaction(ctx.payme_id)
        order = await self.get_order(ctx.account.order_id)

        if not order:
            raise Errors.invalid_account()
        if order.amount * 100 != ctx.amount:
            raise Errors.invalid_amount()

        if tx:
            if tx.state == -1:
                raise Errors.unable_to_perform()
            return ctx.ok(transaction_id=tx.payme_id, create_time=tx.create_time)

        if order.status == OrderStatus.PAID:
            raise Errors.invalid_account()

        existing_tx = await self.get_active_transaction(order.id)
        if existing_tx:
            rejected = PaymeTransaction(
                payme_id=ctx.payme_id,
                order_id=order.id,
                amount=ctx.amount,
                create_time=ctx.time,
                state=-1,
                cancel_time=time_to_payme(),
                reason=3,
            )
            self.db.add(rejected)
            await self.db.commit()
            raise Errors.unable_to_perform()

        tx = PaymeTransaction(
            payme_id=ctx.payme_id,
            order_id=order.id,
            amount=ctx.amount,
            create_time=ctx.time,
            state=1,
        )

        self.db.add(tx)
        await self.db.execute(
            update(Order)
            .where(Order.id == order.id)
            .values(payme_transaction_id=ctx.payme_id)
        )
        await self.db.commit()
        return ctx.ok(transaction_id=tx.payme_id, create_time=tx.create_time)

    async def perform_transaction(self, ctx: PerformTransactionCtx):
        tx = await self.get_transaction(ctx.transaction_id)
        if not tx:
            raise Errors.transaction_not_found()

        if tx.state == 2:
            return ctx.ok(transaction_id=tx.payme_id, perform_time=tx.perform_time, state=2)

        tx.state = 2
        tx.perform_time = time_to_payme()
        await self.db.execute(
            update(Order)
            .where(Order.id == tx.order_id)
            .values(status=OrderStatus.PAID.value)
        )
        await self.db.commit()
        return ctx.ok(transaction_id=tx.payme_id, perform_time=tx.perform_time, state=2)

    async def cancel_transaction(self, ctx: CancelTransactionCtx):
        tx = await self.get_transaction(ctx.transaction_id)
        if not tx:
            raise Errors.transaction_not_found()

        if tx.state in (-1, -2):
            return ctx.ok(
                transaction=tx.payme_id,
                cancel_time=tx.cancel_time,
                state=tx.state,
                reason=tx.reason,
            )

        tx.state = -2 if tx.state == 2 else -1
        tx.cancel_time = time_to_payme()
        tx.reason = ctx.reason
        await self.db.execute(
            update(Order)
            .where(Order.id == tx.order_id)
            .values(status=OrderStatus.CANCELLED.value)
        )
        await self.db.commit()
        return ctx.ok(
            transaction=tx.payme_id,
            state=tx.state,
            cancel_time=tx.cancel_time,
            reason=tx.reason,
        )

    async def check_transaction(self, ctx: CheckTransactionCtx):
        tx = await self.get_transaction(ctx.transaction_id)
        if not tx:
            raise Errors.transaction_not_found()

        return ctx.ok(
            state=tx.state,
            create_time=tx.create_time,
            perform_time=tx.perform_time,
            cancel_time=tx.cancel_time,
            reason=tx.reason,
        )

    async def get_statement(self, ctx: GetStatementCtx):
        from_time = ctx.from_time
        to_time = ctx.to_time
        if from_time > to_time:
            from_time, to_time = to_time, from_time

        txs = await self.get_transactions(from_time, to_time)
        return ctx.ok(transactions=[
            {
                "id": tx.payme_id,
                "time": tx.create_time,
                "amount": tx.amount,
                "account": {"order_id": tx.order_id},
                "state": tx.state,
                "create_time": tx.create_time,
                "perform_time": tx.perform_time or 0,
                "cancel_time": tx.cancel_time or 0,
                "reason": tx.reason,
            }
            for tx in txs
        ])

Обратите внимание — сумма в Payme передаётся в тийинах (1 сум = 100 тийин), поэтому при сравнении умножаем order.amount * 100.

Хендлеры

Теперь создаём роутер и подключаем сервис. Каждый метод Payme — это отдельный декоратор. Dependency injection работает автоматически — просто добавляем нужные зависимости как аргументы функции:

# handlers/payme.py

from aiopayme import Router
from aiopayme.types import *
from sqlalchemy.ext.asyncio import AsyncSession

from services.payme import PaymeService

from deps import bot

router = Router()

@router.check_perform_transaction()
async def check_perform(ctx: CheckPerformTransactionCtx, db: AsyncSession):
    return await PaymeService(db).check_perform(ctx)

@router.create_transaction()
async def create_transaction(ctx: CreateTransactionCtx, db: AsyncSession):
    return await PaymeService(db).create_transaction(ctx)

@router.perform_transaction()
async def perform_transaction(ctx: PerformTransactionCtx, db: AsyncSession):
    result = await PaymeService(db).perform_transaction(ctx)
    tx = await PaymeService(db).get_transaction(ctx.transaction_id)
    order = await PaymeService(db).get_order(tx.order_id)

    await bot.send_message(
        chat_id=order.telegram_id,
        text="✅ Оплата прошла успешно."
    )
    return result


@router.cancel_transaction()
async def cancel_transaction(ctx: CancelTransactionCtx, db: AsyncSession):
    return await PaymeService(db).cancel_transaction(ctx)

@router.check_transaction()
async def check_transaction(ctx: CheckTransactionCtx, db: AsyncSession):
    return await PaymeService(db).check_transaction(ctx)

@router.get_statement()
async def get_statement(ctx: GetStatementCtx, db: AsyncSession):
    return await PaymeService(db).get_statement(ctx)

deps.py

Здесь инициализируем все зависимости — базу данных, бота и Payme:

from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from aiopayme import Payme
from aiogram import Bot

engine = create_async_engine("sqlite+aiosqlite:///./db.sqlite3")
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)

payme = Payme(
    merchant_id="your_merchant_id",
    secret_key=your_secret_test_key,
    sandbox=True,
    echo=True,
)

bot = Bot(token="your_bot_token")

Роутеры

Создаём два роутера — для вебхука Payme и для создания заказа:

# routers/order.py

from fastapi import APIRouter
from sqlalchemy import insert
from pydantic import BaseModel

from deps import SessionLocal, payme
from models import Order

router = APIRouter()

class OrderCreate(BaseModel):
    telegram_id: int
    amount: int

@router.post("/order/create")
async def create_order(data: OrderCreate):
    async with SessionLocal() as db:
        result = await db.execute(
            insert(Order).values(
                telegram_id=data.telegram_id,
                amount=data.amount
            ).returning(Order.id)
        )
        order_id = result.scalar()
        await db.commit()

    pay_link = payme.generate_pay_link(
        amount=data.amount,
        account={
            "order_id": order_id,
            "telegram_id": data.telegram_id
        }
    )
    return {"order_id": order_id, "pay_link": pay_link}
# routers/webhook.py

from fastapi import APIRouter, Request
from deps import payme

router = APIRouter()

@router.post("/payme")
async def payme_webhook(request: Request):
    data = await request.json()
    result = await payme.handle(
        data=data,
        headers=dict(request.headers)
    )
    return result

Payme отправляет все запросы на один эндпоинт POST /payme. Метод handle сам аутентифицирует запрос и направляет его в нужный хендлер.

Telegram бот

Бот принимает команду /pay, создаёт заказ через API и отправляет пользователю ссылку на оплату:

# bot/main.py

import asyncio
import httpx

from aiogram import Dispatcher
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import Command

from deps import bot

dp = Dispatcher()

def get_pay_link_btn(url: str) -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(text="✅ Оплатить", url=url)]
        ]
    )

@dp.message(Command("pay"))
async def cmd_pay(message: Message):
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "http://localhost:8000/order/create",
            json={
                "telegram_id": message.from_user.id,
                "amount": 1000
            }
        )
        data = response.json()

        await message.answer(
            "Оплатить можно по кнопке ниже",
            reply_markup=get_pay_link_btn(data["pay_link"])
        )

async def main():
    await dp.start_polling(bot)

if __name__ == '__main__':
    asyncio.run(main())

После успешной оплаты Payme вызывает perform_transaction — и бот автоматически отправляет пользователю уведомление.

Админка

Для удобного управления заказами и транзакциями подключим sqladmin:

# admin/admin.py

from sqladmin import ModelView
from models import Order, PaymeTransaction

class OrderAdmin(ModelView, model=Order):
    name = "Order"
    name_plural = "Orders"
    column_list = "__all__"

class PaymeTransactionAdmin(ModelView, model=PaymeTransaction):
    name = "Transaction"
    name_plural = "Transactions"
    column_list = "__all__"

Админка будет доступна по адресу http://localhost:8000/admin.

main.py

Собираем всё вместе — FastAPI, Payme, админка:

# main.py

import asyncio
from contextlib import asynccontextmanager

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
from sqladmin import Admin
import uvicorn

from aiopayme import Dispatcher

from models import Base
from deps import engine, SessionLocal, payme
from routers.order import router as order_router
from routers.webhook import router as webhook_router
from handlers.payme import router as payme_router
from admin.admin import OrderAdmin, PaymeTransactionAdmin

dp = Dispatcher()
dp.include_router(payme_router)

payme.setup(dp)
payme.provide(AsyncSession, SessionLocal)

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    yield
    await engine.dispose()

app = FastAPI(lifespan=lifespan)

app.include_router(order_router)
app.include_router(webhook_router)

admin = Admin(app, engine)
admin.add_view(OrderAdmin)
admin.add_view(PaymeTransactionAdmin)

async def main():
    config = uvicorn.Config(app, host="0.0.0.0", port=8000)
    server = uvicorn.Server(config)
    await server.serve()

if __name__ == '__main__':
    asyncio.run(main())

Бот запускается отдельным процессом:

python bot/main.py   # Telegram bot
python main.py # backend

Локальная разработка

туннель Payme должен достучаться до вашего локального сервера. Для этого используем туннель — ngrok или cloudflared.

cloudflared tunnel --url http://0.0.0.0/8000

Активация кассы

Перед боевым использованием необходимо пройти проверку в песочнице Payme. Все методы (CheckPerformTransaction, CreateTransaction, PerformTransaction и т.д.) должны корректно отвечать на тестовые запросы.

Заключение

Интеграция готова! Перед выходом в продакшн не забудьте: - Заменить secret_key на боевой

payme = Payme(
    merchant_id="your_merchant_id",
    secret_key="your_secret_key", # prod
    sandbox=False,
    echo=False
)

Спасибо за внимание! Удачи с интеграцией! ?

Комментарии (0)