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,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BillPay collection services — Order and Customer Control Numbers.
|
|
3
|
+
|
|
4
|
+
Sync: ``BillPayService`` — attach to :class:`~clickpesa.client.ClickPesaClient`.
|
|
5
|
+
Async: ``AsyncBillPayService`` — attach to :class:`~clickpesa.async_client.AsyncClickPesaClient`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..client import ClickPesaClient
|
|
14
|
+
from ..async_client import AsyncClickPesaClient
|
|
15
|
+
|
|
16
|
+
_BASE = "/third-parties/billpay"
|
|
17
|
+
_BULK_LIMIT = 50
|
|
18
|
+
|
|
19
|
+
BillPaymentMode = Literal["ALLOW_PARTIAL_AND_OVER_PAYMENT", "EXACT"]
|
|
20
|
+
BillStatus = Literal["ACTIVE", "INACTIVE"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Sync
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
class BillPayService:
|
|
28
|
+
"""Synchronous BillPay control-number management."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, client: "ClickPesaClient") -> None:
|
|
31
|
+
self._c = client
|
|
32
|
+
|
|
33
|
+
def create_order_control_number(
|
|
34
|
+
self,
|
|
35
|
+
bill_reference: str | None = None,
|
|
36
|
+
amount: float | None = None,
|
|
37
|
+
description: str | None = None,
|
|
38
|
+
payment_mode: BillPaymentMode | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Generate a one-time order control number.
|
|
42
|
+
|
|
43
|
+
All fields are optional. If ``bill_reference`` is omitted the API
|
|
44
|
+
auto-generates one. ``payment_mode`` is only applied when ``amount``
|
|
45
|
+
is also set.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
bill_reference: Custom alphanumeric reference (must be unique).
|
|
49
|
+
amount: Bill amount.
|
|
50
|
+
description: Human-readable bill description.
|
|
51
|
+
payment_mode: ``"ALLOW_PARTIAL_AND_OVER_PAYMENT"`` (default) or ``"EXACT"``.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dict with ``billPayNumber``, ``billDescription``, ``billAmount``, etc.
|
|
55
|
+
"""
|
|
56
|
+
payload: dict[str, Any] = {}
|
|
57
|
+
if bill_reference is not None:
|
|
58
|
+
payload["billReference"] = bill_reference
|
|
59
|
+
if amount is not None:
|
|
60
|
+
payload["billAmount"] = amount
|
|
61
|
+
if description is not None:
|
|
62
|
+
payload["billDescription"] = description
|
|
63
|
+
if payment_mode is not None:
|
|
64
|
+
payload["billPaymentMode"] = payment_mode
|
|
65
|
+
return self._c.request("POST", f"{_BASE}/create-order-control-number", json=payload)
|
|
66
|
+
|
|
67
|
+
def create_customer_control_number(
|
|
68
|
+
self,
|
|
69
|
+
customer_name: str,
|
|
70
|
+
phone: str | None = None,
|
|
71
|
+
email: str | None = None,
|
|
72
|
+
bill_reference: str | None = None,
|
|
73
|
+
amount: float | None = None,
|
|
74
|
+
description: str | None = None,
|
|
75
|
+
payment_mode: BillPaymentMode | None = None,
|
|
76
|
+
) -> dict[str, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Generate a persistent control number tied to a specific customer.
|
|
79
|
+
|
|
80
|
+
Either ``phone`` or ``email`` (or both) must be supplied.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
customer_name: Customer's full name.
|
|
84
|
+
phone: Phone with country code, no ``+`` (e.g. ``"255712345678"``).
|
|
85
|
+
email: Customer email address.
|
|
86
|
+
bill_reference: Custom alphanumeric reference (must be unique).
|
|
87
|
+
amount: Bill amount.
|
|
88
|
+
description: Human-readable bill description.
|
|
89
|
+
payment_mode: ``"ALLOW_PARTIAL_AND_OVER_PAYMENT"`` (default) or ``"EXACT"``.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dict with ``billPayNumber``, ``billCustomerName``, ``billAmount``, etc.
|
|
93
|
+
"""
|
|
94
|
+
if phone is None and email is None:
|
|
95
|
+
raise ValueError("At least one of 'phone' or 'email' must be provided.")
|
|
96
|
+
|
|
97
|
+
payload: dict[str, Any] = {"customerName": customer_name}
|
|
98
|
+
if phone is not None:
|
|
99
|
+
payload["customerPhone"] = phone
|
|
100
|
+
if email is not None:
|
|
101
|
+
payload["customerEmail"] = email
|
|
102
|
+
if bill_reference is not None:
|
|
103
|
+
payload["billReference"] = bill_reference
|
|
104
|
+
if amount is not None:
|
|
105
|
+
payload["billAmount"] = amount
|
|
106
|
+
if description is not None:
|
|
107
|
+
payload["billDescription"] = description
|
|
108
|
+
if payment_mode is not None:
|
|
109
|
+
payload["billPaymentMode"] = payment_mode
|
|
110
|
+
return self._c.request("POST", f"{_BASE}/create-customer-control-number", json=payload)
|
|
111
|
+
|
|
112
|
+
def bulk_create_order_numbers(
|
|
113
|
+
self,
|
|
114
|
+
control_numbers: list[dict[str, Any]],
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Bulk-create up to 50 order control numbers in a single request.
|
|
118
|
+
|
|
119
|
+
Each item may contain: ``billReference``, ``billAmount``,
|
|
120
|
+
``billDescription``, ``billPaymentMode``.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
control_numbers: List of order dicts (1–50 items).
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dict with ``billPayNumbers``, ``created``, ``failed``, and
|
|
127
|
+
optional ``errors`` list.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If the list exceeds 50 items or is empty.
|
|
131
|
+
"""
|
|
132
|
+
if not control_numbers:
|
|
133
|
+
raise ValueError("control_numbers must contain at least 1 item.")
|
|
134
|
+
if len(control_numbers) > _BULK_LIMIT:
|
|
135
|
+
raise ValueError(f"ClickPesa bulk limit is {_BULK_LIMIT} items per request.")
|
|
136
|
+
return self._c.request(
|
|
137
|
+
"POST",
|
|
138
|
+
f"{_BASE}/bulk-create-order-control-numbers",
|
|
139
|
+
json={"controlNumbers": control_numbers},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def bulk_create_customer_numbers(
|
|
143
|
+
self,
|
|
144
|
+
control_numbers: list[dict[str, Any]],
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""
|
|
147
|
+
Bulk-create up to 50 customer control numbers in a single request.
|
|
148
|
+
|
|
149
|
+
Each item must contain ``customerName`` and at least one of
|
|
150
|
+
``customerPhone`` / ``customerEmail``. Optional fields:
|
|
151
|
+
``billReference``, ``billAmount``, ``billDescription``, ``billPaymentMode``.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
control_numbers: List of customer dicts (1–50 items).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Dict with ``billPayNumbers``, ``created``, ``failed``, and
|
|
158
|
+
optional ``errors`` list.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
ValueError: If the list exceeds 50 items or is empty.
|
|
162
|
+
"""
|
|
163
|
+
if not control_numbers:
|
|
164
|
+
raise ValueError("control_numbers must contain at least 1 item.")
|
|
165
|
+
if len(control_numbers) > _BULK_LIMIT:
|
|
166
|
+
raise ValueError(f"ClickPesa bulk limit is {_BULK_LIMIT} items per request.")
|
|
167
|
+
return self._c.request(
|
|
168
|
+
"POST",
|
|
169
|
+
f"{_BASE}/bulk-create-customer-control-numbers",
|
|
170
|
+
json={"controlNumbers": control_numbers},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def get_details(self, bill_pay_number: str) -> dict[str, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Query details of a specific control number.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Dict with ``billPayNumber``, ``billDescription``, ``billAmount``,
|
|
179
|
+
``billPaymentMode``, and ``billCustomerName``.
|
|
180
|
+
"""
|
|
181
|
+
return self._c.request("GET", f"{_BASE}/{bill_pay_number}")
|
|
182
|
+
|
|
183
|
+
def update_reference(
|
|
184
|
+
self,
|
|
185
|
+
bill_pay_number: str,
|
|
186
|
+
amount: float | None = None,
|
|
187
|
+
description: str | None = None,
|
|
188
|
+
status: BillStatus | None = None,
|
|
189
|
+
payment_mode: BillPaymentMode | None = None,
|
|
190
|
+
) -> dict[str, Any]:
|
|
191
|
+
"""
|
|
192
|
+
Partially update a BillPay reference.
|
|
193
|
+
|
|
194
|
+
At least one argument besides ``bill_pay_number`` must be provided.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
bill_pay_number: The BillPay number to update.
|
|
198
|
+
amount: New bill amount (positive, max 2 decimal places).
|
|
199
|
+
description: New description (max 500 characters).
|
|
200
|
+
status: ``"ACTIVE"`` or ``"INACTIVE"``.
|
|
201
|
+
payment_mode: ``"ALLOW_PARTIAL_AND_OVER_PAYMENT"`` or ``"EXACT"``.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Updated BillPay object.
|
|
205
|
+
"""
|
|
206
|
+
data: dict[str, Any] = {}
|
|
207
|
+
if amount is not None:
|
|
208
|
+
data["billAmount"] = amount
|
|
209
|
+
if description is not None:
|
|
210
|
+
data["billDescription"] = description
|
|
211
|
+
if status is not None:
|
|
212
|
+
data["billStatus"] = status
|
|
213
|
+
if payment_mode is not None:
|
|
214
|
+
data["billPaymentMode"] = payment_mode
|
|
215
|
+
if not data:
|
|
216
|
+
raise ValueError("At least one field must be provided to update.")
|
|
217
|
+
return self._c.request("PATCH", f"{_BASE}/{bill_pay_number}", json=data)
|
|
218
|
+
|
|
219
|
+
def update_status(self, bill_pay_number: str, status: BillStatus) -> dict[str, Any]:
|
|
220
|
+
"""
|
|
221
|
+
Convenience method to activate or deactivate a control number.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
bill_pay_number: The BillPay number to update.
|
|
225
|
+
status: ``"ACTIVE"`` or ``"INACTIVE"``.
|
|
226
|
+
"""
|
|
227
|
+
return self.update_reference(bill_pay_number, status=status)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Async
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
class AsyncBillPayService:
|
|
235
|
+
"""Asynchronous BillPay control-number management (mirrors :class:`BillPayService`)."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, client: "AsyncClickPesaClient") -> None:
|
|
238
|
+
self._c = client
|
|
239
|
+
|
|
240
|
+
async def create_order_control_number(
|
|
241
|
+
self,
|
|
242
|
+
bill_reference: str | None = None,
|
|
243
|
+
amount: float | None = None,
|
|
244
|
+
description: str | None = None,
|
|
245
|
+
payment_mode: BillPaymentMode | None = None,
|
|
246
|
+
) -> dict[str, Any]:
|
|
247
|
+
payload: dict[str, Any] = {}
|
|
248
|
+
if bill_reference is not None:
|
|
249
|
+
payload["billReference"] = bill_reference
|
|
250
|
+
if amount is not None:
|
|
251
|
+
payload["billAmount"] = amount
|
|
252
|
+
if description is not None:
|
|
253
|
+
payload["billDescription"] = description
|
|
254
|
+
if payment_mode is not None:
|
|
255
|
+
payload["billPaymentMode"] = payment_mode
|
|
256
|
+
return await self._c.request("POST", f"{_BASE}/create-order-control-number", json=payload)
|
|
257
|
+
|
|
258
|
+
async def create_customer_control_number(
|
|
259
|
+
self,
|
|
260
|
+
customer_name: str,
|
|
261
|
+
phone: str | None = None,
|
|
262
|
+
email: str | None = None,
|
|
263
|
+
bill_reference: str | None = None,
|
|
264
|
+
amount: float | None = None,
|
|
265
|
+
description: str | None = None,
|
|
266
|
+
payment_mode: BillPaymentMode | None = None,
|
|
267
|
+
) -> dict[str, Any]:
|
|
268
|
+
if phone is None and email is None:
|
|
269
|
+
raise ValueError("At least one of 'phone' or 'email' must be provided.")
|
|
270
|
+
payload: dict[str, Any] = {"customerName": customer_name}
|
|
271
|
+
if phone is not None:
|
|
272
|
+
payload["customerPhone"] = phone
|
|
273
|
+
if email is not None:
|
|
274
|
+
payload["customerEmail"] = email
|
|
275
|
+
if bill_reference is not None:
|
|
276
|
+
payload["billReference"] = bill_reference
|
|
277
|
+
if amount is not None:
|
|
278
|
+
payload["billAmount"] = amount
|
|
279
|
+
if description is not None:
|
|
280
|
+
payload["billDescription"] = description
|
|
281
|
+
if payment_mode is not None:
|
|
282
|
+
payload["billPaymentMode"] = payment_mode
|
|
283
|
+
return await self._c.request(
|
|
284
|
+
"POST", f"{_BASE}/create-customer-control-number", json=payload
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def bulk_create_order_numbers(
|
|
288
|
+
self,
|
|
289
|
+
control_numbers: list[dict[str, Any]],
|
|
290
|
+
) -> dict[str, Any]:
|
|
291
|
+
if not control_numbers:
|
|
292
|
+
raise ValueError("control_numbers must contain at least 1 item.")
|
|
293
|
+
if len(control_numbers) > _BULK_LIMIT:
|
|
294
|
+
raise ValueError(f"ClickPesa bulk limit is {_BULK_LIMIT} items per request.")
|
|
295
|
+
return await self._c.request(
|
|
296
|
+
"POST",
|
|
297
|
+
f"{_BASE}/bulk-create-order-control-numbers",
|
|
298
|
+
json={"controlNumbers": control_numbers},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
async def bulk_create_customer_numbers(
|
|
302
|
+
self,
|
|
303
|
+
control_numbers: list[dict[str, Any]],
|
|
304
|
+
) -> dict[str, Any]:
|
|
305
|
+
if not control_numbers:
|
|
306
|
+
raise ValueError("control_numbers must contain at least 1 item.")
|
|
307
|
+
if len(control_numbers) > _BULK_LIMIT:
|
|
308
|
+
raise ValueError(f"ClickPesa bulk limit is {_BULK_LIMIT} items per request.")
|
|
309
|
+
return await self._c.request(
|
|
310
|
+
"POST",
|
|
311
|
+
f"{_BASE}/bulk-create-customer-control-numbers",
|
|
312
|
+
json={"controlNumbers": control_numbers},
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
async def get_details(self, bill_pay_number: str) -> dict[str, Any]:
|
|
316
|
+
return await self._c.request("GET", f"{_BASE}/{bill_pay_number}")
|
|
317
|
+
|
|
318
|
+
async def update_reference(
|
|
319
|
+
self,
|
|
320
|
+
bill_pay_number: str,
|
|
321
|
+
amount: float | None = None,
|
|
322
|
+
description: str | None = None,
|
|
323
|
+
status: BillStatus | None = None,
|
|
324
|
+
payment_mode: BillPaymentMode | None = None,
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
data: dict[str, Any] = {}
|
|
327
|
+
if amount is not None:
|
|
328
|
+
data["billAmount"] = amount
|
|
329
|
+
if description is not None:
|
|
330
|
+
data["billDescription"] = description
|
|
331
|
+
if status is not None:
|
|
332
|
+
data["billStatus"] = status
|
|
333
|
+
if payment_mode is not None:
|
|
334
|
+
data["billPaymentMode"] = payment_mode
|
|
335
|
+
if not data:
|
|
336
|
+
raise ValueError("At least one field must be provided to update.")
|
|
337
|
+
return await self._c.request("PATCH", f"{_BASE}/{bill_pay_number}", json=data)
|
|
338
|
+
|
|
339
|
+
async def update_status(self, bill_pay_number: str, status: BillStatus) -> dict[str, Any]:
|
|
340
|
+
return await self.update_reference(bill_pay_number, status=status)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exchange rate services.
|
|
3
|
+
|
|
4
|
+
Sync: ``ExchangeService`` — attach to :class:`~clickpesa.client.ClickPesaClient`.
|
|
5
|
+
Async: ``AsyncExchangeService`` — 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
|
+
_ENDPOINT = "/third-parties/exchange-rates/all"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Sync
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class ExchangeService:
|
|
24
|
+
"""Synchronous exchange rate methods."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client: "ClickPesaClient") -> None:
|
|
27
|
+
self._c = client
|
|
28
|
+
|
|
29
|
+
def get_rates(
|
|
30
|
+
self,
|
|
31
|
+
source: str | None = None,
|
|
32
|
+
target: str | None = None,
|
|
33
|
+
) -> list[dict[str, Any]]:
|
|
34
|
+
"""
|
|
35
|
+
Fetch the latest exchange rates.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
source: ISO 4217 source currency code (e.g. ``"USD"``).
|
|
39
|
+
When omitted all available source currencies are returned.
|
|
40
|
+
target: ISO 4217 target currency code (e.g. ``"TZS"``).
|
|
41
|
+
When omitted all available target currencies are returned.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of ``{"source": "…", "target": "…", "rate": 2510, "date": "…"}`` dicts.
|
|
45
|
+
"""
|
|
46
|
+
params: dict[str, Any] = {}
|
|
47
|
+
if source is not None:
|
|
48
|
+
params["source"] = source
|
|
49
|
+
if target is not None:
|
|
50
|
+
params["target"] = target
|
|
51
|
+
return self._c.request("GET", _ENDPOINT, params=params)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Async
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
class AsyncExchangeService:
|
|
59
|
+
"""Asynchronous exchange rate methods (mirrors :class:`ExchangeService`)."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, client: "AsyncClickPesaClient") -> None:
|
|
62
|
+
self._c = client
|
|
63
|
+
|
|
64
|
+
async def get_rates(
|
|
65
|
+
self,
|
|
66
|
+
source: str | None = None,
|
|
67
|
+
target: str | None = None,
|
|
68
|
+
) -> list[dict[str, Any]]:
|
|
69
|
+
params: dict[str, Any] = {}
|
|
70
|
+
if source is not None:
|
|
71
|
+
params["source"] = source
|
|
72
|
+
if target is not None:
|
|
73
|
+
params["target"] = target
|
|
74
|
+
return await self._c.request("GET", _ENDPOINT, params=params)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hosted link generation services — Checkout and Payout Links.
|
|
3
|
+
|
|
4
|
+
Sync: ``LinkService`` — attach to :class:`~clickpesa.client.ClickPesaClient`.
|
|
5
|
+
Async: ``AsyncLinkService`` — attach to :class:`~clickpesa.async_client.AsyncClickPesaClient`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..client import ClickPesaClient
|
|
14
|
+
from ..async_client import AsyncClickPesaClient
|
|
15
|
+
|
|
16
|
+
_CHECKOUT = "/third-parties/checkout-link/generate-checkout-url"
|
|
17
|
+
_PAYOUT = "/third-parties/payout-link/generate-payout-url"
|
|
18
|
+
|
|
19
|
+
OrderCurrency = Literal["TZS", "USD"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Sync
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
class LinkService:
|
|
27
|
+
"""Synchronous hosted-link generation methods."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, client: "ClickPesaClient") -> None:
|
|
30
|
+
self._c = client
|
|
31
|
+
|
|
32
|
+
def generate_checkout(
|
|
33
|
+
self,
|
|
34
|
+
order_id: str,
|
|
35
|
+
order_currency: OrderCurrency,
|
|
36
|
+
total_price: str | None = None,
|
|
37
|
+
order_items: list[dict[str, Any]] | None = None,
|
|
38
|
+
customer_name: str | None = None,
|
|
39
|
+
customer_email: str | None = None,
|
|
40
|
+
customer_phone: str | None = None,
|
|
41
|
+
description: str | None = None,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Generate a hosted checkout payment page.
|
|
45
|
+
|
|
46
|
+
Supply **either** ``total_price`` **or** ``order_items`` — not both.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
order_id: Unique alphanumeric order reference.
|
|
50
|
+
order_currency: ``"TZS"`` or ``"USD"``.
|
|
51
|
+
total_price: Order total as a string (Option 1).
|
|
52
|
+
order_items: List of ``{"name": …, "price": …, "quantity": …}``
|
|
53
|
+
dicts (Option 2).
|
|
54
|
+
customer_name: Pre-fill customer name on the checkout page.
|
|
55
|
+
customer_email: Pre-fill customer email.
|
|
56
|
+
customer_phone: Customer phone with country code, no ``+``.
|
|
57
|
+
description: Order description shown on the checkout page.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Dict with ``checkoutLink`` (URL string) and ``clientId``.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If neither or both of ``total_price`` / ``order_items``
|
|
64
|
+
are provided.
|
|
65
|
+
"""
|
|
66
|
+
if total_price is None and order_items is None:
|
|
67
|
+
raise ValueError("Provide either 'total_price' or 'order_items'.")
|
|
68
|
+
if total_price is not None and order_items is not None:
|
|
69
|
+
raise ValueError("Provide either 'total_price' or 'order_items', not both.")
|
|
70
|
+
|
|
71
|
+
payload: dict[str, Any] = {
|
|
72
|
+
"orderReference": order_id,
|
|
73
|
+
"orderCurrency": order_currency,
|
|
74
|
+
}
|
|
75
|
+
if total_price is not None:
|
|
76
|
+
payload["totalPrice"] = str(total_price)
|
|
77
|
+
if order_items is not None:
|
|
78
|
+
payload["orderItems"] = order_items
|
|
79
|
+
if customer_name is not None:
|
|
80
|
+
payload["customerName"] = customer_name
|
|
81
|
+
if customer_email is not None:
|
|
82
|
+
payload["customerEmail"] = customer_email
|
|
83
|
+
if customer_phone is not None:
|
|
84
|
+
payload["customerPhone"] = customer_phone
|
|
85
|
+
if description is not None:
|
|
86
|
+
payload["description"] = description
|
|
87
|
+
|
|
88
|
+
return self._c.request("POST", _CHECKOUT, json=payload)
|
|
89
|
+
|
|
90
|
+
def generate_payout(
|
|
91
|
+
self,
|
|
92
|
+
amount: str,
|
|
93
|
+
order_id: str,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Generate a hosted payout link.
|
|
97
|
+
|
|
98
|
+
The recipient uses the link to enter their own bank / mobile-money
|
|
99
|
+
details without you needing to collect them yourself.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
amount: Payout amount.
|
|
103
|
+
order_id: Unique alphanumeric order reference.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Dict with ``payoutLink`` (URL string) and ``clientId``.
|
|
107
|
+
"""
|
|
108
|
+
payload: dict[str, Any] = {
|
|
109
|
+
"amount": str(amount),
|
|
110
|
+
"orderReference": order_id,
|
|
111
|
+
}
|
|
112
|
+
return self._c.request("POST", _PAYOUT, json=payload)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Async
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
class AsyncLinkService:
|
|
120
|
+
"""Asynchronous hosted-link generation methods (mirrors :class:`LinkService`)."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, client: "AsyncClickPesaClient") -> None:
|
|
123
|
+
self._c = client
|
|
124
|
+
|
|
125
|
+
async def generate_checkout(
|
|
126
|
+
self,
|
|
127
|
+
order_id: str,
|
|
128
|
+
order_currency: OrderCurrency,
|
|
129
|
+
total_price: str | None = None,
|
|
130
|
+
order_items: list[dict[str, Any]] | None = None,
|
|
131
|
+
customer_name: str | None = None,
|
|
132
|
+
customer_email: str | None = None,
|
|
133
|
+
customer_phone: str | None = None,
|
|
134
|
+
description: str | None = None,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
if total_price is None and order_items is None:
|
|
137
|
+
raise ValueError("Provide either 'total_price' or 'order_items'.")
|
|
138
|
+
if total_price is not None and order_items is not None:
|
|
139
|
+
raise ValueError("Provide either 'total_price' or 'order_items', not both.")
|
|
140
|
+
|
|
141
|
+
payload: dict[str, Any] = {
|
|
142
|
+
"orderReference": order_id,
|
|
143
|
+
"orderCurrency": order_currency,
|
|
144
|
+
}
|
|
145
|
+
if total_price is not None:
|
|
146
|
+
payload["totalPrice"] = str(total_price)
|
|
147
|
+
if order_items is not None:
|
|
148
|
+
payload["orderItems"] = order_items
|
|
149
|
+
if customer_name is not None:
|
|
150
|
+
payload["customerName"] = customer_name
|
|
151
|
+
if customer_email is not None:
|
|
152
|
+
payload["customerEmail"] = customer_email
|
|
153
|
+
if customer_phone is not None:
|
|
154
|
+
payload["customerPhone"] = customer_phone
|
|
155
|
+
if description is not None:
|
|
156
|
+
payload["description"] = description
|
|
157
|
+
|
|
158
|
+
return await self._c.request("POST", _CHECKOUT, json=payload)
|
|
159
|
+
|
|
160
|
+
async def generate_payout(
|
|
161
|
+
self,
|
|
162
|
+
amount: str,
|
|
163
|
+
order_id: str,
|
|
164
|
+
) -> dict[str, Any]:
|
|
165
|
+
payload: dict[str, Any] = {
|
|
166
|
+
"amount": str(amount),
|
|
167
|
+
"orderReference": order_id,
|
|
168
|
+
}
|
|
169
|
+
return await self._c.request("POST", _PAYOUT, json=payload)
|