irgateway 0.1.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- irgateway/__init__.py +3 -0
- irgateway/zarinpal/__init__.py +4 -0
- irgateway/zarinpal/client.py +556 -0
- irgateway/zarinpal/enums.py +19 -0
- irgateway/zarinpal/errors.py +348 -0
- irgateway/zarinpal/types.py +447 -0
- irgateway-0.1.0.dist-info/METADATA +191 -0
- irgateway-0.1.0.dist-info/RECORD +10 -0
- irgateway-0.1.0.dist-info/WHEEL +5 -0
- irgateway-0.1.0.dist-info/licenses/LICENSE +21 -0
irgateway/__init__.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
from typing import Optional, List, Dict, Tuple
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from urllib.parse import urljoin
|
|
4
|
+
|
|
5
|
+
from irgateway.zarinpal.errors import ERROR_MAP, ZarinpalException
|
|
6
|
+
from irgateway.zarinpal.types import (
|
|
7
|
+
PaymentRequest, PaymentVerification,
|
|
8
|
+
UnverifiedTransactions, UnverifiedTransaction,
|
|
9
|
+
PaymentMetadata, ZarinpalConfig, PaymentCartData, Wage, FeeCalculation
|
|
10
|
+
)
|
|
11
|
+
from irgateway.zarinpal.enums import Environment, Currency, PaymentStatus
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
REQUESTS_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
REQUESTS_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import aiohttp
|
|
22
|
+
|
|
23
|
+
AIOHTTP_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
AIOHTTP_AVAILABLE = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseClient:
|
|
29
|
+
"""Base client containing shared logic between sync and async clients."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
merchant_id: str,
|
|
34
|
+
environment: Environment = Environment.PRODUCTION,
|
|
35
|
+
config: Optional[ZarinpalConfig] = None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Initialize base client.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
merchant_id: Zarinpal merchant ID (UUID format)
|
|
42
|
+
environment: Payment environment (PRODUCTION or SANDBOX)
|
|
43
|
+
config: Optional configuration override
|
|
44
|
+
"""
|
|
45
|
+
self.merchant_id = merchant_id
|
|
46
|
+
self.environment = environment
|
|
47
|
+
self.config = config or ZarinpalConfig()
|
|
48
|
+
self._base_url = environment.value
|
|
49
|
+
|
|
50
|
+
def _get_endpoint(self, path: str) -> str:
|
|
51
|
+
"""Construct full API endpoint URL."""
|
|
52
|
+
return urljoin(self._base_url, path)
|
|
53
|
+
|
|
54
|
+
def _get_custom_endpoint(self, authority: str) -> str:
|
|
55
|
+
"""Get payment gateway start URL (supports custom StartPay endpoint)."""
|
|
56
|
+
if self.config.startpay_endpoint:
|
|
57
|
+
return urljoin(self.config.startpay_endpoint, authority)
|
|
58
|
+
return urljoin(self._base_url, f"pg/StartPay/{authority}")
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _get_response(response: dict) -> Tuple[Optional[int], dict]:
|
|
62
|
+
"""Normalize API response structure."""
|
|
63
|
+
if response.get('data'):
|
|
64
|
+
data = response['data']
|
|
65
|
+
code = data.get('code')
|
|
66
|
+
elif response.get('errors'):
|
|
67
|
+
data = response['errors']
|
|
68
|
+
code = data.get('code')
|
|
69
|
+
else:
|
|
70
|
+
data = {}
|
|
71
|
+
code = None
|
|
72
|
+
return code, data
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _handle_error(code: Optional[int], message: str = None):
|
|
76
|
+
"""Raise appropriate exception based on error code."""
|
|
77
|
+
if code and code in ERROR_MAP:
|
|
78
|
+
raise ERROR_MAP[code]()
|
|
79
|
+
raise ZarinpalException(code, message or "Unknown error", "")
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _parse_unverified(data: Dict) -> UnverifiedTransactions:
|
|
83
|
+
"""Parse unverified transactions response into structured object."""
|
|
84
|
+
authorities = []
|
|
85
|
+
for auth in data.get("authorities", []):
|
|
86
|
+
authorities.append(UnverifiedTransaction(
|
|
87
|
+
authority=auth.get("authority"),
|
|
88
|
+
amount=auth.get("amount"),
|
|
89
|
+
callback_url=auth.get("callback_url"),
|
|
90
|
+
referer=auth.get("referer"),
|
|
91
|
+
date=datetime.strptime(auth["date"], "%Y-%m-%d %H:%M:%S") if auth.get("date") else None
|
|
92
|
+
))
|
|
93
|
+
return UnverifiedTransactions(
|
|
94
|
+
code=data.get("code", 100),
|
|
95
|
+
authorities=authorities
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ZarinpalClient(BaseClient):
|
|
100
|
+
"""Synchronous client for Zarinpal Payment Gateway."""
|
|
101
|
+
__type__ = 'SYNC'
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
merchant_id: str,
|
|
106
|
+
environment: Environment = Environment.PRODUCTION,
|
|
107
|
+
config: Optional[ZarinpalConfig] = None,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Initialize synchronous Zarinpal client.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
merchant_id: Your Zarinpal merchant ID
|
|
114
|
+
environment: Use SANDBOX for testing
|
|
115
|
+
config: Custom configuration (timeout, custom StartPay, etc.)
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
ImportError: If `requests` library is not installed
|
|
119
|
+
"""
|
|
120
|
+
if not REQUESTS_AVAILABLE:
|
|
121
|
+
raise ImportError("Install 'requests' to use synchronous client: pip install requests")
|
|
122
|
+
|
|
123
|
+
super().__init__(merchant_id, environment, config)
|
|
124
|
+
self._session = requests.Session()
|
|
125
|
+
self._session.headers.update({
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
"Accept": "application/json"
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
def __enter__(self):
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def __exit__(self, *args):
|
|
134
|
+
self.close()
|
|
135
|
+
|
|
136
|
+
def close(self):
|
|
137
|
+
"""Close underlying HTTP session."""
|
|
138
|
+
self._session.close()
|
|
139
|
+
|
|
140
|
+
def request(
|
|
141
|
+
self,
|
|
142
|
+
amount: int,
|
|
143
|
+
callback_url: str,
|
|
144
|
+
description: str,
|
|
145
|
+
currency: Currency = Currency.RIAL,
|
|
146
|
+
metadata: Optional[PaymentMetadata] = None,
|
|
147
|
+
cart_data: Optional[PaymentCartData] = None,
|
|
148
|
+
wages: Optional[List[Wage]] = None,
|
|
149
|
+
) -> PaymentRequest:
|
|
150
|
+
"""
|
|
151
|
+
Create a new payment request.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
amount: Amount in Rial/Toman (min 1000 IRT or 10000 IRR)
|
|
155
|
+
callback_url: URL user returns to after payment
|
|
156
|
+
description: Payment description shown to user
|
|
157
|
+
currency: Currency.RIAL (Toman) or Currency.TOMAN (Rial)
|
|
158
|
+
metadata: Mobile/email or custom metadata
|
|
159
|
+
cart_data: Structured cart information
|
|
160
|
+
wages: List of IBANs and amounts for shared settlement
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
PaymentRequest with authority and payment URL
|
|
164
|
+
"""
|
|
165
|
+
payload = {
|
|
166
|
+
"merchant_id": self.merchant_id,
|
|
167
|
+
"amount": amount,
|
|
168
|
+
"callback_url": callback_url,
|
|
169
|
+
"description": description,
|
|
170
|
+
"currency": currency.value,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if metadata:
|
|
174
|
+
payload["metadata"] = metadata.to_dict()
|
|
175
|
+
if cart_data:
|
|
176
|
+
payload["cart_data"] = cart_data.to_dict()
|
|
177
|
+
if wages:
|
|
178
|
+
payload["wages"] = [w.to_dict() for w in wages]
|
|
179
|
+
|
|
180
|
+
response = self._session.post(
|
|
181
|
+
self._get_endpoint("pg/v4/payment/request.json"),
|
|
182
|
+
json=payload,
|
|
183
|
+
timeout=self.config.timeout
|
|
184
|
+
)
|
|
185
|
+
code, data = self._get_response(response.json())
|
|
186
|
+
|
|
187
|
+
if code != 100:
|
|
188
|
+
self._handle_error(code)
|
|
189
|
+
|
|
190
|
+
payment_url = self._get_custom_endpoint(data["authority"])
|
|
191
|
+
|
|
192
|
+
return PaymentRequest(
|
|
193
|
+
authority=data["authority"],
|
|
194
|
+
code=code,
|
|
195
|
+
payment_url=payment_url,
|
|
196
|
+
fee_type=data.get("fee_type"),
|
|
197
|
+
fee=data.get("fee")
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def verify(self, authority: str, amount: int) -> PaymentVerification:
|
|
201
|
+
"""Verify payment status after user returns from bank."""
|
|
202
|
+
payload = {
|
|
203
|
+
"merchant_id": self.merchant_id,
|
|
204
|
+
"authority": authority,
|
|
205
|
+
"amount": amount
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
response = self._session.post(
|
|
209
|
+
self._get_endpoint("pg/v4/payment/verify.json"),
|
|
210
|
+
json=payload,
|
|
211
|
+
timeout=self.config.timeout
|
|
212
|
+
)
|
|
213
|
+
code, data = self._get_response(response.json())
|
|
214
|
+
|
|
215
|
+
if code != 100:
|
|
216
|
+
self._handle_error(code)
|
|
217
|
+
|
|
218
|
+
return PaymentVerification(
|
|
219
|
+
code=code,
|
|
220
|
+
ref_id=data.get("ref_id"),
|
|
221
|
+
card_pan=data.get("card_pan"),
|
|
222
|
+
card_hash=data.get("card_hash"),
|
|
223
|
+
fee_type=data.get("fee_type"),
|
|
224
|
+
fee=data.get("fee"),
|
|
225
|
+
wages=[Wage(**w) for w in data.get("wages", [])] if data.get("wages") else None,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def unverified(self) -> UnverifiedTransactions:
|
|
229
|
+
"""Retrieve list of successful but unverified transactions."""
|
|
230
|
+
payload = {"merchant_id": self.merchant_id}
|
|
231
|
+
|
|
232
|
+
response = self._session.post(
|
|
233
|
+
self._get_endpoint("pg/v4/payment/unVerified.json"),
|
|
234
|
+
json=payload,
|
|
235
|
+
timeout=self.config.timeout
|
|
236
|
+
)
|
|
237
|
+
code, data = self._get_response(response.json())
|
|
238
|
+
|
|
239
|
+
if code != 100:
|
|
240
|
+
self._handle_error(code)
|
|
241
|
+
|
|
242
|
+
return self._parse_unverified(data)
|
|
243
|
+
|
|
244
|
+
def reverse(self, authority: str) -> bool:
|
|
245
|
+
"""Request refund (requires special permission and access token)."""
|
|
246
|
+
payload = {"merchant_id": self.merchant_id, "authority": authority}
|
|
247
|
+
|
|
248
|
+
response = self._session.post(
|
|
249
|
+
self._get_endpoint("pg/v4/payment/reverse.json"),
|
|
250
|
+
json=payload,
|
|
251
|
+
timeout=self.config.timeout
|
|
252
|
+
)
|
|
253
|
+
code, _ = self._get_response(response.json())
|
|
254
|
+
return code == 100
|
|
255
|
+
|
|
256
|
+
def inquiry(self, authority: str) -> PaymentStatus:
|
|
257
|
+
"""Check current status of a payment authority."""
|
|
258
|
+
payload = {"merchant_id": self.merchant_id, "authority": authority}
|
|
259
|
+
|
|
260
|
+
response = self._session.post(
|
|
261
|
+
self._get_endpoint("pg/v4/payment/inquiry.json"),
|
|
262
|
+
json=payload,
|
|
263
|
+
timeout=self.config.timeout
|
|
264
|
+
)
|
|
265
|
+
code, data = self._get_response(response.json())
|
|
266
|
+
|
|
267
|
+
if code != 100:
|
|
268
|
+
self._handle_error(code)
|
|
269
|
+
|
|
270
|
+
return PaymentStatus(data.get("status"))
|
|
271
|
+
|
|
272
|
+
def calculate_fee(self, amount: int, currency: Currency = Currency.RIAL) -> FeeCalculation:
|
|
273
|
+
"""Calculate gateway fee for a specific amount and authority."""
|
|
274
|
+
payload = {
|
|
275
|
+
"merchant_id": self.merchant_id,
|
|
276
|
+
"amount": amount,
|
|
277
|
+
"currency": currency.value,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
response = self._session.post(
|
|
281
|
+
self._get_endpoint("pg/v4/payment/feeCalculation.json"),
|
|
282
|
+
json=payload,
|
|
283
|
+
timeout=self.config.timeout
|
|
284
|
+
)
|
|
285
|
+
code, data = self._get_response(response.json())
|
|
286
|
+
|
|
287
|
+
if code != 100:
|
|
288
|
+
self._handle_error(code)
|
|
289
|
+
|
|
290
|
+
return FeeCalculation(
|
|
291
|
+
amount=data.get("amount"),
|
|
292
|
+
fee=data.get("fee"),
|
|
293
|
+
fee_type=data.get("fee_type"),
|
|
294
|
+
suggested_amount=data.get("suggested_amount"),
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class AsyncZarinpalClient(BaseClient):
|
|
299
|
+
"""Asynchronous client for Zarinpal Payment Gateway (fully compatible with sync version)."""
|
|
300
|
+
__type__ = 'ASYNC'
|
|
301
|
+
|
|
302
|
+
def __init__(
|
|
303
|
+
self,
|
|
304
|
+
merchant_id: str,
|
|
305
|
+
environment: Environment = Environment.PRODUCTION,
|
|
306
|
+
config: Optional[ZarinpalConfig] = None,
|
|
307
|
+
):
|
|
308
|
+
"""
|
|
309
|
+
Initialize asynchronous client.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
merchant_id: Zarinpal merchant ID
|
|
313
|
+
environment: SANDBOX or PRODUCTION
|
|
314
|
+
config: Optional config (timeout, custom StartPay URL, etc.)
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ImportError: If aiohttp is not installed
|
|
318
|
+
"""
|
|
319
|
+
if not AIOHTTP_AVAILABLE:
|
|
320
|
+
raise ImportError("Install 'aiohttp' to use async client: pip install aiohttp")
|
|
321
|
+
|
|
322
|
+
super().__init__(merchant_id, environment, config)
|
|
323
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
324
|
+
|
|
325
|
+
async def _ensure_session(self):
|
|
326
|
+
if self._session is None or self._session.closed:
|
|
327
|
+
timeout = aiohttp.ClientTimeout(total=self.config.timeout)
|
|
328
|
+
self._session = aiohttp.ClientSession(
|
|
329
|
+
timeout=timeout,
|
|
330
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
async def __aenter__(self):
|
|
334
|
+
await self._ensure_session()
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
async def __aexit__(self, *args):
|
|
338
|
+
await self.close()
|
|
339
|
+
|
|
340
|
+
async def close(self):
|
|
341
|
+
"""Close the underlying aiohttp session."""
|
|
342
|
+
if self._session and not self._session.closed:
|
|
343
|
+
await self._session.close()
|
|
344
|
+
|
|
345
|
+
async def request(
|
|
346
|
+
self,
|
|
347
|
+
amount: int,
|
|
348
|
+
callback_url: str,
|
|
349
|
+
description: str,
|
|
350
|
+
currency: Currency = Currency.RIAL,
|
|
351
|
+
metadata: Optional[PaymentMetadata] = None,
|
|
352
|
+
cart_data: Optional[PaymentCartData] = None,
|
|
353
|
+
wages: Optional[List[Wage]] = None,
|
|
354
|
+
) -> PaymentRequest:
|
|
355
|
+
"""
|
|
356
|
+
Create payment request (async) - identical behavior to sync version.
|
|
357
|
+
"""
|
|
358
|
+
await self._ensure_session()
|
|
359
|
+
|
|
360
|
+
payload = {
|
|
361
|
+
"merchant_id": self.merchant_id,
|
|
362
|
+
"amount": amount,
|
|
363
|
+
"callback_url": callback_url,
|
|
364
|
+
"description": description,
|
|
365
|
+
"currency": currency.value,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if metadata:
|
|
369
|
+
payload["metadata"] = metadata.to_dict()
|
|
370
|
+
if cart_data:
|
|
371
|
+
payload["cart_data"] = cart_data.to_dict()
|
|
372
|
+
if wages:
|
|
373
|
+
payload["wages"] = [w.to_dict() for w in wages]
|
|
374
|
+
|
|
375
|
+
async with self._session.post(
|
|
376
|
+
self._get_endpoint("pg/v4/payment/request.json"),
|
|
377
|
+
json=payload
|
|
378
|
+
) as resp:
|
|
379
|
+
result = await resp.json()
|
|
380
|
+
code, data = self._get_response(result)
|
|
381
|
+
|
|
382
|
+
if code != 100:
|
|
383
|
+
self._handle_error(code)
|
|
384
|
+
|
|
385
|
+
payment_url = self._get_custom_endpoint(data["authority"])
|
|
386
|
+
|
|
387
|
+
return PaymentRequest(
|
|
388
|
+
authority=data["authority"],
|
|
389
|
+
code=code,
|
|
390
|
+
payment_url=payment_url,
|
|
391
|
+
fee_type=data.get("fee_type"),
|
|
392
|
+
fee=data.get("fee")
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
async def verify(self, authority: str, amount: int) -> PaymentVerification:
|
|
396
|
+
"""Verify payment after redirect (async)."""
|
|
397
|
+
await self._ensure_session()
|
|
398
|
+
|
|
399
|
+
payload = {
|
|
400
|
+
"merchant_id": self.merchant_id,
|
|
401
|
+
"authority": authority,
|
|
402
|
+
"amount": amount
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async with self._session.post(
|
|
406
|
+
self._get_endpoint("pg/v4/payment/verify.json"),
|
|
407
|
+
json=payload
|
|
408
|
+
) as resp:
|
|
409
|
+
result = await resp.json()
|
|
410
|
+
code, data = self._get_response(result)
|
|
411
|
+
|
|
412
|
+
if code != 100:
|
|
413
|
+
self._handle_error(code)
|
|
414
|
+
|
|
415
|
+
return PaymentVerification(
|
|
416
|
+
code=code,
|
|
417
|
+
ref_id=data.get("ref_id"),
|
|
418
|
+
card_pan=data.get("card_pan"),
|
|
419
|
+
card_hash=data.get("card_hash"),
|
|
420
|
+
fee_type=data.get("fee_type"),
|
|
421
|
+
fee=data.get("fee"),
|
|
422
|
+
wages=[Wage(**w) for w in data.get("wages", [])] if data.get("wages") else None,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def unverified(self) -> UnverifiedTransactions:
|
|
426
|
+
"""Get list of unverified transactions (async)."""
|
|
427
|
+
await self._ensure_session()
|
|
428
|
+
|
|
429
|
+
payload = {"merchant_id": self.merchant_id}
|
|
430
|
+
|
|
431
|
+
async with self._session.post(
|
|
432
|
+
self._get_endpoint("pg/v4/payment/unVerified.json"),
|
|
433
|
+
json=payload
|
|
434
|
+
) as resp:
|
|
435
|
+
result = await resp.json()
|
|
436
|
+
code, data = self._get_response(result)
|
|
437
|
+
|
|
438
|
+
if code != 100:
|
|
439
|
+
self._handle_error(code)
|
|
440
|
+
|
|
441
|
+
return self._parse_unverified(data)
|
|
442
|
+
|
|
443
|
+
async def reverse(
|
|
444
|
+
self,
|
|
445
|
+
authority: str,
|
|
446
|
+
) -> bool:
|
|
447
|
+
"""
|
|
448
|
+
Request a full refund for a successful payment (reverse transaction).
|
|
449
|
+
|
|
450
|
+
Note:
|
|
451
|
+
- Requires "Refund Permission" enabled on your Zarinpal merchant panel
|
|
452
|
+
- Uses `reverse.json` endpoint (no access token needed)
|
|
453
|
+
- Only works on successfully verified payments
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
authority: The payment authority (from request or verify step)
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
bool: True if refund was successfully requested
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ZarinpalException: With appropriate error code (e.g., -11: already refunded, etc.)
|
|
463
|
+
"""
|
|
464
|
+
await self._ensure_session()
|
|
465
|
+
|
|
466
|
+
payload = {
|
|
467
|
+
"merchant_id": self.merchant_id,
|
|
468
|
+
"authority": authority
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async with self._session.post(
|
|
472
|
+
self._get_endpoint("pg/v4/payment/reverse.json"),
|
|
473
|
+
json=payload
|
|
474
|
+
) as resp:
|
|
475
|
+
result = await resp.json()
|
|
476
|
+
code, _ = self._get_response(result)
|
|
477
|
+
return code == 100
|
|
478
|
+
|
|
479
|
+
async def inquiry(self, authority: str) -> PaymentStatus:
|
|
480
|
+
"""
|
|
481
|
+
Check the current status of a payment using its authority.
|
|
482
|
+
|
|
483
|
+
Useful for:
|
|
484
|
+
- Re-checking payment status after network issues
|
|
485
|
+
- Syncing offline payments
|
|
486
|
+
- Debugging callback failures
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
authority: Payment authority string
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
PaymentStatus enum (e.g., PaymentStatus.SUCCESS, PaymentStatus.PENDING, etc.)
|
|
493
|
+
"""
|
|
494
|
+
await self._ensure_session()
|
|
495
|
+
|
|
496
|
+
payload = {
|
|
497
|
+
"merchant_id": self.merchant_id,
|
|
498
|
+
"authority": authority
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async with self._session.post(
|
|
502
|
+
self._get_endpoint("pg/v4/payment/inquiry.json"),
|
|
503
|
+
json=payload
|
|
504
|
+
) as resp:
|
|
505
|
+
result = await resp.json()
|
|
506
|
+
code, data = self._get_response(result)
|
|
507
|
+
|
|
508
|
+
if code != 100:
|
|
509
|
+
self._handle_error(code)
|
|
510
|
+
|
|
511
|
+
status_code = data.get("status")
|
|
512
|
+
return PaymentStatus(status_code)
|
|
513
|
+
|
|
514
|
+
async def calculate_fee(
|
|
515
|
+
self,
|
|
516
|
+
amount: int,
|
|
517
|
+
currency: Currency = Currency.RIAL
|
|
518
|
+
) -> FeeCalculation:
|
|
519
|
+
"""
|
|
520
|
+
Calculate Zarinpal gateway fee for a given amount and authority.
|
|
521
|
+
|
|
522
|
+
This is useful when you want to:
|
|
523
|
+
- Show exact fee to user before payment
|
|
524
|
+
- Adjust final amount to cover fee (e.g., user pays X, you receive X)
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
amount: Amount to calculate fee for
|
|
528
|
+
currency: Currency.RIAL (Toman) or Currency.TOMAN (Rial)
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
FeeCalculation object with fee, suggested_amount, etc.
|
|
532
|
+
"""
|
|
533
|
+
await self._ensure_session()
|
|
534
|
+
|
|
535
|
+
payload = {
|
|
536
|
+
"merchant_id": self.merchant_id,
|
|
537
|
+
"amount": amount,
|
|
538
|
+
"currency": currency.value,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async with self._session.post(
|
|
542
|
+
self._get_endpoint("pg/v4/payment/feeCalculation.json"),
|
|
543
|
+
json=payload
|
|
544
|
+
) as resp:
|
|
545
|
+
result = await resp.json()
|
|
546
|
+
code, data = self._get_response(result)
|
|
547
|
+
|
|
548
|
+
if code != 100:
|
|
549
|
+
self._handle_error(code)
|
|
550
|
+
|
|
551
|
+
return FeeCalculation(
|
|
552
|
+
amount=data.get("amount"),
|
|
553
|
+
fee=data.get("fee"),
|
|
554
|
+
fee_type=data.get("fee_type"),
|
|
555
|
+
suggested_amount=data.get("suggested_amount"),
|
|
556
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Environment(Enum):
|
|
5
|
+
SANDBOX = "https://sandbox.zarinpal.com/"
|
|
6
|
+
PRODUCTION = "https://payment.zarinpal.com/"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PaymentStatus(Enum):
|
|
10
|
+
PENDING = "PENDING"
|
|
11
|
+
VERIFIED = "VERIFIED"
|
|
12
|
+
FAILED = "FAILED"
|
|
13
|
+
REVERSED = "REVERSED"
|
|
14
|
+
IN_BANK = "IN_BANK"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Currency(Enum):
|
|
18
|
+
TOMAN = "IRT"
|
|
19
|
+
RIAL = "IRR"
|