تقریباً هر پروژهی Django در ایران دیر یا زود به درگاه پرداخت میرسد، و هر درگاه ساز خودش را میزند: زرینپال و زیبال REST/JSON هستند، ملت SOAP است و کاربر را فقط با فرم POST میپذیرد، و ملت بعد از تأیید یک مرحلهی settle هم دارد. django-iranian-payment پکیج متنبازی است که برای همین کار نوشتهام. این نوشته طرز استفاده از آن را مرور میکند و همهی جزئیاتش از README خود پکیج آمده است.
دو لایه: اپ آماده یا هستهی بدون state
اولین تصمیم، انتخاب لایه است:
- لایهی ساده (اپ Django): اپ اختیاری
django_iranian_payment.contrib.djangoخودش رکورد پرداخت را میسازد،authorityرا ذخیره میکند، تأیید را انجام میدهد و وضعیت تراکنش را نگه میدارد. کد شما تقریباً به شروع پرداخت محدود میشود. - لایهی هسته: درگاههایی بدون state که در هر محیطی کار میکنند، حتی بیرون از Django یا در کد async. ذخیرهی
authorityو مدیریت تأیید با خود شماست.
پیشنهاد من لایهی ساده است، چون چند دردسر محیط عملیاتی را خودش حل میکند. view بازگشت از بانک csrf_exempt است، چون ملت و سامان با POST و بدون توکن CSRF برمیگردند. رسید هر پرداخت پیش از verify در فیلد یکتای receipt_key ثبت میشود تا رسید یک خرید روی سفارش دیگری پذیرفته نشود؛ سامان و ملت هر دو verify تکراری را موفق اعلام میکنند. اگر هم درگاه هنگام verify در دسترس نباشد، رکورد گم نمیشود. لایهی هسته برای وقتی است که مدل و migration تازه نمیخواهید یا بیرون از Django کار میکنید؛ آنوقت این مراقبتها با خود شماست و README نمونهی کاملش را در scripts/django_custom_db.py گذاشته است.
کدام درگاهها آمادهاند
قاعدهی پروژه ساده است: هیچ درگاهی پیش از یک تراکنش واقعیِ موفق وارد registry عمومی نمیشود. این چهار درگاه با get_gateway("slug") در دسترساند:
| درگاه | پروتکل | هدایت کاربر | تستشده با | sandbox |
|---|---|---|---|---|
| زرینپال | REST/JSON | redirect | sandbox و تراکنش واقعی | دارد (URL جدا) |
| زیبال | REST/JSON | — | sandbox و تراکنش واقعی | با merchant="zibal" |
| ملت | SOAP | فرم POST | تراکنش live روی محیط عملیاتی | ندارد؛ sandbox=True خطا میدهد |
| سامان (SEP) | REST/JSON | فرم POST | تراکنش واقعی با ترمینال واقعی | ندارد؛ sandbox=True خطا میدهد |
ایرانکیش، نکستپی، سداد و دیجیپی در core.experimental هستند: از روی مستند رسمی کامل پیادهسازی و با تست خودکار (InMemoryTransport) پوشش داده شدهاند، اما هنوز روی sandbox یا ترمینال واقعی تست نشدهاند. برای همین با get_gateway در دسترس نیستند و فقط با import صریح کار میکنند. ملت و سامان هم پیشتر همینجا بودند. Pay.ir و IDPay هم از دسترس خارج شدهاند.
نصب
pip install django-iranian-payment
# ملت (SOAP، با zeep):
pip install "django-iranian-payment[soap]"
# ایرانکیش (تجربی، AES+RSA):
pip install "django-iranian-payment[irankish]"
# سداد (تجربی، 3DES):
pip install "django-iranian-payment[sadad]"
ملت به extra [soap] نیاز دارد که zeep را نصب میکند. برای لایهی ساده، اپ و مسیرهایش را اضافه کنید و migrate بزنید:
INSTALLED_APPS = [
# ...
"django_iranian_payment.contrib.django",
]
# urls.py
from django.urls import path, include
urlpatterns = [
# ...
path("payment/", include("django_iranian_payment.contrib.django.urls")),
]
python manage.py migrate
تنظیمات
همهی تنظیمات در دیکشنری IRANIAN_PAYMENT در settings.py است:
IRANIAN_PAYMENT = {
"sandbox": False, # پیشفرض سراسری
"currency": "rial", # یا "toman"
"return_url": "/payment/result/", # اختیاری
"gateways": {
"zarinpal": {"merchant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"sandbox": True},
"zibal": {"merchant": "zibal"},
"mellat": {
"terminal_id": "1234567",
"username": "your-username",
"password": "your-password",
"settle_mode": "verify_settle", # یا "verify_only"
},
},
}
ریال یا تومان
بانک همیشه ریال میگیرد و amount_to_send، مبلغی که به درگاه و verify میرود، همیشه ریال است. IRANIAN_PAYMENT["currency"] فقط واحد ورودی شما را تعیین میکند: پیشفرض "rial" است و تبدیلی انجام نمیشود؛ با "toman" پکیج یکبار، همان ابتدای کار، مبلغ را به ریال میبرد (هر تومان 10 ریال). این تنظیم هم در services.start_payment اعمال میشود و هم در get_gateway(...).initiate(...)، و اگر currency را روی خود PaymentRequest بدهید، بر تنظیم سراسری مقدم است. مبالغ ذخیرهشده در مدل Payment و خروجی verify همیشه ریالاند؛ برای نمایش تومانی، خودتان بر 10 تقسیم کنید.
جریان پرداخت: ساخت، هدایت، callback، تأیید
ساخت پرداخت و هدایت کاربر
from django.shortcuts import redirect
from django_iranian_payment.contrib.django import services
def start_payment(request, order):
payment, redirect_url = services.start_payment(
"zarinpal",
amount=order.amount,
# به بانک میرود: مسیر callback خود پکیج
callback_url="https://yoursite.com/payment/callback/zarinpal/",
# کاربر پس از verify به اینجا میرود
return_url="https://yoursite.com/orders/result/",
order_id=str(order.id),
description="پرداخت سفارش",
)
return redirect(redirect_url)
callback_url به بانک میرود و باید مسیر callback خود پکیج باشد؛ return_url صفحهی نتیجهی سایت شماست و میشود آن را یکبار در IRANIAN_PAYMENT["return_url"] گذاشت.
ملت و سامان redirect ساده نمیپذیرند و فرم POST میخواهند. برای این دو، کاربر را بهجای redirect_url به reverse("iranian_payment:go-to-gateway", kwargs={"payment_id": payment.id}) بفرستید تا view go_to_gateway فرم auto-submit را بسازد.
callback و verify
با مسیرهای پکیج، برای callback هیچ viewای لازم نیست: بانک به /payment/callback/zarinpal/ برمیگردد، پکیج تأیید میکند و کاربر را با پارامتر payment_status (success، failed یا pending) به return_url میفرستد. اگر منطق خودتان را میخواهید، services.verify_payment(slug, authority) را مستقیم صدا بزنید. رکورد پرداخت این وضعیتها را طی میکند:
WAITING → REDIRECT_TO_BANK → RETURN_FROM_BANK → COMPLETE / FAILED / CANCEL_BY_USER
اگر درگاه هنگام verify جواب ندهد، رکورد در RETURN_FROM_BANK میماند و کاربر با payment_status=pending برمیگردد؛ ممکن است پول از حساب خریدار کم شده ولی هنوز تأیید نشده باشد. این دو فراخوانی را در cron یا Celery بگذارید:
from django_iranian_payment.contrib.django import services
services.reverify_pending()
services.expire_stale(older_than_minutes=30)
همین جریان با لایهی هسته
در لایهی هسته، authority و مبلغ ارسالی را خودتان ذخیره میکنید:
from django_iranian_payment import get_gateway, PaymentRequest
def start_payment(request, order):
gw = get_gateway("zarinpal")
result = gw.initiate(PaymentRequest(
amount=order.amount,
callback_url="https://yoursite.com/verify/",
order_id=str(order.id),
))
order.authority = result.authority
order.amount_sent = result.amount_to_send # برای verify ذخیره کن
order.save()
return redirect(result.redirect_url)
def verify_payment(request):
authority = request.GET.get("Authority")
order = Order.objects.get(authority=authority)
gw = get_gateway("zarinpal")
result = gw.verify(
authority=authority,
amount=order.amount_sent,
order_id=str(order.id),
)
if result.is_success:
order.mark_paid(result.reference_id)
return redirect("/success/")
return redirect("/failure/")
amount_to_send را ذخیره کنید، نه amount: زرینپال مبلغ را در callback برنمیگرداند و verify همان مبلغ ارسالی را میخواهد، که اگر کارمزد (FeeConfig) بر عهدهی مشتری باشد با مبلغ پایه فرق دارد. درگاههای شاپرکی مثل ملت و سامان دادهی بیشتری از callbackِ POST بانک را هم از راه پارامتر extra میخواهند؛ لایهی Django این مقادیر را خودش استخراج میکند.
ملت: دو مرحله و settle_mode
ملت دومرحلهای است: بعد از verify، پول باید settle هم بشود. حالت پیشفرض، settle_mode="verify_settle"، هر دو را اتمیک انجام میدهد و توصیهی من هم همین است. برای واریز با تأخیر، "settle_mode": "verify_only" را در config ملت بگذارید و بعد از verify موفق خودتان settle() را صدا بزنید؛ اگر settle هیچوقت نرسد، بانک ظرف سه ساعت تراکنش را خودکار برمیگرداند (Autoreversal) و پول به خریدار برمیگردد. پکیج sale_order_id و sale_reference_id را در فیلد raw مدل Payment نگه میدارد تا برای settle یا reverse بعدی آماده باشند. در لایهی هسته، مقادیر callback ملت را با extra بدهید:
gw.verify(authority=ref_id, amount=amount, order_id=oid,
extra={"sale_reference_id": "...", "sale_order_id": "...",
"card_number": "...", "res_code": "0"})
اگر کاربر پرداخت را لغو کند، ملت ResCode=17 برمیگرداند؛ آن را با کلید res_code بدهید تا verify بدون تماس SOAP نتیجهی لغوشده برگرداند.
sandbox
sandbox برای هر درگاه جداگانه تعیین میشود، پس میشود همزمان یک درگاه را روی sandbox و دیگری را live داشت. ترتیب اولویت:
get_gateway(..., sandbox=...) > config درگاه > "sandbox" سراسری > False
- زرینپال URL سندباکس جدا دارد و از این فلگ پیروی میکند.
- زیبال با
merchant="zibal"وارد sandbox میشود و فلگsandboxبرایش بیاثر است. - ملت و سامان sandbox واقعی ندارند و
sandbox=Trueرا باGatewayConfigurationErrorرد میکنند. اگرsandboxسراسری راTrueگذاشتهاید، برای این دو صریحاً"sandbox": Falseبگذارید.
با اپ django_iranian_payment.contrib.django، یک Django system check با خطای iranian_payment.E001 جلوی اجرای manage.py را میگیرد تا بیخبر به live وصل نشوید. بدون اپ کامل، django_iranian_payment.contrib.guard را اضافه کنید که بدون مدل و migration فقط همین چک را انجام میدهد.
چند نکتهی آخر
- ارتقا به نسخهی
1.3.0بهpython manage.py migrateنیاز دارد (migrationهای0002و0003). - برای هر درگاه یک راهنمای گامبهگام در پوشهی
docs/gateways/مخزن هست.
پکیج روی PyPI و کدش روی GitHub است. اگر یکی از درگاههای تجربی را با ترمینال واقعی امتحان کردید، خبرم کنید؛ همین تراکنش واقعی است که درگاه را به registry عمومی میبرد.