webirr 1.0.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.
- webirr/__init__.py +25 -0
- webirr/client.py +182 -0
- webirr/models.py +293 -0
- webirr/py.typed +1 -0
- webirr-1.0.0.dist-info/METADATA +493 -0
- webirr-1.0.0.dist-info/RECORD +9 -0
- webirr-1.0.0.dist-info/WHEEL +5 -0
- webirr-1.0.0.dist-info/licenses/LICENSE +21 -0
- webirr-1.0.0.dist-info/top_level.txt +1 -0
webirr/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Official Python client library for WeBirr Payment Gateway APIs."""
|
|
2
|
+
|
|
3
|
+
from .client import WeBirrClient
|
|
4
|
+
from .models import (
|
|
5
|
+
ApiResponse,
|
|
6
|
+
Bill,
|
|
7
|
+
BillResponse,
|
|
8
|
+
PaymentDetail,
|
|
9
|
+
PaymentResponse,
|
|
10
|
+
PaymentStatus,
|
|
11
|
+
Stat,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ApiResponse",
|
|
16
|
+
"Bill",
|
|
17
|
+
"BillResponse",
|
|
18
|
+
"PaymentDetail",
|
|
19
|
+
"PaymentResponse",
|
|
20
|
+
"PaymentStatus",
|
|
21
|
+
"Stat",
|
|
22
|
+
"WeBirrClient",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
webirr/client.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""HTTP client for WeBirr Payment Gateway APIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
from urllib.parse import urljoin
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .models import (
|
|
12
|
+
ApiResponse,
|
|
13
|
+
Bill,
|
|
14
|
+
BillResponse,
|
|
15
|
+
PaymentResponse,
|
|
16
|
+
PaymentStatus,
|
|
17
|
+
Stat,
|
|
18
|
+
list_of,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WeBirrClient:
|
|
25
|
+
"""Client for WeBirr merchant APIs."""
|
|
26
|
+
|
|
27
|
+
TEST_BASE_URL = "https://api.webirr.net"
|
|
28
|
+
PROD_BASE_URL = "https://api.webirr.net:8080"
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
merchant_id: str,
|
|
33
|
+
api_key: str,
|
|
34
|
+
is_test_env: bool = True,
|
|
35
|
+
session: requests.Session | None = None,
|
|
36
|
+
base_url: str | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self.merchant_id = merchant_id or ""
|
|
39
|
+
self.api_key = api_key or ""
|
|
40
|
+
self.base_url = (base_url or (self.TEST_BASE_URL if is_test_env else self.PROD_BASE_URL)).rstrip("/")
|
|
41
|
+
self.session = session or requests.Session()
|
|
42
|
+
self.session.headers.setdefault("Accept", "application/json")
|
|
43
|
+
|
|
44
|
+
def create_bill(self, bill: Bill) -> ApiResponse[str]:
|
|
45
|
+
"""Create a new bill and return the payment code in ``response.res``."""
|
|
46
|
+
|
|
47
|
+
self._prepare_bill(bill)
|
|
48
|
+
return self._send("POST", "einvoice/api/bill", json_body=bill.to_dict())
|
|
49
|
+
|
|
50
|
+
def update_bill(self, bill: Bill) -> ApiResponse[str]:
|
|
51
|
+
"""Update an existing unpaid bill."""
|
|
52
|
+
|
|
53
|
+
self._prepare_bill(bill)
|
|
54
|
+
return self._send("PUT", "einvoice/api/bill", json_body=bill.to_dict())
|
|
55
|
+
|
|
56
|
+
def delete_bill(self, payment_code: str) -> ApiResponse[str]:
|
|
57
|
+
"""Delete an existing unpaid bill by payment code."""
|
|
58
|
+
|
|
59
|
+
return self._send("DELETE", "einvoice/api/bill", params={"wbc_code": payment_code}, json_body={})
|
|
60
|
+
|
|
61
|
+
def get_payment_status(self, payment_code: str) -> ApiResponse[PaymentStatus]:
|
|
62
|
+
"""Get single payment status by payment code."""
|
|
63
|
+
|
|
64
|
+
return self._send(
|
|
65
|
+
"GET",
|
|
66
|
+
"einvoice/api/paymentStatus",
|
|
67
|
+
params={"wbc_code": payment_code},
|
|
68
|
+
res_factory=PaymentStatus.from_dict,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_bill_by_reference(self, bill_reference: str) -> ApiResponse[BillResponse]:
|
|
72
|
+
"""Get a bill by merchant bill reference."""
|
|
73
|
+
|
|
74
|
+
return self._send(
|
|
75
|
+
"GET",
|
|
76
|
+
"einvoice/api/bill",
|
|
77
|
+
params={"bill_reference": bill_reference},
|
|
78
|
+
res_factory=BillResponse.from_dict,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def get_bill_by_payment_code(self, payment_code: str) -> ApiResponse[BillResponse]:
|
|
82
|
+
"""Get a bill by WeBirr payment code / WBC code."""
|
|
83
|
+
|
|
84
|
+
return self._send(
|
|
85
|
+
"GET",
|
|
86
|
+
"einvoice/api/bill",
|
|
87
|
+
params={"wbc_code": payment_code},
|
|
88
|
+
res_factory=BillResponse.from_dict,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def get_bills(
|
|
92
|
+
self,
|
|
93
|
+
payment_status: int = -1,
|
|
94
|
+
last_time_stamp: str = "",
|
|
95
|
+
limit: int = 100,
|
|
96
|
+
) -> ApiResponse[list[BillResponse]]:
|
|
97
|
+
"""List bills updated after a timestamp cursor."""
|
|
98
|
+
|
|
99
|
+
return self._send(
|
|
100
|
+
"GET",
|
|
101
|
+
"einvoice/api/bills",
|
|
102
|
+
params={
|
|
103
|
+
"payment_status": payment_status,
|
|
104
|
+
"last_timestamp": last_time_stamp,
|
|
105
|
+
"limit": limit,
|
|
106
|
+
},
|
|
107
|
+
res_factory=list_of(BillResponse.from_dict),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def get_payments(
|
|
111
|
+
self,
|
|
112
|
+
last_time_stamp: str = "",
|
|
113
|
+
limit: int = 100,
|
|
114
|
+
) -> ApiResponse[list[PaymentResponse]]:
|
|
115
|
+
"""Poll payment updates after a timestamp cursor."""
|
|
116
|
+
|
|
117
|
+
return self._send(
|
|
118
|
+
"GET",
|
|
119
|
+
"einvoice/api/payments",
|
|
120
|
+
params={"last_timestamp": last_time_stamp, "limit": limit},
|
|
121
|
+
res_factory=list_of(PaymentResponse.from_dict),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def get_stat(self, date_from: str, date_to: str) -> ApiResponse[Stat]:
|
|
125
|
+
"""Get basic merchant statistics for a date range."""
|
|
126
|
+
|
|
127
|
+
return self._send(
|
|
128
|
+
"GET",
|
|
129
|
+
"merchant/stat",
|
|
130
|
+
params={"date_from": date_from, "date_to": date_to},
|
|
131
|
+
res_factory=Stat.from_dict,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _prepare_bill(self, bill: Bill) -> Bill:
|
|
135
|
+
if self.merchant_id:
|
|
136
|
+
bill.merchant_id = self.merchant_id
|
|
137
|
+
return bill
|
|
138
|
+
|
|
139
|
+
def _query(self, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
140
|
+
query: dict[str, Any] = {"api_key": self.api_key}
|
|
141
|
+
if self.merchant_id:
|
|
142
|
+
query["merchant_id"] = self.merchant_id
|
|
143
|
+
if params:
|
|
144
|
+
query.update(params)
|
|
145
|
+
return query
|
|
146
|
+
|
|
147
|
+
def _send(
|
|
148
|
+
self,
|
|
149
|
+
method: str,
|
|
150
|
+
path: str,
|
|
151
|
+
params: dict[str, Any] | None = None,
|
|
152
|
+
json_body: Any | None = None,
|
|
153
|
+
res_factory: Callable[[Any], T] | None = None,
|
|
154
|
+
) -> ApiResponse[T]:
|
|
155
|
+
url = urljoin(f"{self.base_url}/", path)
|
|
156
|
+
response = self.session.request(
|
|
157
|
+
method,
|
|
158
|
+
url,
|
|
159
|
+
params=self._query(params),
|
|
160
|
+
json=json_body,
|
|
161
|
+
timeout=30,
|
|
162
|
+
)
|
|
163
|
+
return self._decode_response(response, res_factory)
|
|
164
|
+
|
|
165
|
+
def _decode_response(
|
|
166
|
+
self,
|
|
167
|
+
response: requests.Response,
|
|
168
|
+
res_factory: Callable[[Any], T] | None = None,
|
|
169
|
+
) -> ApiResponse[T]:
|
|
170
|
+
if not 200 <= response.status_code < 300:
|
|
171
|
+
reason = getattr(response, "reason", "") or ""
|
|
172
|
+
return ApiResponse(error=f"http error {response.status_code} {reason}".strip())
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
payload = response.json()
|
|
176
|
+
except (ValueError, json.JSONDecodeError):
|
|
177
|
+
return ApiResponse(error="invalid json response")
|
|
178
|
+
|
|
179
|
+
if not isinstance(payload, dict):
|
|
180
|
+
return ApiResponse(error="invalid response shape")
|
|
181
|
+
|
|
182
|
+
return ApiResponse.from_dict(payload, res_factory)
|
webirr/models.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Models used by the WeBirr Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Generic, Iterable, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _lookup(data: dict[str, Any], *keys: str, default: Any = "") -> Any:
|
|
12
|
+
for key in keys:
|
|
13
|
+
if key in data and data[key] is not None:
|
|
14
|
+
return data[key]
|
|
15
|
+
return default
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _to_dict(data: Any) -> dict[str, Any]:
|
|
19
|
+
if data is None:
|
|
20
|
+
return {}
|
|
21
|
+
if isinstance(data, dict):
|
|
22
|
+
return data
|
|
23
|
+
return vars(data)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ApiResponse(Generic[T]):
|
|
28
|
+
"""Common WeBirr API response wrapper."""
|
|
29
|
+
|
|
30
|
+
error: str | None = None
|
|
31
|
+
res: T | None = None
|
|
32
|
+
error_code: str | None = None
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def errorCode(self) -> str | None:
|
|
36
|
+
"""Gateway-style alias for callers that prefer the wire name."""
|
|
37
|
+
|
|
38
|
+
return self.error_code
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_dict(
|
|
42
|
+
cls,
|
|
43
|
+
data: dict[str, Any],
|
|
44
|
+
res_factory: Callable[[Any], T] | None = None,
|
|
45
|
+
) -> "ApiResponse[T]":
|
|
46
|
+
raw_res = data.get("res")
|
|
47
|
+
res = res_factory(raw_res) if res_factory and raw_res is not None else raw_res
|
|
48
|
+
return cls(
|
|
49
|
+
error=data.get("error"),
|
|
50
|
+
res=res,
|
|
51
|
+
error_code=data.get("errorCode") or data.get("error_code"),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Bill:
|
|
57
|
+
"""Create/update request model for WeBirr bills."""
|
|
58
|
+
|
|
59
|
+
amount: str = ""
|
|
60
|
+
customer_code: str = ""
|
|
61
|
+
customer_name: str = ""
|
|
62
|
+
customer_phone: str = ""
|
|
63
|
+
time: str = ""
|
|
64
|
+
description: str = ""
|
|
65
|
+
bill_reference: str = ""
|
|
66
|
+
merchant_id: str = ""
|
|
67
|
+
extras: dict[str, Any] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
return {
|
|
71
|
+
"amount": self.amount,
|
|
72
|
+
"customerCode": self.customer_code,
|
|
73
|
+
"customerName": self.customer_name,
|
|
74
|
+
"customerPhone": self.customer_phone,
|
|
75
|
+
"time": self.time,
|
|
76
|
+
"description": self.description,
|
|
77
|
+
"billReference": self.bill_reference,
|
|
78
|
+
"merchantID": self.merchant_id,
|
|
79
|
+
"extras": self.extras or {},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_dict(cls, data: Any) -> "Bill":
|
|
84
|
+
source = _to_dict(data)
|
|
85
|
+
return cls(
|
|
86
|
+
amount=str(_lookup(source, "amount")),
|
|
87
|
+
customer_code=str(_lookup(source, "customerCode", "customer_code")),
|
|
88
|
+
customer_name=str(_lookup(source, "customerName", "customer_name")),
|
|
89
|
+
customer_phone=str(_lookup(source, "customerPhone", "customer_phone")),
|
|
90
|
+
time=str(_lookup(source, "time")),
|
|
91
|
+
description=str(_lookup(source, "description")),
|
|
92
|
+
bill_reference=str(_lookup(source, "billReference", "bill_reference")),
|
|
93
|
+
merchant_id=str(_lookup(source, "merchantID", "merchant_id")),
|
|
94
|
+
extras=dict(_lookup(source, "extras", default={}) or {}),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class BillResponse(Bill):
|
|
100
|
+
"""Bill retrieval/list response model."""
|
|
101
|
+
|
|
102
|
+
wbc_code: str = ""
|
|
103
|
+
payment_status: int | None = None
|
|
104
|
+
update_time_stamp: str = ""
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: Any) -> "BillResponse":
|
|
108
|
+
source = _to_dict(data)
|
|
109
|
+
bill = Bill.from_dict(source)
|
|
110
|
+
return cls(
|
|
111
|
+
**bill.__dict__,
|
|
112
|
+
wbc_code=str(_lookup(source, "wbcCode", "wbc_code")),
|
|
113
|
+
payment_status=_lookup(source, "paymentStatus", "payment_status", default=None),
|
|
114
|
+
update_time_stamp=str(_lookup(source, "updateTimeStamp", "update_time_stamp")),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def wbcCode(self) -> str:
|
|
119
|
+
return self.wbc_code
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def paymentStatus(self) -> int | None:
|
|
123
|
+
return self.payment_status
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def updateTimeStamp(self) -> str:
|
|
127
|
+
return self.update_time_stamp
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class PaymentDetail:
|
|
132
|
+
"""Payment detail returned by single payment status."""
|
|
133
|
+
|
|
134
|
+
status: int | None = None
|
|
135
|
+
id: int | str | None = None
|
|
136
|
+
bank_id: str = ""
|
|
137
|
+
payment_reference: str = ""
|
|
138
|
+
payment_date: str = ""
|
|
139
|
+
time: str = ""
|
|
140
|
+
confirmed: bool | None = None
|
|
141
|
+
confirmed_time: str = ""
|
|
142
|
+
amount: str = ""
|
|
143
|
+
wbc_code: str = ""
|
|
144
|
+
update_time_stamp: str = ""
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_dict(cls, data: Any) -> "PaymentDetail":
|
|
148
|
+
source = _to_dict(data)
|
|
149
|
+
payment_date = str(_lookup(source, "paymentDate", "payment_date", "time"))
|
|
150
|
+
time = str(_lookup(source, "time", "paymentDate", "payment_date"))
|
|
151
|
+
return cls(
|
|
152
|
+
status=_lookup(source, "status", default=None),
|
|
153
|
+
id=_lookup(source, "id", default=None),
|
|
154
|
+
bank_id=str(_lookup(source, "bankID", "bank_id")),
|
|
155
|
+
payment_reference=str(_lookup(source, "paymentReference", "payment_reference")),
|
|
156
|
+
payment_date=payment_date,
|
|
157
|
+
time=time,
|
|
158
|
+
confirmed=_lookup(source, "confirmed", default=None),
|
|
159
|
+
confirmed_time=str(_lookup(source, "confirmedTime", "confirmed_time")),
|
|
160
|
+
amount=str(_lookup(source, "amount")),
|
|
161
|
+
wbc_code=str(_lookup(source, "wbcCode", "wbc_code")),
|
|
162
|
+
update_time_stamp=str(_lookup(source, "updateTimeStamp", "update_time_stamp")),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def bankID(self) -> str:
|
|
167
|
+
return self.bank_id
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def paymentReference(self) -> str:
|
|
171
|
+
return self.payment_reference
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def paymentDate(self) -> str:
|
|
175
|
+
return self.payment_date
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def confirmedTime(self) -> str:
|
|
179
|
+
return self.confirmed_time
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def wbcCode(self) -> str:
|
|
183
|
+
return self.wbc_code
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def updateTimeStamp(self) -> str:
|
|
187
|
+
return self.update_time_stamp
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class PaymentStatus:
|
|
192
|
+
"""Single payment status wrapper."""
|
|
193
|
+
|
|
194
|
+
status: int | None = None
|
|
195
|
+
data: PaymentDetail | None = None
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def is_paid(self) -> bool:
|
|
199
|
+
return self.status == 2
|
|
200
|
+
|
|
201
|
+
@classmethod
|
|
202
|
+
def from_dict(cls, data: Any) -> "PaymentStatus":
|
|
203
|
+
source = _to_dict(data)
|
|
204
|
+
raw_data = source.get("data")
|
|
205
|
+
return cls(
|
|
206
|
+
status=_lookup(source, "status", default=None),
|
|
207
|
+
data=PaymentDetail.from_dict(raw_data) if raw_data else None,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class PaymentResponse(PaymentDetail):
|
|
213
|
+
"""Payment item returned by bulk polling and webhook payloads."""
|
|
214
|
+
|
|
215
|
+
canceled: bool | None = None
|
|
216
|
+
canceled_time: str = ""
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def is_paid(self) -> bool:
|
|
220
|
+
return self.status == 2
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def is_reversed(self) -> bool:
|
|
224
|
+
return self.status == 3
|
|
225
|
+
|
|
226
|
+
@classmethod
|
|
227
|
+
def from_dict(cls, data: Any) -> "PaymentResponse":
|
|
228
|
+
source = _to_dict(data)
|
|
229
|
+
detail = PaymentDetail.from_dict(source)
|
|
230
|
+
return cls(
|
|
231
|
+
**detail.__dict__,
|
|
232
|
+
canceled=_lookup(source, "canceled", default=None),
|
|
233
|
+
canceled_time=str(_lookup(source, "canceledTime", "canceled_time")),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def canceledTime(self) -> str:
|
|
238
|
+
return self.canceled_time
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@dataclass
|
|
242
|
+
class Stat:
|
|
243
|
+
"""Basic statistics for bills and payments."""
|
|
244
|
+
|
|
245
|
+
n_bills: int | None = None
|
|
246
|
+
n_bills_paid: int | None = None
|
|
247
|
+
n_bills_unpaid: int | None = None
|
|
248
|
+
amount_bills: str = ""
|
|
249
|
+
amount_paid: str = ""
|
|
250
|
+
amount_unpaid: str = ""
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def from_dict(cls, data: Any) -> "Stat":
|
|
254
|
+
source = _to_dict(data)
|
|
255
|
+
return cls(
|
|
256
|
+
n_bills=_lookup(source, "NBills", "nBills", "n_bills", default=None),
|
|
257
|
+
n_bills_paid=_lookup(source, "NBillsPaid", "nBillsPaid", "n_bills_paid", default=None),
|
|
258
|
+
n_bills_unpaid=_lookup(source, "NBillsUnpaid", "nBillsUnpaid", "n_bills_unpaid", default=None),
|
|
259
|
+
amount_bills=str(_lookup(source, "AmountBills", "amountBills", "amount_bills")),
|
|
260
|
+
amount_paid=str(_lookup(source, "AmountPaid", "amountPaid", "amount_paid")),
|
|
261
|
+
amount_unpaid=str(_lookup(source, "AmountUnpaid", "amountUnpaid", "amount_unpaid")),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def nBills(self) -> int | None:
|
|
266
|
+
return self.n_bills
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def nBillsPaid(self) -> int | None:
|
|
270
|
+
return self.n_bills_paid
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def nBillsUnpaid(self) -> int | None:
|
|
274
|
+
return self.n_bills_unpaid
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def amountBills(self) -> str:
|
|
278
|
+
return self.amount_bills
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def amountPaid(self) -> str:
|
|
282
|
+
return self.amount_paid
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def amountUnpaid(self) -> str:
|
|
286
|
+
return self.amount_unpaid
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def list_of(factory: Callable[[Any], T]) -> Callable[[Iterable[Any]], list[T]]:
|
|
290
|
+
def parse(items: Iterable[Any]) -> list[T]:
|
|
291
|
+
return [factory(item) for item in items or []]
|
|
292
|
+
|
|
293
|
+
return parse
|
webirr/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
webirr/__init__.py,sha256=fGFaAdCTk092PtmF6j-c5GuP0UtZm-15vGahzcEObGQ,428
|
|
2
|
+
webirr/client.py,sha256=B_U5oo1zp4FCEcX0SOeebmZDEULByramR22MkY5XlIw,5708
|
|
3
|
+
webirr/models.py,sha256=Qw6j1fTtxjES_WYMGWK1Ouyv8vT99J4a5TrdxJDNcNI,8641
|
|
4
|
+
webirr/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
webirr-1.0.0.dist-info/licenses/LICENSE,sha256=YAsTWjq1dNhMcg_G_cdQNDp4Y7ZsfYGWqJmohMZiMt4,1058
|
|
6
|
+
webirr-1.0.0.dist-info/METADATA,sha256=2Yfj9eZ6qGAtbVaceadb0etpLd8ZBDuKK9o7yCjevEw,17208
|
|
7
|
+
webirr-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
webirr-1.0.0.dist-info/top_level.txt,sha256=VKdC2Vutf6dR68mk5x3zSRQZTziIohdGU1sL74BNxOA,7
|
|
9
|
+
webirr-1.0.0.dist-info/RECORD,,
|
|
@@ -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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
webirr
|