clickpesa-python-sdk 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.
- clickpesa/__init__.py +146 -0
- clickpesa/_version.py +1 -0
- clickpesa/async_client.py +307 -0
- clickpesa/client.py +302 -0
- clickpesa/exceptions.py +100 -0
- clickpesa/py.typed +0 -0
- clickpesa/security.py +74 -0
- clickpesa/services/__init__.py +21 -0
- clickpesa/services/account.py +87 -0
- clickpesa/services/billpay.py +340 -0
- clickpesa/services/exchange.py +74 -0
- clickpesa/services/links.py +169 -0
- clickpesa/services/payments.py +248 -0
- clickpesa/services/payouts.py +299 -0
- clickpesa/webhooks.py +42 -0
- clickpesa_python_sdk-0.1.0.dist-info/METADATA +512 -0
- clickpesa_python_sdk-0.1.0.dist-info/RECORD +20 -0
- clickpesa_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- clickpesa_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- clickpesa_python_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Payment collection services — USSD Push and Card Payments.
|
|
3
|
+
|
|
4
|
+
Sync: ``PaymentService`` — attach to :class:`~clickpesa.client.ClickPesaClient`.
|
|
5
|
+
Async: ``AsyncPaymentService`` — attach to :class:`~clickpesa.async_client.AsyncClickPesaClient`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..client import ClickPesaClient
|
|
14
|
+
from ..async_client import AsyncClickPesaClient
|
|
15
|
+
|
|
16
|
+
_BASE = "/third-parties/payments"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Sync
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class PaymentService:
|
|
24
|
+
"""Synchronous payment collection methods."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client: "ClickPesaClient") -> None:
|
|
27
|
+
self._c = client
|
|
28
|
+
|
|
29
|
+
def preview_ussd_push(
|
|
30
|
+
self,
|
|
31
|
+
amount: str,
|
|
32
|
+
order_id: str,
|
|
33
|
+
phone: str | None = None,
|
|
34
|
+
currency: str = "TZS",
|
|
35
|
+
fetch_sender_details: bool = False,
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""
|
|
38
|
+
Validate a USSD Push request and check available payment methods.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
amount: Payment amount.
|
|
42
|
+
order_id: Unique alphanumeric order reference.
|
|
43
|
+
phone: Customer phone with country code, no ``+``
|
|
44
|
+
(e.g. ``"255712345678"``). Optional.
|
|
45
|
+
currency: Must be ``"TZS"`` (default).
|
|
46
|
+
fetch_sender_details: When ``True`` the response includes the
|
|
47
|
+
sender's name, number and provider.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dict with ``activeMethods`` list and optional ``sender`` object.
|
|
51
|
+
"""
|
|
52
|
+
payload: dict[str, Any] = {
|
|
53
|
+
"amount": str(amount),
|
|
54
|
+
"currency": currency,
|
|
55
|
+
"orderReference": order_id,
|
|
56
|
+
"fetchSenderDetails": fetch_sender_details,
|
|
57
|
+
}
|
|
58
|
+
if phone is not None:
|
|
59
|
+
payload["phoneNumber"] = phone
|
|
60
|
+
return self._c.request("POST", f"{_BASE}/preview-ussd-push-request", json=payload)
|
|
61
|
+
|
|
62
|
+
def initiate_ussd_push(
|
|
63
|
+
self,
|
|
64
|
+
amount: str,
|
|
65
|
+
phone: str,
|
|
66
|
+
order_id: str,
|
|
67
|
+
currency: str = "TZS",
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Trigger the USSD PIN prompt on the customer's phone.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
amount: Payment amount.
|
|
74
|
+
phone: Customer phone with country code, no ``+``.
|
|
75
|
+
order_id: Unique alphanumeric order reference.
|
|
76
|
+
currency: Must be ``"TZS"`` (default).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Transaction object with ``id``, ``status``, ``channel``, etc.
|
|
80
|
+
"""
|
|
81
|
+
payload: dict[str, Any] = {
|
|
82
|
+
"amount": str(amount),
|
|
83
|
+
"phoneNumber": phone,
|
|
84
|
+
"currency": currency,
|
|
85
|
+
"orderReference": order_id,
|
|
86
|
+
}
|
|
87
|
+
return self._c.request("POST", f"{_BASE}/initiate-ussd-push-request", json=payload)
|
|
88
|
+
|
|
89
|
+
def preview_card(
|
|
90
|
+
self,
|
|
91
|
+
amount: str,
|
|
92
|
+
order_id: str,
|
|
93
|
+
currency: str = "USD",
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Validate card payment details and check available card methods.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
amount: Payment amount.
|
|
100
|
+
order_id: Unique alphanumeric order reference.
|
|
101
|
+
currency: Must be ``"USD"`` (default).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dict with ``activeMethods`` list (VISA / MASTER CARD).
|
|
105
|
+
"""
|
|
106
|
+
payload: dict[str, Any] = {
|
|
107
|
+
"amount": str(amount),
|
|
108
|
+
"currency": currency,
|
|
109
|
+
"orderReference": order_id,
|
|
110
|
+
}
|
|
111
|
+
return self._c.request("POST", f"{_BASE}/preview-card-payment", json=payload)
|
|
112
|
+
|
|
113
|
+
def initiate_card(
|
|
114
|
+
self,
|
|
115
|
+
amount: str,
|
|
116
|
+
order_id: str,
|
|
117
|
+
customer: dict[str, str],
|
|
118
|
+
currency: str = "USD",
|
|
119
|
+
) -> dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Generate a hosted card payment link for the customer.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
amount: Payment amount.
|
|
125
|
+
order_id: Unique alphanumeric order reference.
|
|
126
|
+
customer: Either ``{"id": "…"}`` **or**
|
|
127
|
+
``{"fullName": "…", "email": "…", "phoneNumber": "…"}``.
|
|
128
|
+
currency: Must be ``"USD"`` (default).
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dict with ``cardPaymentLink`` and ``clientId``.
|
|
132
|
+
"""
|
|
133
|
+
payload: dict[str, Any] = {
|
|
134
|
+
"amount": str(amount),
|
|
135
|
+
"orderReference": order_id,
|
|
136
|
+
"currency": currency,
|
|
137
|
+
"customer": customer,
|
|
138
|
+
}
|
|
139
|
+
return self._c.request("POST", f"{_BASE}/initiate-card-payment", json=payload)
|
|
140
|
+
|
|
141
|
+
def get_status(self, order_reference: str) -> list[dict[str, Any]]:
|
|
142
|
+
"""
|
|
143
|
+
Query the status of a payment by its order reference.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of payment objects matching the reference.
|
|
147
|
+
"""
|
|
148
|
+
return self._c.request("GET", f"{_BASE}/{order_reference}")
|
|
149
|
+
|
|
150
|
+
def list_all(self, **filters: Any) -> dict[str, Any]:
|
|
151
|
+
"""
|
|
152
|
+
Query all payments with optional filtering and pagination.
|
|
153
|
+
|
|
154
|
+
Keyword Args:
|
|
155
|
+
startDate (str): ``YYYY-MM-DD`` or ``DD-MM-YYYY``.
|
|
156
|
+
endDate (str): ``YYYY-MM-DD`` or ``DD-MM-YYYY``.
|
|
157
|
+
status (str): ``SUCCESS`` | ``SETTLED`` | ``PROCESSING``
|
|
158
|
+
| ``PENDING`` | ``FAILED``.
|
|
159
|
+
collectedCurrency (str): e.g. ``"TZS"`` or ``"USD"``.
|
|
160
|
+
channel (str): Payment channel identifier.
|
|
161
|
+
orderReference (str): Filter by specific reference.
|
|
162
|
+
sortBy (str): Any response field (default ``createdAt``).
|
|
163
|
+
orderBy (str): ``ASC`` or ``DESC`` (default ``DESC``).
|
|
164
|
+
skip (int): Pagination offset (default ``0``).
|
|
165
|
+
limit (int): Page size (default ``20``).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dict with ``data`` (list) and ``totalCount`` (int).
|
|
169
|
+
"""
|
|
170
|
+
return self._c.request("GET", f"{_BASE}/all", params=filters)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Async
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
class AsyncPaymentService:
|
|
178
|
+
"""Asynchronous payment collection methods (mirrors :class:`PaymentService`)."""
|
|
179
|
+
|
|
180
|
+
def __init__(self, client: "AsyncClickPesaClient") -> None:
|
|
181
|
+
self._c = client
|
|
182
|
+
|
|
183
|
+
async def preview_ussd_push(
|
|
184
|
+
self,
|
|
185
|
+
amount: str,
|
|
186
|
+
order_id: str,
|
|
187
|
+
phone: str | None = None,
|
|
188
|
+
currency: str = "TZS",
|
|
189
|
+
fetch_sender_details: bool = False,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
payload: dict[str, Any] = {
|
|
192
|
+
"amount": str(amount),
|
|
193
|
+
"currency": currency,
|
|
194
|
+
"orderReference": order_id,
|
|
195
|
+
"fetchSenderDetails": fetch_sender_details,
|
|
196
|
+
}
|
|
197
|
+
if phone is not None:
|
|
198
|
+
payload["phoneNumber"] = phone
|
|
199
|
+
return await self._c.request("POST", f"{_BASE}/preview-ussd-push-request", json=payload)
|
|
200
|
+
|
|
201
|
+
async def initiate_ussd_push(
|
|
202
|
+
self,
|
|
203
|
+
amount: str,
|
|
204
|
+
phone: str,
|
|
205
|
+
order_id: str,
|
|
206
|
+
currency: str = "TZS",
|
|
207
|
+
) -> dict[str, Any]:
|
|
208
|
+
payload: dict[str, Any] = {
|
|
209
|
+
"amount": str(amount),
|
|
210
|
+
"phoneNumber": phone,
|
|
211
|
+
"currency": currency,
|
|
212
|
+
"orderReference": order_id,
|
|
213
|
+
}
|
|
214
|
+
return await self._c.request("POST", f"{_BASE}/initiate-ussd-push-request", json=payload)
|
|
215
|
+
|
|
216
|
+
async def preview_card(
|
|
217
|
+
self,
|
|
218
|
+
amount: str,
|
|
219
|
+
order_id: str,
|
|
220
|
+
currency: str = "USD",
|
|
221
|
+
) -> dict[str, Any]:
|
|
222
|
+
payload: dict[str, Any] = {
|
|
223
|
+
"amount": str(amount),
|
|
224
|
+
"currency": currency,
|
|
225
|
+
"orderReference": order_id,
|
|
226
|
+
}
|
|
227
|
+
return await self._c.request("POST", f"{_BASE}/preview-card-payment", json=payload)
|
|
228
|
+
|
|
229
|
+
async def initiate_card(
|
|
230
|
+
self,
|
|
231
|
+
amount: str,
|
|
232
|
+
order_id: str,
|
|
233
|
+
customer: dict[str, str],
|
|
234
|
+
currency: str = "USD",
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
payload: dict[str, Any] = {
|
|
237
|
+
"amount": str(amount),
|
|
238
|
+
"orderReference": order_id,
|
|
239
|
+
"currency": currency,
|
|
240
|
+
"customer": customer,
|
|
241
|
+
}
|
|
242
|
+
return await self._c.request("POST", f"{_BASE}/initiate-card-payment", json=payload)
|
|
243
|
+
|
|
244
|
+
async def get_status(self, order_reference: str) -> list[dict[str, Any]]:
|
|
245
|
+
return await self._c.request("GET", f"{_BASE}/{order_reference}")
|
|
246
|
+
|
|
247
|
+
async def list_all(self, **filters: Any) -> dict[str, Any]:
|
|
248
|
+
return await self._c.request("GET", f"{_BASE}/all", params=filters)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Disbursement services — Mobile Money and Bank Payouts.
|
|
3
|
+
|
|
4
|
+
Sync: ``PayoutService`` — attach to :class:`~clickpesa.client.ClickPesaClient`.
|
|
5
|
+
Async: ``AsyncPayoutService`` — attach to :class:`~clickpesa.async_client.AsyncClickPesaClient`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..client import ClickPesaClient
|
|
14
|
+
from ..async_client import AsyncClickPesaClient
|
|
15
|
+
|
|
16
|
+
_BASE = "/third-parties/payouts"
|
|
17
|
+
_BANKS = "/third-parties/list/banks"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Sync
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
class PayoutService:
|
|
25
|
+
"""Synchronous disbursement methods."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, client: "ClickPesaClient") -> None:
|
|
28
|
+
self._c = client
|
|
29
|
+
|
|
30
|
+
# --- Mobile Money ---
|
|
31
|
+
|
|
32
|
+
def preview_mobile_money(
|
|
33
|
+
self,
|
|
34
|
+
amount: float,
|
|
35
|
+
phone: str,
|
|
36
|
+
order_id: str,
|
|
37
|
+
currency: str = "TZS",
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Check fees and account balance before disbursing to a mobile wallet.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
amount: Payout amount.
|
|
44
|
+
phone: Recipient phone with country code, no ``+``.
|
|
45
|
+
order_id: Unique alphanumeric order reference.
|
|
46
|
+
currency: Source currency — ``"TZS"`` or ``"USD"`` (default ``"TZS"``).
|
|
47
|
+
Recipient always receives funds in TZS.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Preview dict with ``amount``, ``balance``, ``fee``, ``receiver``, etc.
|
|
51
|
+
"""
|
|
52
|
+
payload: dict[str, Any] = {
|
|
53
|
+
"amount": amount,
|
|
54
|
+
"phoneNumber": phone,
|
|
55
|
+
"currency": currency,
|
|
56
|
+
"orderReference": order_id,
|
|
57
|
+
}
|
|
58
|
+
return self._c.request("POST", f"{_BASE}/preview-mobile-money-payout", json=payload)
|
|
59
|
+
|
|
60
|
+
def create_mobile_money(
|
|
61
|
+
self,
|
|
62
|
+
amount: float,
|
|
63
|
+
phone: str,
|
|
64
|
+
order_id: str,
|
|
65
|
+
currency: str = "TZS",
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
"""
|
|
68
|
+
Disburse funds to a mobile money wallet.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
amount: Payout amount.
|
|
72
|
+
phone: Recipient phone with country code, no ``+``.
|
|
73
|
+
order_id: Unique alphanumeric order reference.
|
|
74
|
+
currency: Source currency — ``"TZS"`` or ``"USD"`` (default ``"TZS"``).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Transaction object with ``id``, ``status``, ``beneficiary``, etc.
|
|
78
|
+
"""
|
|
79
|
+
payload: dict[str, Any] = {
|
|
80
|
+
"amount": amount,
|
|
81
|
+
"phoneNumber": phone,
|
|
82
|
+
"currency": currency,
|
|
83
|
+
"orderReference": order_id,
|
|
84
|
+
}
|
|
85
|
+
return self._c.request("POST", f"{_BASE}/create-mobile-money-payout", json=payload)
|
|
86
|
+
|
|
87
|
+
# --- Bank Payouts ---
|
|
88
|
+
|
|
89
|
+
def preview_bank(
|
|
90
|
+
self,
|
|
91
|
+
amount: float,
|
|
92
|
+
account_number: str,
|
|
93
|
+
bic: str,
|
|
94
|
+
order_id: str,
|
|
95
|
+
transfer_type: str = "ACH",
|
|
96
|
+
currency: str = "TZS",
|
|
97
|
+
account_currency: str = "TZS",
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Validate bank details and check fees before an ACH / RTGS transfer.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
amount: Payout amount.
|
|
104
|
+
account_number: Beneficiary bank account number.
|
|
105
|
+
bic: Bank identifier code — fetch via :meth:`get_banks`.
|
|
106
|
+
order_id: Unique alphanumeric order reference.
|
|
107
|
+
transfer_type: ``"ACH"`` (default) or ``"RTGS"``.
|
|
108
|
+
currency: Source currency — ``"TZS"`` or ``"USD"`` (default ``"TZS"``).
|
|
109
|
+
account_currency: Receiving currency — currently only ``"TZS"`` (default).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Preview dict with ``amount``, ``balance``, ``fee``, ``receiver``, etc.
|
|
113
|
+
"""
|
|
114
|
+
payload: dict[str, Any] = {
|
|
115
|
+
"amount": amount,
|
|
116
|
+
"accountNumber": account_number,
|
|
117
|
+
"bic": bic,
|
|
118
|
+
"orderReference": order_id,
|
|
119
|
+
"transferType": transfer_type,
|
|
120
|
+
"currency": currency,
|
|
121
|
+
"accountCurrency": account_currency,
|
|
122
|
+
}
|
|
123
|
+
return self._c.request("POST", f"{_BASE}/preview-bank-payout", json=payload)
|
|
124
|
+
|
|
125
|
+
def create_bank(
|
|
126
|
+
self,
|
|
127
|
+
amount: float,
|
|
128
|
+
account_number: str,
|
|
129
|
+
account_name: str,
|
|
130
|
+
bic: str,
|
|
131
|
+
order_id: str,
|
|
132
|
+
transfer_type: str = "ACH",
|
|
133
|
+
currency: str = "TZS",
|
|
134
|
+
account_currency: str = "TZS",
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
"""
|
|
137
|
+
Disburse funds to a bank account.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
amount: Payout amount.
|
|
141
|
+
account_number: Beneficiary bank account number.
|
|
142
|
+
account_name: Beneficiary name as registered with the bank.
|
|
143
|
+
bic: Bank identifier code — fetch via :meth:`get_banks`.
|
|
144
|
+
order_id: Unique alphanumeric order reference.
|
|
145
|
+
transfer_type: ``"ACH"`` (default) or ``"RTGS"``.
|
|
146
|
+
currency: Source currency — ``"TZS"`` or ``"USD"`` (default ``"TZS"``).
|
|
147
|
+
account_currency: Receiving currency — currently only ``"TZS"`` (default).
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Transaction object with ``id``, ``status``, ``beneficiary``, etc.
|
|
151
|
+
"""
|
|
152
|
+
payload: dict[str, Any] = {
|
|
153
|
+
"amount": amount,
|
|
154
|
+
"accountNumber": account_number,
|
|
155
|
+
"accountName": account_name,
|
|
156
|
+
"bic": bic,
|
|
157
|
+
"orderReference": order_id,
|
|
158
|
+
"transferType": transfer_type,
|
|
159
|
+
"currency": currency,
|
|
160
|
+
"accountCurrency": account_currency,
|
|
161
|
+
}
|
|
162
|
+
return self._c.request("POST", f"{_BASE}/create-bank-payout", json=payload)
|
|
163
|
+
|
|
164
|
+
# --- Utilities ---
|
|
165
|
+
|
|
166
|
+
def get_banks(self) -> list[dict[str, Any]]:
|
|
167
|
+
"""
|
|
168
|
+
Fetch the list of supported banks and their BIC codes.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of ``{"name": "…", "bic": "…"}`` dicts.
|
|
172
|
+
"""
|
|
173
|
+
return self._c.request("GET", _BANKS)
|
|
174
|
+
|
|
175
|
+
def get_status(self, order_reference: str) -> list[dict[str, Any]]:
|
|
176
|
+
"""
|
|
177
|
+
Query the status of a payout by its order reference.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of payout objects matching the reference.
|
|
181
|
+
"""
|
|
182
|
+
return self._c.request("GET", f"{_BASE}/{order_reference}")
|
|
183
|
+
|
|
184
|
+
def list_all(self, **filters: Any) -> dict[str, Any]:
|
|
185
|
+
"""
|
|
186
|
+
Query payout history with optional filtering and pagination.
|
|
187
|
+
|
|
188
|
+
Keyword Args:
|
|
189
|
+
startDate (str): ``YYYY-MM-DD`` or ``DD-MM-YYYY``.
|
|
190
|
+
endDate (str): ``YYYY-MM-DD`` or ``DD-MM-YYYY``.
|
|
191
|
+
channel (str): ``"BANK TRANSFER"`` | ``"MOBILE MONEY"``.
|
|
192
|
+
currency (str): e.g. ``"TZS"`` or ``"USD"``.
|
|
193
|
+
orderReference (str): Filter by specific reference.
|
|
194
|
+
status (str): ``SUCCESS`` | ``PROCESSING`` | ``PENDING``
|
|
195
|
+
| ``FAILED`` | ``REFUNDED`` | ``REVERSED``.
|
|
196
|
+
transferType (str): ``"ACH"`` | ``"RTGS"``.
|
|
197
|
+
sortBy (str): Any response field (default ``createdAt``).
|
|
198
|
+
orderBy (str): ``ASC`` or ``DESC`` (default ``DESC``).
|
|
199
|
+
skip (int): Pagination offset (default ``0``).
|
|
200
|
+
limit (int): Page size (default ``20``).
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dict with ``data`` (list) and ``totalCount`` (int).
|
|
204
|
+
"""
|
|
205
|
+
return self._c.request("GET", f"{_BASE}/all", params=filters)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Async
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
class AsyncPayoutService:
|
|
213
|
+
"""Asynchronous disbursement methods (mirrors :class:`PayoutService`)."""
|
|
214
|
+
|
|
215
|
+
def __init__(self, client: "AsyncClickPesaClient") -> None:
|
|
216
|
+
self._c = client
|
|
217
|
+
|
|
218
|
+
async def preview_mobile_money(
|
|
219
|
+
self,
|
|
220
|
+
amount: float,
|
|
221
|
+
phone: str,
|
|
222
|
+
order_id: str,
|
|
223
|
+
currency: str = "TZS",
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
|
+
payload: dict[str, Any] = {
|
|
226
|
+
"amount": amount,
|
|
227
|
+
"phoneNumber": phone,
|
|
228
|
+
"currency": currency,
|
|
229
|
+
"orderReference": order_id,
|
|
230
|
+
}
|
|
231
|
+
return await self._c.request("POST", f"{_BASE}/preview-mobile-money-payout", json=payload)
|
|
232
|
+
|
|
233
|
+
async def create_mobile_money(
|
|
234
|
+
self,
|
|
235
|
+
amount: float,
|
|
236
|
+
phone: str,
|
|
237
|
+
order_id: str,
|
|
238
|
+
currency: str = "TZS",
|
|
239
|
+
) -> dict[str, Any]:
|
|
240
|
+
payload: dict[str, Any] = {
|
|
241
|
+
"amount": amount,
|
|
242
|
+
"phoneNumber": phone,
|
|
243
|
+
"currency": currency,
|
|
244
|
+
"orderReference": order_id,
|
|
245
|
+
}
|
|
246
|
+
return await self._c.request("POST", f"{_BASE}/create-mobile-money-payout", json=payload)
|
|
247
|
+
|
|
248
|
+
async def preview_bank(
|
|
249
|
+
self,
|
|
250
|
+
amount: float,
|
|
251
|
+
account_number: str,
|
|
252
|
+
bic: str,
|
|
253
|
+
order_id: str,
|
|
254
|
+
transfer_type: str = "ACH",
|
|
255
|
+
currency: str = "TZS",
|
|
256
|
+
account_currency: str = "TZS",
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
payload: dict[str, Any] = {
|
|
259
|
+
"amount": amount,
|
|
260
|
+
"accountNumber": account_number,
|
|
261
|
+
"bic": bic,
|
|
262
|
+
"orderReference": order_id,
|
|
263
|
+
"transferType": transfer_type,
|
|
264
|
+
"currency": currency,
|
|
265
|
+
"accountCurrency": account_currency,
|
|
266
|
+
}
|
|
267
|
+
return await self._c.request("POST", f"{_BASE}/preview-bank-payout", json=payload)
|
|
268
|
+
|
|
269
|
+
async def create_bank(
|
|
270
|
+
self,
|
|
271
|
+
amount: float,
|
|
272
|
+
account_number: str,
|
|
273
|
+
account_name: str,
|
|
274
|
+
bic: str,
|
|
275
|
+
order_id: str,
|
|
276
|
+
transfer_type: str = "ACH",
|
|
277
|
+
currency: str = "TZS",
|
|
278
|
+
account_currency: str = "TZS",
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
payload: dict[str, Any] = {
|
|
281
|
+
"amount": amount,
|
|
282
|
+
"accountNumber": account_number,
|
|
283
|
+
"accountName": account_name,
|
|
284
|
+
"bic": bic,
|
|
285
|
+
"orderReference": order_id,
|
|
286
|
+
"transferType": transfer_type,
|
|
287
|
+
"currency": currency,
|
|
288
|
+
"accountCurrency": account_currency,
|
|
289
|
+
}
|
|
290
|
+
return await self._c.request("POST", f"{_BASE}/create-bank-payout", json=payload)
|
|
291
|
+
|
|
292
|
+
async def get_banks(self) -> list[dict[str, Any]]:
|
|
293
|
+
return await self._c.request("GET", _BANKS)
|
|
294
|
+
|
|
295
|
+
async def get_status(self, order_reference: str) -> list[dict[str, Any]]:
|
|
296
|
+
return await self._c.request("GET", f"{_BASE}/{order_reference}")
|
|
297
|
+
|
|
298
|
+
async def list_all(self, **filters: Any) -> dict[str, Any]:
|
|
299
|
+
return await self._c.request("GET", f"{_BASE}/all", params=filters)
|
clickpesa/webhooks.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook signature verification helpers.
|
|
3
|
+
|
|
4
|
+
Usage::
|
|
5
|
+
|
|
6
|
+
from clickpesa import WebhookValidator
|
|
7
|
+
|
|
8
|
+
is_valid = WebhookValidator.verify(
|
|
9
|
+
payload=request.json(),
|
|
10
|
+
signature=request.headers["X-ClickPesa-Signature"],
|
|
11
|
+
checksum_key="your-checksum-secret",
|
|
12
|
+
)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from .security import SecurityManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WebhookValidator:
|
|
21
|
+
"""Static helper for validating ClickPesa webhook payloads."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def verify(payload: dict, signature: str, checksum_key: str) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Verify that an incoming webhook was genuinely sent by ClickPesa.
|
|
27
|
+
|
|
28
|
+
Uses constant-time comparison (``hmac.compare_digest``) to prevent
|
|
29
|
+
timing-based attacks.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
payload: Parsed JSON body of the webhook request.
|
|
33
|
+
signature: Value of the ``X-ClickPesa-Signature`` header.
|
|
34
|
+
checksum_key: Your application's checksum secret key.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
``True`` if the signature is valid, ``False`` otherwise.
|
|
38
|
+
"""
|
|
39
|
+
return SecurityManager.verify_webhook(checksum_key, payload, signature)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["WebhookValidator"]
|