Skip to main content

Unified payment library for Uzbekistan payment systems (Payme, Click, Uzum, Paynet, Octo)

Project description

tolov

PyPI version Python Versions License: MIT

Tolov is a unified payment library for integrating with popular payment systems in Uzbekistan. It provides a simple and consistent interface for working with Payme, Click, Uzum, and Paynet payment gateways.

Features

  • API: Consistent interface for multiple payment providers
  • Secure: Built-in security features for payment processing
  • Framework Integration: Native support for Django and FastAPI
  • Webhook Handling: Easy-to-use webhook handlers for payment notifications
  • Transaction Management: Automatic transaction tracking and management
  • Extensible: Easy to add new payment providers

Installation

Basic Installation

pip install tolov

Framework-Specific Installation

# For Django
pip install tolov[django]

# For FastAPI
pip install tolov[fastapi]

# For Flask
pip install tolov[flask]

Quick Start

Generate Payment Links

from tolov.gateways.payme import PaymeGateway
from tolov.gateways.click import ClickGateway
from tolov.gateways.uzum.client import UzumGateway
from tolov.gateways.paynet import PaynetGateway

# Initialize Payme gateway
payme = PaymeGateway(
    payme_id="your_payme_id",
    payme_key="your_payme_key",
    is_test_mode=True  # Set to False in production environment
)

# Initialize Click gateway
click = ClickGateway(
    service_id="your_service_id",
    merchant_id="your_merchant_id",
    merchant_user_id="your_merchant_user_id",
    secret_key="your_secret_key",
    is_test_mode=True  # Set to False in production environment
)

# Initialize Uzum gateway (Biller/open-service)
uzum = UzumGateway(
    service_id="your_service_id",  # Uzum Service ID
    is_test_mode=True  # Set to False in production environment
)

# Initialize Paynet gateway
paynet = PaynetGateway(
    merchant_id="your_merchant_id",  # Paynet Merchant ID (accepts both str and int)
    is_test_mode=False  # Set to True for testing
)

# Generate payment links
payme_link = payme.create_payment(
    id="order_123",
    amount=150000,  # amount in UZS
    return_url="https://example.com/return",
    account_field_name="id"  # Payme-specific: field name for account ID (default: "order_id")
)
# Note: account_field_name is only used for Payme and specifies the field name
# that will be used in the payment URL (e.g., ac.id=123).
# Other payment gateways (Click, Uzum) don't use this parameter.

click_link = click.create_payment(
    id="order_123",
    amount=150000,  # amount in UZS
    description="Test payment",
    return_url="https://example.com/return"
)


# Generate Uzum Biller payment URL
# URL format: https://www.uzumbank.uz/open-service?serviceId=...&order_id=...&amount=...&redirectUrl=...
uzum_link = uzum.create_payment(
    id="order_123",  # Order ID (order_id parameter)
    amount=100000,  # amount in som (will be converted to tiyin)
    return_url="https://example.com/callback"  # redirectUrl parameter
)
# Result: https://www.uzumbank.uz/open-service?serviceId=your_service_id&order_id=order_123&amount=10000000&redirectUrl=https%3A%2F%2Fexample.com%2Fcallback

# Generate Paynet payment URL
# URL format: https://app.paynet.uz/?m={merchant_id}&c={payment_id}&a={amount}
paynet_link = paynet.create_payment(
    id="order_123",  # Payment ID (c parameter)
    amount=15000000  # amount in tiyin (optional, a parameter) - 150000 som = 15000000 tiyin
)
# Result: https://app.paynet.uz/?m=your_merchant_id&c=order_123&a=15000000

# Or without amount (amount will be configured on Paynet's side)
paynet_link_no_amount = paynet.create_payment(id="order_123")
# Result: https://app.paynet.uz/?m=your_merchant_id&c=order_123

Important Notes

Payme account_field_name Parameter

Example:

# Using default account_field_name = "order_id"
payme_link = payme.create_payment(
    id="123",
    amount=150000,
    return_url="https://example.com/return"
)
# Using custom account_field_name
payme_link = payme.create_payment(
    id="123",
    amount=150000,
    return_url="https://example.com/return",
    account_field_name="id"
)

Note: Other payment gateways (Click, Uzum, Paynet) do not use the account_field_name parameter.

Paynet Payment Gateway

Paynet uses a unique URL-based payment system:

  • URL Format: https://app.paynet.uz/?m={merchant_id}&c={payment_id}&a={amount}
  • merchant_id: Accepts both str and int types (automatically converted to string)
  • amount: Optional parameter in tiyin. If provided, it will be included in the URL as a parameter
  • No return_url: Paynet does NOT support return URL parameter
  • Mobile-first: Payment is completed in the Paynet mobile app
    • Desktop users: QR code is displayed to scan
    • Mobile users: Direct link to open Paynet app
  • Webhooks: Payment status updates are handled through JSON-RPC 2.0 webhooks

Django Integration

  1. Create Order model:
# models.py
from django.db import models
from django.utils import timezone

class Order(models.Model):
    STATUS_CHOICES = (
        ('pending', 'Pending'),
        ('paid', 'Paid'),
        ('cancelled', 'Cancelled'),
        ('delivered', 'Delivered'),
    )

    product_name = models.CharField(max_length=255)
    amount = models.DecimalField(max_digits=12, decimal_places=2)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    created_at = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return f"{self.id} - {self.product_name} ({self.amount})"
  1. Add to INSTALLED_APPS and configure settings:
# settings.py
INSTALLED_APPS = [
    # ...
    'tolov.integrations.django',
]

TOLOV = {
    'PAYME': {
        'PAYME_ID': 'your_payme_id',
        'PAYME_KEY': 'your_payme_key',
        'ACCOUNT_MODEL': 'your_app.models.Order',  # For example: 'orders.models.Order'
        'ACCOUNT_FIELD': 'id',
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
    },
    'CLICK': {
        'SERVICE_ID': 'your_service_id',
        'MERCHANT_ID': 'your_merchant_id',
        'MERCHANT_USER_ID': 'your_merchant_user_id',
        'SECRET_KEY': 'your_secret_key',
        'ACCOUNT_MODEL': 'your_app.models.Order',
        'ACCOUNT_FIELD': 'id',
        'COMMISSION_PERCENT': 0.0,
        'ONE_TIME_PAYMENT': True,
    },

    'UZUM': {
        'SERVICE_ID': 'your_service_id',  # Uzum Service ID for Biller URL
        'USERNAME': 'your_uzum_username',  # For webhook Basic Auth
        'PASSWORD': 'your_uzum_password',  # For webhook Basic Auth
        'ACCOUNT_MODEL': 'your_app.models.Order',
        'ACCOUNT_FIELD': 'order_id',  # or 'id'
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
    },
    'PAYNET': {
        'SERVICE_ID': 'your_paynet_service_id',
        'USERNAME': 'your_paynet_username',
        'PASSWORD': 'your_paynet_password',
        'ACCOUNT_MODEL': 'your_app.models.Order',
        'ACCOUNT_FIELD': 'id',
        'AMOUNT_FIELD': 'amount',
        'ONE_TIME_PAYMENT': True,
    }
}

Note: The IS_TEST_MODE parameter is configured when creating payment gateways (e.g., PaymeGateway, ClickGateway), not in webhook settings. Webhooks receive requests on the same URL regardless of test or production environment.

  1. Create webhook handlers:
# views.py
from tolov.integrations.django.views import (
    BasePaymeWebhookView,
    BaseClickWebhookView,
    BaseUzumWebhookView,
    BasePaynetWebhookView
)
from .models import Order

class PaymeWebhookView(BasePaymeWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()

    def get_check_data(self, params, account): # optional
        # Return additional data for CheckPerformTransaction (fiscal receipt)
        return {
            "additional": {"first_name": account.first_name, "balance": account.balance},
            "detail": {
                "receipt_type": 0,
                "shipping": {"title": "Yetkazib berish", "price": 10000},
                "items": [
                    {
                        "discount": 0,
                        "title": account.product_name,
                        "price": int(account.amount * 100),
                        "count": 1,
                        "code": "00001",
                        "units": 1,
                        "vat_percent": 0,
                        "package_code": "123456"
                    }
                ]
            }
        }

class ClickWebhookView(BaseClickWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()



class UzumWebhookView(BaseUzumWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()

    def get_check_data(self, params, account):
        # Return additional data for check/create/status/confirm actions
        # Example: returning user's full name
        return {
            "fio": {
                "value": "Ivanov Ivan"
            }
        }

class PaynetWebhookView(BasePaynetWebhookView):
    def successfully_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'paid'
        order.save()

    def cancelled_payment(self, params, transaction):
        order = Order.objects.get(id=transaction.account_id)
        order.status = 'cancelled'
        order.save()

    def get_check_data(self, params, account) # optional:
        # Return additional data for GetInformation
        order = Order.objects.get(id=account.id)
        # You can use any key value pairs
        return {
            "fields": {
                "first_name": order.user.first_name,
                "balance": order.user.balance
            }
        }
  1. Add webhook URLs to urls.py:
# urls.py
from django.urls import path
from .views import PaymeWebhookView, ClickWebhookView, UzumWebhookView, PaynetWebhookView

urlpatterns = [
    # ...
    path('payments/webhook/payme/', PaymeWebhookView.as_view(), name='payme_webhook'),
    path('payments/webhook/click/', ClickWebhookView.as_view(), name='click_webhook'),

    path('payments/webhook/uzum/<str:action>/', UzumWebhookView.as_view(), name='uzum_webhook'),
    path('payments/webhook/paynet/', PaynetWebhookView.as_view(), name='paynet_webhook'),
]

FastAPI Integration

  1. Set up database models:
from datetime import datetime, timezone

from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime

from tolov.integrations.fastapi import Base as PaymentsBase
from tolov.integrations.fastapi.models import run_migrations


# Create database engine
SQLALCHEMY_DATABASE_URL = "sqlite:///./payments.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)

# Create base declarative class
Base = declarative_base()

# Create Order model
class Order(Base):
    __tablename__ = "orders"

    id = Column(Integer, primary_key=True, index=True)
    product_name = Column(String, index=True)
    amount = Column(Float)
    status = Column(String, default="pending")
    created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))

# Create payment tables using run_migrations
run_migrations(engine)

# Create Order table
Base.metadata.create_all(bind=engine)

# Create session
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
  1. Create webhook handlers:
from fastapi import FastAPI, Request, Depends

from sqlalchemy.orm import Session

from tolov.integrations.fastapi import PaymeWebhookHandler, ClickWebhookHandler


app = FastAPI()

# Dependency to get the database session
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

class CustomPaymeWebhookHandler(PaymeWebhookHandler):
    def successfully_payment(self, params, transaction):
        # Handle successful payment
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "paid"
        self.db.commit()

    def cancelled_payment(self, params, transaction):
        # Handle cancelled payment
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "cancelled"
        self.db.commit()

class CustomClickWebhookHandler(ClickWebhookHandler):
    def successfully_payment(self, params, transaction):
        # Handle successful payment
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "paid"
        self.db.commit()

    def cancelled_payment(self, params, transaction):
        # Handle cancelled payment
        order = self.db.query(Order).filter(Order.id == transaction.account_id).first()
        order.status = "cancelled"
        self.db.commit()

@app.post("/payments/payme/webhook")
async def payme_webhook(request: Request, db: Session = Depends(get_db)):
    handler = CustomPaymeWebhookHandler(
        db=db,
        payme_id="your_merchant_id",
        payme_key="your_merchant_key",
        account_model=Order,
        account_field='id',
        amount_field='amount'
    )
    return await handler.handle_webhook(request)

@app.post("/payments/click/webhook")
async def click_webhook(request: Request, db: Session = Depends(get_db)):
    handler = CustomClickWebhookHandler(
        db=db,
        service_id="your_service_id",
        secret_key="your_secret_key",
        account_model=Order,
        account_field='id',
        one_time_payment=True
    )
    return await handler.handle_webhook(request)

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

tolov-1.0.0.tar.gz (55.9 kB view details)

Uploaded Source

Built Distribution

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

tolov-1.0.0-py3-none-any.whl (77.3 kB view details)

Uploaded Python 3

File details

Details for the file tolov-1.0.0.tar.gz.

File metadata

  • Download URL: tolov-1.0.0.tar.gz
  • Upload date:
  • Size: 55.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.19

File hashes

Hashes for tolov-1.0.0.tar.gz
Algorithm Hash digest
SHA256 40a2157b278f0e0b79694038d3f91c9706a0f1a50db4e6af39122c184661ac6b
MD5 b307cc109b69e107c5075fd09008b73e
BLAKE2b-256 a29298cc983f4f041c749e33f5c35641a335b5438f0c5a89b77fb61900feec5f

See more details on using hashes here.

File details

Details for the file tolov-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: tolov-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 77.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.19

File hashes

Hashes for tolov-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fb538833fe372276f9a5d833a8d0bdc90d007db2c75e075e2df83a4218cfe215
MD5 13e5323e385fa1a37cb1cd09ab454571
BLAKE2b-256 f3cf99557f750bdee16cd9608ec924ebfbd8d3b1b072057e1e255c8f931f4995

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