-
Notifications
You must be signed in to change notification settings - Fork 106
Description
Background:
The current implementation of the Zarinpal gateway in our repository uses a SOAP‑based integration for all environments. However, as of now, Zarinpal has deprecated the old SOAP-based sandbox endpoint. The sandbox is no longer supported via the SOAP API, resulting in errors (e.g., a 404 on the old SOAP sandbox WSDL and an HTTP 422 when attempting to call those methods).
Issue:
Sandbox payment requests are now failing because Zarinpal requires a completely new approach for the sandbox environment. According to the latest Zarinpal docs and the sandbox guide (docs link), the sandbox integration should now use a JSON‑based REST API rather than SOAP. The new endpoints are:
- Payment Request:
https://sandbox.zarinpal.com/pg/v4/payment/request.json - Payment Verification:
https://sandbox.zarinpal.com/pg/v4/payment/verify.json
Additionally, the redirect URL format for sandbox transactions is updated tohttps://sandbox.zarinpal.com/pg/StartPay/{authority}, and the expected payload field names have changed (e.g., using snake_case keys likemerchant_id,callback_url, etc.).
Proposed Fix & Changes:
-
Sandbox Mode Implementation:
- In sandbox mode, switch from using SOAP calls to using the REST API via the
requestslibrary. - Build JSON payloads that conform to the new API’s format and omit fields that are not provided (e.g., avoid sending
Nonefor email).
- In sandbox mode, switch from using SOAP calls to using the REST API via the
-
Production Compatibility:
- Maintain the existing SOAP integration for production since the production endpoints remain unchanged.
-
Error Handling:
- Improve logging for HTTP errors by capturing the response status code and full response body, so any 422 errors seen in sandbox mode can be debugged more easily.
Attached Code Example:
Below is an updated version of the Zarinpal class implementing the new sandbox integration:
import logging
import requests # for REST calls in sandbox mode
from zeep import Client, Transport
from azbankgateways.banks import BaseBank
from azbankgateways.exceptions import SettingDoesNotExist
from azbankgateways.exceptions.exceptions import BankGatewayRejectPayment
from azbankgateways.models import BankType, CurrencyEnum, PaymentStatus
class Zarinpal(BaseBank):
_merchant_code = None
_sandbox = None
def __init__(self, **kwargs):
kwargs.setdefault("SANDBOX", 0)
super(Zarinpal, self).__init__(**kwargs)
self.set_gateway_currency(CurrencyEnum.IRT)
# Production URL remains unchanged.
self._payment_url = "https://www.zarinpal.com/pg/StartPay/{}/ZarinGate"
# Updated sandbox redirect URL per documentation.
self._sandbox_url = "https://sandbox.zarinpal.com/pg/StartPay/{}"
def get_bank_type(self):
return BankType.ZARINPAL
def set_default_settings(self):
for item in ["MERCHANT_CODE", "SANDBOX"]:
if item not in self.default_setting_kwargs:
raise SettingDoesNotExist(f"{item} does not exist in default_setting_kwargs")
setattr(self, f"_{item.lower()}", self.default_setting_kwargs[item])
@classmethod
def get_minimum_amount(cls):
return 1000
def _get_gateway_payment_url_parameter(self):
if self._sandbox:
return self._sandbox_url.format(self.get_reference_number())
return self._payment_url.format(self.get_reference_number())
def _get_gateway_payment_parameter(self):
return {}
def _get_gateway_payment_method_parameter(self):
return "GET"
def get_pay_data(self):
# Production SOAP payload.
description = "خرید با شماره پیگیری - {}".format(self.get_tracking_code())
return {
"Description": description,
"MerchantID": self._merchant_code,
"Amount": self.get_gateway_amount(),
"Email": None,
"Mobile": self.get_mobile_number(),
"CallbackURL": self._get_gateway_callback_url(),
}
def prepare_pay(self):
super(Zarinpal, self).prepare_pay()
def pay(self):
super(Zarinpal, self).pay()
if self._sandbox:
# Use the new REST API for the sandbox
request_url = "https://sandbox.zarinpal.com/pg/v4/payment/request.json"
payload = {
"merchant_id": self._merchant_code,
"amount": self.get_gateway_amount(),
"callback_url": self._get_gateway_callback_url(),
"description": "خرید با شماره پیگیری - {}".format(self.get_tracking_code())
}
metadata = {}
mobile = self.get_mobile_number()
if mobile:
metadata["mobile"] = mobile
email = None # Set your email here, if available.
if email:
metadata["email"] = email
if metadata:
payload["metadata"] = metadata
response = requests.post(request_url, json=payload)
if response.status_code != 200:
logging.critical("Zarinpal sandbox payment request failed: HTTP %s - %s",
response.status_code, response.text)
raise BankGatewayRejectPayment("HTTP Error: " + str(response.status_code))
json_data = response.json()
result_code = json_data.get("data", {}).get("code")
if result_code == 100:
token = json_data.get("data", {}).get("authority")
self._set_reference_number(token)
else:
logging.critical("Zarinpal sandbox gateway rejected payment: %s", json_data)
raise BankGatewayRejectPayment(self.get_transaction_status_text())
else:
data = self.get_pay_data()
client = self._get_client()
result = client.service.PaymentRequest(**data)
if result.Status == 100:
token = result.Authority
self._set_reference_number(token)
else:
logging.critical("Zarinpal gateway rejected payment")
raise BankGatewayRejectPayment(self.get_transaction_status_text())
def prepare_verify_from_gateway(self):
super(Zarinpal, self).prepare_verify_from_gateway()
token = self.get_request().GET.get("Authority", None)
self._set_reference_number(token)
self._set_bank_record()
def verify_from_gateway(self, request):
super(Zarinpal, self).verify_from_gateway(request)
def get_verify_data(self):
super(Zarinpal, self).get_verify_data()
return {
"MerchantID": self._merchant_code,
"Authority": self.get_reference_number(),
"Amount": self.get_gateway_amount(),
}
def prepare_verify(self, tracking_code):
super(Zarinpal, self).prepare_verify(tracking_code)
def verify(self, transaction_code):
super(Zarinpal, self).verify(transaction_code)
data = self.get_verify_data()
if self._sandbox:
verify_url = "https://sandbox.zarinpal.com/pg/v4/payment/verify.json"
payload = {
"merchant_id": self._merchant_code,
"authority": self.get_reference_number(),
"amount": self.get_gateway_amount(),
}
response = requests.post(verify_url, json=payload)
if response.status_code != 200:
logging.debug("Zarinpal sandbox verification failed: HTTP %s - %s",
response.status_code, response.text)
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
return
json_data = response.json()
result_code = json_data.get("data", {}).get("code")
if result_code in [100, 101]:
self._set_payment_status(PaymentStatus.COMPLETE)
else:
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
logging.debug("Zarinpal sandbox payment unapproved: %s", json_data)
else:
client = self._get_client(timeout=10)
result = client.service.PaymentVerification(**data)
if result.Status in [100, 101]:
self._set_payment_status(PaymentStatus.COMPLETE)
else:
self._set_payment_status(PaymentStatus.CANCEL_BY_USER)
logging.debug("Zarinpal gateway unapproved payment")
def _get_client(self, timeout=5):
transport = Transport(timeout=timeout, operation_timeout=timeout)
# In sandbox mode, we use REST, so we always return the production WSDL for SOAP.
return Client(
"https://www.zarinpal.com/pg/services/WebGate/wsdl",
transport=transport,
)