Skip to main content

django-iranian-payment

درگاه‌های پرداخت ایرانی برای Django، با معماری دو لایه:

  • لایه‌ی ساده (توصیه‌شده): یک اپ Django اختیاری که خودش رکورد پرداخت، ذخیره‌ی authority، تأیید، و مدیریت وضعیت تراکنش را انجام می‌دهد. کاربر تقریباً هیچ منطقی نمی‌نویسد.
  • لایه‌ی هسته (پیشرفته): درگاه‌های بدون state که در هر محیطی (حتی غیر Django یا async) قابل استفاده‌اند. کاربر خودش authority را ذخیره و تأیید را مدیریت می‌کند.

واحد بانک همیشه ریال است، اما می‌توانی واحد ورودی خودت را به تومان تغییر دهی (IRANIAN_PAYMENT["currency"] = "toman")؛ پکیج خودکار به ریال تبدیل می‌کند. بخش واحد پول را ببین.

درگاه‌های آماده و تست‌شده (registry عمومی)

این درگاه‌ها راستی‌آزمایی شده و با get_gateway("slug") در دسترس‌اند:

درگاه نوع وضعیت وابستگی اختیاری
زرین‌پال REST/JSON ✅ sandbox و تراکنش واقعی تست‌شده —
زیبال REST/JSON ✅ sandbox و تراکنش واقعی تست‌شده —
ملت SOAP ✅ تراکنش live واقعی تست‌شده (sandbox ندارد) [soap] (zeep)
سامان (SEP) REST/JSON ✅ تراکنش واقعی با ترمینال واقعی تست‌شده (sandbox ندارد) —

✅ هر چهار درگاه با تراکنش واقعی تأیید شده‌اند. زرین‌پال/زیبال هم در sandbox و هم با تراکنش واقعی تست شدند. ملت روی محیط عملیاتی (bpm.shaparak.ir) تست شد: تراکنش موفق (ResCode=0، SaleReferenceId، CardHolderPan، FinalAmount) و سناریوی کنسل کاربر (ResCode=17) هر دو تأیید شدند. سامان با تراکنش واقعی روی ترمینال واقعی تست و در نسخه‌ی 1.0.0 عمومی شد.

⛔ سامان و ملت sandbox واقعی ندارند و sandbox=True را با خطا رد می‌کنند (جزئیات). فقط live‌اند.

⚠️ ملت نیاز به zeep دارد: pip install "django-iranian-payment[soap]". ملت دومرحله‌ای است؛ حالت پیش‌فرض verify_settle (تأیید+واریز اتمیک) توصیه می‌شود. برای واریز با تأخیر، settle_mode="verify_only" را در config بگذار و خودت settle() را صدا بزن (وگرنه بانک در ۳ ساعت Autoreversal می‌زند). هدایت کاربر به ملت با فرم POST است (نه redirect ساده)؛ لایه‌ی Django این فرم auto-submit را در view go_to_gateway خودکار می‌سازد.

درگاه‌های تجربی (در core.experimental)

این درگاه‌ها پیاده‌سازی کامل از مستند رسمی دارند و منطقشان با تست خودکار (InMemoryTransport) پوشش داده شده، اما هنوز هیچ تست sandbox یا ترمینال واقعی روی آن‌ها انجام نشده — برخلاف زرین‌پال/زیبال که حداقل sandbox دارند، این درگاه‌ها به‌دلیل محدودیت‌های دسترسی (نیاز به قرارداد پذیرندگی، ثبت IP، کلید واقعی) حتی تست script سندباکسشان هم انجام نشده است.

طبق قانون طلایی پروژه، تا تست واقعی موفق در registry عمومی قرار نمی‌گیرند و با get_gateway در دسترس نیستند؛ فقط با import صریح:

from django_iranian_payment.core.experimental.irankish import IrankishGateway
from django_iranian_payment.core.experimental.nextpay import NextPayGateway
from django_iranian_payment.core.experimental.sadad import SadadGateway
from django_iranian_payment.core.experimental.digipay import DigipayGateway
درگاه نوع رمزنگاری وابستگی اختیاری
ایران‌کیش REST/JSON AES + RSA [irankish]
نکست‌پی REST/JSON — —
سداد REST/JSON (WebApi) 3DES [sadad]
دیجی‌پی REST/JSON (OAuth2) — —

ℹ️ ملت و سامان پیش‌تر تجربی بودند؛ پس از تست تراکنش واقعی به registry عمومی منتقل شدند (بالاتر را ببین). دیگر از core.experimental import نمی‌شوند و مستقیم با get_gateway("mellat") / get_gateway("saman") در دسترس‌اند.

ایران‌کیش و سداد به وابستگی رمزنگاری نیاز دارند: pip install "django-iranian-payment[irankish]" (شامل pycryptodome و rsa) یا pip install "django-iranian-payment[sadad]" (شامل pycryptodome).

⚠️ سداد همان درگاهی است که از سایت بانک ملی به آن هدایت می‌شوید؛ بانک ملی درگاه مستقل خود را به سداد سپرده است.

⚠️ دیجی‌پی برخلاف بقیه احراز هویت دومرحله‌ای OAuth2 دارد: config آن پنج فیلد اجباری می‌خواهد (username, password, client_id, client_secret, provider_id). در verify به trackingCode (که در نتیجه‌ی پرداخت/callback برمی‌گردد) از طریق extra نیاز دارد. اگر پرداخت موفق را verify نکنی، پس از مدتی خودکار لغو و وجه مرجوع می‌شود.

درگاه‌های از کار افتاده (دیگر سرویس نمی‌دهند)

❌ پی‌آی‌آر (Pay.ir) و آیدی‌پی (IDPay) کلاً از دسترس خارج شده‌اند و دیگر سرویس‌دهی نمی‌کنند. کدشان در core.experimental به‌عنوان آرشیو باقی مانده ولی برای استفاده توصیه نمی‌شود و get_gateway("pay_ir") / get_gateway("idpay") کار نمی‌کند. تنها در صورتی که این سرویس‌ها روزی بازگردند، با import صریح در دسترس‌اند: from django_iranian_payment.core.experimental import PayIrGateway, IDPayGateway

نصب

pip install django-iranian-payment
# درگاه‌های SOAP (ملت و سایر درگاه‌های SOAP):
pip install "django-iranian-payment[soap]"
# درگاه ایران‌کیش (AES+RSA):
pip install "django-iranian-payment[irankish]"
# درگاه سداد (3DES):
pip install "django-iranian-payment[sadad]"

تنظیمات

در settings.py:

IRANIAN_PAYMENT = {
    "sandbox": False,    # پیش‌فرض سراسری
    "currency": "rial",  # واحد ورودی مبلغ: "rial" (پیش‌فرض) یا "toman"
    "return_url": "/payment/result/",  # اختیاری (روش ۱): صفحه‌ی نتیجه پس از verify
    "gateways": {
        "zarinpal": {"merchant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                     "sandbox": True},   # sandbox مجزای همین درگاه
        "zibal": {"merchant": "zibal"},  # زیبال sandbox را با merchant کنترل می‌کند
        "mellat": {
            "terminal_id": "1234567",
            "username": "your-username",
            "password": "your-password",
            "settle_mode": "verify_settle",  # یا "verify_only"
            # بدون "sandbox" → از پیش‌فرض سراسری (False = live) پیروی می‌کند
        },
    },
}

واحد پول (ریال یا تومان)

بانک‌های ایرانی همیشه با ریال کار می‌کنند؛ این واحد بانکِ پکیج است و amount_to_send که به درگاه و verify می‌رود همیشه ریال است. اما می‌توانی واحدی که خودت ورودی می‌دهی را با IRANIAN_PAYMENT["currency"] به‌صورت سراسری انتخاب کنی:

IRANIAN_PAYMENT = {
    "currency": "toman",   # حالا amount ها را به تومان می‌دهی
    "gateways": {...},
}

# با تنظیم بالا (هر دو مسیر یکسان عمل می‌کنند):
services.start_payment("zarinpal", amount=15_000, ...)   # ۱۵۰۰۰ تومان → بانک ۱۵۰۰۰۰ ریال
# مسیر toolkit هم واحد سراسری را رعایت می‌کند (get_gateway آن را تزریق می‌کند):
get_gateway("zarinpal").initiate(PaymentRequest(amount=15_000, ...))  # هم ۱۵۰۰۰۰ ریال
  • پیش‌فرض "rial" است (سازگار با قبل؛ هیچ تبدیلی انجام نمی‌شود).
  • واحد سراسری در هر دو مسیر اعمال می‌شود: هم start_payment، هم get_gateway(...).initiate(PaymentRequest(...)). در مسیر toolkit، get_gateway واحد سراسری را در درخواستی که currency مشخص نکرده تزریق می‌کند. اگر currency را روی خود PaymentRequest بدهی، بر تنظیم سراسری اولویت دارد.
  • تبدیل ۱ تومان = ۱۰ ریال فقط یک‌بار و در همان ابتدای کار انجام می‌شود.
  • مبالغ ذخیره‌شده در مدل Payment و مقادیر بازگشتی verify همیشه ریال‌اند (واحد بانک)؛ currency فقط واحد ورودی را تعیین می‌کند. برای نمایش به کاربر در تومان، خودت بر ۱۰ تقسیم کن.
  • کارمزد: rate_bps واحد‌مستقل است؛ ولی fixed و max_fee در همان واحد ورودی تفسیر و خودکار به ریال تبدیل می‌شوند.
  • در لایه‌ی هسته‌ی خالص (بدون Django و بدون get_gateway) واحد را روی خود درخواست بده: PaymentRequest(amount=15_000, currency="toman", ...). برای خواندن مقدار سراسری از settings هم from django_iranian_payment import get_default_currency هست.

sandbox مجزای هر درگاه

sandbox هر درگاه جداگانه تعیین می‌شود. کلید "sandbox" داخل config همان درگاه بر مقدار سراسری اولویت دارد، پس می‌توانی یک درگاه را sandbox و درگاه دیگری را live داشته باشی هم‌زمان. اولویت کامل:

get_gateway(..., sandbox=...)  >  config درگاه  >  "sandbox" سراسری  >  False

فقط درگاه‌هایی که URL سندباکس جدا دارند (زرین‌پال، دیجی‌پی) به این فلگ واکنش می‌دهند. زیبال با مقدار merchant="zibal" و ایران‌کیش/نکست‌پی/سداد اصلاً URL سندباکس جدا ندارند (فلگ sandbox برایشان بی‌اثر است).

⛔ سامان و ملت sandbox واقعی ندارند و sandbox=True را رد می‌کنند (خطای GatewayConfigurationError). اگر sandbox سراسری را True گذاشته‌ای، برای این دو صریحاً "sandbox": False بگذار.

اگر اپ django_iranian_payment.contrib.django در INSTALLED_APPS باشد، این تناقض با یک Django system check در startup گرفته می‌شود: manage.py runserver (و هر manage.py) با خطای iranian_payment.E001 اجرا نمی‌شود تا اصلاحش کنی — نه اینکه بی‌صدا به live وصل شوی.

حالت مدیریت دستی DB (اپ کامل را نصب نمی‌کنی): این system check اجرا نمی‌شود و runserver بی‌صدا بالا می‌آید (خطا فقط هنگام اولین get_gateway). برای گارد startup اپ سبک زیر را اضافه کن — بدون model و migration، فقط همین چک:

INSTALLED_APPS = [
    # ...
    "django_iranian_payment.contrib.guard",
]

راهنمای کامل هر درگاه

برای هر درگاه یک راهنمای گام‌به‌گام و خودکفا (هر دو حالت مدیریت دیتابیس) در docs/gateways/ هست: زرین‌پال · زیبال · ملت · سامان · ایران‌کیش · نکست‌پی · سداد · دیجی‌پی.

توجه: درگاه‌های تجربی (ایران‌کیش، نکست‌پی، سداد، دیجی‌پی) در registry عمومی نیستند. برای استفاده‌شان با get_gateway و لایه‌ی Django باید یک‌بار صریحاً register شوند (روش بالا را ببین). درگاه‌های registry عمومی (زرین‌پال، زیبال، ملت، سامان) به این ثبت نیاز ندارند و مستقیم با get_gateway("slug") در دسترس‌اند.

برای استفاده از لایه‌ی ساده (مدل و ردیابی خودکار)، اپ را هم اضافه کن:

INSTALLED_APPS = [
    # ...
    "django_iranian_payment.contrib.django",
]

سپس migrate:

python manage.py migrate

و مسیرهای داخلی را در urls.py پروژه اضافه کن:

from django.urls import path, include

urlpatterns = [
    # ...
    path("payment/", include("django_iranian_payment.contrib.django.urls")),
]

روش ۱: لایه‌ی ساده (توصیه‌شده)

پکیج خودش رکورد می‌سازد، authority را ذخیره می‌کند، و تأیید را انجام می‌دهد.

شروع پرداخت

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 خود پکیج (نه view خودت)
        callback_url="https://yoursite.com/payment/callback/zarinpal/",
        # کاربر پس از verify به اینجا می‌رود (صفحه‌ی نتیجه‌ی خودت)
        return_url="https://yoursite.com/orders/result/",
        order_id=str(order.id),
        description="پرداخت سفارش",
        mobile="09120000000",                     # اختیاری
    )
    # نیازی به ذخیره‌ی authority نیست؛ پکیج خودش نگه می‌دارد.
    # ملت و سامان فرم POST می‌خواهند: به‌جای redirect_url، کاربر را به
    # reverse("iranian_payment:go-to-gateway", kwargs={"payment_id": payment.id}) بفرست.
    return redirect(redirect_url)

بازگشت از بانک (callback)

اگر مسیرهای داخلی را در urls.py اضافه کرده باشی، هیچ view ای برای callback لازم نیست. بانک به callback_url (مسیر /payment/callback/zarinpal/ پکیج) برمی‌گردد، پکیج خودش تأیید می‌کند و کاربر را به return_url با پارامتر payment_status (success، failed یا pending) هدایت می‌کند:

https://yoursite.com/orders/result/?payment_status=success&order_id=123
  • callback_url و return_url دو آدرس جدا هستند: اولی به بانک می‌رود و باید مسیر callback پکیج باشد؛ دومی صفحه‌ی نتیجه‌ی سایت توست. (تا نسخه‌ی 1.2.4 کاربر پس از verify به callback_url — یعنی دوباره به خود callback — برگردانده می‌شد و ۴۰۴ می‌گرفت.)
  • به‌جای return_url در هر فراخوانی می‌توانی یک‌بار IRANIAN_PAYMENT["return_url"] بگذاری. اگر هیچ‌کدام نباشد، صفحه‌ی نتیجه‌ی ساده‌ی پکیج نمایش داده می‌شود (iranian_payment/result.html؛ در templates/ پروژه override‌پذیر است).
  • view callback پکیج csrf_exempt است، چون ملت و سامان با POST از دامنه‌ی بانک برمی‌گردند و توکن CSRF ندارند. اگر callback را با view خودت می‌نویسی (روش ۲)، آن را هم @csrf_exempt کن، وگرنه CsrfViewMiddleware پاسخ ۴۰۳ می‌دهد.
  • template های پکیج (فرم POST ملت/سامان و صفحه‌ی نتیجه) با "APP_DIRS": True در TEMPLATES پیدا می‌شوند (پیش‌فرض پروژه‌ی Django).
  • جلوگیری از double spending: سامان یک رسید (RefNum) را هر بار که ارائه شود دوباره تأیید می‌کند و ملت verify تکراری را «قبلاً تأیید شده» = موفق برمی‌گرداند، پس خریدار می‌توانست رسید یک خرید را روی callback سفارش دیگری بفرستد. پکیج رسید را پیش از verify در فیلد یکتای receipt_key به همان پرداخت می‌دهد و callbackی را که رسیدش مال پرداخت دیگری است (یا در ملت SaleOrderId سفارش دیگری را نشان می‌دهد) بدون تماس با بانک رد می‌کند؛ این در log با سطح warning ثبت می‌شود.

اگر می‌خواهی منطق خودت را اجرا کنی، به‌جای استفاده از callback داخلی، خودت services.verify_payment(slug, authority) را صدا بزن.

تأیید مجدد تراکنش‌های ناتمام (درگاه در دسترس نبود)

گاهی کاربر از بانک برمی‌گردد ولی درگاه هنگام verify بی‌پاسخ می‌دهد یا ۵۰۰ برمی‌گرداند (بی‌ثباتی زرین‌پال و ...). پول ممکن است از کاربر کم شده باشد ولی تأیید نشده. این حالت خودکار مدیریت می‌شود:

  • در callback، پیش از تماس با بانک، رکورد به RETURN_FROM_BANK علامت می‌خورد. اگر verify با خطای شبکه شکست بخورد، رکورد در همین حالت می‌ماند (نه گم می‌شود، نه expire_stale منقضی‌اش می‌کند) و کاربر با payment_status=pending برمی‌گردد.
  • یک job دوره‌ای این رکوردها را دوباره verify می‌کند. verify دوباره امن است: زرین‌پال کد ۱۰۱ (قبلاً تأیید شده) را DUPLICATE = موفق برمی‌گرداند.
  • پاسخ قطعی بانک رکورد را پایانی می‌کند: موفق → COMPLETE، ناموفق → FAILED، لغو کاربر → CANCEL_BY_USER. فقط رکوردی که verify آن به خطای شبکه خورده RETURN_FROM_BANK می‌ماند، پس job دوره‌ای پرداخت‌های ناموفق را دوباره امتحان نمی‌کند.

در cron یا celery اجرا کن:

from django_iranian_payment.contrib.django import services

services.reverify_pending()       # رکوردهای returnedِ ناتمام را دوباره verify می‌زند
services.expire_stale(older_than_minutes=30)  # رکوردهای خیلی قدیمیِ نرفته به درگاه

reverify_pending خودش خطای در دسترس نبودن درگاه را می‌بلعد (رکورد returned می‌ماند و اجرای بعدی دوباره تلاش می‌کند)، و extra لازم برای verify درگاه‌های شاپرکی (ملت و ...) را از raw رکورد بازمی‌خواند. خطای دیگر یک رکورد (مثلاً تنظیمات درگاهش از settings حذف شده) log می‌شود و اجرا با رکورد بعدی ادامه می‌یابد. older_than_minutes را روی ۳۰+ بگذار تا رکورد معلق فرصت reverify داشته باشد پیش از انقضا.

حالت مدیریت دستی دیتابیس (روش ۲): همین منطق را باید خودت پیاده کنی — نمونه‌ی کامل (callback با payment_status=pending + reverify_pending + management command) در scripts/django_custom_db.py.

وضعیت‌های رکورد

WAITING → REDIRECT_TO_BANK → RETURN_FROM_BANK → COMPLETE / FAILED / CANCEL_BY_USER

حالت‌های پایانی: COMPLETE، FAILED (بانک verify را رد کرد)، CANCEL_BY_USER و EXPIRE_GATEWAY_TOKEN (با expire_stale). RETURN_FROM_BANK یعنی «هنوز پاسخ قطعی نگرفته» (مثلاً درگاه هنگام verify در دسترس نبود). رکورد FAILED با callback معتبرِ بعدی هنوز می‌تواند COMPLETE شود؛ فقط COMPLETE جلوی verify دوباره را می‌گیرد.


روش ۲: لایه‌ی هسته (بدون مدل، پیشرفته)

اگر نمی‌خواهی مدل و migration داشته باشی (یا خارج از Django کار می‌کنی)، مستقیم از هسته استفاده کن. در این حالت خودت باید 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),
        description="پرداخت سفارش",
    ))
    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,   # همان مبلغی که در initiate رفت (با کارمزد)
        order_id=str(order.id),
    )
    if result.is_success:
        order.mark_paid(result.reference_id)
        return redirect("/success/")
    return redirect("/failure/")

چرا amount_sent و نه amount؟ بعضی درگاه‌ها (زرین‌پال، پی‌پینگ) مبلغ را در callback برنمی‌گردانند و verify به همان مبلغ ارسالی نیاز دارد. اگر کارمزد روی مشتری اعمال شده باشد، مبلغ ارسالی با مبلغ پایه فرق دارد.

درگاه‌هایی که در verify به داده‌ی بیشتری نیاز دارند (پارامتر extra)

برخی درگاه‌های شاپرکی در verify به داده‌ای فراتر از یک authority نیاز دارند که در callbackِ POST از بانک برمی‌گردد. این داده‌ها از طریق extra پاس داده می‌شوند. اگر از لایه‌ی Django استفاده کنی، view‌های پکیج این مقادیر را خودکار از callback استخراج و پاس می‌دهند.

# ملت (registry عمومی): به sale_reference_id و sale_order_id نیاز دارد.
# اگر کاربر کنسل کرده باشد، res_code از callback (=17) را هم بده تا بدون
# تماس SOAP وضعیت CANCELLED برگردد.
gw.verify(authority=ref_id, amount=amount, order_id=oid,
          extra={"sale_reference_id": "...", "sale_order_id": "...",
                 "card_number": "...", "res_code": "0"})

# سامان (registry عمومی): به RefNum نیاز دارد
gw.verify(authority=token, amount=amount, order_id=oid,
          extra={"ref_num": "...", "state": "OK"})

# ایران‌کیش (تجربی): به token و reference_id نیاز دارد
gw.verify(authority=token, amount=amount, order_id=oid,
          extra={"token": "...", "reference_id": "...", "result_code": "100"})

# دیجی‌پی (تجربی): به trackingCode نیاز دارد (providerId همان order_id است)
gw.verify(authority=ticket, amount=amount, order_id=oid,
          extra={"tracking_code": "...", "result": "SUCCESS"})

⚠️ double spending (سامان و ملت): بانک رسید تکراری را دوباره تأیید می‌کند، پس در این لایه یکتایی رسید با توست (مستند سامان ۳.۶، ص ۱۴–۱۵). پیش از verify رسید (SamanGateway.receipt_from_extra(extra) / MellatGateway.receipt_from_extra(extra)) را در یک فیلد unique به همان سفارش بده و اگر مال سفارش دیگری بود verify نزن؛ در ملت MellatGateway.order_id_from_extra(extra) هم باید همان سفارش خودت باشد. نمونه‌ی کامل: scripts/django_custom_db.py.

درگاه‌های ساده (زرین‌پال، زیبال) پارامتر extra را نادیده می‌گیرند.


کارمزد

کارمزد به‌صورت اختیاری روی هر پرداخت قابل تنظیم است. می‌توانی تعیین کنی کارمزد به مبلغ اضافه شود (مشتری بپردازد) یا نشود (پذیرنده بپردازد، مبلغ بانک تغییر نکند).

from django_iranian_payment import FeeConfig, FeePayer

fee = FeeConfig(
    rate_bps=200,                  # ۲٪ (۲۰۰ واحد پایه). ۱٪ = ۱۰۰
    fixed=0,                       # کارمزد ثابت به ریال (اختیاری)
    who_pays=FeePayer.CUSTOMER,    # CUSTOMER: به مبلغ اضافه می‌شود | MERCHANT: نمی‌شود
    max_fee=None,                  # سقف کارمزد به ریال (اختیاری)
)

# لایه‌ی ساده:
services.start_payment("zarinpal", amount=100_000, callback_url="...",
                       order_id="1", fee=fee)

# لایه‌ی هسته:
PaymentRequest(amount=100_000, callback_url="...", order_id="1", fee=fee)

نکات کارمزد:

  • نرخ به bps (واحد پایه) است تا محاسبه‌ی پول با float انجام نشود (۲٪ = ۲۰۰).
  • کارمزد همیشه رو به بالا گرد می‌شود تا پذیرنده کسری ضرر نکند.
  • اگر who_pays=CUSTOMER، مبلغ ارسالی به بانک = مبلغ پایه + کارمزد. همین مبلغ در verify هم استفاده می‌شود (در لایه‌ی ساده خودکار است).
  • اگر who_pays=MERCHANT، مبلغ بانک تغییر نمی‌کند و کارمزد فقط برای گزارش/حسابداری محاسبه می‌شود.

افزودن درگاه جدید / عمومی‌کردن یک درگاه تجربی

درگاه‌های core.experimental پس از تست با اطلاعات و ترمینال واقعی، با انتقال فایل به core/gateways/ و افزودن یک خط به core/gateways/__init__.py عمومی می‌شوند. تا آن لحظه با get_gateway در دسترس نیستند و فقط با import صریح قابل دسترسی‌اند:

from django_iranian_payment.core.experimental.sadad import SadadGateway

نمونه‌ی واقعی این ارتقا: ملت و سامان که پس از تست تراکنش واقعی از core.experimental به core.gateways منتقل شدند و حالا با get_gateway("mellat") / get_gateway("saman") در دسترس‌اند.


تاریخچه‌ی تغییرات درگاه‌ها

  • نسخه‌ی 1.3.0 — یک باگ امنیتی و چهار باگ لایه‌ی Django (همه تست رگرسیون دارند). پس از ارتقا python manage.py migrate لازم است (migration 0002: حالت FAILED و فیلد return_url؛ 0003: فیلد یکتای receipt_key و ثبت رسید پرداخت‌های قبلی).

    • امنیتی — double spending: رسید یک پرداخت سامان (RefNum) یا ملت (SaleReferenceId) روی callback سفارش پرداخت‌نشده‌ی دیگری آن را هم COMPLETE می‌کرد (سامان رسید تکراری را دوباره تأیید می‌کند؛ ملت کد ۴۳ را موفق می‌دهد و مبلغ را چک نمی‌کند). حالا رسید پیش از verify در receipt_key یکتا ثبت می‌شود و در ملت SaleOrderId باید همان سفارش باشد. migration رسید پرداخت‌های کامل‌شده‌ی قبلی را هم ثبت می‌کند و رسیدی را که پیش‌تر دو بار مصرف شده در log گزارش می‌دهد.
    • view callback csrf_exempt نبود؛ ملت و سامان که با POST برمی‌گردند در پروژه‌ای با CsrfViewMiddleware خطای ۴۰۳ می‌گرفتند.
    • template ها (post_redirect.html) در wheel نبودند چون package-data تعریف نشده بود؛ go_to_gateway ملت/سامان در نصب از PyPI TemplateDoesNotExist می‌داد.
    • callback_url هم به بانک می‌رفت و هم مقصد redirect پس از verify بود، پس کاربر دوباره به خود callback برمی‌گشت (۴۰۴). پارامتر جدید return_url در start_payment و کلید سراسری IRANIAN_PAYMENT["return_url"]؛ بدون هیچ‌کدام صفحه‌ی نتیجه‌ی پکیج.
    • verify ناموفق رکورد را RETURN_FROM_BANK نگه می‌داشت و reverify_pending تا ابد دوباره امتحانش می‌کرد؛ کنسل ملت و State غیر OK سامان حتی reverify_pending را در هر اجرا با GatewayPaymentError می‌شکستند. حالا پاسخ قطعی پایانی است (FAILED / CANCEL_BY_USER) و خطای یک رکورد بقیه‌ی reverify را متوقف نمی‌کند. رکوردهای ناموفقِ قدیمی که RETURN_FROM_BANK مانده‌اند در اولین reverify_pending پس از ارتقا یک‌بار دیگر verify و نهایی می‌شوند.
  • نسخه‌ی 0.7.0 — انتخاب واحد پول (ریال/تومان). کلید سراسری IRANIAN_PAYMENT["currency"] ("rial" پیش‌فرض یا "toman") اضافه شد. کاربر می‌تواند مبلغ‌ها را به تومان بدهد و پکیج خودکار به ریال (واحد بانک) تبدیل می‌کند (۱ تومان = ۱۰ ریال). تبدیل در PaymentRequest.resolve_amount() انجام می‌شود، پس هیچ درگاهی نیاز به تغییر نداشت. در لایه‌ی هسته PaymentRequest(currency=...) و در settings کلید currency. سازگار با قبل (پیش‌فرض ریال = بدون تبدیل). مبالغ ذخیره/بازگشتی همیشه ریال‌اند. تست: tests/test_currency.py.

  • نسخه‌ی 0.6.0 — دو تغییر:

    • sandbox مجزای هر درگاه: کلید "sandbox" داخل config هر درگاه بر مقدار سراسری اولویت دارد؛ get_gateway(..., sandbox=...) هم اضافه شد. حالا می‌توان یک درگاه را live و دیگری را sandbox داشت هم‌زمان (سازگار با قبل: فقط sandbox سراسری مثل قبل کار می‌کند).
    • رفع باگ callback لایه‌ی Django برای درگاه‌های شاپرکی: پیش‌تر view callback پکیج رکورد را فقط با چند نام پارامتر محدود پیدا می‌کرد و برای نکست‌پی (trans_id)، سداد (Token)، سامان (ResNum — توکن در callback نیست) و دیجی‌پی (providerId — ticket در callback نیست) رکورد را نمی‌یافت و ۴۰۴ می‌داد. اکنون یک جدول مشخصات (_CALLBACK_SPEC) در contrib/django/views.py برای هر درگاه نام پارامتر درست و کلید پیدا کردن رکورد (authority یا order_id) را تعریف می‌کند. ملت همچنان با RefId (authority یکتا) پیدا می‌شود تا رفتار live تست‌شده‌اش دست‌نخورده بماند.
    • راهنمای کامل هر درگاه برای هر دو حالت مدیریت دیتابیس در docs/gateways/ افزوده شد.
  • ۱۴۰۵/۰۴/۰۳ (نسخه‌ی 0.5.0) — درگاه ملت پس از تست تراکنش واقعی روی محیط عملیاتی (bpm.shaparak.ir) از core.experimental به registry عمومی منتقل شد و حالا با get_gateway("mellat") در دسترس است. در جریان تست live دو باگ کشف و رفع شد (هر دو تست رگرسیون دارند):

    • کنسل کاربر (ResCode=17 بدون SaleReferenceId) باعث crash می‌شد؛ حالا verify ابتدا res_code از callback را بررسی و بدون تماس SOAP، CANCELLED/FAILED برمی‌گرداند.
    • CardHolderPan از callback ذخیره نمی‌شد. اکنون شماره‌ی کارت ماسک‌شده در card_number و مقادیر sale_order_id/sale_reference_id/final_amount در raw (مدل Payment) ذخیره می‌شوند تا برای settle/reverse بعدی در دسترس باشند.
  • ۱۴۰۵/۰۳/۲۷ — درگاه‌های مستقل بانک تجارت و بانک ملی حذف شدند. این بانک‌ها درگاه مستقل خود را کنار گذاشته‌اند و پرداخت را به PSPهای دیگر سپرده‌اند:

    • بانک تجارت → ایران‌کیش: از درگاه بانک تجارت به ایران‌کیش هدایت می‌شوید. به‌جای درگاه تجارت از IrankishGateway استفاده کنید.
    • بانک ملی → سداد: از سایت بانک ملی به درگاه سداد هدایت می‌شوید. به‌جای درگاه ملی از SadadGateway استفاده کنید.

    اسکلت‌های tejarat.py و melli.py (که هرگز پیاده‌سازی واقعی نداشتند و فقط TODO بودند) از پروژه حذف شدند تا فایل اضافی نماند. اگر روزی این بانک‌ها درگاه مستقل خود را دوباره راه‌اندازی کنند، در نسخه‌های بعدی دوباره افزوده خواهند شد.

لایسنس

MIT

Metadata

Release files for django-iranian-payment 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-iranian-payment 1.3.0
File Size Uploaded Provenance Actions
django_iranian_payment-1.3.0.tar.gz 104.6 kB Without provenance Details for django_iranian_payment-1.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-iranian-payment 1.3.0
File Interpreter ABI Platform Actions
django_iranian_payment-1.3.0-py3-none-any.whl Python 3 none any Details for django_iranian_payment-1.3.0-py3-none-any.whl

Total release size: 191.9 kB

Release files / django_iranian_payment-1.3.0.tar.gz

Download URL django_iranian_payment-1.3.0.tar.gz
Size 104.6 kB
Tags Source
SHA-256 checksum
How to use checksums
2e4e3661f73d4b79abac2379d9cabef43429b30f77b8c441a8a87cf7fd53e53b
BLAKE2b-256 checksum
How to use checksums
75b97d9ef096bb15912a4a02314fb814da5d0b86567b0d2d6ed9941d2bd2aa52
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.13

Release files / django_iranian_payment-1.3.0-py3-none-any.whl

Download URL django_iranian_payment-1.3.0-py3-none-any.whl
Size 87.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b2f6603e16f216036867308b11598b53aad0107756c1bca9a9ee3113ca2494de
BLAKE2b-256 checksum
How to use checksums
469c5f10ed1b62d0718d832e26fbc0a21908be37df886e53015ed57d00883662
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.13

Security for django-iranian-payment 1.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page