raqm-core 0.2.0__tar.gz → 0.2.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: raqm-core
3
- Version: 0.2.0
3
+ Version: 0.2.5
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) | 🚧 Header utils ready |
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 # EasyPaisa client
222
+ ├── easypaisa.py # EasyPaisa client
223
+ ├── jazzcash.py # JazzCash client
224
+ ├── exceptions/
225
+ │ └── exceptions.py # Custom exception hierarchy
194
226
  ├── headers/
195
- │ ├── easypaisa.py # Basic Auth header
196
- │ └── jazzcash.py # SHA-256 secure hash
227
+ │ ├── easypaisa.py # Basic Auth header
228
+ │ └── jazzcash.py # SHA-256 secure hash
197
229
  └── schemas/
198
- └── easypaisa.py # EasyPaisa Pydantic models
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
- - [ ] Standardised error handling across gateways
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) | 🚧 Header utils ready |
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 # EasyPaisa client
207
+ ├── easypaisa.py # EasyPaisa client
208
+ ├── jazzcash.py # JazzCash client
209
+ ├── exceptions/
210
+ │ └── exceptions.py # Custom exception hierarchy
179
211
  ├── headers/
180
- │ ├── easypaisa.py # Basic Auth header
181
- │ └── jazzcash.py # SHA-256 secure hash
212
+ │ ├── easypaisa.py # Basic Auth header
213
+ │ └── jazzcash.py # SHA-256 secure hash
182
214
  └── schemas/
183
- └── easypaisa.py # EasyPaisa Pydantic models
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
- - [ ] Standardised error handling across gateways
324
+ - [x] Standardised error handling across gateways
291
325
  - [ ] Request/response logging middleware
292
326
  - [ ] CI/CD + automated testing
293
327
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "raqm-core"
3
- version = "0.2.0"
3
+ version = "0.2.5"
4
4
  description = "Unified Python SDK for Pakistani payment gateways (EasyPaisa, JazzCash, HBL, ABL, UBL, etc.)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -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
- header = generate_auth_header(self.username, self.password)
36
- response = await self._client.post(
37
- self.base_url + endpoint,
38
- json=payload,
39
- headers={"Authorization": header},
40
- )
41
- return response.json()
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
- return EasyPaisaOTCResponse(**data)
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
- return EasyPaisaMAResponse(**data)
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
- return EasyPaisaInquireTransactionResponse(**data)
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
@@ -0,0 +1,12 @@
1
+ class RaqmCoreError(Exception):
2
+ pass
3
+
4
+
5
+ class NetworkError(RaqmCoreError):
6
+ pass
7
+
8
+
9
+ class PaymentError(RaqmCoreError):
10
+ def __init__(self, message: str, response=None):
11
+ self.response = response
12
+ super().__init__(message)
@@ -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
- values = "&".join(param[1] for param in sorted(params.items()))
6
- my_str = hash_key + "&" + values
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 EasyPaisaResponse(BaseModel):
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 EasyPaisaMAResponse(EasyPaisaResponse):
43
+ class EasyaisaMAResponse(EasypaisaResponse):
44
44
  transactionId: str = Field(..., description="Transaction ID of Ericsson (EWP ID)")
45
45
 
46
46
 
47
- class EasyPaisaOTCResponse(EasyPaisaResponse):
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(EasyPaisaResponse):
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.0
3
+ Version: 0.2.5
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) | 🚧 Header utils ready |
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 # EasyPaisa client
222
+ ├── easypaisa.py # EasyPaisa client
223
+ ├── jazzcash.py # JazzCash client
224
+ ├── exceptions/
225
+ │ └── exceptions.py # Custom exception hierarchy
194
226
  ├── headers/
195
- │ ├── easypaisa.py # Basic Auth header
196
- │ └── jazzcash.py # SHA-256 secure hash
227
+ │ ├── easypaisa.py # Basic Auth header
228
+ │ └── jazzcash.py # SHA-256 secure hash
197
229
  └── schemas/
198
- └── easypaisa.py # EasyPaisa Pydantic models
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
- - [ ] Standardised error handling across gateways
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
- tests/test_easypaisa.py
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=200, json=response_body)
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
- result = await ep.pay_via_ma(
87
- order_id="abc123",
88
- amount="1.23",
89
- email="test@example.com",
90
- mobile_number="03458508726",
91
- )
92
- assert result.responseCode == expected_code
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
- result = await ep.pay_via_otc(
123
- order_id="abc123",
124
- amount="1.23",
125
- email="test@example.com",
126
- msisdn="03458508726",
127
- token_expiry="11/12/2018 11:30 PM",
128
- )
129
- assert result.responseCode == expected_code
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
- result = await ep.inquire_transaction_status(
160
- order_id="abc123", account_number="123456789"
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