cheki 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.
- cheki-0.1.0.dist-info/METADATA +409 -0
- cheki-0.1.0.dist-info/RECORD +23 -0
- cheki-0.1.0.dist-info/WHEEL +4 -0
- cheki-0.1.0.dist-info/entry_points.txt +2 -0
- ethio_receipt_verify/__init__.py +52 -0
- ethio_receipt_verify/banks/__init__.py +28 -0
- ethio_receipt_verify/banks/awash.py +48 -0
- ethio_receipt_verify/banks/base.py +29 -0
- ethio_receipt_verify/banks/boa.py +89 -0
- ethio_receipt_verify/banks/cbe.py +103 -0
- ethio_receipt_verify/banks/cbebirr.py +46 -0
- ethio_receipt_verify/banks/dashen.py +43 -0
- ethio_receipt_verify/banks/kaafiebirr.py +21 -0
- ethio_receipt_verify/banks/mpesa.py +65 -0
- ethio_receipt_verify/banks/siinqee.py +20 -0
- ethio_receipt_verify/banks/telebirr.py +88 -0
- ethio_receipt_verify/banks/zemen.py +48 -0
- ethio_receipt_verify/cli.py +251 -0
- ethio_receipt_verify/client.py +416 -0
- ethio_receipt_verify/client_types.py +415 -0
- ethio_receipt_verify/errors.py +14 -0
- ethio_receipt_verify/registry.py +28 -0
- ethio_receipt_verify/result.py +48 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cheki
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Free, open-source Ethiopian bank/wallet receipt verification. Reverse-engineered public endpoints.
|
|
5
|
+
Project-URL: Homepage, https://github.com/1RB/cheki
|
|
6
|
+
Project-URL: Repository, https://github.com/1RB/cheki
|
|
7
|
+
Project-URL: Issues, https://github.com/1RB/cheki/issues
|
|
8
|
+
Author: 1RB
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: bank,cbe,ethiopia,receipt,telebirr,verification
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Requires-Dist: beautifulsoup4>=4.11.0
|
|
21
|
+
Requires-Dist: pdfplumber>=0.9.0
|
|
22
|
+
Requires-Dist: requests>=2.28.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: reportlab>=4.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: responses>=0.23.0; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# cheki Python SDK
|
|
31
|
+
|
|
32
|
+
Free, open-source **Ethiopian bank/wallet receipt verification** for Python.
|
|
33
|
+
|
|
34
|
+
cheki verifies that a payment receipt is real by fetching it directly from the
|
|
35
|
+
bank's own public receipt endpoint — no API keys, no paywalls, no scraping with
|
|
36
|
+
Selenium. This SDK mirrors the [cheki](https://github.com/1RB/cheki) project's
|
|
37
|
+
other official SDKs (TypeScript, Go, PHP, Dart).
|
|
38
|
+
|
|
39
|
+
## Two modes
|
|
40
|
+
|
|
41
|
+
The SDK offers two complementary ways to verify receipts:
|
|
42
|
+
|
|
43
|
+
| Mode | When to use | How it works |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| **API client** (recommended) | Production apps, servers, anything that wants reliability | Calls the hosted cheki REST API, which handles geo-blocking, QR decryption, PDF parsing, and bank-endpoint rotation for you. |
|
|
46
|
+
| **Direct verification** (advanced) | Self-hosting, no external dependency, research | Fetches bank endpoints *directly* from your machine. No round-trip to cheki, but geo-blocked banks (telebirr, M-Pesa) will fail outside Ethiopia. |
|
|
47
|
+
|
|
48
|
+
Both are importable from the top-level package:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from ethio_receipt_verify import ChekiClient, verify, supported_banks
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick start — API client
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from ethio_receipt_verify import ChekiClient
|
|
60
|
+
|
|
61
|
+
cheki = ChekiClient() # uses https://cheki-pi.vercel.app by default
|
|
62
|
+
|
|
63
|
+
# CBE requires the receiving account number
|
|
64
|
+
result = cheki.verify("cbe", "FT26140P01YB", account_number="1000560536171")
|
|
65
|
+
|
|
66
|
+
if result.is_verified:
|
|
67
|
+
print(f"{result.sender_name} sent {result.amount} {result.currency}")
|
|
68
|
+
print(f"to {result.receiver_name} on {result.date}")
|
|
69
|
+
else:
|
|
70
|
+
print(f"Not verified: {result.error}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Batch verification
|
|
74
|
+
|
|
75
|
+
Verify up to 50 receipts in a single request:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
results = cheki.verify_batch([
|
|
79
|
+
{"bank": "cbe", "reference": "FT26140P01YB", "accountNumber": "1000560536171"},
|
|
80
|
+
{"bank": "telebirr", "reference": "DET8FJGUJ4"},
|
|
81
|
+
{"bank": "dashen", "reference": "B22WDTI261620001"},
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
print(f"{results.verified}/{results.total} verified")
|
|
85
|
+
for r in results.results:
|
|
86
|
+
print(r.reference, r.is_verified, r.error)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Discover supported banks
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
for bank in cheki.get_banks():
|
|
93
|
+
print(f"{bank.code:<12} {bank.name} [{bank.status}]")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Health check
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
health = cheki.get_health()
|
|
100
|
+
print(health.status) # "ok"
|
|
101
|
+
for check in health.checks:
|
|
102
|
+
print(f" {check.name}: {check.status} ({check.latency_ms}ms)")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Context manager
|
|
106
|
+
|
|
107
|
+
`ChekiClient` reuses a connection pool and can be used as a context manager:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
with ChekiClient(timeout=10, max_retries=5) as cheki:
|
|
111
|
+
result = cheki.verify("cbe", "FT26140P01YB", account_number="1000560536171")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Quick start — Direct verification (advanced)
|
|
117
|
+
|
|
118
|
+
Direct verification fetches the bank endpoint from *your* machine. It needs no
|
|
119
|
+
cheki server but is subject to geo-blocking and bank-side changes.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from ethio_receipt_verify import verify, supported_banks
|
|
123
|
+
|
|
124
|
+
# CBE needs the full receiving account number
|
|
125
|
+
result = verify("cbe", "FT26140P01YB", account_number="1000560536171")
|
|
126
|
+
|
|
127
|
+
print(result.status) # VerificationStatus.VERIFIED
|
|
128
|
+
print(result.exists) # True
|
|
129
|
+
print(result.amount) # 20000.0
|
|
130
|
+
print(result.sender_name)
|
|
131
|
+
|
|
132
|
+
# telebirr / M-Pesa only work from Ethiopian IP addresses
|
|
133
|
+
print(supported_banks())
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Direct verification returns a `VerificationResult` (different from the API
|
|
137
|
+
client's `ClientVerifyResult`). See the [API reference](#api-reference) below.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Installation
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pip install cheki
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
From source (development):
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
git clone https://github.com/1RB/cheki.git
|
|
151
|
+
cd cheki/python
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Dependencies: `requests`, `beautifulsoup4`, `pdfplumber` (the latter two are only
|
|
156
|
+
needed for direct verification of PDF/HTML receipts).
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## API reference
|
|
161
|
+
|
|
162
|
+
### `ChekiClient`
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
ChekiClient(
|
|
166
|
+
base_url="https://cheki-pi.vercel.app",
|
|
167
|
+
api_key=None,
|
|
168
|
+
timeout=30,
|
|
169
|
+
max_retries=3,
|
|
170
|
+
session=None,
|
|
171
|
+
user_agent=None,
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Parameter | Type | Default | Description |
|
|
176
|
+
| --- | --- | --- | --- |
|
|
177
|
+
| `base_url` | `str` | `https://cheki-pi.vercel.app` | cheki API root URL. |
|
|
178
|
+
| `api_key` | `str \| None` | `None` | Optional bearer token. The public API does not require one. |
|
|
179
|
+
| `timeout` | `float` | `30` | Per-request timeout in seconds. |
|
|
180
|
+
| `max_retries` | `int` | `3` | Retries on HTTP 408/429/5xx (in addition to the first attempt). |
|
|
181
|
+
| `session` | `requests.Session \| None` | `None` | Reuse a custom session (connection pool, proxies, etc.). |
|
|
182
|
+
| `user_agent` | `str \| None` | auto | `User-Agent` header value. |
|
|
183
|
+
|
|
184
|
+
#### Methods
|
|
185
|
+
|
|
186
|
+
##### `verify(bank, reference, account_number=None, phone_number=None, qr_data=None) -> ClientVerifyResult`
|
|
187
|
+
|
|
188
|
+
Verify a single receipt.
|
|
189
|
+
|
|
190
|
+
##### `verify_batch(receipts) -> ClientBatchResult`
|
|
191
|
+
|
|
192
|
+
Verify up to 50 receipts. Each receipt is a dict with `bank`, `reference`, and
|
|
193
|
+
optional `accountNumber` / `phoneNumber` / `qrData`. Results are returned in
|
|
194
|
+
input order with computed `total` / `verified` / `failed` counts.
|
|
195
|
+
|
|
196
|
+
##### `get_banks() -> list[ClientBankInfo]`
|
|
197
|
+
|
|
198
|
+
List supported banks/wallets.
|
|
199
|
+
|
|
200
|
+
##### `get_health() -> ClientHealthStatus`
|
|
201
|
+
|
|
202
|
+
Check service health (per-bank reachability).
|
|
203
|
+
|
|
204
|
+
##### `get_receipt_url(bank, reference, account_number=None) -> str`
|
|
205
|
+
|
|
206
|
+
Build a direct receipt-viewer URL (no network request).
|
|
207
|
+
|
|
208
|
+
##### `close()`
|
|
209
|
+
|
|
210
|
+
Close the underlying HTTP session.
|
|
211
|
+
|
|
212
|
+
### Response types
|
|
213
|
+
|
|
214
|
+
#### `ClientVerifyResult`
|
|
215
|
+
|
|
216
|
+
| Field | Type | Notes |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `success` | `bool` | HTTP-level success. |
|
|
219
|
+
| `verified` | `bool \| None` | Whether the receipt is legitimate. |
|
|
220
|
+
| `bank`, `bank_code`, `reference` | `str \| None` | Identifiers. |
|
|
221
|
+
| `source_url` | `str \| None` | Where the receipt was fetched from. |
|
|
222
|
+
| `sender_name`, `sender_account` | `str \| None` | Sender details. |
|
|
223
|
+
| `receiver_name`, `receiver_account` | `str \| None` | Receiver details. |
|
|
224
|
+
| `amount`, `currency` | `float \| None`, `str \| None` | Payment amount. |
|
|
225
|
+
| `date` | `str \| None` | Transaction date (as reported by the bank). |
|
|
226
|
+
| `branch`, `reason` | `str \| None` | Extra metadata. |
|
|
227
|
+
| `duration_ms` | `int \| None` | Server-side processing time. |
|
|
228
|
+
| `invoice_number`, `transaction_status` | `str \| None` | Wallet-specific. |
|
|
229
|
+
| `settled_amount`, `stamp_duty`, `discount_amount` | `float \| None` | Wallet fees. |
|
|
230
|
+
| `service_fee`, `service_fee_vat`, `total_paid` | `float \| None` | Wallet fees. |
|
|
231
|
+
| `amount_in_words`, `payment_mode`, `payment_channel` | `str \| None` | Wallet metadata. |
|
|
232
|
+
| `bank_account_number`, `bank_account_name` | `str \| None` | Wallet metadata. |
|
|
233
|
+
| `error` | `str \| None` | Error message on failure. |
|
|
234
|
+
| `fallback_url` | `str \| None` | Direct URL for geo-blocked banks. |
|
|
235
|
+
| `index` | `int \| None` | Position within a batch. |
|
|
236
|
+
| `raw` | `dict` | The unparsed API payload. |
|
|
237
|
+
|
|
238
|
+
Helper: `result.is_verified` → `True` when `success` and `verified` are both true.
|
|
239
|
+
|
|
240
|
+
#### `ClientBatchResult`
|
|
241
|
+
|
|
242
|
+
`success`, `total`, `verified`, `failed`, `results` (list of
|
|
243
|
+
`ClientVerifyResult`), `error`, `raw`.
|
|
244
|
+
|
|
245
|
+
#### `ClientBankInfo`
|
|
246
|
+
|
|
247
|
+
`code`, `name`, `swift`, `type`, `status`, `requires_account`,
|
|
248
|
+
`account_digits`, `requires_phone`, `endpoint`, `color`, `initials`, `raw`.
|
|
249
|
+
Helper: `bank.is_live`.
|
|
250
|
+
|
|
251
|
+
#### `ClientHealthStatus`
|
|
252
|
+
|
|
253
|
+
`success`, `status`, `version`, `timestamp`, `checks` (list of
|
|
254
|
+
`ClientHealthCheck`), `raw`. Helper: `health.is_ok`.
|
|
255
|
+
|
|
256
|
+
#### `ClientHealthCheck`
|
|
257
|
+
|
|
258
|
+
`name`, `status`, `latency_ms`, `raw`.
|
|
259
|
+
|
|
260
|
+
### Direct verification
|
|
261
|
+
|
|
262
|
+
#### `verify(bank, reference, **kwargs) -> VerificationResult`
|
|
263
|
+
|
|
264
|
+
Fetch and parse a receipt directly from the bank endpoint.
|
|
265
|
+
|
|
266
|
+
- `cbe`, `boa`: pass `account_number=` (full receiving account).
|
|
267
|
+
- `cbebirr`: pass `phone_number=` (payer phone, `2519XXXXXXXXX`).
|
|
268
|
+
|
|
269
|
+
Returns a `VerificationResult` with `status` (`VerificationStatus`), `exists`,
|
|
270
|
+
`amount`, `sender_name`, `receiver_name`, `transaction_date`, `source_url`, etc.
|
|
271
|
+
|
|
272
|
+
#### `supported_banks() -> dict[str, str]`
|
|
273
|
+
|
|
274
|
+
Map of bank code → human name for the banks with direct verifiers.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Error handling
|
|
279
|
+
|
|
280
|
+
### API client errors
|
|
281
|
+
|
|
282
|
+
All API client errors derive from `ChekiClientError`:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from ethio_receipt_verify import (
|
|
286
|
+
ChekiClient, ChekiClientError, ChekiAPIError, ChekiNetworkError, ChekiTimeoutError,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
cheki = ChekiClient()
|
|
290
|
+
try:
|
|
291
|
+
result = cheki.verify("cbe", "FT26140P01YB", account_number="1000560536171")
|
|
292
|
+
except ChekiTimeoutError as exc:
|
|
293
|
+
print("Request timed out:", exc)
|
|
294
|
+
except ChekiNetworkError as exc:
|
|
295
|
+
print("Network error:", exc)
|
|
296
|
+
except ChekiAPIError as exc:
|
|
297
|
+
print(f"API error (HTTP {exc.status_code}):", exc.message)
|
|
298
|
+
except ChekiClientError as exc:
|
|
299
|
+
print("Client error:", exc)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
| Error | Meaning |
|
|
303
|
+
| --- | --- |
|
|
304
|
+
| `ChekiAPIError` | Non-2xx API response. Carries `status_code`, `message`, `body`. |
|
|
305
|
+
| `ChekiNetworkError` | Connection failure. |
|
|
306
|
+
| `ChekiTimeoutError` | Request timed out (subclass of `ChekiNetworkError`). |
|
|
307
|
+
|
|
308
|
+
> **Note:** a verification that *finds no receipt* is **not** an error — the API
|
|
309
|
+
> returns `success: false` with an `error` message in the `ClientVerifyResult`.
|
|
310
|
+
> Exceptions are reserved for transport/server failures.
|
|
311
|
+
|
|
312
|
+
### Direct verification errors
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
from ethio_receipt_verify import verify
|
|
316
|
+
from ethio_receipt_verify.errors import (
|
|
317
|
+
VerificationError, ReceiptNotFoundError, UpstreamError, UnsupportedBankError,
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
| Error | Meaning |
|
|
322
|
+
| --- | --- |
|
|
323
|
+
| `UnsupportedBankError` | The bank code is not supported. |
|
|
324
|
+
| `ReceiptNotFoundError` | The bank returned a "not found" response. |
|
|
325
|
+
| `UpstreamError` | The bank endpoint is unreachable or returned an error. |
|
|
326
|
+
|
|
327
|
+
### Retries
|
|
328
|
+
|
|
329
|
+
`ChekiClient` automatically retries on HTTP `408`, `429`, and `5xx` using
|
|
330
|
+
exponential backoff with full jitter (up to `max_retries` extra attempts). A
|
|
331
|
+
`Retry-After` header, when present, is honored. Network and timeout errors are
|
|
332
|
+
also retried.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## CLI
|
|
337
|
+
|
|
338
|
+
Install the package to get the `ethio-verify` command:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# API client (recommended)
|
|
342
|
+
ethio-verify cbe FT26140P01YB --account 1000560536171 --api
|
|
343
|
+
ethio-verify telebirr DET8FJGUJ4 --api --json
|
|
344
|
+
|
|
345
|
+
# Direct verification (advanced)
|
|
346
|
+
ethio-verify cbe FT26140P01YB --account 1000560536171
|
|
347
|
+
|
|
348
|
+
# Service health & supported banks (via API)
|
|
349
|
+
ethio-verify --health
|
|
350
|
+
ethio-verify --list-banks --api
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Flags
|
|
354
|
+
|
|
355
|
+
| Flag | Description |
|
|
356
|
+
| --- | --- |
|
|
357
|
+
| `bank` | Bank/wallet code (e.g. `cbe`, `telebirr`, `boa`, `mpesa`). |
|
|
358
|
+
| `reference` | Transaction reference number. |
|
|
359
|
+
| `--account` | Receiving account number (required for cbe, boa). |
|
|
360
|
+
| `--phone` | Payer phone number (required for cbebirr). |
|
|
361
|
+
| `--qr` | Raw QR payload (Bank of Abyssinia inter-bank receipts). |
|
|
362
|
+
| `--api` | Use the hosted cheki REST API instead of direct verification. |
|
|
363
|
+
| `--base-url` | cheki API base URL (default: `https://cheki-pi.vercel.app`). |
|
|
364
|
+
| `--api-key` | Optional bearer token. |
|
|
365
|
+
| `--timeout` | Per-request timeout in seconds (API mode, default: 30). |
|
|
366
|
+
| `--json` | Output raw JSON. |
|
|
367
|
+
| `--list-banks` | List supported banks and exit. |
|
|
368
|
+
| `--health` | Check cheki API health and exit (implies `--api`). |
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Configuration
|
|
373
|
+
|
|
374
|
+
### Custom base URL / self-hosting
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
cheki = ChekiClient(base_url="https://cheki.my-server.com")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Proxies & custom session
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
import requests
|
|
384
|
+
|
|
385
|
+
session = requests.Session()
|
|
386
|
+
session.proxies = {"https": "http://proxy.local:8080"}
|
|
387
|
+
cheki = ChekiClient(session=session)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Timeouts & retries
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
cheki = ChekiClient(timeout=10, max_retries=5)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Supported banks
|
|
399
|
+
|
|
400
|
+
cbe, telebirr, boa, mpesa, dashen, zemen, cbebirr, siinqee, kaafiebirr (and
|
|
401
|
+
more — run `cheki.get_banks()` or `ethio-verify --list-banks --api` for the
|
|
402
|
+
current list). Availability depends on the bank endpoint's status and
|
|
403
|
+
geo-restrictions.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
MIT © [1RB](https://github.com/1RB)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
ethio_receipt_verify/__init__.py,sha256=_jQnEt2TCMpGHg2_dKSXq36FVqiQqSip5xY78mCfy-g,1466
|
|
2
|
+
ethio_receipt_verify/cli.py,sha256=5GWn2ms7iDywI1P3h5sDZnBit1UroLSiXrxndJHQGHA,9226
|
|
3
|
+
ethio_receipt_verify/client.py,sha256=kL4_niQdeZ6FSH49r4nk_xlKU-JkrYfR_jZsYY-s-NU,15813
|
|
4
|
+
ethio_receipt_verify/client_types.py,sha256=mqB6tw5053vOawLXH5UPvKidqLLRJlx7W1Vh9oh6igg,14629
|
|
5
|
+
ethio_receipt_verify/errors.py,sha256=c2A5TWbD98fp2tUe77QVz57_VhaPWKgfJYEZss8Usi8,407
|
|
6
|
+
ethio_receipt_verify/registry.py,sha256=UWgQiKzHFopjGRmGQXZAmMAEtXhzghguHc-37ry5BgY,929
|
|
7
|
+
ethio_receipt_verify/result.py,sha256=YqGT7dAxxHbcVUcAA3wmntZkfOTzw7knPujrDFKxi0g,1491
|
|
8
|
+
ethio_receipt_verify/banks/__init__.py,sha256=D3VbGX-yYSxcwz6UwLWOIfHShpTnMmnbT1oU5Y5WatQ,1099
|
|
9
|
+
ethio_receipt_verify/banks/awash.py,sha256=BBwk_wZjdwS-4UErDldAipztc7Le4dl1p7cz7M_6HYw,1967
|
|
10
|
+
ethio_receipt_verify/banks/base.py,sha256=5IytWjyb_69SVfVCbjpwNVYikKvIfAVsf4QukmzRzdY,895
|
|
11
|
+
ethio_receipt_verify/banks/boa.py,sha256=Cku7iXnfkY3sFzhb8eK7ul_FlYHh1H1AK1SigFduIKA,3235
|
|
12
|
+
ethio_receipt_verify/banks/cbe.py,sha256=EclLuMIjGbKSMn7WwAQMWywEgsYg-l0dmHMkqHI81ns,4215
|
|
13
|
+
ethio_receipt_verify/banks/cbebirr.py,sha256=rQqLh_vstvp4iKxA0jSgh7v-xcXGqw9ljVXP2cXIbvg,1783
|
|
14
|
+
ethio_receipt_verify/banks/dashen.py,sha256=UmfpvjWzQW2zmUg2qCQYmjYpxQEI6cBIvJNrNj2l9AE,1669
|
|
15
|
+
ethio_receipt_verify/banks/kaafiebirr.py,sha256=M8xMGd4JvKXX9SIxzz0-ahPHbAylqjjUx2RMz8MYLlA,749
|
|
16
|
+
ethio_receipt_verify/banks/mpesa.py,sha256=A8ZT_90zzYxrJDeZrCzUEnZSEN3TYagTZnp0pbROqbo,2624
|
|
17
|
+
ethio_receipt_verify/banks/siinqee.py,sha256=zUZ2hRhYzc2OER5euwSONba04TIcmgB4VLie7Y0dMx0,746
|
|
18
|
+
ethio_receipt_verify/banks/telebirr.py,sha256=i0wjqI9W68QM8DZp62NDOSIQJl_cyQ2xWTIdpYM1Ers,3570
|
|
19
|
+
ethio_receipt_verify/banks/zemen.py,sha256=fQAWiuZV_L6aERu2dQmsTnozDNGbSPHjPSOc_TD8_pc,1749
|
|
20
|
+
cheki-0.1.0.dist-info/METADATA,sha256=VdNqmuJsGjY1jTFxuwB7_FfYG6JvkNTgbSe9e390XHQ,12795
|
|
21
|
+
cheki-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
22
|
+
cheki-0.1.0.dist-info/entry_points.txt,sha256=DQsgi_8ckpUP5oOKGfVANUb-0GpaJmXXffK6GIK8FZM,63
|
|
23
|
+
cheki-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Free, open-source Ethiopian bank/wallet receipt verification.
|
|
2
|
+
|
|
3
|
+
cheki offers two complementary verification modes:
|
|
4
|
+
|
|
5
|
+
1. **API client** — :class:`ChekiClient` wraps the hosted cheki REST API
|
|
6
|
+
(https://cheki-pi.vercel.app). This is the simplest path and matches
|
|
7
|
+
the other cheki SDKs.
|
|
8
|
+
|
|
9
|
+
2. **Direct verification** — :func:`verify` fetches bank endpoints
|
|
10
|
+
directly from your machine (advanced; subject to geo-blocking).
|
|
11
|
+
|
|
12
|
+
Both are importable from the top-level package::
|
|
13
|
+
|
|
14
|
+
from ethio_receipt_verify import ChekiClient, verify, supported_banks
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ethio_receipt_verify.result import VerificationResult, VerificationStatus
|
|
18
|
+
from ethio_receipt_verify.registry import verify, supported_banks
|
|
19
|
+
from ethio_receipt_verify.client import ChekiClient, DEFAULT_BASE_URL
|
|
20
|
+
from ethio_receipt_verify.client_types import (
|
|
21
|
+
ChekiClientError,
|
|
22
|
+
ChekiAPIError,
|
|
23
|
+
ChekiNetworkError,
|
|
24
|
+
ChekiTimeoutError,
|
|
25
|
+
ClientVerifyResult,
|
|
26
|
+
ClientBatchResult,
|
|
27
|
+
ClientBankInfo,
|
|
28
|
+
ClientHealthCheck,
|
|
29
|
+
ClientHealthStatus,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Direct verification (advanced)
|
|
36
|
+
"VerificationResult",
|
|
37
|
+
"VerificationStatus",
|
|
38
|
+
"verify",
|
|
39
|
+
"supported_banks",
|
|
40
|
+
# API client
|
|
41
|
+
"ChekiClient",
|
|
42
|
+
"DEFAULT_BASE_URL",
|
|
43
|
+
"ChekiClientError",
|
|
44
|
+
"ChekiAPIError",
|
|
45
|
+
"ChekiNetworkError",
|
|
46
|
+
"ChekiTimeoutError",
|
|
47
|
+
"ClientVerifyResult",
|
|
48
|
+
"ClientBatchResult",
|
|
49
|
+
"ClientBankInfo",
|
|
50
|
+
"ClientHealthCheck",
|
|
51
|
+
"ClientHealthStatus",
|
|
52
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Bank/wallet specific verifiers."""
|
|
2
|
+
|
|
3
|
+
from ethio_receipt_verify.banks.base import BankVerifier
|
|
4
|
+
from ethio_receipt_verify.banks.cbe import CBEVerifier
|
|
5
|
+
from ethio_receipt_verify.banks.telebirr import TelebirrVerifier
|
|
6
|
+
from ethio_receipt_verify.banks.boa import BOAVerifier
|
|
7
|
+
from ethio_receipt_verify.banks.mpesa import MPesaVerifier
|
|
8
|
+
from ethio_receipt_verify.banks.zemen import ZemenVerifier
|
|
9
|
+
from ethio_receipt_verify.banks.dashen import DashenVerifier
|
|
10
|
+
from ethio_receipt_verify.banks.awash import AwashVerifier
|
|
11
|
+
from ethio_receipt_verify.banks.cbebirr import CBEBirrVerifier
|
|
12
|
+
from ethio_receipt_verify.banks.siinqee import SiinqeeVerifier
|
|
13
|
+
from ethio_receipt_verify.banks.kaafiebirr import KaafiEbirrbankVerifier
|
|
14
|
+
|
|
15
|
+
VERIFIERS: dict[str, type[BankVerifier]] = {
|
|
16
|
+
"cbe": CBEVerifier,
|
|
17
|
+
"telebirr": TelebirrVerifier,
|
|
18
|
+
"boa": BOAVerifier,
|
|
19
|
+
"mpesa": MPesaVerifier,
|
|
20
|
+
"zemen": ZemenVerifier,
|
|
21
|
+
"dashen": DashenVerifier,
|
|
22
|
+
"awash": AwashVerifier,
|
|
23
|
+
"cbebirr": CBEBirrVerifier,
|
|
24
|
+
"siinqee": SiinqeeVerifier,
|
|
25
|
+
"kaafiebirr": KaafiEbirrbankVerifier,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
__all__ = ["VERIFIERS", "BankVerifier"]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from ethio_receipt_verify.banks.base import BankVerifier
|
|
4
|
+
from ethio_receipt_verify.errors import ReceiptNotFoundError, UpstreamError
|
|
5
|
+
from ethio_receipt_verify.result import VerificationResult, VerificationStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AwashVerifier(BankVerifier):
|
|
9
|
+
"""Awash Bank receipt verifier.
|
|
10
|
+
|
|
11
|
+
Known public endpoint (from check.et):
|
|
12
|
+
https://awashpay.awashbank.com:8225/-<REFERENCE>
|
|
13
|
+
Port 8225 currently returns 403; port 443 returns a generic landing page.
|
|
14
|
+
The exact working URL format is still unknown.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
BANK_CODE = "awash"
|
|
18
|
+
BANK_NAME = "Awash Bank"
|
|
19
|
+
|
|
20
|
+
def verify(self, reference: str, **kwargs: object) -> VerificationResult:
|
|
21
|
+
urls = [
|
|
22
|
+
f"https://awashpay.awashbank.com:8225/-{reference}",
|
|
23
|
+
f"https://awashpay.awashbank.com/{reference}",
|
|
24
|
+
f"https://awashpay.awashbank.com/receipt/{reference}",
|
|
25
|
+
]
|
|
26
|
+
last_exc: Exception | None = None
|
|
27
|
+
for url in urls:
|
|
28
|
+
try:
|
|
29
|
+
resp = self._get(url, verify=False)
|
|
30
|
+
except requests.RequestException as exc:
|
|
31
|
+
last_exc = exc
|
|
32
|
+
continue
|
|
33
|
+
if resp.status_code == 200 and "Invalid receipt id" not in resp.text and "Servicecops" not in resp.text:
|
|
34
|
+
return VerificationResult(
|
|
35
|
+
bank=self.BANK_CODE,
|
|
36
|
+
reference=reference,
|
|
37
|
+
status=VerificationStatus.VERIFIED,
|
|
38
|
+
exists=True,
|
|
39
|
+
source_url=url,
|
|
40
|
+
raw={"response": resp.text[:2000]},
|
|
41
|
+
message="Awash receipt fetched. Detailed parsing is not yet implemented.",
|
|
42
|
+
)
|
|
43
|
+
if resp.status_code == 404 or "Invalid receipt id" in resp.text:
|
|
44
|
+
raise ReceiptNotFoundError("Awash did not find a receipt for this reference.")
|
|
45
|
+
|
|
46
|
+
raise UpstreamError(
|
|
47
|
+
f"Awash receipt endpoint could not be reached. Last error: {last_exc}"
|
|
48
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from ethio_receipt_verify.result import VerificationResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BankVerifier(ABC):
|
|
9
|
+
"""Base class for bank/wallet receipt verifiers."""
|
|
10
|
+
|
|
11
|
+
BANK_CODE: str
|
|
12
|
+
BANK_NAME: str
|
|
13
|
+
|
|
14
|
+
def __init__(self, session: requests.Session | None = None):
|
|
15
|
+
self.session = session or requests.Session()
|
|
16
|
+
self.session.headers.update({
|
|
17
|
+
"User-Agent": "Mozilla/5.0 (compatible; ethio-receipt-verify/0.1.0)"
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def verify(self, reference: str, **kwargs: object) -> VerificationResult:
|
|
22
|
+
"""Verify a receipt by its reference number."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
def _get(self, url: str, **kwargs) -> requests.Response:
|
|
26
|
+
return self.session.get(url, timeout=30, **kwargs)
|
|
27
|
+
|
|
28
|
+
def _post(self, url: str, **kwargs) -> requests.Response:
|
|
29
|
+
return self.session.post(url, timeout=30, **kwargs)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from ethio_receipt_verify.banks.base import BankVerifier
|
|
7
|
+
from ethio_receipt_verify.errors import ReceiptNotFoundError, UpstreamError
|
|
8
|
+
from ethio_receipt_verify.result import VerificationResult, VerificationStatus
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BOAVerifier(BankVerifier):
|
|
12
|
+
"""Bank of Abyssinia receipt verifier.
|
|
13
|
+
|
|
14
|
+
BOA serves a SPA receipt page, but the data comes from a public T24 API:
|
|
15
|
+
https://cs.bankofabyssinia.com/api/onlineSlip/getDetails/?id=<trx_id>
|
|
16
|
+
where trx_id is the transaction reference concatenated with the last 5
|
|
17
|
+
digits of the receiver account.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
BANK_CODE = "boa"
|
|
21
|
+
BANK_NAME = "Bank of Abyssinia"
|
|
22
|
+
|
|
23
|
+
def verify(self, reference: str, **kwargs: object) -> VerificationResult:
|
|
24
|
+
account_number = str(kwargs.get("account_number", "")).replace(" ", "")
|
|
25
|
+
if not account_number or len(account_number) < 5:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"BOA verification requires the full receiving account_number (at least 5 digits)."
|
|
28
|
+
)
|
|
29
|
+
suffix = account_number[-5:]
|
|
30
|
+
trx_id = f"{reference}{suffix}"
|
|
31
|
+
url = f"https://cs.bankofabyssinia.com/api/onlineSlip/getDetails/?id={trx_id}"
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
resp = self._get(url)
|
|
35
|
+
except requests.RequestException as exc:
|
|
36
|
+
raise UpstreamError(f"BOA receipt endpoint unreachable: {exc}") from exc
|
|
37
|
+
|
|
38
|
+
if resp.status_code != 200:
|
|
39
|
+
raise UpstreamError(f"BOA returned status {resp.status_code}.")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
payload = resp.json()
|
|
43
|
+
except ValueError as exc:
|
|
44
|
+
raise UpstreamError("BOA returned non-JSON response.") from exc
|
|
45
|
+
|
|
46
|
+
body = payload.get("body", [])
|
|
47
|
+
if not body or body[0].get("Payer's Name") == "Invalid reference number":
|
|
48
|
+
raise ReceiptNotFoundError(
|
|
49
|
+
"BOA did not find a transaction for this reference and account suffix."
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
row = body[0]
|
|
53
|
+
parsed = self._parse_row(row)
|
|
54
|
+
parsed["source_url"] = url
|
|
55
|
+
parsed["reference"] = reference
|
|
56
|
+
parsed["bank"] = self.BANK_CODE
|
|
57
|
+
parsed["raw"] = payload
|
|
58
|
+
return VerificationResult(
|
|
59
|
+
status=VerificationStatus.VERIFIED,
|
|
60
|
+
exists=True,
|
|
61
|
+
**parsed,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def _parse_row(self, row: dict) -> dict:
|
|
65
|
+
data: dict = {}
|
|
66
|
+
|
|
67
|
+
data["sender_name"] = row.get("Source Account Name")
|
|
68
|
+
data["sender_account"] = row.get("Source Account")
|
|
69
|
+
data["receiver_name"] = row.get("Receiver's Name")
|
|
70
|
+
data["receiver_account"] = row.get("Receiver's Account")
|
|
71
|
+
data["currency"] = row.get("currency", "ETB")
|
|
72
|
+
|
|
73
|
+
amount_raw = row.get("Transferred Amount", "")
|
|
74
|
+
if amount_raw:
|
|
75
|
+
try:
|
|
76
|
+
data["amount"] = float(re.sub(r"[^0-9.]", "", str(amount_raw)))
|
|
77
|
+
except ValueError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
date_raw = row.get("Transaction Date", "")
|
|
81
|
+
if date_raw:
|
|
82
|
+
for fmt in ("%d-%b-%Y", "%d/%m/%Y", "%Y-%m-%d", "%d-%m-%Y %H:%M:%S"):
|
|
83
|
+
try:
|
|
84
|
+
data["transaction_date"] = datetime.strptime(date_raw, fmt)
|
|
85
|
+
break
|
|
86
|
+
except ValueError:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
return data
|