raqm-core 0.2.0__tar.gz → 0.2.6__tar.gz
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.
- {raqm_core-0.2.0 → raqm_core-0.2.6}/PKG-INFO +42 -8
- {raqm_core-0.2.0 → raqm_core-0.2.6}/README.md +41 -7
- {raqm_core-0.2.0 → raqm_core-0.2.6}/pyproject.toml +1 -1
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/easypaisa.py +37 -11
- raqm_core-0.2.6/src/raqm_core/exceptions/exceptions.py +12 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/headers/jazzcash.py +3 -2
- raqm_core-0.2.6/src/raqm_core/jazzcash.py +88 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/schemas/easypaisa.py +4 -4
- raqm_core-0.2.6/src/raqm_core/schemas/jazzcash.py +93 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core.egg-info/PKG-INFO +42 -8
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core.egg-info/SOURCES.txt +5 -1
- {raqm_core-0.2.0 → raqm_core-0.2.6}/tests/test_easypaisa.py +42 -20
- raqm_core-0.2.6/tests/test_jazzcash.py +105 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/setup.cfg +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/__init__.py +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/headers/__init__.py +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/headers/easypaisa.py +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core/schemas/__init__.py +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core.egg-info/dependency_links.txt +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core.egg-info/requires.txt +0 -0
- {raqm_core-0.2.0 → raqm_core-0.2.6}/src/raqm_core.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: raqm-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Unified Python SDK for Pakistani payment gateways (EasyPaisa, JazzCash, HBL, ABL, UBL, etc.)
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.9
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -19,7 +19,9 @@ Requires-Dist: pydantic>=2.0
|
|
|
19
19
|
|
|
20
20
|
One async, type-safe interface across multiple processors — EasyPaisa, JazzCash, HBL, ABL, UBL, and more.
|
|
21
21
|
|
|
22
|
-
> ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. Contributions welcome!
|
|
22
|
+
> ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. JazzCash is in progress. Contributions welcome!
|
|
23
|
+
>
|
|
24
|
+
> ⚠️ **Breaking change in v0.2.4** — methods now **raise `PaymentError`** on business-level failures instead of returning a result with a non-success response code. Wrap calls in try/except to handle errors gracefully.
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
@@ -28,7 +30,7 @@ One async, type-safe interface across multiple processors — EasyPaisa, JazzCas
|
|
|
28
30
|
| Gateway | Status |
|
|
29
31
|
|---------|--------|
|
|
30
32
|
| [EasyPaisa](https://easypaisa.com.pk) | ✅ Live |
|
|
31
|
-
| [JazzCash](https://jazzcash.com.pk) | 🚧
|
|
33
|
+
| [JazzCash](https://jazzcash.com.pk) | 🚧 In progress |
|
|
32
34
|
| HBL | 📅 Planned |
|
|
33
35
|
| ABL | 📅 Planned |
|
|
34
36
|
| UBL | 📅 Planned |
|
|
@@ -173,6 +175,33 @@ result.paymentMode # str ("MA", "OTC", "CC")
|
|
|
173
175
|
|
|
174
176
|
---
|
|
175
177
|
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
All gateway methods raise typed exceptions on failure:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
RaqmCoreError # Base exception for the SDK
|
|
184
|
+
├── NetworkError # HTTP/transport errors (connection, timeout, 4xx/5xx)
|
|
185
|
+
└── PaymentError # Business-level failures (non-success response code)
|
|
186
|
+
# .response — the full gateway response object
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
result = await ep.pay_via_ma(...)
|
|
194
|
+
except NetworkError as e:
|
|
195
|
+
print(f"Network issue: {e}")
|
|
196
|
+
except PaymentError as e:
|
|
197
|
+
print(f"Payment failed: {e}")
|
|
198
|
+
print(f"Response code: {e.response.responseCode}")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`PaymentError` carries the gateway's response object in `exc.response`, so you can inspect the raw error details even when the SDK raises.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
176
205
|
## Architecture
|
|
177
206
|
|
|
178
207
|
Each payment gateway follows a consistent 3-layer structure:
|
|
@@ -190,15 +219,20 @@ Current structure:
|
|
|
190
219
|
|
|
191
220
|
```
|
|
192
221
|
src/
|
|
193
|
-
├── easypaisa.py
|
|
222
|
+
├── easypaisa.py # EasyPaisa client
|
|
223
|
+
├── jazzcash.py # JazzCash client
|
|
224
|
+
├── exceptions/
|
|
225
|
+
│ └── exceptions.py # Custom exception hierarchy
|
|
194
226
|
├── headers/
|
|
195
|
-
│ ├── easypaisa.py
|
|
196
|
-
│ └── jazzcash.py
|
|
227
|
+
│ ├── easypaisa.py # Basic Auth header
|
|
228
|
+
│ └── jazzcash.py # SHA-256 secure hash
|
|
197
229
|
└── schemas/
|
|
198
|
-
|
|
230
|
+
├── easypaisa.py # EasyPaisa Pydantic models
|
|
231
|
+
└── jazzcash.py # JazzCash Pydantic models
|
|
199
232
|
|
|
200
233
|
tests/
|
|
201
234
|
├── test_easypaisa.py # EasyPaisa integration tests
|
|
235
|
+
├── test_jazzcash.py # JazzCash integration tests
|
|
202
236
|
└── headers/
|
|
203
237
|
├── test_easypaisa.py # Auth header unit tests
|
|
204
238
|
└── test_jazzcash.py # Secure hash unit tests
|
|
@@ -302,7 +336,7 @@ Add the new gateway to the **Supported Gateways** table with the appropriate sta
|
|
|
302
336
|
- [ ] HBL payment gateway
|
|
303
337
|
- [ ] ABL payment gateway
|
|
304
338
|
- [ ] UBL payment gateway
|
|
305
|
-
- [
|
|
339
|
+
- [x] Standardised error handling across gateways
|
|
306
340
|
- [ ] Request/response logging middleware
|
|
307
341
|
- [ ] CI/CD + automated testing
|
|
308
342
|
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
One async, type-safe interface across multiple processors — EasyPaisa, JazzCash, HBL, ABL, UBL, and more.
|
|
6
6
|
|
|
7
|
-
> ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. Contributions welcome!
|
|
7
|
+
> ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. JazzCash is in progress. Contributions welcome!
|
|
8
|
+
>
|
|
9
|
+
> ⚠️ **Breaking change in v0.2.4** — methods now **raise `PaymentError`** on business-level failures instead of returning a result with a non-success response code. Wrap calls in try/except to handle errors gracefully.
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
@@ -13,7 +15,7 @@ One async, type-safe interface across multiple processors — EasyPaisa, JazzCas
|
|
|
13
15
|
| Gateway | Status |
|
|
14
16
|
|---------|--------|
|
|
15
17
|
| [EasyPaisa](https://easypaisa.com.pk) | ✅ Live |
|
|
16
|
-
| [JazzCash](https://jazzcash.com.pk) | 🚧
|
|
18
|
+
| [JazzCash](https://jazzcash.com.pk) | 🚧 In progress |
|
|
17
19
|
| HBL | 📅 Planned |
|
|
18
20
|
| ABL | 📅 Planned |
|
|
19
21
|
| UBL | 📅 Planned |
|
|
@@ -158,6 +160,33 @@ result.paymentMode # str ("MA", "OTC", "CC")
|
|
|
158
160
|
|
|
159
161
|
---
|
|
160
162
|
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
All gateway methods raise typed exceptions on failure:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
RaqmCoreError # Base exception for the SDK
|
|
169
|
+
├── NetworkError # HTTP/transport errors (connection, timeout, 4xx/5xx)
|
|
170
|
+
└── PaymentError # Business-level failures (non-success response code)
|
|
171
|
+
# .response — the full gateway response object
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
result = await ep.pay_via_ma(...)
|
|
179
|
+
except NetworkError as e:
|
|
180
|
+
print(f"Network issue: {e}")
|
|
181
|
+
except PaymentError as e:
|
|
182
|
+
print(f"Payment failed: {e}")
|
|
183
|
+
print(f"Response code: {e.response.responseCode}")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`PaymentError` carries the gateway's response object in `exc.response`, so you can inspect the raw error details even when the SDK raises.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
161
190
|
## Architecture
|
|
162
191
|
|
|
163
192
|
Each payment gateway follows a consistent 3-layer structure:
|
|
@@ -175,15 +204,20 @@ Current structure:
|
|
|
175
204
|
|
|
176
205
|
```
|
|
177
206
|
src/
|
|
178
|
-
├── easypaisa.py
|
|
207
|
+
├── easypaisa.py # EasyPaisa client
|
|
208
|
+
├── jazzcash.py # JazzCash client
|
|
209
|
+
├── exceptions/
|
|
210
|
+
│ └── exceptions.py # Custom exception hierarchy
|
|
179
211
|
├── headers/
|
|
180
|
-
│ ├── easypaisa.py
|
|
181
|
-
│ └── jazzcash.py
|
|
212
|
+
│ ├── easypaisa.py # Basic Auth header
|
|
213
|
+
│ └── jazzcash.py # SHA-256 secure hash
|
|
182
214
|
└── schemas/
|
|
183
|
-
|
|
215
|
+
├── easypaisa.py # EasyPaisa Pydantic models
|
|
216
|
+
└── jazzcash.py # JazzCash Pydantic models
|
|
184
217
|
|
|
185
218
|
tests/
|
|
186
219
|
├── test_easypaisa.py # EasyPaisa integration tests
|
|
220
|
+
├── test_jazzcash.py # JazzCash integration tests
|
|
187
221
|
└── headers/
|
|
188
222
|
├── test_easypaisa.py # Auth header unit tests
|
|
189
223
|
└── test_jazzcash.py # Secure hash unit tests
|
|
@@ -287,7 +321,7 @@ Add the new gateway to the **Supported Gateways** table with the appropriate sta
|
|
|
287
321
|
- [ ] HBL payment gateway
|
|
288
322
|
- [ ] ABL payment gateway
|
|
289
323
|
- [ ] UBL payment gateway
|
|
290
|
-
- [
|
|
324
|
+
- [x] Standardised error handling across gateways
|
|
291
325
|
- [ ] Request/response logging middleware
|
|
292
326
|
- [ ] CI/CD + automated testing
|
|
293
327
|
|
|
@@ -2,10 +2,12 @@ from typing import Optional
|
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
4
|
|
|
5
|
+
from src.raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
6
|
+
|
|
5
7
|
from .headers.easypaisa import generate_auth_header
|
|
6
8
|
from .schemas.easypaisa import (
|
|
9
|
+
EasyaisaMAResponse,
|
|
7
10
|
EasyPaisaInquireTransactionResponse,
|
|
8
|
-
EasyPaisaMAResponse,
|
|
9
11
|
EasyPaisaOTCResponse,
|
|
10
12
|
)
|
|
11
13
|
|
|
@@ -32,13 +34,19 @@ class EasyPaisa:
|
|
|
32
34
|
)
|
|
33
35
|
|
|
34
36
|
async def _post(self, endpoint: str, payload: dict) -> dict:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
try:
|
|
38
|
+
header = generate_auth_header(self.username, self.password)
|
|
39
|
+
response = await self._client.post(
|
|
40
|
+
self.base_url + endpoint,
|
|
41
|
+
json=payload,
|
|
42
|
+
headers={"Authorization": header},
|
|
43
|
+
)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
return response.json()
|
|
46
|
+
except httpx.HTTPStatusError as e:
|
|
47
|
+
raise NetworkError(str(e)) from e
|
|
48
|
+
except httpx.RequestError as e:
|
|
49
|
+
raise NetworkError(str(e)) from e
|
|
42
50
|
|
|
43
51
|
async def pay_via_otc(
|
|
44
52
|
self, order_id: str, amount: str, email: str, msisdn: str, token_expiry: str
|
|
@@ -53,7 +61,13 @@ class EasyPaisa:
|
|
|
53
61
|
"tokenExpiry": token_expiry,
|
|
54
62
|
}
|
|
55
63
|
data = await self._post("initiate-otc-transaction", request_payload)
|
|
56
|
-
|
|
64
|
+
result = EasyPaisaOTCResponse(**data)
|
|
65
|
+
|
|
66
|
+
if result.responseCode != "0000":
|
|
67
|
+
raise PaymentError(
|
|
68
|
+
f"Payment failed: {result.responseCode}", response=result
|
|
69
|
+
)
|
|
70
|
+
return result
|
|
57
71
|
|
|
58
72
|
async def pay_via_ma(
|
|
59
73
|
self, order_id: str, amount: str, email: str, mobile_number: str
|
|
@@ -67,7 +81,13 @@ class EasyPaisa:
|
|
|
67
81
|
"emailAddress": email,
|
|
68
82
|
}
|
|
69
83
|
data = await self._post("initiate-ma-transaction", request_payload)
|
|
70
|
-
|
|
84
|
+
result = EasyaisaMAResponse(**data)
|
|
85
|
+
|
|
86
|
+
if result.responseCode != "0000":
|
|
87
|
+
raise PaymentError(
|
|
88
|
+
f"Payment failed: {result.responseCode}", response=result
|
|
89
|
+
)
|
|
90
|
+
return result
|
|
71
91
|
|
|
72
92
|
async def inquire_transaction_status(self, order_id: str, account_number: str):
|
|
73
93
|
request_payload = {
|
|
@@ -76,4 +96,10 @@ class EasyPaisa:
|
|
|
76
96
|
"accountNum": account_number,
|
|
77
97
|
}
|
|
78
98
|
data = await self._post("inquire-transaction", request_payload)
|
|
79
|
-
|
|
99
|
+
result = EasyPaisaInquireTransactionResponse(**data)
|
|
100
|
+
|
|
101
|
+
if result.responseCode != "0000":
|
|
102
|
+
raise PaymentError(
|
|
103
|
+
f"Payment failed: {result.responseCode}", response=result
|
|
104
|
+
)
|
|
105
|
+
return result
|
|
@@ -2,8 +2,9 @@ from hashlib import sha256
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def generate_secure_hash(hash_key: str, params: dict) -> str:
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
|
|
6
|
+
values = "&".join(param[1] for param in sorted(params.items()) if param[1])
|
|
7
|
+
my_str = hash_key + ("&" + values if values else "")
|
|
7
8
|
|
|
8
9
|
my_bytes = str.encode(my_str)
|
|
9
10
|
my_sha256 = sha256(my_bytes)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from src.raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
7
|
+
from src.raqm_core.headers.jazzcash import generate_secure_hash
|
|
8
|
+
from src.raqm_core.schemas.jazzcash import JazzcashMWalletRequest, JazzcashResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JazzCash:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
merchant_id: str,
|
|
15
|
+
sub_merchant_id: str,
|
|
16
|
+
hash_key: str,
|
|
17
|
+
password: str,
|
|
18
|
+
sandbox: bool,
|
|
19
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
20
|
+
):
|
|
21
|
+
self.merchant_id = merchant_id
|
|
22
|
+
self.sub_merchant_id = sub_merchant_id
|
|
23
|
+
self.password = password
|
|
24
|
+
self.sandbox = sandbox
|
|
25
|
+
self._client = client or httpx.AsyncClient()
|
|
26
|
+
self.hash_key = hash_key
|
|
27
|
+
self.base_url = (
|
|
28
|
+
"https://sandbox.jazzcash.com.pk/ApplicationAPI/API/2.0/"
|
|
29
|
+
if self.sandbox
|
|
30
|
+
else "https://payments.jazzcash.com.pk/ApplicationAPI/API/2.0/"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def _post(self, endpoint: str, payload: dict) -> dict:
|
|
34
|
+
try:
|
|
35
|
+
response = await self._client.post(
|
|
36
|
+
self.base_url + endpoint,
|
|
37
|
+
json=payload,
|
|
38
|
+
)
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
return response.json()
|
|
41
|
+
except httpx.HTTPStatusError as e:
|
|
42
|
+
raise NetworkError(str(e)) from e
|
|
43
|
+
except httpx.RequestError as e:
|
|
44
|
+
raise NetworkError(str(e)) from e
|
|
45
|
+
|
|
46
|
+
async def pay_via_mwallet(
|
|
47
|
+
self,
|
|
48
|
+
mobile_number: str,
|
|
49
|
+
amount: str,
|
|
50
|
+
description: str,
|
|
51
|
+
bill_reference: str,
|
|
52
|
+
txn_ref_no: str,
|
|
53
|
+
) -> (
|
|
54
|
+
JazzcashResponse
|
|
55
|
+
): # txn_ref_no — unique transaction ID, developer generates this
|
|
56
|
+
now = datetime.now()
|
|
57
|
+
|
|
58
|
+
pp_TxnDateTime = now.strftime("%Y%m%d%H%M%S")
|
|
59
|
+
pp_TxnExpiryDateTime = (now + timedelta(hours=1)).strftime("%Y%m%d%H%M%S")
|
|
60
|
+
|
|
61
|
+
request = JazzcashMWalletRequest(
|
|
62
|
+
pp_Language="EN",
|
|
63
|
+
pp_MerchantID=self.merchant_id,
|
|
64
|
+
pp_SubMerchantID=self.sub_merchant_id,
|
|
65
|
+
pp_Password=self.password,
|
|
66
|
+
pp_TxnRefNo=txn_ref_no,
|
|
67
|
+
pp_Amount=amount,
|
|
68
|
+
pp_TxnCurrency="PKR",
|
|
69
|
+
pp_TxnDateTime=pp_TxnDateTime,
|
|
70
|
+
pp_BillReference=bill_reference,
|
|
71
|
+
pp_Description=description,
|
|
72
|
+
pp_TxnExpiryDateTime=pp_TxnExpiryDateTime,
|
|
73
|
+
pp_MobileNumber=mobile_number,
|
|
74
|
+
) # type: ignore
|
|
75
|
+
|
|
76
|
+
payload = request.model_dump()
|
|
77
|
+
secure_hash = generate_secure_hash(hash_key=self.hash_key, params=payload)
|
|
78
|
+
|
|
79
|
+
payload["pp_SecureHash"] = secure_hash
|
|
80
|
+
data = await self._post(endpoint="DoMWalletTransaction", payload=payload)
|
|
81
|
+
|
|
82
|
+
result = JazzcashResponse(**data)
|
|
83
|
+
|
|
84
|
+
if result.pp_ResponseCode != "000":
|
|
85
|
+
raise PaymentError(
|
|
86
|
+
f"Payment failed: {result.pp_ResponseCode}", response=result
|
|
87
|
+
)
|
|
88
|
+
return result
|
|
@@ -19,7 +19,7 @@ class EasypaisaResponseCode(str, Enum):
|
|
|
19
19
|
INVALID_EXPIRY = "0016" # date should be future date
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class
|
|
22
|
+
class EasypaisaResponse(BaseModel):
|
|
23
23
|
orderId: str = Field(
|
|
24
24
|
..., min_length=1, description="Merchant’s system generated Order ID"
|
|
25
25
|
)
|
|
@@ -40,18 +40,18 @@ class EasyPaisaResponse(BaseModel):
|
|
|
40
40
|
)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
class
|
|
43
|
+
class EasyaisaMAResponse(EasypaisaResponse):
|
|
44
44
|
transactionId: str = Field(..., description="Transaction ID of Ericsson (EWP ID)")
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class EasyPaisaOTCResponse(
|
|
47
|
+
class EasyPaisaOTCResponse(EasypaisaResponse):
|
|
48
48
|
paymentToken: str = Field(..., min_length=1, description="Token generated by OTC")
|
|
49
49
|
paymentTokenExpiryDateTime: str = Field(
|
|
50
50
|
..., description="Format = dd/MM/yyyy hh:mm [AM/PM]"
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
class EasyPaisaInquireTransactionResponse(
|
|
54
|
+
class EasyPaisaInquireTransactionResponse(EasypaisaResponse):
|
|
55
55
|
accountNum: str = Field(
|
|
56
56
|
..., min_length=1, description="Merchant’s EWP Account Number"
|
|
57
57
|
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JazzCashResponseCode(str, Enum):
|
|
8
|
+
SUCCESS = "000"
|
|
9
|
+
LIMIT_EXCEEDED = "001"
|
|
10
|
+
ACCOUNT_NOT_FOUND = "002"
|
|
11
|
+
ACCOUNT_INACTIVE = "003"
|
|
12
|
+
LOW_BALANCE = "004"
|
|
13
|
+
INVALID_HASH = "115"
|
|
14
|
+
SYSTEM_ERROR = "199"
|
|
15
|
+
TRANSACTION_TIMED_OUT = "403"
|
|
16
|
+
INSUFFICIENT_BALANCE = "405"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── REQUEST MODELS ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JazzcashRequest(BaseModel):
|
|
23
|
+
pp_Version: str = Field("1.1", description="Payment Portal Version.")
|
|
24
|
+
pp_Language: str = Field("EN", description="Display language. Fixed value 'EN'.")
|
|
25
|
+
pp_MerchantID: str = Field(
|
|
26
|
+
..., description="Unique merchant ID assigned by JazzCash."
|
|
27
|
+
)
|
|
28
|
+
pp_SubMerchantID: str = Field(
|
|
29
|
+
"", description="Sub merchant ID, leave empty if unused."
|
|
30
|
+
)
|
|
31
|
+
pp_Password: str = Field(..., description="Password assigned by JazzCash.")
|
|
32
|
+
pp_BankID: str = Field("", description="Bank identifier, leave empty if unused.")
|
|
33
|
+
pp_ProductID: str = Field(
|
|
34
|
+
"", description="Product identifier, leave empty if unused."
|
|
35
|
+
)
|
|
36
|
+
pp_TxnRefNo: str = Field(..., description="Unique transaction reference number.")
|
|
37
|
+
pp_Amount: str = Field(..., description="Transaction amount, no decimal places.")
|
|
38
|
+
pp_TxnCurrency: str = Field(
|
|
39
|
+
"PKR", description="Transaction currency. Fixed value 'PKR'."
|
|
40
|
+
)
|
|
41
|
+
pp_TxnDateTime: str = Field(
|
|
42
|
+
..., description="Transaction datetime. Format: yyyyMMddHHmmss."
|
|
43
|
+
)
|
|
44
|
+
pp_BillReference: str = Field(..., description="Bill/invoice number being settled.")
|
|
45
|
+
pp_Description: str = Field(..., description="Transaction description.")
|
|
46
|
+
pp_TxnExpiryDateTime: str = Field(
|
|
47
|
+
..., description="Expiry datetime. Format: yyyyMMddHHmmss."
|
|
48
|
+
)
|
|
49
|
+
ppmpf_1: str = ""
|
|
50
|
+
ppmpf_2: str = ""
|
|
51
|
+
ppmpf_3: str = ""
|
|
52
|
+
ppmpf_4: str = ""
|
|
53
|
+
ppmpf_5: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class JazzcashMWalletRequest(JazzcashRequest):
|
|
57
|
+
pp_TxnType: str = Field("MWALLET", description="Transaction type.")
|
|
58
|
+
pp_MobileNumber: str = Field(..., description="Customer's JazzCash mobile number.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── RESPONSE MODELS ───────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class JazzcashResponse(BaseModel):
|
|
65
|
+
pp_Amount: str = Field(..., description="Transaction amount.")
|
|
66
|
+
pp_AuthCode: Optional[str] = None
|
|
67
|
+
pp_BankID: Optional[str] = None
|
|
68
|
+
pp_BillReference: Optional[str] = None
|
|
69
|
+
pp_Language: Optional[str] = None
|
|
70
|
+
pp_MerchantID: str = Field(..., description="Merchant ID.")
|
|
71
|
+
pp_ResponseCode: Union[JazzCashResponseCode, str] = Field(
|
|
72
|
+
..., description="Response code."
|
|
73
|
+
)
|
|
74
|
+
pp_ResponseMessage: str = Field(..., description="Response message.")
|
|
75
|
+
pp_RetreivalReferenceNo: Optional[str] = None
|
|
76
|
+
pp_SubMerchantId: Optional[str] = None
|
|
77
|
+
pp_TxnCurrency: Optional[str] = None
|
|
78
|
+
pp_TxnDateTime: Optional[str] = None
|
|
79
|
+
pp_TxnRefNo: str = Field(..., description="Transaction reference number.")
|
|
80
|
+
pp_SettlementExpiry: Optional[str] = None
|
|
81
|
+
pp_TxnType: Optional[str] = None
|
|
82
|
+
pp_Version: Optional[str] = None
|
|
83
|
+
ppmbf_1: Optional[str] = None
|
|
84
|
+
ppmbf_2: Optional[str] = None
|
|
85
|
+
ppmbf_3: Optional[str] = None
|
|
86
|
+
ppmbf_4: Optional[str] = None
|
|
87
|
+
ppmbf_5: Optional[str] = None
|
|
88
|
+
ppmpf_1: Optional[str] = None
|
|
89
|
+
ppmpf_2: Optional[str] = None
|
|
90
|
+
ppmpf_3: Optional[str] = None
|
|
91
|
+
ppmpf_4: Optional[str] = None
|
|
92
|
+
ppmpf_5: Optional[str] = None
|
|
93
|
+
pp_SecureHash: Optional[str] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: raqm-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Unified Python SDK for Pakistani payment gateways (EasyPaisa, JazzCash, HBL, ABL, UBL, etc.)
|
|
5
5
|
Classifier: Programming Language :: Python :: 3.9
|
|
6
6
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -19,7 +19,9 @@ Requires-Dist: pydantic>=2.0
|
|
|
19
19
|
|
|
20
20
|
One async, type-safe interface across multiple processors — EasyPaisa, JazzCash, HBL, ABL, UBL, and more.
|
|
21
21
|
|
|
22
|
-
> ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. Contributions welcome!
|
|
22
|
+
> ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. JazzCash is in progress. Contributions welcome!
|
|
23
|
+
>
|
|
24
|
+
> ⚠️ **Breaking change in v0.2.4** — methods now **raise `PaymentError`** on business-level failures instead of returning a result with a non-success response code. Wrap calls in try/except to handle errors gracefully.
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
@@ -28,7 +30,7 @@ One async, type-safe interface across multiple processors — EasyPaisa, JazzCas
|
|
|
28
30
|
| Gateway | Status |
|
|
29
31
|
|---------|--------|
|
|
30
32
|
| [EasyPaisa](https://easypaisa.com.pk) | ✅ Live |
|
|
31
|
-
| [JazzCash](https://jazzcash.com.pk) | 🚧
|
|
33
|
+
| [JazzCash](https://jazzcash.com.pk) | 🚧 In progress |
|
|
32
34
|
| HBL | 📅 Planned |
|
|
33
35
|
| ABL | 📅 Planned |
|
|
34
36
|
| UBL | 📅 Planned |
|
|
@@ -173,6 +175,33 @@ result.paymentMode # str ("MA", "OTC", "CC")
|
|
|
173
175
|
|
|
174
176
|
---
|
|
175
177
|
|
|
178
|
+
## Error Handling
|
|
179
|
+
|
|
180
|
+
All gateway methods raise typed exceptions on failure:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
RaqmCoreError # Base exception for the SDK
|
|
184
|
+
├── NetworkError # HTTP/transport errors (connection, timeout, 4xx/5xx)
|
|
185
|
+
└── PaymentError # Business-level failures (non-success response code)
|
|
186
|
+
# .response — the full gateway response object
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
result = await ep.pay_via_ma(...)
|
|
194
|
+
except NetworkError as e:
|
|
195
|
+
print(f"Network issue: {e}")
|
|
196
|
+
except PaymentError as e:
|
|
197
|
+
print(f"Payment failed: {e}")
|
|
198
|
+
print(f"Response code: {e.response.responseCode}")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`PaymentError` carries the gateway's response object in `exc.response`, so you can inspect the raw error details even when the SDK raises.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
176
205
|
## Architecture
|
|
177
206
|
|
|
178
207
|
Each payment gateway follows a consistent 3-layer structure:
|
|
@@ -190,15 +219,20 @@ Current structure:
|
|
|
190
219
|
|
|
191
220
|
```
|
|
192
221
|
src/
|
|
193
|
-
├── easypaisa.py
|
|
222
|
+
├── easypaisa.py # EasyPaisa client
|
|
223
|
+
├── jazzcash.py # JazzCash client
|
|
224
|
+
├── exceptions/
|
|
225
|
+
│ └── exceptions.py # Custom exception hierarchy
|
|
194
226
|
├── headers/
|
|
195
|
-
│ ├── easypaisa.py
|
|
196
|
-
│ └── jazzcash.py
|
|
227
|
+
│ ├── easypaisa.py # Basic Auth header
|
|
228
|
+
│ └── jazzcash.py # SHA-256 secure hash
|
|
197
229
|
└── schemas/
|
|
198
|
-
|
|
230
|
+
├── easypaisa.py # EasyPaisa Pydantic models
|
|
231
|
+
└── jazzcash.py # JazzCash Pydantic models
|
|
199
232
|
|
|
200
233
|
tests/
|
|
201
234
|
├── test_easypaisa.py # EasyPaisa integration tests
|
|
235
|
+
├── test_jazzcash.py # JazzCash integration tests
|
|
202
236
|
└── headers/
|
|
203
237
|
├── test_easypaisa.py # Auth header unit tests
|
|
204
238
|
└── test_jazzcash.py # Secure hash unit tests
|
|
@@ -302,7 +336,7 @@ Add the new gateway to the **Supported Gateways** table with the appropriate sta
|
|
|
302
336
|
- [ ] HBL payment gateway
|
|
303
337
|
- [ ] ABL payment gateway
|
|
304
338
|
- [ ] UBL payment gateway
|
|
305
|
-
- [
|
|
339
|
+
- [x] Standardised error handling across gateways
|
|
306
340
|
- [ ] Request/response logging middleware
|
|
307
341
|
- [ ] CI/CD + automated testing
|
|
308
342
|
|
|
@@ -2,14 +2,18 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
src/raqm_core/__init__.py
|
|
4
4
|
src/raqm_core/easypaisa.py
|
|
5
|
+
src/raqm_core/jazzcash.py
|
|
5
6
|
src/raqm_core.egg-info/PKG-INFO
|
|
6
7
|
src/raqm_core.egg-info/SOURCES.txt
|
|
7
8
|
src/raqm_core.egg-info/dependency_links.txt
|
|
8
9
|
src/raqm_core.egg-info/requires.txt
|
|
9
10
|
src/raqm_core.egg-info/top_level.txt
|
|
11
|
+
src/raqm_core/exceptions/exceptions.py
|
|
10
12
|
src/raqm_core/headers/__init__.py
|
|
11
13
|
src/raqm_core/headers/easypaisa.py
|
|
12
14
|
src/raqm_core/headers/jazzcash.py
|
|
13
15
|
src/raqm_core/schemas/__init__.py
|
|
14
16
|
src/raqm_core/schemas/easypaisa.py
|
|
15
|
-
|
|
17
|
+
src/raqm_core/schemas/jazzcash.py
|
|
18
|
+
tests/test_easypaisa.py
|
|
19
|
+
tests/test_jazzcash.py
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import httpx
|
|
2
2
|
import pytest
|
|
3
3
|
|
|
4
|
+
from src.raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
4
5
|
from src.raqm_core.easypaisa import EasyPaisa
|
|
5
6
|
|
|
6
7
|
MA_SUCCESS_BODY = {
|
|
@@ -40,9 +41,9 @@ INQUIRE_SUCCESS_BODY = {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def make_client(response_body: dict) -> httpx.AsyncClient:
|
|
44
|
+
def make_client(response_body: dict, status_code: int = 200) -> httpx.AsyncClient:
|
|
44
45
|
def handler(request: httpx.Request) -> httpx.Response:
|
|
45
|
-
return httpx.Response(status_code=
|
|
46
|
+
return httpx.Response(status_code=status_code, json=response_body)
|
|
46
47
|
|
|
47
48
|
return httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
48
49
|
|
|
@@ -83,13 +84,14 @@ class TestPayViaMA:
|
|
|
83
84
|
async def test_error_codes(self, body, expected_code):
|
|
84
85
|
payload = {**MA_SUCCESS_BODY, **body}
|
|
85
86
|
ep = make_ep(payload)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
with pytest.raises(PaymentError) as exc_info:
|
|
88
|
+
await ep.pay_via_ma(
|
|
89
|
+
order_id="abc123",
|
|
90
|
+
amount="1.23",
|
|
91
|
+
email="test@example.com",
|
|
92
|
+
mobile_number="03458508726",
|
|
93
|
+
)
|
|
94
|
+
assert exc_info.value.response.responseCode == expected_code
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
class TestPayViaOTC:
|
|
@@ -119,14 +121,15 @@ class TestPayViaOTC:
|
|
|
119
121
|
async def test_error_codes(self, body, expected_code):
|
|
120
122
|
payload = {**OTC_SUCCESS_BODY, **body}
|
|
121
123
|
ep = make_ep(payload)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
with pytest.raises(PaymentError) as exc_info:
|
|
125
|
+
await ep.pay_via_otc(
|
|
126
|
+
order_id="abc123",
|
|
127
|
+
amount="1.23",
|
|
128
|
+
email="test@example.com",
|
|
129
|
+
msisdn="03458508726",
|
|
130
|
+
token_expiry="11/12/2018 11:30 PM",
|
|
131
|
+
)
|
|
132
|
+
assert exc_info.value.response.responseCode == expected_code
|
|
130
133
|
|
|
131
134
|
|
|
132
135
|
class TestInquireTransaction:
|
|
@@ -156,7 +159,26 @@ class TestInquireTransaction:
|
|
|
156
159
|
async def test_error_codes(self, body, expected_code):
|
|
157
160
|
payload = {**INQUIRE_SUCCESS_BODY, **body}
|
|
158
161
|
ep = make_ep(payload)
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
with pytest.raises(PaymentError) as exc_info:
|
|
163
|
+
await ep.inquire_transaction_status(
|
|
164
|
+
order_id="abc123", account_number="123456789"
|
|
165
|
+
)
|
|
166
|
+
assert exc_info.value.response.responseCode == expected_code
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_network_error():
|
|
171
|
+
ep = EasyPaisa(
|
|
172
|
+
store_id="43",
|
|
173
|
+
username="admin",
|
|
174
|
+
password="123",
|
|
175
|
+
sandbox=True,
|
|
176
|
+
client=make_client({"error": "Internal Server Error"}, status_code=500),
|
|
177
|
+
)
|
|
178
|
+
with pytest.raises(NetworkError):
|
|
179
|
+
await ep.pay_via_ma(
|
|
180
|
+
order_id="abc123",
|
|
181
|
+
amount="1.23",
|
|
182
|
+
email="test@example.com",
|
|
183
|
+
mobile_number="03458508726",
|
|
161
184
|
)
|
|
162
|
-
assert result.responseCode == expected_code
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from src.raqm_core.exceptions.exceptions import NetworkError, PaymentError
|
|
5
|
+
from src.raqm_core.jazzcash import JazzCash
|
|
6
|
+
|
|
7
|
+
MWALLET_SUCCESS_BODY = {
|
|
8
|
+
"pp_Amount": "1000",
|
|
9
|
+
"pp_AuthCode": "123456",
|
|
10
|
+
"pp_BankID": "",
|
|
11
|
+
"pp_BillReference": "bill123",
|
|
12
|
+
"pp_Language": "EN",
|
|
13
|
+
"pp_MerchantID": "MERCH123",
|
|
14
|
+
"pp_ResponseCode": "000",
|
|
15
|
+
"pp_ResponseMessage": "Success",
|
|
16
|
+
"pp_RetreivalReferenceNo": "RET123",
|
|
17
|
+
"pp_SubMerchantId": "SUBMERCH123",
|
|
18
|
+
"pp_TxnCurrency": "PKR",
|
|
19
|
+
"pp_TxnDateTime": "20260101120000",
|
|
20
|
+
"pp_TxnRefNo": "TXN123456",
|
|
21
|
+
"pp_SettlementExpiry": "20260101130000",
|
|
22
|
+
"pp_TxnType": "MWALLET",
|
|
23
|
+
"pp_Version": "1.1",
|
|
24
|
+
"pp_SecureHash": "abc123hash",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def make_client(response_body: dict, status_code: int = 200) -> httpx.AsyncClient:
|
|
29
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
30
|
+
return httpx.Response(status_code=status_code, json=response_body)
|
|
31
|
+
|
|
32
|
+
return httpx.AsyncClient(transport=httpx.MockTransport(handler))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_jc(response_body: dict) -> JazzCash:
|
|
36
|
+
return JazzCash(
|
|
37
|
+
merchant_id="MERCH123",
|
|
38
|
+
sub_merchant_id="SUBMERCH123",
|
|
39
|
+
hash_key="test_hash_key",
|
|
40
|
+
password="test_password",
|
|
41
|
+
sandbox=True,
|
|
42
|
+
client=make_client(response_body),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestPayViaMWallet:
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_success(self):
|
|
49
|
+
jc = make_jc(MWALLET_SUCCESS_BODY)
|
|
50
|
+
result = await jc.pay_via_mwallet(
|
|
51
|
+
mobile_number="03001234567",
|
|
52
|
+
amount="1000",
|
|
53
|
+
description="Test payment",
|
|
54
|
+
bill_reference="bill123",
|
|
55
|
+
txn_ref_no="TXN123456",
|
|
56
|
+
)
|
|
57
|
+
assert result.pp_ResponseCode == "000"
|
|
58
|
+
assert result.pp_ResponseMessage == "Success"
|
|
59
|
+
assert result.pp_TxnRefNo == "TXN123456"
|
|
60
|
+
assert result.pp_Amount == "1000"
|
|
61
|
+
assert result.pp_MerchantID == "MERCH123"
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize(
|
|
64
|
+
("body", "expected_code"),
|
|
65
|
+
[
|
|
66
|
+
({"pp_ResponseCode": "001", "pp_ResponseMessage": "LIMIT EXCEEDED"}, "001"),
|
|
67
|
+
({"pp_ResponseCode": "002", "pp_ResponseMessage": "ACCOUNT NOT FOUND"}, "002"),
|
|
68
|
+
({"pp_ResponseCode": "115", "pp_ResponseMessage": "INVALID HASH"}, "115"),
|
|
69
|
+
({"pp_ResponseCode": "199", "pp_ResponseMessage": "SYSTEM ERROR"}, "199"),
|
|
70
|
+
({"pp_ResponseCode": "403", "pp_ResponseMessage": "TRANSACTION TIMED OUT"}, "403"),
|
|
71
|
+
({"pp_ResponseCode": "405", "pp_ResponseMessage": "INSUFFICIENT BALANCE"}, "405"),
|
|
72
|
+
],
|
|
73
|
+
)
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_error_codes(self, body, expected_code):
|
|
76
|
+
payload = {**MWALLET_SUCCESS_BODY, **body}
|
|
77
|
+
jc = make_jc(payload)
|
|
78
|
+
with pytest.raises(PaymentError) as exc_info:
|
|
79
|
+
await jc.pay_via_mwallet(
|
|
80
|
+
mobile_number="03001234567",
|
|
81
|
+
amount="1000",
|
|
82
|
+
description="Test payment",
|
|
83
|
+
bill_reference="bill123",
|
|
84
|
+
txn_ref_no="TXN123456",
|
|
85
|
+
)
|
|
86
|
+
assert exc_info.value.response.pp_ResponseCode == expected_code
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_network_error(self):
|
|
90
|
+
jc = JazzCash(
|
|
91
|
+
merchant_id="MERCH123",
|
|
92
|
+
sub_merchant_id="SUBMERCH123",
|
|
93
|
+
hash_key="test_hash_key",
|
|
94
|
+
password="test_password",
|
|
95
|
+
sandbox=True,
|
|
96
|
+
client=make_client({"error": "Internal Server Error"}, status_code=500),
|
|
97
|
+
)
|
|
98
|
+
with pytest.raises(NetworkError):
|
|
99
|
+
await jc.pay_via_mwallet(
|
|
100
|
+
mobile_number="03001234567",
|
|
101
|
+
amount="1000",
|
|
102
|
+
description="Test payment",
|
|
103
|
+
bill_reference="bill123",
|
|
104
|
+
txn_ref_no="TXN123456",
|
|
105
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|