Skip to main content

Async Python library for Payme integration

Project description

aiopayme

PyPI version Python versions License Downloads Docs Telegram

Async Python library for Payme integration.

Quick Start

1. Install

pip install aiopayme

2. Create your models

# models/order.py

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


class Base(DeclarativeBase):
    pass


class OrderStatus(enum.Enum):
    PENDING = "pending"
    PAID = "paid"
    CANCELLED = "cancelled"


class Order(Base):
    __tablename__ = "orders"

    id = Column(Integer, primary_key=True)
    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)

3. Create 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.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.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
        ])

4. Add router

# handlers/payme.py

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

from services.payme import PaymeService

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):
    return await PaymeService(db).perform_transaction(ctx)

@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)

5. Setup and mount to FastAPI

# main.py

from fastapi import FastAPI, Request
from sqlalchemy.ext.asyncio import AsyncSession
from aiopayme import Payme, Dispatcher
from handlers.payme import router as payme_router

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

dp = Dispatcher()
dp.include_router(payme_router)
payme.setup(dp)
payme.provide(AsyncSession, SessionLocal)

app = FastAPI()

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

6. Generate pay link

@app.post("/order/create")
async def create_order(data: OrderCreate):
    async with SessionLocal() as db:
        result = await db.execute(
            insert(Order).values(
                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,
        }
    )

    return {"order_id": order_id, "pay_link": pay_link}

Documentation

Full documentation at aiopayme.github.io

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

aiopayme-0.1.8.tar.gz (9.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aiopayme-0.1.8-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file aiopayme-0.1.8.tar.gz.

File metadata

  • Download URL: aiopayme-0.1.8.tar.gz
  • Upload date:
  • Size: 9.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for aiopayme-0.1.8.tar.gz
Algorithm Hash digest
SHA256 ed5c18adaa5fd39e94aae453da1f1159ee858419ba58d71b7fda7251f5ada1b7
MD5 adb0112a4503a9869cba742df181e3fe
BLAKE2b-256 50217f6b639b6ca1759ad8b3b99a79ec5cd31c482cb6c96ac1eaaaa2a7225828

See more details on using hashes here.

File details

Details for the file aiopayme-0.1.8-py3-none-any.whl.

File metadata

  • Download URL: aiopayme-0.1.8-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for aiopayme-0.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 e8378eb02caf737d0005b659d0c82576def62849dad6502e8fe9e5755ff809e6
MD5 2b3a928a069255c9549ac9d101c4e694
BLAKE2b-256 132324bc6e547cfb48b8baa9ba2531ca124b988072e564c2799e8bf5402b30de

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page