zenopay-sdk 0.1.0__py3-none-any.whl → 0.2.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.
- elusion/zenopay/__init__.py +1 -1
- elusion/zenopay/client.py +23 -101
- elusion/zenopay/config.py +24 -21
- elusion/zenopay/http/client.py +162 -48
- elusion/zenopay/models/__init__.py +10 -7
- elusion/zenopay/models/common.py +12 -10
- elusion/zenopay/models/disbursement.py +70 -0
- elusion/zenopay/models/order.py +25 -84
- elusion/zenopay/services/__init__.py +2 -4
- elusion/zenopay/services/base.py +63 -37
- elusion/zenopay/services/disbursements.py +45 -0
- elusion/zenopay/services/orders.py +44 -40
- elusion/zenopay/utils/__init__.py +2 -8
- elusion/zenopay/utils/helpers.py +56 -0
- {zenopay_sdk-0.1.0.dist-info → zenopay_sdk-0.2.0.dist-info}/METADATA +195 -90
- zenopay_sdk-0.2.0.dist-info/RECORD +25 -0
- zenopay_sdk-0.1.0.dist-info/RECORD +0 -23
- {zenopay_sdk-0.1.0.dist-info → zenopay_sdk-0.2.0.dist-info}/WHEEL +0 -0
- {zenopay_sdk-0.1.0.dist-info → zenopay_sdk-0.2.0.dist-info}/licenses/LICENSE +0 -0
elusion/zenopay/http/client.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""HTTP client for the ZenoPay SDK."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import Any, Dict, Optional
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
5
|
|
6
6
|
import httpx
|
7
7
|
|
@@ -74,11 +74,50 @@ class HTTPClient:
|
|
74
74
|
self._sync_client.close()
|
75
75
|
self._sync_client = None
|
76
76
|
|
77
|
+
def _clean_params(self, params: Optional[Dict[str, Any]]) -> Optional[Dict[str, str]]:
|
78
|
+
"""Clean query parameters by removing None values and converting to strings.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
params: Query parameters to clean.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
Cleaned parameters dictionary or None.
|
85
|
+
"""
|
86
|
+
if not params:
|
87
|
+
return None
|
88
|
+
|
89
|
+
cleaned_params: Dict[str, str] = {}
|
90
|
+
for key, value in params.items():
|
91
|
+
if value is not None:
|
92
|
+
cleaned_params[str(key)] = str(value)
|
93
|
+
|
94
|
+
return cleaned_params if cleaned_params else None
|
95
|
+
|
96
|
+
def _clean_data(self, data: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
97
|
+
"""Clean form data by removing None values and converting to strings.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
data: Form data to clean.
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
Cleaned data dictionary or None.
|
104
|
+
"""
|
105
|
+
if not data:
|
106
|
+
return None
|
107
|
+
|
108
|
+
cleaned_data: Dict[str, Any] = {}
|
109
|
+
for key, value in data.items():
|
110
|
+
if value is not None:
|
111
|
+
cleaned_data[key] = str(value)
|
112
|
+
|
113
|
+
return cleaned_data if cleaned_data else None
|
114
|
+
|
77
115
|
async def request(
|
78
116
|
self,
|
79
117
|
method: str,
|
80
118
|
url: str,
|
81
119
|
data: Optional[Dict[str, Any]] = None,
|
120
|
+
params: Optional[Dict[str, Any]] = None,
|
82
121
|
headers: Optional[Dict[str, str]] = None,
|
83
122
|
**kwargs: Any,
|
84
123
|
) -> Dict[str, Any]:
|
@@ -87,7 +126,8 @@ class HTTPClient:
|
|
87
126
|
Args:
|
88
127
|
method: HTTP method (GET, POST, PUT, DELETE, etc.).
|
89
128
|
url: Request URL.
|
90
|
-
data: Form data to send.
|
129
|
+
data: Form data to send (for POST/PUT requests).
|
130
|
+
params: Query parameters to send (for GET requests).
|
91
131
|
headers: Additional headers.
|
92
132
|
**kwargs: Additional arguments for httpx.
|
93
133
|
|
@@ -105,15 +145,10 @@ class HTTPClient:
|
|
105
145
|
if headers:
|
106
146
|
request_headers.update(headers)
|
107
147
|
|
108
|
-
|
109
|
-
|
110
|
-
for key, value in data.items():
|
111
|
-
if value is not None:
|
112
|
-
cleaned_data[key] = str(value)
|
113
|
-
data = cleaned_data
|
148
|
+
cleaned_data = self._clean_data(data)
|
149
|
+
cleaned_params = self._clean_params(params)
|
114
150
|
|
115
151
|
try:
|
116
|
-
logger.debug(f"Making {method} request to {url}")
|
117
152
|
|
118
153
|
if self._client is None:
|
119
154
|
raise ZenoPayNetworkError("Async HTTP client is not initialized.", None)
|
@@ -121,7 +156,8 @@ class HTTPClient:
|
|
121
156
|
response = await self._client.request(
|
122
157
|
method=method,
|
123
158
|
url=url,
|
124
|
-
data=
|
159
|
+
data=cleaned_data,
|
160
|
+
params=cleaned_params,
|
125
161
|
headers=request_headers,
|
126
162
|
**kwargs,
|
127
163
|
)
|
@@ -129,16 +165,13 @@ class HTTPClient:
|
|
129
165
|
return await self._handle_response(response)
|
130
166
|
|
131
167
|
except httpx.TimeoutException as e:
|
132
|
-
logger.error(f"Request timeout: {e}")
|
133
168
|
raise ZenoPayTimeoutError(
|
134
169
|
f"Request timeout after {self.config.timeout} seconds",
|
135
170
|
self.config.timeout,
|
136
171
|
) from e
|
137
172
|
except httpx.NetworkError as e:
|
138
|
-
logger.error(f"Network error: {e}")
|
139
173
|
raise ZenoPayNetworkError(f"Network error: {str(e)}", e) from e
|
140
174
|
except Exception as e:
|
141
|
-
logger.error(f"Unexpected error: {e}")
|
142
175
|
raise ZenoPayNetworkError(f"Unexpected error: {str(e)}", e) from e
|
143
176
|
|
144
177
|
def request_sync(
|
@@ -146,6 +179,7 @@ class HTTPClient:
|
|
146
179
|
method: str,
|
147
180
|
url: str,
|
148
181
|
data: Optional[Dict[str, Any]] = None,
|
182
|
+
params: Optional[Dict[str, Any]] = None,
|
149
183
|
headers: Optional[Dict[str, str]] = None,
|
150
184
|
**kwargs: Any,
|
151
185
|
) -> Dict[str, Any]:
|
@@ -154,7 +188,8 @@ class HTTPClient:
|
|
154
188
|
Args:
|
155
189
|
method: HTTP method (GET, POST, PUT, DELETE, etc.).
|
156
190
|
url: Request URL.
|
157
|
-
data: Form data to send.
|
191
|
+
data: Form data to send (for POST/PUT requests).
|
192
|
+
params: Query parameters to send (for GET requests).
|
158
193
|
headers: Additional headers.
|
159
194
|
**kwargs: Additional arguments for httpx.
|
160
195
|
|
@@ -172,15 +207,10 @@ class HTTPClient:
|
|
172
207
|
if headers:
|
173
208
|
request_headers.update(headers)
|
174
209
|
|
175
|
-
|
176
|
-
|
177
|
-
for key, value in data.items():
|
178
|
-
if value is not None:
|
179
|
-
cleaned_data[key] = str(value)
|
180
|
-
data = cleaned_data
|
210
|
+
cleaned_data = self._clean_data(data)
|
211
|
+
cleaned_params = self._clean_params(params)
|
181
212
|
|
182
213
|
try:
|
183
|
-
logger.debug(f"Making {method} request to {url}")
|
184
214
|
|
185
215
|
if self._sync_client is None:
|
186
216
|
raise ZenoPayNetworkError("Sync HTTP client is not initialized.", None)
|
@@ -188,24 +218,24 @@ class HTTPClient:
|
|
188
218
|
response = self._sync_client.request(
|
189
219
|
method=method,
|
190
220
|
url=url,
|
191
|
-
data=
|
221
|
+
data=cleaned_data,
|
222
|
+
params=cleaned_params,
|
192
223
|
headers=request_headers,
|
193
224
|
**kwargs,
|
194
225
|
)
|
195
226
|
|
227
|
+
# Log response details for debugging
|
228
|
+
|
196
229
|
return self._handle_response_sync(response)
|
197
230
|
|
198
231
|
except httpx.TimeoutException as e:
|
199
|
-
logger.error(f"Request timeout: {e}")
|
200
232
|
raise ZenoPayTimeoutError(
|
201
233
|
f"Request timeout after {self.config.timeout} seconds",
|
202
234
|
self.config.timeout,
|
203
235
|
) from e
|
204
236
|
except httpx.NetworkError as e:
|
205
|
-
logger.error(f"Network error: {e}")
|
206
237
|
raise ZenoPayNetworkError(f"Network error: {str(e)}", e) from e
|
207
238
|
except Exception as e:
|
208
|
-
logger.error(f"Unexpected error: {e}")
|
209
239
|
raise ZenoPayNetworkError(f"Unexpected error: {str(e)}", e) from e
|
210
240
|
|
211
241
|
async def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
@@ -220,17 +250,14 @@ class HTTPClient:
|
|
220
250
|
Raises:
|
221
251
|
ZenoPayAPIError: For API errors.
|
222
252
|
"""
|
223
|
-
|
253
|
+
|
254
|
+
response_data: Dict[str, Any] = {}
|
224
255
|
|
225
256
|
try:
|
226
|
-
# Try to parse as JSON first
|
227
257
|
response_data = response.json()
|
228
258
|
except Exception:
|
229
|
-
# If JSON parsing fails, treat as text response
|
230
259
|
response_text = response.text
|
231
|
-
logger.debug(f"Non-JSON response: {response_text}")
|
232
260
|
|
233
|
-
# For successful responses that aren't JSON, create a basic structure
|
234
261
|
if response.is_success:
|
235
262
|
return {
|
236
263
|
"success": True,
|
@@ -238,20 +265,18 @@ class HTTPClient:
|
|
238
265
|
"message": "Request successful",
|
239
266
|
}
|
240
267
|
else:
|
241
|
-
response_data
|
268
|
+
response_data = {
|
242
269
|
"success": False,
|
243
270
|
"error": response_text or f"HTTP {response.status_code}",
|
244
271
|
"message": f"Request failed with status {response.status_code}",
|
272
|
+
"status_code": response.status_code,
|
245
273
|
}
|
246
274
|
|
247
275
|
if response.is_success:
|
248
276
|
return response_data
|
249
277
|
|
250
|
-
|
251
|
-
|
252
|
-
error_code = response_data.get("code")
|
253
|
-
|
254
|
-
logger.error(f"API error: {response.status_code} - {error_message}")
|
278
|
+
error_message = self._extract_error_message(response_data, response)
|
279
|
+
error_code = response_data.get("code") or response_data.get("error_code")
|
255
280
|
|
256
281
|
raise create_api_error(
|
257
282
|
status_code=response.status_code,
|
@@ -272,17 +297,14 @@ class HTTPClient:
|
|
272
297
|
Raises:
|
273
298
|
ZenoPayAPIError: For API errors.
|
274
299
|
"""
|
275
|
-
|
300
|
+
|
301
|
+
response_data: Dict[str, Any] = {}
|
276
302
|
|
277
303
|
try:
|
278
|
-
# Try to parse as JSON first
|
279
304
|
response_data = response.json()
|
280
305
|
except Exception:
|
281
|
-
# If JSON parsing fails, treat as text response
|
282
306
|
response_text = response.text
|
283
|
-
logger.debug(f"Non-JSON response: {response_text}")
|
284
307
|
|
285
|
-
# For successful responses that aren't JSON, create a basic structure
|
286
308
|
if response.is_success:
|
287
309
|
return {
|
288
310
|
"success": True,
|
@@ -290,19 +312,18 @@ class HTTPClient:
|
|
290
312
|
"message": "Request successful",
|
291
313
|
}
|
292
314
|
else:
|
293
|
-
response_data
|
315
|
+
response_data = {
|
294
316
|
"success": False,
|
295
317
|
"error": response_text or f"HTTP {response.status_code}",
|
296
318
|
"message": f"Request failed with status {response.status_code}",
|
319
|
+
"status_code": response.status_code,
|
297
320
|
}
|
298
321
|
|
299
322
|
if response.is_success:
|
300
323
|
return response_data
|
301
324
|
|
302
|
-
error_message =
|
303
|
-
error_code = response_data.get("code")
|
304
|
-
|
305
|
-
logger.error(f"API error: {response.status_code} - {error_message}")
|
325
|
+
error_message = self._extract_error_message(response_data, response)
|
326
|
+
error_code = response_data.get("code") or response_data.get("error_code")
|
306
327
|
|
307
328
|
raise create_api_error(
|
308
329
|
status_code=response.status_code,
|
@@ -311,10 +332,103 @@ class HTTPClient:
|
|
311
332
|
error_code=error_code,
|
312
333
|
)
|
313
334
|
|
335
|
+
def _extract_error_message(self, response_data: Dict[str, Any], response: httpx.Response) -> str:
|
336
|
+
"""Extract detailed error message from response data.
|
337
|
+
|
338
|
+
Args:
|
339
|
+
response_data: Parsed response data
|
340
|
+
response: HTTP response object
|
341
|
+
|
342
|
+
Returns:
|
343
|
+
Detailed error message
|
344
|
+
"""
|
345
|
+
|
346
|
+
error_fields = ["error_description", "error", "message", "detail", "details", "error_message", "msg", "description", "reason"]
|
347
|
+
|
348
|
+
for field in error_fields:
|
349
|
+
if field in response_data and response_data[field]:
|
350
|
+
error_msg: str = response_data[field]
|
351
|
+
if isinstance(error_msg, dict):
|
352
|
+
return str(error_msg)
|
353
|
+
return str(error_msg)
|
354
|
+
|
355
|
+
if "errors" in response_data:
|
356
|
+
errors: Dict[str, Any] = response_data["errors"]
|
357
|
+
error_parts: List[str] = []
|
358
|
+
for field, field_errors in errors.items():
|
359
|
+
field_name: str = field
|
360
|
+
if isinstance(field_errors, list):
|
361
|
+
error_strs = [str(e) for e in field_errors] # type: ignore
|
362
|
+
error_parts.append(f"{field_name}: {', '.join(error_strs)}")
|
363
|
+
else:
|
364
|
+
error_parts.append(f"{field_name}: {str(field_errors)}")
|
365
|
+
formatted_errors = "; ".join(error_parts)
|
366
|
+
return formatted_errors
|
367
|
+
|
368
|
+
if "success" in response_data and response_data["success"] is False:
|
369
|
+
print("DEBUG: Response indicates failure, checking for error details")
|
370
|
+
|
371
|
+
if "error" in response_data and isinstance(response_data["error"], dict):
|
372
|
+
nested_error: Dict[str, Any] = response_data["error"] # type: ignore
|
373
|
+
for field in error_fields:
|
374
|
+
if field in nested_error:
|
375
|
+
return str(nested_error[field])
|
376
|
+
|
377
|
+
response_text = response.text
|
378
|
+
if response_text and len(response_text) < 1000:
|
379
|
+
return f"HTTP {response.status_code}: {response_text}"
|
380
|
+
|
381
|
+
fallback = f"HTTP {response.status_code} - {response.reason_phrase or 'Unknown error'}"
|
382
|
+
return fallback
|
383
|
+
|
314
384
|
async def post(self, url: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
315
|
-
"""Make
|
385
|
+
"""Make an async POST request.
|
386
|
+
|
387
|
+
Args:
|
388
|
+
url: Request URL.
|
389
|
+
data: Form data to send.
|
390
|
+
**kwargs: Additional arguments for httpx.
|
391
|
+
|
392
|
+
Returns:
|
393
|
+
Parsed response data.
|
394
|
+
"""
|
316
395
|
return await self.request("POST", url, data=data, **kwargs)
|
317
396
|
|
318
397
|
def post_sync(self, url: str, data: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
319
|
-
"""Make a sync POST request.
|
398
|
+
"""Make a sync POST request.
|
399
|
+
|
400
|
+
Args:
|
401
|
+
url: Request URL.
|
402
|
+
data: Form data to send.
|
403
|
+
**kwargs: Additional arguments for httpx.
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
Parsed response data.
|
407
|
+
"""
|
320
408
|
return self.request_sync("POST", url, data=data, **kwargs)
|
409
|
+
|
410
|
+
async def get(self, url: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
411
|
+
"""Make an async GET request.
|
412
|
+
|
413
|
+
Args:
|
414
|
+
url: Request URL.
|
415
|
+
params: Query parameters to send.
|
416
|
+
**kwargs: Additional arguments for httpx.
|
417
|
+
|
418
|
+
Returns:
|
419
|
+
Parsed response data.
|
420
|
+
"""
|
421
|
+
return await self.request("GET", url, params=params, **kwargs)
|
422
|
+
|
423
|
+
def get_sync(self, url: str, params: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Dict[str, Any]:
|
424
|
+
"""Make a sync GET request.
|
425
|
+
|
426
|
+
Args:
|
427
|
+
url: Request URL.
|
428
|
+
params: Query parameters to send.
|
429
|
+
**kwargs: Additional arguments for httpx.
|
430
|
+
|
431
|
+
Returns:
|
432
|
+
Parsed response data.
|
433
|
+
"""
|
434
|
+
return self.request_sync("GET", url, params=params, **kwargs)
|
@@ -1,10 +1,6 @@
|
|
1
1
|
"""Models package for the ZenoPay SDK."""
|
2
2
|
|
3
|
-
from elusion.zenopay.models.common import
|
4
|
-
PAYMENT_STATUSES,
|
5
|
-
APIResponse,
|
6
|
-
StatusCheckRequest,
|
7
|
-
)
|
3
|
+
from elusion.zenopay.models.common import PAYMENT_STATUSES, APIResponse, StatusCheckRequest, UtilityCodes
|
8
4
|
|
9
5
|
from elusion.zenopay.models.order import (
|
10
6
|
OrderBase,
|
@@ -13,7 +9,6 @@ from elusion.zenopay.models.order import (
|
|
13
9
|
Order,
|
14
10
|
OrderResponse,
|
15
11
|
OrderStatusResponse,
|
16
|
-
OrderListParams,
|
17
12
|
)
|
18
13
|
|
19
14
|
from elusion.zenopay.models.webhook import (
|
@@ -22,12 +17,18 @@ from elusion.zenopay.models.webhook import (
|
|
22
17
|
WebhookResponse,
|
23
18
|
)
|
24
19
|
|
20
|
+
from elusion.zenopay.models.disbursement import (
|
21
|
+
NewDisbursement,
|
22
|
+
DisbursementSuccessResponse,
|
23
|
+
)
|
24
|
+
|
25
25
|
__all__ = [
|
26
26
|
# Constants and utilities
|
27
27
|
"PAYMENT_STATUSES",
|
28
28
|
# Common models
|
29
29
|
"APIResponse",
|
30
30
|
"StatusCheckRequest",
|
31
|
+
"UtilityCodes",
|
31
32
|
# Order models
|
32
33
|
"OrderBase",
|
33
34
|
"NewOrder",
|
@@ -35,9 +36,11 @@ __all__ = [
|
|
35
36
|
"Order",
|
36
37
|
"OrderResponse",
|
37
38
|
"OrderStatusResponse",
|
38
|
-
"OrderListParams",
|
39
39
|
# Webhook models
|
40
40
|
"WebhookPayload",
|
41
41
|
"WebhookEvent",
|
42
42
|
"WebhookResponse",
|
43
|
+
# Disbursement
|
44
|
+
"NewDisbursement",
|
45
|
+
"DisbursementSuccessResponse",
|
43
46
|
]
|
elusion/zenopay/models/common.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Common models and types used across the ZenoPay SDK."""
|
2
2
|
|
3
3
|
from datetime import datetime
|
4
|
+
from enum import Enum
|
4
5
|
from typing import Generic, List, Optional, TypeVar
|
5
6
|
|
6
7
|
from pydantic import BaseModel, ConfigDict, Field
|
@@ -12,7 +13,7 @@ class APIResponse(BaseModel, Generic[T]):
|
|
12
13
|
"""Generic API response wrapper."""
|
13
14
|
|
14
15
|
success: bool = Field(..., description="Whether the request was successful")
|
15
|
-
|
16
|
+
results: T = Field(..., description="Response data")
|
16
17
|
message: Optional[str] = Field(None, description="Response message")
|
17
18
|
error: Optional[str] = Field(None, description="Error message if applicable")
|
18
19
|
|
@@ -43,10 +44,6 @@ class ValidationError(BaseModel):
|
|
43
44
|
class ZenoPayAPIRequest(BaseModel):
|
44
45
|
"""Base model for ZenoPay API requests."""
|
45
46
|
|
46
|
-
api_key: Optional[str] = Field(None, description="API key (usually null in requests)")
|
47
|
-
secret_key: Optional[str] = Field(None, description="Secret key (usually null in requests)")
|
48
|
-
account_id: str = Field(..., description="ZenoPay account ID")
|
49
|
-
|
50
47
|
def to_form_data(self) -> dict[str, str]:
|
51
48
|
"""Convert to form data format as expected by ZenoPay API."""
|
52
49
|
data = self.model_dump(exclude_unset=True, by_alias=True)
|
@@ -62,22 +59,27 @@ class ZenoPayAPIRequest(BaseModel):
|
|
62
59
|
class StatusCheckRequest(ZenoPayAPIRequest):
|
63
60
|
"""Request model for checking order status."""
|
64
61
|
|
65
|
-
check_status: int = Field(1, description="Always 1 for status check requests")
|
66
62
|
order_id: str = Field(..., description="Order ID to check")
|
67
63
|
|
68
64
|
model_config = ConfigDict(
|
69
65
|
json_schema_extra={
|
70
66
|
"example": {
|
71
|
-
"check_status": 1,
|
72
67
|
"order_id": "66c4bb9c9abb1",
|
73
|
-
"account_id": "zp87778",
|
74
|
-
"api_key": "null",
|
75
|
-
"secret_key": "null",
|
76
68
|
}
|
77
69
|
}
|
78
70
|
)
|
79
71
|
|
80
72
|
|
73
|
+
class UtilityCodes(str, Enum):
|
74
|
+
"""Uility codes"""
|
75
|
+
|
76
|
+
CASHIN = "CASHIN"
|
77
|
+
|
78
|
+
def __str__(self) -> str:
|
79
|
+
"""String representation of the utility codes."""
|
80
|
+
return self.value
|
81
|
+
|
82
|
+
|
81
83
|
# Common status constants
|
82
84
|
PAYMENT_STATUSES = {
|
83
85
|
"PENDING": "PENDING",
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from typing import Any, Dict, List
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field
|
3
|
+
from elusion.zenopay.models.common import UtilityCodes
|
4
|
+
|
5
|
+
|
6
|
+
class NewDisbursement(BaseModel):
|
7
|
+
transid: str = Field(..., description="Unique transaction ID (e.g., UUID) to prevent duplication.")
|
8
|
+
utilitycode: str = Field(default=UtilityCodes.CASHIN, description='Set to "CASHIN" for disbursements.')
|
9
|
+
utilityref: str = Field(..., description="Mobile number to receive the funds (e.g., 0744963858).")
|
10
|
+
amount: int = Field(..., description="Amount to send in Tanzanian Shillings (TZS).")
|
11
|
+
pin: int = Field(..., description="4-digit wallet PIN to authorize the transaction.", ge=1000, le=9999)
|
12
|
+
|
13
|
+
model_config = ConfigDict(
|
14
|
+
json_schema_extra={
|
15
|
+
"example": {"transid": "7pbBX-lnnASw-erwnn-nrrr09AZ", "utilitycode": "CASHIN", "utilityref": "07XXXXXXXX", "amount": 1000, "pin": 0000}
|
16
|
+
}
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
class ZenoPayResponse(BaseModel):
|
21
|
+
reference: str
|
22
|
+
transid: str
|
23
|
+
resultcode: str
|
24
|
+
result: str
|
25
|
+
message: str
|
26
|
+
data: List[Dict[str, Any]]
|
27
|
+
|
28
|
+
model_config = ConfigDict(
|
29
|
+
json_schema_extra={
|
30
|
+
"example": {
|
31
|
+
"reference": "0949694808",
|
32
|
+
"transid": "7pbBXlnnASwerdsadasdwnnnrrr09AZ",
|
33
|
+
"resultcode": "000",
|
34
|
+
"result": "SUCCESS",
|
35
|
+
"message": "\nMpesa\nTo JOHN DOE(2557XXXXXXXX)\nFrom ZENO\nAmount 1,000.00\n\nReference 0949694808\n26/06/2025 7:21:24 PM",
|
36
|
+
"data": [],
|
37
|
+
}
|
38
|
+
}
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
class DisbursementSuccessResponse(BaseModel):
|
43
|
+
status: str
|
44
|
+
message: str
|
45
|
+
fee: int
|
46
|
+
amount_sent_to_customer: int
|
47
|
+
total_deducted: int
|
48
|
+
new_balance: str
|
49
|
+
zenopay_response: ZenoPayResponse
|
50
|
+
|
51
|
+
model_config = ConfigDict(
|
52
|
+
json_schema_extra={
|
53
|
+
"example": {
|
54
|
+
"status": "success",
|
55
|
+
"message": "Wallet Cashin processed successfully.",
|
56
|
+
"fee": 1500,
|
57
|
+
"amount_sent_to_customer": 3000,
|
58
|
+
"total_deducted": 4500,
|
59
|
+
"new_balance": "62984034.00",
|
60
|
+
"zenopay_response": {
|
61
|
+
"reference": "0949694808",
|
62
|
+
"transid": "7pbBXlnnASwerdsadasdwnnnrrr09AZ",
|
63
|
+
"resultcode": "000",
|
64
|
+
"result": "SUCCESS",
|
65
|
+
"message": "\nMpesa\nTo JOHN DOE(2557XXXXXXXX)\nFrom ZENO\nAmount 1,000.00\n\nReference 0949694808\n26/06/2025 7:21:24 PM",
|
66
|
+
"data": [],
|
67
|
+
},
|
68
|
+
}
|
69
|
+
}
|
70
|
+
)
|