raqm-core 0.1.0__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.
raqm_core/__init__.py ADDED
File without changes
raqm_core/easypaisa.py ADDED
@@ -0,0 +1,77 @@
1
+ import httpx
2
+
3
+ from .headers.easypaisa import generate_auth_header
4
+ from .schemas.easypaisa import (
5
+ EasyPaisaInquireTransactionResponse,
6
+ EasyPaisaMAResponse,
7
+ EasyPaisaOTCResponse,
8
+ )
9
+
10
+
11
+ class EasyPaisa:
12
+ def __init__(
13
+ self,
14
+ store_id: str,
15
+ username: str,
16
+ password: str,
17
+ sandbox: bool,
18
+ client: httpx.AsyncClient | None = None,
19
+ ):
20
+ self.store_id = store_id
21
+ self.username = username
22
+ self.password = password
23
+ self.sandbox = sandbox
24
+ self._client = client or httpx.AsyncClient()
25
+
26
+ self.base_url = (
27
+ "https://easypaystg.easypaisa.com.pk/easypay-service/rest/v4/"
28
+ if self.sandbox
29
+ else "https://easypay.easypaisa.com.pk/easypay-service/rest/v4/"
30
+ )
31
+
32
+ async def _post(self, endpoint: str, payload: dict) -> dict:
33
+ header = generate_auth_header(self.username, self.password)
34
+ response = await self._client.post(
35
+ self.base_url + endpoint,
36
+ json=payload,
37
+ headers={"Authorization": header},
38
+ )
39
+ return response.json()
40
+
41
+ async def pay_via_otc(
42
+ self, order_id: str, amount: str, email: str, msisdn: str, token_expiry: str
43
+ ):
44
+ request_payload = {
45
+ "orderId": order_id,
46
+ "storeId": self.store_id,
47
+ "transactionAmount": amount,
48
+ "transactionType": "OTC",
49
+ "msisdn": msisdn,
50
+ "emailAddress": email,
51
+ "tokenExpiry": token_expiry,
52
+ }
53
+ data = await self._post("initiate-otc-transaction", request_payload)
54
+ return EasyPaisaOTCResponse(**data)
55
+
56
+ async def pay_via_ma(
57
+ self, order_id: str, amount: str, email: str, mobile_number: str
58
+ ):
59
+ request_payload = {
60
+ "orderId": order_id,
61
+ "storeId": self.store_id,
62
+ "transactionAmount": amount,
63
+ "transactionType": "MA",
64
+ "mobileAccountNo": mobile_number,
65
+ "emailAddress": email,
66
+ }
67
+ data = await self._post("initiate-ma-transaction", request_payload)
68
+ return EasyPaisaMAResponse(**data)
69
+
70
+ async def inquire_transaction_status(self, order_id: str, account_number: str):
71
+ request_payload = {
72
+ "orderId": order_id,
73
+ "storeId": self.store_id,
74
+ "accountNum": account_number,
75
+ }
76
+ data = await self._post("inquire-transaction", request_payload)
77
+ return EasyPaisaInquireTransactionResponse(**data)
File without changes
@@ -0,0 +1,7 @@
1
+ import base64
2
+
3
+
4
+ def generate_auth_header(username: str, password: str) -> str:
5
+ encoded_string = str.encode(username + ":" + password)
6
+ base64_encoded = base64.b64encode(encoded_string)
7
+ return "Basic " + base64_encoded.decode()
@@ -0,0 +1,10 @@
1
+ from hashlib import sha256
2
+
3
+
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
7
+
8
+ my_bytes = str.encode(my_str)
9
+ my_sha256 = sha256(my_bytes)
10
+ return my_sha256.hexdigest()
File without changes
@@ -0,0 +1,66 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class EasypaisaResponseCode(str, Enum):
8
+ SUCCESS = "0000"
9
+ SYSTEM_ERROR = "0001"
10
+ REQUIRED_FIELD_MISSING = "0002"
11
+ MERCHANT_ACCOUNT_NOT_ACTIVE = "0005"
12
+ INVALID_STORE_ID = "0006"
13
+ STORE_NOT_ACTIVE = "0007"
14
+ PAYMENT_METHOD_NOT_ENABLED = "0008"
15
+ INVALID_CREDENTIALS = "0010"
16
+ LOW_BALANCE = "0013"
17
+ ACCOUNT_DOES_NOT_EXIST = "0014"
18
+ INVALID_TOKEN_EXPIRY = "0015"
19
+ INVALID_EXPIRY = "0016" # date should be future date
20
+
21
+
22
+ class EasyPaisaResponse(BaseModel):
23
+ orderId: str = Field(
24
+ ..., min_length=1, description="Merchant’s system generated Order ID"
25
+ )
26
+ storeId: int = Field(
27
+ ge=0, description="Store ID generated during merchant registration in Easypaisa"
28
+ )
29
+ transactionDateTime: str = Field(
30
+ ..., description="Format = dd/MM/yyyy hh:mm [AM/PM]"
31
+ )
32
+ responseCode: EasypaisaResponseCode = Field(
33
+ ...,
34
+ min_length=4,
35
+ max_length=4,
36
+ description="Easypaisa generated response codes",
37
+ )
38
+ responseDesc: str = Field(
39
+ ..., min_length=3, description="Easypaisa generated response descriptions"
40
+ )
41
+
42
+
43
+ class EasyPaisaMAResponse(EasyPaisaResponse):
44
+ transactionId: str = Field(..., description="Transaction ID of Ericsson (EWP ID)")
45
+
46
+
47
+ class EasyPaisaOTCResponse(EasyPaisaResponse):
48
+ paymentToken: str = Field(..., min_length=1, description="Token generated by OTC")
49
+ paymentTokenExpiryDateTime: str = Field(
50
+ ..., description="Format = dd/MM/yyyy hh:mm [AM/PM]"
51
+ )
52
+
53
+
54
+ class EasyPaisaInquireTransactionResponse(EasyPaisaResponse):
55
+ accountNum: str = Field(
56
+ ..., min_length=1, description="Merchant’s EWP Account Number"
57
+ )
58
+ storeName: str = Field(..., description="Store Name")
59
+ paymentToken: Optional[str] = Field(None, description="Token generated by OTC")
60
+ transactionStatus: str = Field(..., description="The status of transaction")
61
+ transactionAmount: str = Field(..., description="Total transaction amount ")
62
+ paymentTokenExpiryDateTime: Optional[str] = Field(
63
+ None, description="[Only For OTC] Format = dd/MM/yyyy hh:mm [AM/PM]"
64
+ )
65
+ msisdn: str = Field(..., description="Customer MSISDN")
66
+ paymentMode: str = Field(..., description="Type of transaction: OTC, MA, CC")
@@ -0,0 +1,335 @@
1
+ Metadata-Version: 2.4
2
+ Name: raqm-core
3
+ Version: 0.1.0
4
+ Summary: Unified Python SDK for Pakistani payment gateways (EasyPaisa, JazzCash, HBL, ABL, UBL, etc.)
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: pydantic>=2.0
9
+
10
+ # raqm-core
11
+
12
+ **Unified Python SDK for Pakistani payment gateways.**
13
+
14
+ One async, type-safe interface across multiple processors — EasyPaisa, JazzCash, HBL, ABL, UBL, and more.
15
+
16
+ > ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. Contributions welcome!
17
+
18
+ ---
19
+
20
+ ## Supported Gateways
21
+
22
+ | Gateway | Status |
23
+ |---------|--------|
24
+ | [EasyPaisa](https://easypaisa.com.pk) | ✅ Live |
25
+ | [JazzCash](https://jazzcash.com.pk) | 🚧 Header utils ready |
26
+ | HBL | 📅 Planned |
27
+ | ABL | 📅 Planned |
28
+ | UBL | 📅 Planned |
29
+ | … | 📅 Your gateway here |
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ - **Async-first** — built on `httpx.AsyncClient` for non-blocking I/O
36
+ - **Type-safe** — all responses are validated through Pydantic models
37
+ - **Consistent API** — same patterns across all gateways
38
+ - **Mock-friendly** — inject your own `httpx.AsyncClient` for testing
39
+ - **Minimal dependencies** — just `httpx` and `pydantic`
40
+
41
+ ---
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install raqm-core
47
+ ```
48
+
49
+ Or with `uv`:
50
+
51
+ ```bash
52
+ uv add raqm-core
53
+ ```
54
+
55
+ > Requires Python 3.12+.
56
+
57
+ ---
58
+
59
+ ## Quick Start
60
+
61
+ ```python
62
+ import asyncio
63
+ from raqm_core import EasyPaisa
64
+
65
+
66
+ async def main():
67
+ ep = EasyPaisa(
68
+ store_id="43",
69
+ username="your_username",
70
+ password="your_password",
71
+ sandbox=True,
72
+ )
73
+
74
+ result = await ep.pay_via_ma(
75
+ order_id="order_123",
76
+ amount="1000.00",
77
+ email="customer@example.com",
78
+ mobile_number="03451234567",
79
+ )
80
+
81
+ print(f"Status: {result.responseCode} — {result.responseDesc}")
82
+ print(f"Transaction ID: {result.transactionId}")
83
+
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Usage
91
+
92
+ ### EasyPaisa
93
+
94
+ #### Initialisation
95
+
96
+ ```python
97
+ from raqm_core import EasyPaisa
98
+
99
+ ep = EasyPaisa(
100
+ store_id="43",
101
+ username="your_username",
102
+ password="your_password",
103
+ sandbox=True, # set False for production
104
+ )
105
+ ```
106
+
107
+ You can optionally inject your own `httpx.AsyncClient` — useful for testing with `httpx.MockTransport`:
108
+
109
+ ```python
110
+ import httpx
111
+
112
+ client = httpx.AsyncClient(...)
113
+ ep = EasyPaisa(store_id="43", username="...", password="...", sandbox=True, client=client)
114
+ ```
115
+
116
+ #### Mobile Account (MA) Payment
117
+
118
+ ```python
119
+ result = await ep.pay_via_ma(
120
+ order_id="order_123",
121
+ amount="500.00",
122
+ email="customer@example.com",
123
+ mobile_number="03451234567",
124
+ )
125
+
126
+ # EasyPaisaMAResponse fields:
127
+ result.orderId # str
128
+ result.storeId # int
129
+ result.transactionId # str
130
+ result.transactionDateTime # str (dd/MM/yyyy hh:mm AM/PM)
131
+ result.responseCode # EasypaisaResponseCode (enum)
132
+ result.responseDesc # str
133
+ ```
134
+
135
+ #### Over-the-Counter (OTC) Payment
136
+
137
+ ```python
138
+ result = await ep.pay_via_otc(
139
+ order_id="order_456",
140
+ amount="1500.00",
141
+ email="customer@example.com",
142
+ msisdn="03451234567",
143
+ token_expiry="01/01/2025 11:59 PM",
144
+ )
145
+
146
+ # EasyPaisaOTCResponse fields (extends base):
147
+ result.paymentToken # str
148
+ result.paymentTokenExpiryDateTime # str
149
+ ```
150
+
151
+ #### Transaction Inquiry
152
+
153
+ ```python
154
+ result = await ep.inquire_transaction_status(
155
+ order_id="order_123",
156
+ account_number="123456789",
157
+ )
158
+
159
+ # EasyPaisaInquireTransactionResponse fields:
160
+ result.transactionStatus # str (e.g. "COMPLETED")
161
+ result.transactionAmount # str
162
+ result.accountNum # str
163
+ result.storeName # str
164
+ result.msisdn # str
165
+ result.paymentMode # str ("MA", "OTC", "CC")
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Architecture
171
+
172
+ Each payment gateway follows a consistent 3-layer structure:
173
+
174
+ ```
175
+ src/
176
+ ├── <gateway>.py # Client class — public API
177
+ ├── headers/
178
+ │ └── <gateway>.py # Auth / signing helpers
179
+ └── schemas/
180
+ └── <gateway>.py # Pydantic request/response models
181
+ ```
182
+
183
+ Current structure:
184
+
185
+ ```
186
+ src/
187
+ ├── easypaisa.py # EasyPaisa client
188
+ ├── headers/
189
+ │ ├── easypaisa.py # Basic Auth header
190
+ │ └── jazzcash.py # SHA-256 secure hash
191
+ └── schemas/
192
+ └── easypaisa.py # EasyPaisa Pydantic models
193
+
194
+ tests/
195
+ ├── test_easypaisa.py # EasyPaisa integration tests
196
+ └── headers/
197
+ ├── test_easypaisa.py # Auth header unit tests
198
+ └── test_jazzcash.py # Secure hash unit tests
199
+ ```
200
+
201
+ ---
202
+
203
+ ## Adding a New Gateway
204
+
205
+ Want to add support for a new processor? Follow this checklist.
206
+
207
+ ### 1. Research the API
208
+
209
+ Understand the gateway's:
210
+ - Authentication mechanism (Basic Auth, HMAC, API key, etc.)
211
+ - Endpoints and request/response formats
212
+ - Error/response codes
213
+
214
+ ### 2. Create header helpers
215
+
216
+ `src/headers/<gateway>.py` — authentication or signing logic.
217
+
218
+ ```python
219
+ # src/headers/hbl.py
220
+ def generate_signature(api_key: str, payload: dict) -> str:
221
+ ...
222
+ ```
223
+
224
+ Write unit tests in `tests/headers/test_<gateway>.py`.
225
+
226
+ ### 3. Create Pydantic schemas
227
+
228
+ `src/schemas/<gateway>.py` — response models and enums.
229
+
230
+ ```python
231
+ # src/schemas/hbl.py
232
+ from pydantic import BaseModel, Field
233
+
234
+
235
+ class HBLResponse(BaseModel):
236
+ orderId: str = Field(...)
237
+ responseCode: str = Field(...)
238
+ responseDesc: str = Field(...)
239
+ ```
240
+
241
+ ### 4. Create the client class
242
+
243
+ `src/<gateway>.py` — async client with a `_post()` helper and public methods.
244
+
245
+ ```python
246
+ # src/hbl.py
247
+ import httpx
248
+ from .headers.hbl import generate_signature
249
+ from .schemas.hbl import HBLResponse
250
+
251
+
252
+ class HBL:
253
+ def __init__(self, api_key: str, sandbox: bool, client: httpx.AsyncClient | None = None):
254
+ self._client = client or httpx.AsyncClient()
255
+ ...
256
+
257
+ async def _post(self, endpoint: str, payload: dict) -> dict:
258
+ ...
259
+
260
+ async def pay(self, order_id: str, amount: str, ...) -> HBLResponse:
261
+ ...
262
+ ```
263
+
264
+ ### 5. Write integration tests
265
+
266
+ `tests/test_<gateway>.py` — use `httpx.MockTransport` to mock responses.
267
+
268
+ ```python
269
+ # tests/test_hbl.py
270
+ import httpx
271
+ import pytest
272
+ from raqm_core import HBL
273
+
274
+
275
+ @pytest.mark.asyncio
276
+ async def test_pay_success():
277
+ def handler(request: httpx.Request) -> httpx.Response:
278
+ return httpx.Response(status_code=200, json={...})
279
+
280
+ client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
281
+ hbl = HBL(api_key="test", sandbox=True, client=client)
282
+ result = await hbl.pay(...)
283
+ assert result.responseCode == "0000"
284
+ ```
285
+
286
+ ### 6. Update this README
287
+
288
+ Add the new gateway to the **Supported Gateways** table with the appropriate status badge.
289
+
290
+ ---
291
+
292
+ ## Roadmap
293
+
294
+ - [x] EasyPaisa (MA, OTC, inquiry)
295
+ - [ ] JazzCash client + schemas
296
+ - [ ] HBL payment gateway
297
+ - [ ] ABL payment gateway
298
+ - [ ] UBL payment gateway
299
+ - [ ] Standardised error handling across gateways
300
+ - [ ] Request/response logging middleware
301
+ - [ ] CI/CD + automated testing
302
+
303
+ ---
304
+
305
+ ## Development
306
+
307
+ ```bash
308
+ # Clone the repo
309
+ git clone https://github.com/your-username/raqm-core.git
310
+ cd raqm-core
311
+
312
+ # Create a virtual environment
313
+ uv venv
314
+ source .venv/bin/activate
315
+
316
+ # Install dependencies
317
+ uv sync
318
+
319
+ # Run tests
320
+ pytest
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Contributing
326
+
327
+ Contributions are welcome! See [Adding a New Gateway](#adding-a-new-gateway) for the detailed guide.
328
+
329
+ Please open an issue first to discuss your proposed changes.
330
+
331
+ ---
332
+
333
+ ## License
334
+
335
+ MIT
@@ -0,0 +1,11 @@
1
+ raqm_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ raqm_core/easypaisa.py,sha256=3qOhp9iJwRqrb8ZV1CJJ-VjKOKSytJb6l5OzUoV_RJk,2518
3
+ raqm_core/headers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ raqm_core/headers/easypaisa.py,sha256=4DFcfYpkohshhvIhwGTaMtFW5wuSut32Pj6NcWx0_48,238
5
+ raqm_core/headers/jazzcash.py,sha256=2xObdRqXtrAl-s2czZhp1Ce2RRSratZPqYaSUJV71ag,297
6
+ raqm_core/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ raqm_core/schemas/easypaisa.py,sha256=8bUMiwg-UbIeQJCb1Dau7e8gB7Eb05XFN_g8LwUonBA,2346
8
+ raqm_core-0.1.0.dist-info/METADATA,sha256=VPzt8L3L6lfwQ35gOZf7P--L7PLxLT37ik2S4qWGOCE,7335
9
+ raqm_core-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
10
+ raqm_core-0.1.0.dist-info/top_level.txt,sha256=czlRB0RPtVl23KDFQZk7rtchAIxTZlNnNd5hFiXkDT4,10
11
+ raqm_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ raqm_core