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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ethio-verify = ethio_receipt_verify.cli:main
@@ -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