raqm-core 0.1.0__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.
@@ -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,326 @@
1
+ # raqm-core
2
+
3
+ **Unified Python SDK for Pakistani payment gateways.**
4
+
5
+ One async, type-safe interface across multiple processors — EasyPaisa, JazzCash, HBL, ABL, UBL, and more.
6
+
7
+ > ⚠️ **Work in progress.** Currently only EasyPaisa is fully implemented. Contributions welcome!
8
+
9
+ ---
10
+
11
+ ## Supported Gateways
12
+
13
+ | Gateway | Status |
14
+ |---------|--------|
15
+ | [EasyPaisa](https://easypaisa.com.pk) | ✅ Live |
16
+ | [JazzCash](https://jazzcash.com.pk) | 🚧 Header utils ready |
17
+ | HBL | 📅 Planned |
18
+ | ABL | 📅 Planned |
19
+ | UBL | 📅 Planned |
20
+ | … | 📅 Your gateway here |
21
+
22
+ ---
23
+
24
+ ## Features
25
+
26
+ - **Async-first** — built on `httpx.AsyncClient` for non-blocking I/O
27
+ - **Type-safe** — all responses are validated through Pydantic models
28
+ - **Consistent API** — same patterns across all gateways
29
+ - **Mock-friendly** — inject your own `httpx.AsyncClient` for testing
30
+ - **Minimal dependencies** — just `httpx` and `pydantic`
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install raqm-core
38
+ ```
39
+
40
+ Or with `uv`:
41
+
42
+ ```bash
43
+ uv add raqm-core
44
+ ```
45
+
46
+ > Requires Python 3.12+.
47
+
48
+ ---
49
+
50
+ ## Quick Start
51
+
52
+ ```python
53
+ import asyncio
54
+ from raqm_core import EasyPaisa
55
+
56
+
57
+ async def main():
58
+ ep = EasyPaisa(
59
+ store_id="43",
60
+ username="your_username",
61
+ password="your_password",
62
+ sandbox=True,
63
+ )
64
+
65
+ result = await ep.pay_via_ma(
66
+ order_id="order_123",
67
+ amount="1000.00",
68
+ email="customer@example.com",
69
+ mobile_number="03451234567",
70
+ )
71
+
72
+ print(f"Status: {result.responseCode} — {result.responseDesc}")
73
+ print(f"Transaction ID: {result.transactionId}")
74
+
75
+
76
+ asyncio.run(main())
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Usage
82
+
83
+ ### EasyPaisa
84
+
85
+ #### Initialisation
86
+
87
+ ```python
88
+ from raqm_core import EasyPaisa
89
+
90
+ ep = EasyPaisa(
91
+ store_id="43",
92
+ username="your_username",
93
+ password="your_password",
94
+ sandbox=True, # set False for production
95
+ )
96
+ ```
97
+
98
+ You can optionally inject your own `httpx.AsyncClient` — useful for testing with `httpx.MockTransport`:
99
+
100
+ ```python
101
+ import httpx
102
+
103
+ client = httpx.AsyncClient(...)
104
+ ep = EasyPaisa(store_id="43", username="...", password="...", sandbox=True, client=client)
105
+ ```
106
+
107
+ #### Mobile Account (MA) Payment
108
+
109
+ ```python
110
+ result = await ep.pay_via_ma(
111
+ order_id="order_123",
112
+ amount="500.00",
113
+ email="customer@example.com",
114
+ mobile_number="03451234567",
115
+ )
116
+
117
+ # EasyPaisaMAResponse fields:
118
+ result.orderId # str
119
+ result.storeId # int
120
+ result.transactionId # str
121
+ result.transactionDateTime # str (dd/MM/yyyy hh:mm AM/PM)
122
+ result.responseCode # EasypaisaResponseCode (enum)
123
+ result.responseDesc # str
124
+ ```
125
+
126
+ #### Over-the-Counter (OTC) Payment
127
+
128
+ ```python
129
+ result = await ep.pay_via_otc(
130
+ order_id="order_456",
131
+ amount="1500.00",
132
+ email="customer@example.com",
133
+ msisdn="03451234567",
134
+ token_expiry="01/01/2025 11:59 PM",
135
+ )
136
+
137
+ # EasyPaisaOTCResponse fields (extends base):
138
+ result.paymentToken # str
139
+ result.paymentTokenExpiryDateTime # str
140
+ ```
141
+
142
+ #### Transaction Inquiry
143
+
144
+ ```python
145
+ result = await ep.inquire_transaction_status(
146
+ order_id="order_123",
147
+ account_number="123456789",
148
+ )
149
+
150
+ # EasyPaisaInquireTransactionResponse fields:
151
+ result.transactionStatus # str (e.g. "COMPLETED")
152
+ result.transactionAmount # str
153
+ result.accountNum # str
154
+ result.storeName # str
155
+ result.msisdn # str
156
+ result.paymentMode # str ("MA", "OTC", "CC")
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Architecture
162
+
163
+ Each payment gateway follows a consistent 3-layer structure:
164
+
165
+ ```
166
+ src/
167
+ ├── <gateway>.py # Client class — public API
168
+ ├── headers/
169
+ │ └── <gateway>.py # Auth / signing helpers
170
+ └── schemas/
171
+ └── <gateway>.py # Pydantic request/response models
172
+ ```
173
+
174
+ Current structure:
175
+
176
+ ```
177
+ src/
178
+ ├── easypaisa.py # EasyPaisa client
179
+ ├── headers/
180
+ │ ├── easypaisa.py # Basic Auth header
181
+ │ └── jazzcash.py # SHA-256 secure hash
182
+ └── schemas/
183
+ └── easypaisa.py # EasyPaisa Pydantic models
184
+
185
+ tests/
186
+ ├── test_easypaisa.py # EasyPaisa integration tests
187
+ └── headers/
188
+ ├── test_easypaisa.py # Auth header unit tests
189
+ └── test_jazzcash.py # Secure hash unit tests
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Adding a New Gateway
195
+
196
+ Want to add support for a new processor? Follow this checklist.
197
+
198
+ ### 1. Research the API
199
+
200
+ Understand the gateway's:
201
+ - Authentication mechanism (Basic Auth, HMAC, API key, etc.)
202
+ - Endpoints and request/response formats
203
+ - Error/response codes
204
+
205
+ ### 2. Create header helpers
206
+
207
+ `src/headers/<gateway>.py` — authentication or signing logic.
208
+
209
+ ```python
210
+ # src/headers/hbl.py
211
+ def generate_signature(api_key: str, payload: dict) -> str:
212
+ ...
213
+ ```
214
+
215
+ Write unit tests in `tests/headers/test_<gateway>.py`.
216
+
217
+ ### 3. Create Pydantic schemas
218
+
219
+ `src/schemas/<gateway>.py` — response models and enums.
220
+
221
+ ```python
222
+ # src/schemas/hbl.py
223
+ from pydantic import BaseModel, Field
224
+
225
+
226
+ class HBLResponse(BaseModel):
227
+ orderId: str = Field(...)
228
+ responseCode: str = Field(...)
229
+ responseDesc: str = Field(...)
230
+ ```
231
+
232
+ ### 4. Create the client class
233
+
234
+ `src/<gateway>.py` — async client with a `_post()` helper and public methods.
235
+
236
+ ```python
237
+ # src/hbl.py
238
+ import httpx
239
+ from .headers.hbl import generate_signature
240
+ from .schemas.hbl import HBLResponse
241
+
242
+
243
+ class HBL:
244
+ def __init__(self, api_key: str, sandbox: bool, client: httpx.AsyncClient | None = None):
245
+ self._client = client or httpx.AsyncClient()
246
+ ...
247
+
248
+ async def _post(self, endpoint: str, payload: dict) -> dict:
249
+ ...
250
+
251
+ async def pay(self, order_id: str, amount: str, ...) -> HBLResponse:
252
+ ...
253
+ ```
254
+
255
+ ### 5. Write integration tests
256
+
257
+ `tests/test_<gateway>.py` — use `httpx.MockTransport` to mock responses.
258
+
259
+ ```python
260
+ # tests/test_hbl.py
261
+ import httpx
262
+ import pytest
263
+ from raqm_core import HBL
264
+
265
+
266
+ @pytest.mark.asyncio
267
+ async def test_pay_success():
268
+ def handler(request: httpx.Request) -> httpx.Response:
269
+ return httpx.Response(status_code=200, json={...})
270
+
271
+ client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
272
+ hbl = HBL(api_key="test", sandbox=True, client=client)
273
+ result = await hbl.pay(...)
274
+ assert result.responseCode == "0000"
275
+ ```
276
+
277
+ ### 6. Update this README
278
+
279
+ Add the new gateway to the **Supported Gateways** table with the appropriate status badge.
280
+
281
+ ---
282
+
283
+ ## Roadmap
284
+
285
+ - [x] EasyPaisa (MA, OTC, inquiry)
286
+ - [ ] JazzCash client + schemas
287
+ - [ ] HBL payment gateway
288
+ - [ ] ABL payment gateway
289
+ - [ ] UBL payment gateway
290
+ - [ ] Standardised error handling across gateways
291
+ - [ ] Request/response logging middleware
292
+ - [ ] CI/CD + automated testing
293
+
294
+ ---
295
+
296
+ ## Development
297
+
298
+ ```bash
299
+ # Clone the repo
300
+ git clone https://github.com/your-username/raqm-core.git
301
+ cd raqm-core
302
+
303
+ # Create a virtual environment
304
+ uv venv
305
+ source .venv/bin/activate
306
+
307
+ # Install dependencies
308
+ uv sync
309
+
310
+ # Run tests
311
+ pytest
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Contributing
317
+
318
+ Contributions are welcome! See [Adding a New Gateway](#adding-a-new-gateway) for the detailed guide.
319
+
320
+ Please open an issue first to discuss your proposed changes.
321
+
322
+ ---
323
+
324
+ ## License
325
+
326
+ MIT
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "raqm-core"
3
+ version = "0.1.0"
4
+ description = "Unified Python SDK for Pakistani payment gateways (EasyPaisa, JazzCash, HBL, ABL, UBL, etc.)"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "httpx>=0.28.1",
9
+ "pydantic>=2.0",
10
+ ]
11
+
12
+ [dependency-groups]
13
+ dev = [
14
+ "pytest>=9.1.1",
15
+ "pytest-asyncio>=1.4.0",
16
+ ]
17
+
18
+ [tool.pytest.ini_options]
19
+ asyncio_mode = "auto"
20
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -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,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/raqm_core/__init__.py
4
+ src/raqm_core/easypaisa.py
5
+ src/raqm_core.egg-info/PKG-INFO
6
+ src/raqm_core.egg-info/SOURCES.txt
7
+ src/raqm_core.egg-info/dependency_links.txt
8
+ src/raqm_core.egg-info/requires.txt
9
+ src/raqm_core.egg-info/top_level.txt
10
+ src/raqm_core/headers/__init__.py
11
+ src/raqm_core/headers/easypaisa.py
12
+ src/raqm_core/headers/jazzcash.py
13
+ src/raqm_core/schemas/__init__.py
14
+ src/raqm_core/schemas/easypaisa.py
15
+ tests/test_easypaisa.py
@@ -0,0 +1,2 @@
1
+ httpx>=0.28.1
2
+ pydantic>=2.0
@@ -0,0 +1 @@
1
+ raqm_core
@@ -0,0 +1,162 @@
1
+ import httpx
2
+ import pytest
3
+
4
+ from src.raqm_core.easypaisa import EasyPaisa
5
+
6
+ MA_SUCCESS_BODY = {
7
+ "orderId": "abc123",
8
+ "storeId": 43,
9
+ "transactionId": "253184",
10
+ "transactionDateTime": "11/08/2018 11:30 PM",
11
+ "responseCode": "0000",
12
+ "responseDesc": "SUCCESS",
13
+ }
14
+
15
+ OTC_SUCCESS_BODY = {
16
+ "orderId": "abc123",
17
+ "storeId": 43,
18
+ "transactionDateTime": "11/08/2021 11:30 PM",
19
+ "responseCode": "0000",
20
+ "responseDesc": "SUCCESS",
21
+ "paymentToken": "tok_abc123",
22
+ "paymentTokenExpiryDateTime": "12/08/2021 11:30 PM",
23
+ }
24
+
25
+ INQUIRE_SUCCESS_BODY = {
26
+ "orderId": "abc123",
27
+ "storeId": 43,
28
+ "transactionId": "253184",
29
+ "transactionDateTime": "11/08/2021 11:30 PM",
30
+ "responseCode": "0000",
31
+ "responseDesc": "SUCCESS",
32
+ "accountNum": "123456789",
33
+ "storeName": "Test Store",
34
+ "paymentToken": None,
35
+ "transactionStatus": "COMPLETED",
36
+ "transactionAmount": "1.23",
37
+ "paymentTokenExpiryDateTime": None,
38
+ "msisdn": "03458508726",
39
+ "paymentMode": "MA",
40
+ }
41
+
42
+
43
+ def make_client(response_body: dict) -> httpx.AsyncClient:
44
+ def handler(request: httpx.Request) -> httpx.Response:
45
+ return httpx.Response(status_code=200, json=response_body)
46
+
47
+ return httpx.AsyncClient(transport=httpx.MockTransport(handler))
48
+
49
+
50
+ def make_ep(response_body: dict) -> EasyPaisa:
51
+ return EasyPaisa(
52
+ store_id="43",
53
+ username="admin",
54
+ password="123",
55
+ sandbox=True,
56
+ client=make_client(response_body),
57
+ )
58
+
59
+
60
+ class TestPayViaMA:
61
+ @pytest.mark.asyncio
62
+ async def test_success(self):
63
+ ep = make_ep(MA_SUCCESS_BODY)
64
+ result = await ep.pay_via_ma(
65
+ order_id="abc123",
66
+ amount="1.23",
67
+ email="test@example.com",
68
+ mobile_number="03458508726",
69
+ )
70
+ assert result.orderId == "abc123"
71
+ assert result.transactionId == "253184"
72
+ assert result.responseCode == "0000"
73
+
74
+ @pytest.mark.parametrize(
75
+ ("body", "expected_code"),
76
+ [
77
+ ({"responseCode": "0001", "responseDesc": "SYSTEM ERROR"}, "0001"),
78
+ ({"responseCode": "0006", "responseDesc": "INVALID STORE ID"}, "0006"),
79
+ ({"responseCode": "0010", "responseDesc": "INVALID CREDENTIALS"}, "0010"),
80
+ ],
81
+ )
82
+ @pytest.mark.asyncio
83
+ async def test_error_codes(self, body, expected_code):
84
+ payload = {**MA_SUCCESS_BODY, **body}
85
+ 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
93
+
94
+
95
+ class TestPayViaOTC:
96
+ @pytest.mark.asyncio
97
+ async def test_success(self):
98
+ ep = make_ep(OTC_SUCCESS_BODY)
99
+ result = await ep.pay_via_otc(
100
+ order_id="abc123",
101
+ amount="1.23",
102
+ email="test@example.com",
103
+ msisdn="03458508726",
104
+ token_expiry="11/12/2018 11:30 PM",
105
+ )
106
+ assert result.orderId == "abc123"
107
+ assert result.responseCode == "0000"
108
+ assert result.paymentToken == "tok_abc123"
109
+
110
+ @pytest.mark.parametrize(
111
+ ("body", "expected_code"),
112
+ [
113
+ ({"responseCode": "0001", "responseDesc": "SYSTEM ERROR"}, "0001"),
114
+ ({"responseCode": "0006", "responseDesc": "INVALID STORE ID"}, "0006"),
115
+ ({"responseCode": "0010", "responseDesc": "INVALID CREDENTIALS"}, "0010"),
116
+ ],
117
+ )
118
+ @pytest.mark.asyncio
119
+ async def test_error_codes(self, body, expected_code):
120
+ payload = {**OTC_SUCCESS_BODY, **body}
121
+ 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
130
+
131
+
132
+ class TestInquireTransaction:
133
+ @pytest.mark.asyncio
134
+ async def test_success(self):
135
+ ep = make_ep(INQUIRE_SUCCESS_BODY)
136
+ result = await ep.inquire_transaction_status(
137
+ order_id="abc123", account_number="123456789"
138
+ )
139
+ assert result.orderId == "abc123"
140
+ assert result.responseCode == "0000"
141
+ assert result.transactionStatus == "COMPLETED"
142
+ assert result.accountNum == "123456789"
143
+ assert result.storeName == "Test Store"
144
+ assert result.msisdn == "03458508726"
145
+ assert result.paymentMode == "MA"
146
+
147
+ @pytest.mark.parametrize(
148
+ ("body", "expected_code"),
149
+ [
150
+ ({"responseCode": "0001", "responseDesc": "SYSTEM ERROR"}, "0001"),
151
+ ({"responseCode": "0006", "responseDesc": "INVALID STORE ID"}, "0006"),
152
+ ({"responseCode": "0010", "responseDesc": "INVALID CREDENTIALS"}, "0010"),
153
+ ],
154
+ )
155
+ @pytest.mark.asyncio
156
+ async def test_error_codes(self, body, expected_code):
157
+ payload = {**INQUIRE_SUCCESS_BODY, **body}
158
+ ep = make_ep(payload)
159
+ result = await ep.inquire_transaction_status(
160
+ order_id="abc123", account_number="123456789"
161
+ )
162
+ assert result.responseCode == expected_code