webirr 1.0.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.
webirr-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) WeBirr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
webirr-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,493 @@
1
+ Metadata-Version: 2.4
2
+ Name: webirr
3
+ Version: 1.0.0
4
+ Summary: Official Python Client Library for WeBirr Payment Gateway APIs
5
+ Author: WeBirr
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/webirr/webirr-api-python-client
8
+ Project-URL: Source, https://github.com/webirr/webirr-api-python-client
9
+ Project-URL: Issues, https://github.com/webirr/webirr-api-python-client/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Office/Business :: Financial
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: requests>=2.28
22
+ Dynamic: license-file
23
+
24
+ Official Python Client Library for WeBirr Payment Gateway APIs
25
+
26
+ This Client Library provides convenient access to WeBirr Payment Gateway APIs from Python Applications.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ $ pip install webirr
32
+ ```
33
+
34
+ For local development from this repository:
35
+
36
+ ```bash
37
+ $ pip install -e .
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ The library needs to be configured with a *merchant Id* & *API key*. You can get it by contacting [webirr.com](https://webirr.net)
43
+
44
+ > You can use this library for production or test environments. you will need to set is_test_env=True for test, and False for production apps when creating objects of class WeBirrClient
45
+
46
+ Examples assume the WeBirr TestEnv and read credentials from environment variables:
47
+
48
+ ```bash
49
+ export WEBIRR_TEST_ENV_MERCHANT_ID="YOUR_TEST_MERCHANT_ID"
50
+ export WEBIRR_TEST_ENV_API_KEY="YOUR_TEST_API_KEY"
51
+ ```
52
+
53
+ Create the client with merchant ID, API key, and environment once. The client automatically sets `Bill.merchant_id` before sending bill create/update requests, so application code and examples should not set `merchant_id` on the bill object.
54
+
55
+ ## Example
56
+
57
+ The examples below keep the PHP and .NET README flow: create the client, call the API, check `error`, handle the success branch, and print `error_code` on failure.
58
+
59
+ ### Creating a new Bill / Updating an existing Bill on WeBirr Servers
60
+
61
+ ```python
62
+ import os
63
+ from datetime import datetime
64
+
65
+ from webirr import Bill, WeBirrClient
66
+
67
+
68
+ def main():
69
+ api_key = os.getenv("WEBIRR_TEST_ENV_API_KEY", "")
70
+ merchant_id = os.getenv("WEBIRR_TEST_ENV_MERCHANT_ID", "")
71
+
72
+ # api_key = "YOUR_API_KEY"
73
+ # merchant_id = "YOUR_MERCHANT_ID"
74
+
75
+ api = WeBirrClient(merchant_id, api_key, True)
76
+
77
+ bill = Bill()
78
+ bill.amount = "270.90"
79
+ bill.customer_code = "cc01" # it can be email address or phone number if you dont have customer code
80
+ bill.customer_name = "Elias Haileselassie"
81
+ bill.customer_phone = "0911000000" # optional; used for SMS notification when enabled for the merchant
82
+ bill.time = "2021-07-22 22:14" # your bill time, always in this format
83
+ bill.description = "hotel booking"
84
+ bill.bill_reference = "python/example/" + datetime.now().strftime("%Y%m%d%H%M%S") # your unique reference number
85
+
86
+ print("\nCreating Bill...")
87
+ res = api.create_bill(bill)
88
+
89
+ if not res.error:
90
+ # success
91
+ payment_code = res.res # returns paymentcode such as 429 723 975
92
+ print(f"\nPayment Code = {payment_code}") # we may want to save payment code in local db.
93
+ else:
94
+ # fail
95
+ print(f"\nerror: {res.error}")
96
+ print(f"\nerrorCode: {res.error_code}") # can be used to handle specific business error such as ERROR_INVALID_INPUT_DUP_REF
97
+
98
+ # Update existing bill if it is not paid
99
+ bill.amount = "278.00"
100
+ bill.customer_name = "Elias python"
101
+ # bill.bill_reference = "WE CAN NOT CHANGE THIS"
102
+
103
+ print("\nUpdating Bill...")
104
+ res = api.update_bill(bill)
105
+
106
+ if not res.error:
107
+ # success
108
+ print("\nbill is updated succesfully") # res.res will be 'OK' no need to check here!
109
+ else:
110
+ # fail
111
+ print(f"\nerror: {res.error}")
112
+ print(f"\nerrorCode: {res.error_code}") # can be used to handle specific business error such as ERROR_INVALID_INPUT
113
+
114
+
115
+ main()
116
+ ```
117
+
118
+ ### Getting a Bill and Listing Bills
119
+
120
+ ```python
121
+ import os
122
+
123
+ from webirr import WeBirrClient
124
+
125
+
126
+ def main():
127
+ api_key = os.getenv("WEBIRR_TEST_ENV_API_KEY", "")
128
+ merchant_id = os.getenv("WEBIRR_TEST_ENV_MERCHANT_ID", "")
129
+
130
+ api = WeBirrClient(merchant_id, api_key, True)
131
+
132
+ bill_reference = "YOUR_BILL_REFERENCE" # BILL_REFERENCE_YOU_SAVED_AFTER_CREATING_A_NEW_BILL
133
+ payment_code = "YOUR_PAYMENT_CODE" # PAYMENT_CODE_YOU_SAVED_AFTER_CREATING_A_NEW_BILL
134
+
135
+ print("\nGetting bill by reference...")
136
+ res = api.get_bill_by_reference(bill_reference)
137
+ if not res.error:
138
+ # success
139
+ print("\nBill found by reference.")
140
+ print(f"\nBill Reference: {res.res.bill_reference}")
141
+ print(f"\nPayment Code: {res.res.wbc_code}")
142
+ print(f"\nAmount: {res.res.amount}")
143
+ print(f"\nPayment Status: {res.res.payment_status}")
144
+ print(f"\nUpdate Timestamp: {res.res.update_time_stamp}")
145
+ else:
146
+ # fail
147
+ print(f"\nError: {res.error}")
148
+ print(f"\nError Code: {res.error_code}")
149
+
150
+ print("\nGetting bill by payment code...")
151
+ res = api.get_bill_by_payment_code(payment_code)
152
+ if not res.error:
153
+ # success
154
+ print("\nBill found by payment code.")
155
+ print(f"\nBill Reference: {res.res.bill_reference}")
156
+ print(f"\nPayment Code: {res.res.wbc_code}")
157
+ else:
158
+ # fail
159
+ print(f"\nError: {res.error}")
160
+ print(f"\nError Code: {res.error_code}")
161
+
162
+ print("\nListing bills...")
163
+ payment_status = -1 # -1 all, 0 pending, 1 unconfirmed payment, 2 paid.
164
+ last_time_stamp = "20251231" # Date-only cursor; use "20251231235959" when you need time precision.
165
+ limit = 10
166
+
167
+ res = api.get_bills(payment_status, last_time_stamp, limit)
168
+ if not res.error:
169
+ # success
170
+ print(f"\nBills returned: {len(res.res)}")
171
+ for bill in res.res:
172
+ print("\n-----------------------------")
173
+ print(f"\nBill Reference: {bill.bill_reference}")
174
+ print(f"\nPayment Code: {bill.wbc_code}")
175
+ print(f"\nAmount: {bill.amount}")
176
+ print(f"\nPayment Status: {bill.payment_status}")
177
+ print(f"\nUpdate Timestamp: {bill.update_time_stamp}")
178
+ else:
179
+ # fail
180
+ print(f"\nError: {res.error}")
181
+ print(f"\nError Code: {res.error_code}")
182
+
183
+
184
+ main()
185
+ ```
186
+
187
+ Timestamp cursors can be date-only (`yyyyMMdd`) or include time (`yyyyMMddHHmmss`). Use empty string only when you intentionally want all history from the beginning.
188
+
189
+ ### Getting Payment status of an existing Bill from WeBirr Servers
190
+
191
+ ```python
192
+ import os
193
+
194
+ from webirr import WeBirrClient
195
+
196
+
197
+ def main():
198
+ api_key = os.getenv("WEBIRR_TEST_ENV_API_KEY", "")
199
+ merchant_id = os.getenv("WEBIRR_TEST_ENV_MERCHANT_ID", "")
200
+
201
+ # api_key = "YOUR_API_KEY"
202
+ # merchant_id = "YOUR_MERCHANT_ID"
203
+
204
+ api = WeBirrClient(merchant_id, api_key, True)
205
+ payment_code = "PAYMENT_CODE_YOU_SAVED_AFTER_CREATING_A_NEW_BILL"
206
+
207
+ print("\nGetting Payment Status...")
208
+ res = api.get_payment_status(payment_code)
209
+
210
+ if not res.error:
211
+ # success
212
+ if res.res and res.res.is_paid:
213
+ payment = res.res.data
214
+ print("\nbill is paid")
215
+ print("\nbill payment detail")
216
+ print(f"\nBank: {payment.bank_id}")
217
+ print(f"\nBank Reference Number: {payment.payment_reference}")
218
+ print(f"\nAmount Paid: {payment.amount}")
219
+ print(f"\nPayment Date: {payment.payment_date}")
220
+ else:
221
+ print("\nbill is pending payment")
222
+ else:
223
+ # fail
224
+ print(f"\nerror: {res.error}")
225
+ print(f"\nerrorCode: {res.error_code}") # can be used to handle specific business error such as ERROR_INVALID_INPUT
226
+
227
+
228
+ main()
229
+ ```
230
+
231
+ *Sample object returned from getPaymentStatus()*
232
+
233
+ ```python
234
+ sample = {
235
+ "error": None,
236
+ "res": {
237
+ "status": 2,
238
+ "data": {
239
+ "status": 2,
240
+ "id": 111219507,
241
+ "bankID": "cbe_mobile",
242
+ "paymentReference": "TX70e78862148f4c249606",
243
+ "paymentDate": "2025-02-26 22:17:19",
244
+ "confirmed": True,
245
+ "confirmedTime": "2025-02-26 22:17:19",
246
+ "amount": "278",
247
+ "wbcCode": "149 233 514",
248
+ "updateTimeStamp": "2025022622171981338",
249
+ },
250
+ },
251
+ "errorCode": None,
252
+ }
253
+ ```
254
+
255
+ Use `payment_date` as the payment time field. `time` remains available as a deprecated backward-compatible alias.
256
+
257
+ ### Deleting an existing Bill from WeBirr Servers (if it is not paid)
258
+
259
+ ```python
260
+ import os
261
+
262
+ from webirr import WeBirrClient
263
+
264
+
265
+ def main():
266
+ api_key = os.getenv("WEBIRR_TEST_ENV_API_KEY", "")
267
+ merchant_id = os.getenv("WEBIRR_TEST_ENV_MERCHANT_ID", "")
268
+
269
+ # api_key = "YOUR_API_KEY"
270
+ # merchant_id = "YOUR_MERCHANT_ID"
271
+
272
+ api = WeBirrClient(merchant_id, api_key, True)
273
+ payment_code = "PAYMENT_CODE_YOU_SAVED_AFTER_CREATING_A_NEW_BILL"
274
+
275
+ print("\nDeleting Bill...")
276
+ res = api.delete_bill(payment_code)
277
+
278
+ if not res.error:
279
+ # success
280
+ print("\nbill is deleted succesfully") # res.res will be 'OK' no need to check here!
281
+ else:
282
+ # fail
283
+ print(f"\nerror: {res.error}")
284
+ print(f"\nerrorCode: {res.error_code}") # can be used to handle specific business error such as ERROR_INVALID_INPUT
285
+
286
+
287
+ main()
288
+ ```
289
+
290
+ ### Getting list of Payments and process them with Bulk Polling Consumer
291
+
292
+ ```python
293
+ import os
294
+ import time
295
+
296
+ from webirr import WeBirrClient
297
+
298
+
299
+ class PaymentProcessor:
300
+ def __init__(self):
301
+ api_key = os.getenv("WEBIRR_TEST_ENV_API_KEY", "")
302
+ merchant_id = os.getenv("WEBIRR_TEST_ENV_MERCHANT_ID", "")
303
+ self.api = WeBirrClient(merchant_id, api_key, True)
304
+ self.last_time_stamp = "20251231" # Example cursor. Save the last processed payment updateTimeStamp in your database.
305
+
306
+ def run_once(self):
307
+ print("\nRetrieving Payments...")
308
+ self.fetch_and_process_payments()
309
+
310
+ def run_forever(self):
311
+ while True:
312
+ self.run_once()
313
+ print("\nSleeping for 5 seconds...")
314
+ time.sleep(5)
315
+
316
+ def fetch_and_process_payments(self):
317
+ limit = 100 # Number of records to retrieve depending on your processing requirement & capacity
318
+ response = self.api.get_payments(self.last_time_stamp, limit)
319
+
320
+ if not response.error:
321
+ # success
322
+ if len(response.res) == 0:
323
+ print("\nNo new payments found.")
324
+ for payment in response.res:
325
+ self.process_payment(payment)
326
+ print("\n-----------------------------")
327
+
328
+ if len(response.res) > 0:
329
+ self.last_time_stamp = response.res[-1].update_time_stamp
330
+ print(f"\nLast Timestamp: {self.last_time_stamp}") # save updateTimeStamp to your database for the next get_payments() call
331
+ else:
332
+ # fail
333
+ print(f"\nerror: {response.error}")
334
+ print(f"\nerrorCode: {response.error_code}") # can be used to handle specific business error such as ERROR_INVALID_INPUT
335
+
336
+ def process_payment(self, payment):
337
+ # Process Payment should be implemented as idempotent operation for production use cases.
338
+ # This method and logic can be shared among all payment processing consumers: 1. bulk polling, 2. webhook, 3. single payment polling.
339
+ print(f"\nPayment Status: {payment.status}")
340
+ if payment.is_paid:
341
+ print("\nPayment Status Text: Paid.")
342
+ if payment.is_reversed:
343
+ print("\nPayment Status Text: Reversed.")
344
+ print(f"\nBank: {payment.bank_id}")
345
+ print(f"\nBank Reference Number: {payment.payment_reference}")
346
+ print(f"\nAmount Paid: {payment.amount}")
347
+ print(f"\nPayment Date: {payment.payment_date}")
348
+ print(f"\nReversal/Cancel Date: {payment.canceled_time}")
349
+ print(f"\nUpdate Timestamp: {payment.update_time_stamp}")
350
+
351
+
352
+ PaymentProcessor().run_once()
353
+ ```
354
+
355
+ Bulk polling should persist `updateTimeStamp` only after processing the batch successfully. Polling processors should be idempotent because duplicate/redundant reads are possible.
356
+
357
+ ### Webhooks - Payment processing using Webhook Callbacks
358
+
359
+ ```python
360
+ import hmac
361
+ import json
362
+ import os
363
+
364
+ from webirr import PaymentResponse
365
+
366
+
367
+ class Webhook:
368
+ # Webhook handler for processing payment updates from WeBirr.
369
+ # This endpoint should be hosted on a secure server with HTTPS enabled.
370
+ def handle_request(self, method, provided_auth_key, raw_payload):
371
+ # Validate request method is POST
372
+ if method.upper() != "POST":
373
+ return self.json_response(405, {"error": "Method Not Allowed. POST required."})
374
+
375
+ # Authenticate using authKey query string parameter.
376
+ if not self.is_authenticated(provided_auth_key):
377
+ return self.json_response(403, {"error": "Unauthorized access. Invalid authKey."})
378
+
379
+ if not raw_payload:
380
+ return self.json_response(400, {"error": "Empty request body."})
381
+
382
+ try:
383
+ payload = json.loads(raw_payload)
384
+ except json.JSONDecodeError:
385
+ return self.json_response(400, {"error": "Invalid JSON format."})
386
+
387
+ payment_data = payload.get("data", payload)
388
+ if not payment_data:
389
+ return self.json_response(400, {"error": "Invalid payment data."})
390
+
391
+ payment = PaymentResponse.from_dict(payment_data)
392
+ self.process_payment(payment)
393
+
394
+ return self.json_response(200, {"success": True, "message": "Payment received and queued for processing"})
395
+
396
+ def is_authenticated(self, provided_auth_key):
397
+ expected_auth_key = os.getenv("WEBIRR_WEBHOOK_AUTH_KEY", "YOUR_WEBHOOK_AUTH_KEY")
398
+ return bool(expected_auth_key) and hmac.compare_digest(expected_auth_key, provided_auth_key or "")
399
+
400
+ def process_payment(self, payment):
401
+ # Process Payment should be implemented as idempotent operation for production use cases.
402
+ # This method and logic can be shared among all payment processing consumers: 1. bulk polling, 2. webhook, 3. single payment polling.
403
+ print(f"\nPayment Status: {payment.status}")
404
+ if payment.is_paid:
405
+ print("\nPayment Status Text: Paid.")
406
+ if payment.is_reversed:
407
+ print("\nPayment Status Text: Reversed.")
408
+ print(f"\nBank: {payment.bank_id}")
409
+ print(f"\nBank Reference Number: {payment.payment_reference}")
410
+ print(f"\nAmount Paid: {payment.amount}")
411
+ print(f"\nPayment Date: {payment.payment_date}")
412
+ print(f"\nReversal/Cancel Date: {payment.canceled_time}")
413
+ print(f"\nUpdate Timestamp: {payment.update_time_stamp}")
414
+
415
+ def json_response(self, status_code, body):
416
+ return {"status_code": status_code, "content_type": "application/json", "body": json.dumps(body)}
417
+
418
+
419
+ # Once hosted, the webhook URL needs to be shared with WeBirr for configuration.
420
+ ```
421
+
422
+ ### Gettting basic Statistics about bills created and payments received for a date range
423
+
424
+ ```python
425
+ import os
426
+
427
+ from webirr import WeBirrClient
428
+
429
+
430
+ def main():
431
+ api_key = os.getenv("WEBIRR_TEST_ENV_API_KEY", "")
432
+ merchant_id = os.getenv("WEBIRR_TEST_ENV_MERCHANT_ID", "")
433
+
434
+ # api_key = "YOUR_API_KEY"
435
+ # merchant_id = "YOUR_MERCHANT_ID"
436
+
437
+ api = WeBirrClient(merchant_id, api_key, True)
438
+
439
+ date_from = "2025-01-01" # YYYY-MM-DD
440
+ date_to = "2030-01-31" # YYYY-MM-DD
441
+
442
+ print("\nRetrieving Statistics...")
443
+ print(f"\nDate From: {date_from}")
444
+ print(f"\nDate To: {date_to}")
445
+
446
+ response = api.get_stat(date_from, date_to)
447
+
448
+ if not response.error:
449
+ # success
450
+ stat = response.res
451
+ print(f"\nNumber of Bills Created: {stat.n_bills}")
452
+ print(f"\nNumber of Paid Bills: {stat.n_bills_paid}")
453
+ print(f"\nNumber of Unpaid Bills: {stat.n_bills_unpaid}")
454
+ print(f"\nAmount of Bills: {stat.amount_bills}")
455
+ print(f"\nAmount Paid: {stat.amount_paid}")
456
+ print(f"\nAmount Unpaid: {stat.amount_unpaid}")
457
+ else:
458
+ # fail
459
+ print(f"\nError: {response.error}")
460
+ print(f"\nError Code: {response.error_code}")
461
+
462
+
463
+ main()
464
+ ```
465
+
466
+ ## Examples
467
+
468
+ The `examples` directory includes separate workflows matching the PHP SDK examples:
469
+
470
+ ```bash
471
+ python examples/example1_create_update_bill.py
472
+ python examples/example2_payment_status_single_poll.py
473
+ python examples/example3_delete_bill.py
474
+ python examples/example4_payment_status_bulk_poll.py
475
+ python examples/example5_stat_report.py
476
+ python examples/example6_payment_status_webhook.py
477
+ python examples/example7_get_bill_and_list_bills.py
478
+ ```
479
+
480
+ ## Reusable HTTP Session
481
+
482
+ For batch or mass bill workloads, pass a configured `requests.Session` so your application can reuse connections, configure adapters, and apply its own retry policy:
483
+
484
+ ```python
485
+ import requests
486
+
487
+ from webirr import WeBirrClient
488
+
489
+ session = requests.Session()
490
+ api = WeBirrClient(merchant_id, api_key, True, session=session)
491
+ ```
492
+
493
+ The SDK does not silently retry bill creation. Configure retry behavior in your application so duplicate create/update processing remains under your control.