Skip to main content

A production-oriented Python SDK for common EPay-style payment gateways

Project description

epay-sdk

A production-oriented Python SDK for common EPay-style payment gateways.

The project focuses on the full payment lifecycle rather than only assembling order parameters.

Highlights

  • MD5 signing and signature verification
  • Create-order URL generation and raw gateway submission
  • Order query requests and response parsing
  • Callback signature verification
  • Optional query reconciliation after callbacks
  • Atomic, idempotent repository protocol
  • Optional SQLAlchemy repository implementation
  • Decimal-based amount normalization
  • Library-friendly logging defaults
  • Django integration template that keeps framework code outside the core package

Installation

uv (recommended)

Core package:

uv add epay-sdk

With the optional SQLAlchemy integration:

uv add 'epay-sdk[sqlalchemy]'

With the Django example dependencies:

uv add 'epay-sdk[django]'

For local development in this repository:

uv sync --extra dev

pip fallback

If you are not using uv, the package is also available on PyPI:

pip install epay-sdk

Quick start

from epay_sdk import EPayClient, EPayConfig, PayType, PaymentService

client = EPayClient(
    EPayConfig(
        pid="1001",
        key="your_secret_key",
        base_url="https://your-epay.com",
        environment="production",
    )
)
service = PaymentService(client)

pay_url = service.create_order(
    pay_type=PayType.ALIPAY,
    order_no="ORDER_10001",
    amount="9.90",
    subject="Membership top-up",
    notify_url="https://api.example.com/pay/notify",
    return_url="https://www.example.com/pay/success",
)
print(pay_url)

PaymentService also keeps convenience helpers:

  • create_alipay_order(...)
  • create_wxpay_order(...)
  • create_qqpay_order(...)

Choosing an order-creation API

  • PaymentService.create_order(...) — the recommended business-facing entry point. It normalizes the amount and returns a ready-to-use payment URL.
  • EPayClient.create_order_url(CreateOrderRequest) — lower level; use it when you already build CreateOrderRequest yourself.
  • EPayClient.create_order(CreateOrderRequest) — sends the request to the gateway and returns the raw upstream response body.

If your use case is simply “generate a payment URL”, prefer PaymentService.create_order(...) or one of its convenience helpers.

Recommended callback flow

A production-friendly callback flow looks like this:

  1. Receive the gateway callback form payload.
  2. Call client.verify_callback() to verify the signature and validate pid, sign_type, required fields, and amount format.
  3. Call CallbackProcessor.process() to load the local order and validate the amount.
  4. If verify_with_query=True, the processor performs query_order() + parse_query_result() for upstream reconciliation.
  5. Persist the paid transition through mark_paid_if_unpaid().
  6. Record callback audit stages through record_callback_attempt(..., stage=...).
  7. Let the application decide whether to commit or roll back the surrounding transaction.
  8. Return success for accepted callbacks and fail for rejected/retryable callbacks.

Current callback audit stages:

  • RECEIVED
  • RECONCILED
  • APPLIED
  • REJECTED

Repository contract

You are expected to implement your own order repository, but it should satisfy this protocol:

class OrderRepository(Protocol):
    def get_order(self, order_no: str) -> Optional[OrderRecord]: ...
    def mark_paid_if_unpaid(self, order_no: str, gateway_trade_no: str, raw_payload: str) -> bool: ...
    def record_callback_attempt(
        self,
        order_no: str,
        accepted: bool,
        reason: Optional[str],
        raw_payload: str,
        *,
        stage: str,
    ) -> None: ...

Important behavior notes:

  • mark_paid_if_unpaid() must be atomic, typically via a conditional database update.
  • record_callback_attempt() must persist the stage field — it is part of the public contract.
  • If your repository supports transaction control, it may additionally expose commit() / rollback().
  • CallbackProcessor.process() defaults to not committing or rolling back external transactions.
  • If you explicitly want the processor to manage the repository transaction boundary, use CallbackProcessor(..., manage_transaction=True).

Optional SQLAlchemy integration

Install the SQLAlchemy extra with:

uv add 'epay-sdk[sqlalchemy]'

The top-level package lazily exposes these symbols when SQLAlchemy is installed:

  • Base
  • PaymentOrderModel
  • CallbackAuditModel
  • SQLAlchemyOrderRepository
  • create_sqlite_engine

Example:

from sqlalchemy.orm import Session
from epay_sdk import Base, PaymentOrderModel, SQLAlchemyOrderRepository, create_sqlite_engine

engine = create_sqlite_engine("sqlite:///./epay.db")
Base.metadata.create_all(engine)

with Session(engine) as session:
    session.add(PaymentOrderModel(order_no="A001", amount="9.90", status="UNPAID"))
    session.commit()

    repo = SQLAlchemyOrderRepository(session)
    if repo.mark_paid_if_unpaid("A001", "G100", '{"trade_no":"G100"}'):
        repo.record_callback_attempt("A001", True, None, '{"trade_no":"G100"}', stage="APPLIED")
        repo.commit()

If SQLAlchemy is not installed, import epay_sdk still works. Only accessing SQLAlchemy-specific exports requires the extra.

Django integration template

The Django strategy is intentionally app-layer oriented:

  • src/epay_sdk/ stays framework-agnostic
  • Django models, repositories, views, URLs, and transaction boundaries live in the Django app layer
  • the repository ships examples/django_app/ as a ready-to-copy integration template

This keeps the SDK generic while still providing a concrete Django reference implementation.

Install the example dependency set with:

uv add 'epay-sdk[django]'

Or, when working from this repository:

uv sync --extra django

Django transaction pattern

For Django, the recommended transaction pattern is:

  • keep CallbackProcessor(..., manage_transaction=False) (the default)
  • wrap callback processing in transaction.atomic()
  • let Django own the commit/rollback behavior

Example:

from django.db import transaction

with transaction.atomic():
    callback = client.verify_callback(request.POST.dict())
    repo = DjangoOrderRepository()
    processor = CallbackProcessor(client, repo, verify_with_query=True)
    result = processor.process(callback)
return HttpResponse(result.response_text, content_type="text/plain")

Mapping the repository contract in Django

In Django terms, the repository usually maps like this:

  • get_order(order_no) — load a Django model and map it to OrderRecord
  • mark_paid_if_unpaid(order_no, gateway_trade_no, raw_payload) — use a single conditional update
  • record_callback_attempt(...) — write a callback audit row

The most important part is keeping mark_paid_if_unpaid() atomic. In Django, that typically means:

updated = PaymentOrder.objects.filter(
    order_no=order_no,
    status="UNPAID",
).update(
    status="PAID",
    gateway_trade_no=gateway_trade_no,
    raw_payload=raw_payload,
    paid_at=timezone.now(),
)
first_success = updated == 1

That mirrors the core SDK requirement that only UNPAID -> PAID is allowed for the idempotent paid transition.

What the Django example includes

examples/django_app/ currently shows:

  • Django model definitions
  • a DjangoOrderRepository implementation via duck typing
  • an order-creation view using PaymentService.create_order(...)
  • callback handling through client.verify_callback(...) + CallbackProcessor.process(...)
  • transaction.atomic() for transaction control
  • app/project URL wiring and Django tests

Security and runtime notes

  • The signing protocol follows the common EPay convention: sorted non-empty parameters plus the merchant key, hashed with MD5. This is for gateway compatibility, not a modern standalone transport-security mechanism.
  • Use HTTPS in production and keep verify_ssl=True. The SDK rejects verify_ssl=False when environment="production".
  • SQLite is fine for local development and examples, but production callback processing is better served by PostgreSQL/MySQL or another database with stronger concurrency semantics.
  • Always verify callbacks before processing them.
  • The environment field is currently a safety/configuration signal. It influences validation and warnings, but does not automatically switch gateway endpoints.

Examples

  • examples/flask_app.py — minimal Flask integration with an in-memory repository
  • examples/fastapi_app.py — FastAPI + SQLAlchemy integration with blocking work pushed into the thread pool
  • examples/django_app/ — Django integration template that keeps framework-specific code outside the core package

Development

Run the test suite with uv:

uv run pytest tests
uv run python examples/django_app/manage.py test payments

Build release artifacts with:

uv build

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

epay_sdk-0.4.1.tar.gz (22.7 kB view details)

Uploaded Source

Built Distribution

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

epay_sdk-0.4.1-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file epay_sdk-0.4.1.tar.gz.

File metadata

  • Download URL: epay_sdk-0.4.1.tar.gz
  • Upload date:
  • Size: 22.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for epay_sdk-0.4.1.tar.gz
Algorithm Hash digest
SHA256 1c14f3630d47ceada1d0b4efe04d99d9d05d5e7403f741f3f7c412ba456d0dd4
MD5 78f9f88f492e24d50101412358cb3743
BLAKE2b-256 f1ff202da4c3b398d6239962dd6675d555f088f5ab22bb98d74a8464c8d9ed94

See more details on using hashes here.

File details

Details for the file epay_sdk-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: epay_sdk-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 16.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for epay_sdk-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 6dfed073be3bc86a6070606242049e9d8aaf1bbf7d4a2ecc0da5fd91d76f801a
MD5 660da428a40a7010ed3ff03b65fe1877
BLAKE2b-256 8fbd74d3dd81c4282719fc8de2190fc6fffcd732bc36a71134a2418384c27a86

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