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/models/order.py
CHANGED
@@ -1,19 +1,18 @@
|
|
1
1
|
"""Order-related models for the ZenoPay SDK."""
|
2
2
|
|
3
|
-
from
|
4
|
-
from typing import Any, Dict, Optional, Union
|
5
|
-
|
3
|
+
from typing import Any, Dict, List, Optional
|
6
4
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
7
5
|
|
8
6
|
|
9
7
|
class OrderBase(BaseModel):
|
10
8
|
"""Base order model with common fields."""
|
11
9
|
|
10
|
+
order_id: str = Field(..., description="Unique order id")
|
12
11
|
buyer_email: str = Field(..., description="Buyer's email address")
|
13
12
|
buyer_name: str = Field(..., description="Buyer's full name")
|
14
13
|
buyer_phone: str = Field(..., description="Buyer's phone number")
|
15
14
|
amount: int = Field(..., gt=0, description="Order amount in smallest currency unit")
|
16
|
-
webhook_url: Optional[str] = Field(None, description="URL to receive webhook notifications")
|
15
|
+
webhook_url: Optional[str] = Field(default=None, description="URL to receive webhook notifications")
|
17
16
|
|
18
17
|
@field_validator("buyer_email")
|
19
18
|
def validate_email(cls, v: str) -> str:
|
@@ -25,7 +24,6 @@ class OrderBase(BaseModel):
|
|
25
24
|
@field_validator("buyer_phone")
|
26
25
|
def validate_phone(cls, v: str) -> str:
|
27
26
|
"""Validate phone number format."""
|
28
|
-
# Remove any non-digit characters except +
|
29
27
|
cleaned = "".join(c for c in v if c.isdigit() or c == "+")
|
30
28
|
if len(cleaned) < 10:
|
31
29
|
raise ValueError("Phone number must be at least 10 digits")
|
@@ -44,7 +42,7 @@ class OrderBase(BaseModel):
|
|
44
42
|
class NewOrder(OrderBase):
|
45
43
|
"""Model for creating a new order."""
|
46
44
|
|
47
|
-
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional order metadata")
|
45
|
+
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Additional order metadata")
|
48
46
|
|
49
47
|
@field_validator("metadata")
|
50
48
|
def validate_metadata(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
@@ -95,11 +93,6 @@ class Order(BaseModel):
|
|
95
93
|
webhook_url: Optional[str] = Field(None, description="Webhook URL")
|
96
94
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Order metadata")
|
97
95
|
|
98
|
-
# Timestamps
|
99
|
-
created_at: Optional[datetime] = Field(None, description="Order creation time")
|
100
|
-
updated_at: Optional[datetime] = Field(None, description="Last update time")
|
101
|
-
completed_at: Optional[datetime] = Field(None, description="Payment completion time")
|
102
|
-
|
103
96
|
@field_validator("payment_status")
|
104
97
|
def validate_payment_status(cls, v: str) -> str:
|
105
98
|
"""Validate payment status."""
|
@@ -155,9 +148,6 @@ class Order(BaseModel):
|
|
155
148
|
"size": "L",
|
156
149
|
"custom_notes": "Please gift-wrap this item.",
|
157
150
|
},
|
158
|
-
"created_at": "2025-06-15T10:00:00Z",
|
159
|
-
"updated_at": "2025-06-15T10:05:00Z",
|
160
|
-
"completed_at": "2025-06-15T10:05:00Z",
|
161
151
|
}
|
162
152
|
}
|
163
153
|
)
|
@@ -166,86 +156,37 @@ class Order(BaseModel):
|
|
166
156
|
class OrderResponse(BaseModel):
|
167
157
|
"""Response model for order operations."""
|
168
158
|
|
169
|
-
status: str
|
170
|
-
message: str
|
171
|
-
|
172
|
-
|
173
|
-
)
|
174
|
-
order_id: str = Field(..., description="ID of the created or updated order")
|
159
|
+
status: str
|
160
|
+
message: str
|
161
|
+
resultcode: int
|
162
|
+
order_id: str
|
175
163
|
|
176
164
|
model_config = ConfigDict(
|
177
165
|
json_schema_extra={
|
178
166
|
"example": {
|
167
|
+
"order_id": "2cd93967-0d48-46c7-a9ab-f0a0a21a11cd",
|
168
|
+
"resultcode": "000",
|
179
169
|
"status": "success",
|
180
170
|
"message": "Order created successfully",
|
181
|
-
"order_id": "66c4bb9c9abb1",
|
182
171
|
}
|
183
172
|
}
|
184
173
|
)
|
185
174
|
|
186
175
|
|
187
|
-
class
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
class OrderListParams(BaseModel):
|
198
|
-
"""Parameters for listing orders."""
|
199
|
-
|
200
|
-
status: Optional[str] = Field(None, description="Filter by payment status")
|
201
|
-
buyer_email: Optional[str] = Field(None, description="Filter by buyer email")
|
202
|
-
date_from: Optional[Union[datetime, str]] = Field(None, description="Filter orders from this date")
|
203
|
-
date_to: Optional[Union[datetime, str]] = Field(None, description="Filter orders to this date")
|
204
|
-
limit: Optional[int] = Field(50, ge=1, le=100, description="Number of orders to return")
|
205
|
-
|
206
|
-
@field_validator("status")
|
207
|
-
def validate_status(cls, v: Optional[str]) -> Optional[str]:
|
208
|
-
"""Validate status filter."""
|
209
|
-
if v is not None:
|
210
|
-
valid_statuses = ["PENDING", "COMPLETED", "FAILED", "CANCELLED"]
|
211
|
-
if v not in valid_statuses:
|
212
|
-
raise ValueError(f"Invalid status. Must be one of: {', '.join(valid_statuses)}")
|
213
|
-
return v
|
214
|
-
|
215
|
-
@field_validator("date_from", mode="before")
|
216
|
-
def parse_date_from(cls, v: Union[str, datetime, None]) -> Union[datetime, None]:
|
217
|
-
"""Parse string dates to datetime objects for date_from."""
|
218
|
-
if isinstance(v, str):
|
219
|
-
try:
|
220
|
-
return datetime.fromisoformat(v.replace("Z", "+00:00"))
|
221
|
-
except ValueError:
|
222
|
-
try:
|
223
|
-
return datetime.strptime(v, "%Y-%m-%d")
|
224
|
-
except ValueError:
|
225
|
-
raise ValueError(f"Invalid date format: {v}")
|
226
|
-
return v
|
176
|
+
class OrderData(BaseModel):
|
177
|
+
order_id: str
|
178
|
+
creation_date: str
|
179
|
+
amount: str
|
180
|
+
payment_status: str
|
181
|
+
transid: Optional[str] = None
|
182
|
+
channel: Optional[str] = None
|
183
|
+
reference: Optional[str] = None
|
184
|
+
msisdn: Optional[str] = None
|
227
185
|
|
228
|
-
@field_validator("date_to", mode="before")
|
229
|
-
def parse_date_to(cls, v: Union[str, datetime, None]) -> Union[datetime, None]:
|
230
|
-
"""Parse string dates to datetime objects for date_to."""
|
231
|
-
if isinstance(v, str):
|
232
|
-
try:
|
233
|
-
return datetime.fromisoformat(v.replace("Z", "+00:00"))
|
234
|
-
except ValueError:
|
235
|
-
try:
|
236
|
-
return datetime.strptime(v, "%Y-%m-%d")
|
237
|
-
except ValueError:
|
238
|
-
raise ValueError(f"Invalid date format: {v}")
|
239
|
-
return v
|
240
186
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
"date_to": "2025-06-15",
|
248
|
-
"limit": 50,
|
249
|
-
}
|
250
|
-
}
|
251
|
-
)
|
187
|
+
class OrderStatusResponse(BaseModel):
|
188
|
+
reference: str
|
189
|
+
resultcode: str
|
190
|
+
result: str
|
191
|
+
message: str
|
192
|
+
data: List[OrderData]
|
@@ -1,7 +1,5 @@
|
|
1
1
|
from elusion.zenopay.services.orders import OrderService
|
2
2
|
from elusion.zenopay.services.webhooks import WebhookService
|
3
|
+
from elusion.zenopay.services.disbursements import DisbursementService
|
3
4
|
|
4
|
-
__all__ = [
|
5
|
-
"OrderService",
|
6
|
-
"WebhookService",
|
7
|
-
]
|
5
|
+
__all__ = ["OrderService", "WebhookService", "DisbursementService"]
|
elusion/zenopay/services/base.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Base service class for all ZenoPay SDK services."""
|
2
2
|
|
3
|
-
from typing import Any, Dict, Type, TypeVar, Union
|
3
|
+
from typing import Any, Dict, Type, TypeVar, Union, Optional
|
4
4
|
|
5
5
|
from pydantic import BaseModel, ValidationError
|
6
6
|
|
@@ -53,16 +53,29 @@ class BaseService:
|
|
53
53
|
else:
|
54
54
|
request_data = data.copy()
|
55
55
|
|
56
|
-
request_data.update(
|
57
|
-
{
|
58
|
-
"account_id": self.config.account_id,
|
59
|
-
"api_key": self.config.api_key or "null",
|
60
|
-
"secret_key": self.config.secret_key or "null",
|
61
|
-
}
|
62
|
-
)
|
63
|
-
|
64
56
|
return request_data
|
65
57
|
|
58
|
+
def _prepare_query_params(self, params: Optional[Union[BaseModel, Dict[str, Any]]] = None) -> Dict[str, Any]:
|
59
|
+
"""Prepare and validate query parameters for GET requests.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
params: Parameters to prepare for the request.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Prepared query parameters dictionary.
|
66
|
+
|
67
|
+
Raises:
|
68
|
+
ZenoPayValidationError: If validation fails.
|
69
|
+
"""
|
70
|
+
if params is None:
|
71
|
+
query_params = {}
|
72
|
+
elif isinstance(params, BaseModel):
|
73
|
+
query_params = params.model_dump(exclude_unset=True, by_alias=True)
|
74
|
+
else:
|
75
|
+
query_params = params.copy()
|
76
|
+
|
77
|
+
return query_params
|
78
|
+
|
66
79
|
def _parse_response(
|
67
80
|
self,
|
68
81
|
response_data: Dict[str, Any],
|
@@ -81,27 +94,14 @@ class BaseService:
|
|
81
94
|
ZenoPayValidationError: If response parsing fails.
|
82
95
|
"""
|
83
96
|
try:
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
error = response_data.get("error")
|
89
|
-
|
90
|
-
if success and data:
|
91
|
-
parsed_data = model_class.model_validate(data)
|
92
|
-
else:
|
93
|
-
# Error response
|
94
|
-
raise ZenoPayValidationError(error or "Unknown API error")
|
95
|
-
else:
|
96
|
-
# Direct data response
|
97
|
-
parsed_data = model_class.model_validate(response_data)
|
98
|
-
success = True
|
99
|
-
message = None
|
100
|
-
error = None
|
97
|
+
parsed_data = model_class.model_validate(response_data)
|
98
|
+
success = True
|
99
|
+
message = response_data.get("message", None)
|
100
|
+
error = None
|
101
101
|
|
102
102
|
return APIResponse[model_class](
|
103
103
|
success=success,
|
104
|
-
|
104
|
+
results=parsed_data,
|
105
105
|
message=message,
|
106
106
|
error=error,
|
107
107
|
)
|
@@ -131,16 +131,16 @@ class BaseService:
|
|
131
131
|
url = self._build_url(endpoint)
|
132
132
|
prepared_data = self._prepare_request_data(data)
|
133
133
|
|
134
|
-
response_data = await self.http_client.post(url,
|
134
|
+
response_data = await self.http_client.post(url, json=prepared_data)
|
135
135
|
return self._parse_response(response_data, model_class)
|
136
136
|
|
137
|
-
|
137
|
+
def post_sync(
|
138
138
|
self,
|
139
139
|
endpoint: str,
|
140
140
|
data: Union[BaseModel, Dict[str, Any]],
|
141
141
|
model_class: Type[T],
|
142
142
|
) -> APIResponse[T]:
|
143
|
-
"""Make a POST request
|
143
|
+
"""Make a sync POST request.
|
144
144
|
|
145
145
|
Args:
|
146
146
|
endpoint: API endpoint name.
|
@@ -150,26 +150,52 @@ class BaseService:
|
|
150
150
|
Returns:
|
151
151
|
Parsed API response.
|
152
152
|
"""
|
153
|
-
|
153
|
+
url = self._build_url(endpoint)
|
154
|
+
prepared_data = self._prepare_request_data(data)
|
154
155
|
|
155
|
-
|
156
|
+
response_data = self.http_client.post_sync(url, json=prepared_data)
|
157
|
+
return self._parse_response(response_data, model_class)
|
158
|
+
|
159
|
+
async def get_async(
|
156
160
|
self,
|
157
161
|
endpoint: str,
|
158
|
-
data: Union[BaseModel, Dict[str, Any]],
|
159
162
|
model_class: Type[T],
|
163
|
+
params: Optional[Union[BaseModel, Dict[str, Any]]] = None,
|
160
164
|
) -> APIResponse[T]:
|
161
|
-
"""Make
|
165
|
+
"""Make an async GET request.
|
162
166
|
|
163
167
|
Args:
|
164
168
|
endpoint: API endpoint name.
|
165
|
-
data: Data to send in the request.
|
166
169
|
model_class: Model class to parse response into.
|
170
|
+
params: Optional query parameters to send with the request.
|
167
171
|
|
168
172
|
Returns:
|
169
173
|
Parsed API response.
|
170
174
|
"""
|
171
175
|
url = self._build_url(endpoint)
|
172
|
-
|
176
|
+
query_params = self._prepare_query_params(params)
|
177
|
+
|
178
|
+
response_data = await self.http_client.get(url, params=query_params)
|
179
|
+
return self._parse_response(response_data, model_class)
|
180
|
+
|
181
|
+
def get_sync(
|
182
|
+
self,
|
183
|
+
endpoint: str,
|
184
|
+
model_class: Type[T],
|
185
|
+
params: Optional[Union[BaseModel, Dict[str, Any]]] = None,
|
186
|
+
) -> APIResponse[T]:
|
187
|
+
"""Make a sync GET request.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
endpoint: API endpoint name.
|
191
|
+
model_class: Model class to parse response into.
|
192
|
+
params: Optional query parameters to send with the request.
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
Parsed API response.
|
196
|
+
"""
|
197
|
+
url = self._build_url(endpoint)
|
198
|
+
query_params = self._prepare_query_params(params)
|
173
199
|
|
174
|
-
response_data = self.http_client.
|
200
|
+
response_data = self.http_client.get_sync(url, params=query_params)
|
175
201
|
return self._parse_response(response_data, model_class)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
"""Disbursement service for the ZenoPay SDK"""
|
2
|
+
|
3
|
+
from elusion.zenopay.config import ZenoPayConfig
|
4
|
+
from elusion.zenopay.http import HTTPClient
|
5
|
+
from elusion.zenopay.models.common import APIResponse
|
6
|
+
from elusion.zenopay.models.disbursement import (
|
7
|
+
NewDisbursement,
|
8
|
+
DisbursementSuccessResponse,
|
9
|
+
)
|
10
|
+
from elusion.zenopay.services.base import BaseService
|
11
|
+
|
12
|
+
|
13
|
+
class DisbursementSyncMethods(BaseService):
|
14
|
+
"""Sync methods for DisbursementService - inherits from BaseService for direct access."""
|
15
|
+
|
16
|
+
def disburse(self, disbursement_data: NewDisbursement) -> APIResponse[DisbursementSuccessResponse]:
|
17
|
+
"""Send money to mobile wallet (sync).
|
18
|
+
|
19
|
+
Args:
|
20
|
+
disbursement_data: Disbursement data with recipient details.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
Disbursement response with transaction details and fees.
|
24
|
+
"""
|
25
|
+
return self.post_sync("disbursement", disbursement_data, DisbursementSuccessResponse)
|
26
|
+
|
27
|
+
|
28
|
+
class DisbursementService(BaseService):
|
29
|
+
"""Service for sending money to mobile wallets."""
|
30
|
+
|
31
|
+
def __init__(self, http_client: HTTPClient, config: ZenoPayConfig):
|
32
|
+
"""Initialize DisbursementService with sync namespace."""
|
33
|
+
super().__init__(http_client, config)
|
34
|
+
self.sync = DisbursementSyncMethods(http_client, config)
|
35
|
+
|
36
|
+
async def disburse(self, disbursement_data: NewDisbursement) -> APIResponse[DisbursementSuccessResponse]:
|
37
|
+
"""Send money to mobile wallet (async).
|
38
|
+
|
39
|
+
Args:
|
40
|
+
disbursement_data: Disbursement data with recipient details.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Disbursement response with transaction details and fees.
|
44
|
+
"""
|
45
|
+
return await self.post_async("disbursement", disbursement_data, DisbursementSuccessResponse)
|
@@ -1,10 +1,10 @@
|
|
1
1
|
"""Order service for the ZenoPay SDK"""
|
2
2
|
|
3
|
-
from typing import Dict, Union
|
3
|
+
from typing import Any, Dict, Union
|
4
4
|
|
5
5
|
from elusion.zenopay.config import ZenoPayConfig
|
6
6
|
from elusion.zenopay.http import HTTPClient
|
7
|
-
from elusion.zenopay.models.common import APIResponse
|
7
|
+
from elusion.zenopay.models.common import APIResponse
|
8
8
|
from elusion.zenopay.models.order import (
|
9
9
|
NewOrder,
|
10
10
|
OrderResponse,
|
@@ -16,7 +16,7 @@ from elusion.zenopay.services.base import BaseService
|
|
16
16
|
class OrderSyncMethods(BaseService):
|
17
17
|
"""Sync methods for OrderService - inherits from BaseService for direct access."""
|
18
18
|
|
19
|
-
def create(self, order_data:
|
19
|
+
def create(self, order_data: NewOrder) -> APIResponse[OrderResponse]:
|
20
20
|
"""Create a new order and initiate USSD payment (sync).
|
21
21
|
|
22
22
|
Args:
|
@@ -30,11 +30,10 @@ class OrderSyncMethods(BaseService):
|
|
30
30
|
... response = zenopay_client.orders.sync.create(order_data)
|
31
31
|
... print(f"Order created: {response.data.order_id}")
|
32
32
|
"""
|
33
|
-
# ✅ Direct access to post_sync - no parent needed
|
34
33
|
return self.post_sync("create_order", order_data, OrderResponse)
|
35
34
|
|
36
|
-
def
|
37
|
-
"""Check the status of an existing order (sync).
|
35
|
+
def check_status(self, order_id: str) -> APIResponse[OrderStatusResponse]:
|
36
|
+
"""Check the status of an existing order using GET request (sync).
|
38
37
|
|
39
38
|
Args:
|
40
39
|
order_id: The order ID to check status for.
|
@@ -42,21 +41,16 @@ class OrderSyncMethods(BaseService):
|
|
42
41
|
Returns:
|
43
42
|
Order status response with payment details.
|
44
43
|
"""
|
45
|
-
|
46
|
-
order_id
|
47
|
-
|
48
|
-
|
49
|
-
account_id=self.config.account_id or "",
|
50
|
-
check_status=1,
|
51
|
-
)
|
52
|
-
# ✅ Direct access - clean and simple
|
53
|
-
return self.post_sync("order_status", status_request, OrderStatusResponse)
|
44
|
+
params: Dict[str, Any] = {
|
45
|
+
"order_id": order_id,
|
46
|
+
}
|
47
|
+
return self.get_sync("order_status", OrderStatusResponse, params=params)
|
54
48
|
|
55
49
|
def check_payment(self, order_id: str) -> bool:
|
56
50
|
"""Check if an order has been paid (sync)."""
|
57
51
|
try:
|
58
|
-
status_response = self.
|
59
|
-
return status_response.data.payment_status == "COMPLETED"
|
52
|
+
status_response = self.check_status(order_id)
|
53
|
+
return status_response.results.data[0].payment_status == "COMPLETED"
|
60
54
|
except Exception:
|
61
55
|
return False
|
62
56
|
|
@@ -67,12 +61,12 @@ class OrderSyncMethods(BaseService):
|
|
67
61
|
start_time = time.time()
|
68
62
|
|
69
63
|
while True:
|
70
|
-
status_response = self.
|
64
|
+
status_response = self.check_status(order_id)
|
71
65
|
|
72
|
-
if status_response.data.payment_status == "COMPLETED":
|
66
|
+
if status_response.results.data[0].payment_status == "COMPLETED":
|
73
67
|
return status_response
|
74
68
|
|
75
|
-
if status_response.data.payment_status == "FAILED":
|
69
|
+
if status_response.results.data[0].payment_status == "FAILED":
|
76
70
|
raise Exception(f"Payment failed for order {order_id}")
|
77
71
|
|
78
72
|
elapsed = time.time() - start_time
|
@@ -91,41 +85,51 @@ class OrderService(BaseService):
|
|
91
85
|
self.sync = OrderSyncMethods(http_client, config)
|
92
86
|
|
93
87
|
async def create(self, order_data: Union[NewOrder, Dict[str, str]]) -> APIResponse[OrderResponse]:
|
94
|
-
"""Create a new order and initiate USSD payment.
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
order_id
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
)
|
106
|
-
|
88
|
+
"""Create a new order and initiate USSD payment (async).
|
89
|
+
|
90
|
+
Args:
|
91
|
+
order_data: Order creation data.
|
92
|
+
|
93
|
+
Returns:
|
94
|
+
Created order response with order_id and status.
|
95
|
+
"""
|
96
|
+
return await self.post_async("create_order", order_data, OrderResponse)
|
97
|
+
|
98
|
+
async def check_status(self, order_id: str) -> APIResponse[OrderStatusResponse]:
|
99
|
+
"""Check the status of an existing order using GET request (async).
|
100
|
+
|
101
|
+
Args:
|
102
|
+
order_id: The order ID to check status for.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
Order status response with payment details.
|
106
|
+
"""
|
107
|
+
params: Dict[str, Any] = {
|
108
|
+
"order_id": order_id,
|
109
|
+
}
|
110
|
+
return await self.get_async("order_status", OrderStatusResponse, params=params)
|
107
111
|
|
108
112
|
async def check_payment(self, order_id: str) -> bool:
|
109
|
-
"""Check if an order has been paid."""
|
113
|
+
"""Check if an order has been paid (async)."""
|
110
114
|
try:
|
111
|
-
status_response = await self.
|
112
|
-
return status_response.data.payment_status == "COMPLETED"
|
115
|
+
status_response = await self.check_status(order_id)
|
116
|
+
return status_response.results.data[0].payment_status == "COMPLETED"
|
113
117
|
except Exception:
|
114
118
|
return False
|
115
119
|
|
116
120
|
async def wait_for_payment(self, order_id: str, timeout: int = 300, poll_interval: int = 10) -> APIResponse[OrderStatusResponse]:
|
117
|
-
"""Wait for an order to be paid."""
|
121
|
+
"""Wait for an order to be paid (async)."""
|
118
122
|
import asyncio
|
119
123
|
|
120
124
|
start_time = asyncio.get_event_loop().time()
|
121
125
|
|
122
126
|
while True:
|
123
|
-
status_response = await self.
|
127
|
+
status_response = await self.check_status(order_id)
|
124
128
|
|
125
|
-
if status_response.data.payment_status == "COMPLETED":
|
129
|
+
if status_response.results.data[0].payment_status == "COMPLETED":
|
126
130
|
return status_response
|
127
131
|
|
128
|
-
if status_response.data.payment_status == "FAILED":
|
132
|
+
if status_response.results.data[0].payment_status == "FAILED":
|
129
133
|
raise Exception(f"Payment failed for order {order_id}")
|
130
134
|
|
131
135
|
elapsed = asyncio.get_event_loop().time() - start_time
|
@@ -1,9 +1,3 @@
|
|
1
|
-
from elusion.zenopay.utils.helpers import
|
2
|
-
format_amount,
|
3
|
-
parse_amount,
|
4
|
-
)
|
1
|
+
from elusion.zenopay.utils.helpers import format_amount, parse_amount, generate_order_id, generate_short_order_id
|
5
2
|
|
6
|
-
__all__ = [
|
7
|
-
"format_amount",
|
8
|
-
"parse_amount",
|
9
|
-
]
|
3
|
+
__all__ = ["format_amount", "parse_amount", "generate_order_id", "generate_short_order_id"]
|
elusion/zenopay/utils/helpers.py
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
import uuid
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
|
1
5
|
def format_amount(amount: int, currency: str = "TZS") -> str:
|
2
6
|
"""Format payment amount for display.
|
3
7
|
|
@@ -33,3 +37,55 @@ def parse_amount(amount_str: str, currency: str = "TZS") -> int:
|
|
33
37
|
return int(amount_float * 100)
|
34
38
|
except (ValueError, TypeError):
|
35
39
|
raise ValueError(f"Invalid amount format: {amount_str}")
|
40
|
+
|
41
|
+
|
42
|
+
def generate_order_id(prefix: Optional[str] = None) -> str:
|
43
|
+
"""Generate a unique order ID using UUID.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
prefix: Optional prefix to add to the order ID.
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
Unique order ID string.
|
50
|
+
|
51
|
+
Examples:
|
52
|
+
>>> generate_order_id()
|
53
|
+
'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
54
|
+
|
55
|
+
>>> generate_order_id(prefix="ORDER")
|
56
|
+
'ORDER_f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
57
|
+
|
58
|
+
>>> generate_order_id(prefix="SHOP")
|
59
|
+
'SHOP_f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
60
|
+
"""
|
61
|
+
unique_id = str(uuid.uuid4())
|
62
|
+
|
63
|
+
if prefix:
|
64
|
+
return f"{prefix}_{unique_id}"
|
65
|
+
|
66
|
+
return unique_id
|
67
|
+
|
68
|
+
|
69
|
+
def generate_short_order_id(prefix: Optional[str] = None, length: int = 8) -> str:
|
70
|
+
"""Generate a shorter unique order ID using UUID.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
prefix: Optional prefix to add to the order ID.
|
74
|
+
length: Length of the UUID portion (default: 8 characters).
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Shorter unique order ID string.
|
78
|
+
|
79
|
+
Examples:
|
80
|
+
>>> generate_short_order_id()
|
81
|
+
'f47ac10b'
|
82
|
+
|
83
|
+
>>> generate_short_order_id(prefix="ORD", length=12)
|
84
|
+
'ORD_f47ac10b58cc'
|
85
|
+
"""
|
86
|
+
unique_id = str(uuid.uuid4()).replace("-", "")[:length]
|
87
|
+
|
88
|
+
if prefix:
|
89
|
+
return f"{prefix}_{unique_id}"
|
90
|
+
|
91
|
+
return unique_id
|