zenopay-sdk 0.0.1__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.
@@ -0,0 +1,251 @@
1
+ """Order-related models for the ZenoPay SDK."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
7
+
8
+
9
+ class OrderBase(BaseModel):
10
+ """Base order model with common fields."""
11
+
12
+ buyer_email: str = Field(..., description="Buyer's email address")
13
+ buyer_name: str = Field(..., description="Buyer's full name")
14
+ buyer_phone: str = Field(..., description="Buyer's phone number")
15
+ 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")
17
+
18
+ @field_validator("buyer_email")
19
+ def validate_email(cls, v: str) -> str:
20
+ """Validate email format."""
21
+ if "@" not in v or "." not in v:
22
+ raise ValueError("Invalid email format")
23
+ return v.lower().strip()
24
+
25
+ @field_validator("buyer_phone")
26
+ def validate_phone(cls, v: str) -> str:
27
+ """Validate phone number format."""
28
+ # Remove any non-digit characters except +
29
+ cleaned = "".join(c for c in v if c.isdigit() or c == "+")
30
+ if len(cleaned) < 10:
31
+ raise ValueError("Phone number must be at least 10 digits")
32
+ return cleaned
33
+
34
+ @field_validator("webhook_url")
35
+ def validate_webhook_url(cls, v: Optional[str]) -> Optional[str]:
36
+ """Validate webhook URL format."""
37
+ if v is not None:
38
+ v = v.strip()
39
+ if not v.startswith(("http://", "https://")):
40
+ raise ValueError("Webhook URL must start with http:// or https://")
41
+ return v
42
+
43
+
44
+ class NewOrder(OrderBase):
45
+ """Model for creating a new order."""
46
+
47
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Additional order metadata")
48
+
49
+ @field_validator("metadata")
50
+ def validate_metadata(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
51
+ """Keep metadata as dict, convert to JSON only when needed."""
52
+ return v
53
+
54
+ model_config = ConfigDict(
55
+ json_schema_extra={
56
+ "example": {
57
+ "buyer_email": "jackson@gmail.com",
58
+ "buyer_name": "Jackson Dastani",
59
+ "buyer_phone": "0652449389",
60
+ "amount": 1000,
61
+ "webhook_url": "https://yourwebsite.com/webhook",
62
+ "metadata": {
63
+ "product_id": "12345",
64
+ "color": "blue",
65
+ "size": "L",
66
+ "custom_notes": "Please gift-wrap this item.",
67
+ },
68
+ }
69
+ }
70
+ )
71
+
72
+
73
+ class OrderStatus(BaseModel):
74
+ """Model for checking order status."""
75
+
76
+ order_id: str = Field(..., description="Order ID to check status for")
77
+
78
+ model_config = ConfigDict(json_schema_extra={"example": {"order_id": "66c4bb9c9abb1"}})
79
+
80
+
81
+ class Order(BaseModel):
82
+ """Complete order model with all fields."""
83
+
84
+ # Order details
85
+ buyer_email: str = Field(..., description="Buyer's email address")
86
+ buyer_name: str = Field(..., description="Buyer's full name")
87
+ buyer_phone: str = Field(..., description="Buyer's phone number")
88
+ amount: int = Field(..., description="Order amount")
89
+
90
+ # Payment details
91
+ payment_status: str = Field("PENDING", description="Current payment status")
92
+ reference: Optional[str] = Field(None, description="Payment reference number")
93
+
94
+ # Additional information
95
+ webhook_url: Optional[str] = Field(None, description="Webhook URL")
96
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Order metadata")
97
+
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
+ @field_validator("payment_status")
104
+ def validate_payment_status(cls, v: str) -> str:
105
+ """Validate payment status."""
106
+ valid_statuses = ["PENDING", "COMPLETED", "FAILED", "CANCELLED"]
107
+ if v not in valid_statuses:
108
+ raise ValueError(f"Invalid payment status. Must be one of: {', '.join(valid_statuses)}")
109
+ return v
110
+
111
+ @field_validator("metadata")
112
+ def validate_metadata(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
113
+ return v
114
+
115
+ @property
116
+ def is_paid(self) -> bool:
117
+ """Check if the order has been paid."""
118
+ return self.payment_status == "COMPLETED"
119
+
120
+ @property
121
+ def is_pending(self) -> bool:
122
+ """Check if the order is still pending."""
123
+ return self.payment_status == "PENDING"
124
+
125
+ @property
126
+ def has_failed(self) -> bool:
127
+ """Check if the payment has failed."""
128
+ return self.payment_status == "FAILED"
129
+
130
+ @property
131
+ def is_cancelled(self) -> bool:
132
+ """Check if the order has been cancelled."""
133
+ return self.payment_status == "CANCELLED"
134
+
135
+ def get_metadata_value(self, key: str, default: Any = None) -> Any:
136
+ """Get a specific value from the metadata."""
137
+ if self.metadata:
138
+ return self.metadata.get(key, default)
139
+ return default
140
+
141
+ model_config = ConfigDict(
142
+ json_schema_extra={
143
+ "example": {
144
+ "id": "66c4bb9c9abb1",
145
+ "buyer_email": "jackson@gmail.com",
146
+ "buyer_name": "Jackson Dastani",
147
+ "buyer_phone": "0652449389",
148
+ "amount": 1000,
149
+ "payment_status": "COMPLETED",
150
+ "reference": "1003020496",
151
+ "webhook_url": "https://yourwebsite.com/webhook",
152
+ "metadata": {
153
+ "product_id": "12345",
154
+ "color": "blue",
155
+ "size": "L",
156
+ "custom_notes": "Please gift-wrap this item.",
157
+ },
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
+ }
162
+ }
163
+ )
164
+
165
+
166
+ class OrderResponse(BaseModel):
167
+ """Response model for order operations."""
168
+
169
+ status: str = Field("success", description="Status of the operation (success or error)")
170
+ message: str = Field(
171
+ "Order created successfully",
172
+ description="Message describing the operation result",
173
+ )
174
+ order_id: str = Field(..., description="ID of the created or updated order")
175
+
176
+ model_config = ConfigDict(
177
+ json_schema_extra={
178
+ "example": {
179
+ "status": "success",
180
+ "message": "Order created successfully",
181
+ "order_id": "66c4bb9c9abb1",
182
+ }
183
+ }
184
+ )
185
+
186
+
187
+ class OrderStatusResponse(BaseModel):
188
+ status: str = Field("success", description="Status of the operation (success or error)")
189
+ order_id: str = Field(..., description="ID of the order")
190
+ message: str = Field(
191
+ "Order status retrieved successfully",
192
+ description="Message describing the operation result",
193
+ )
194
+ payment_status: str = Field("PENDING", description="Current payment status of the order")
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
227
+
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
+
241
+ model_config = ConfigDict(
242
+ json_schema_extra={
243
+ "example": {
244
+ "status": "COMPLETED",
245
+ "buyer_email": "jackson@gmail.com",
246
+ "date_from": "2025-06-01",
247
+ "date_to": "2025-06-15",
248
+ "limit": 50,
249
+ }
250
+ }
251
+ )
@@ -0,0 +1,304 @@
1
+ """Payment models for the ZenoPay SDK."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Dict, Optional, Union
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
8
+
9
+
10
+ class PaymentStatus(str, Enum):
11
+ """Enumeration of payment statuses."""
12
+
13
+ PENDING = "PENDING"
14
+ COMPLETED = "COMPLETED"
15
+ FAILED = "FAILED"
16
+ CANCELLED = "CANCELLED"
17
+
18
+ @classmethod
19
+ def from_string(cls, status: str) -> "PaymentStatus":
20
+ """Create PaymentStatus from string.
21
+
22
+ Args:
23
+ status: Status string.
24
+
25
+ Returns:
26
+ PaymentStatus enum value.
27
+
28
+ Raises:
29
+ ValueError: If status is invalid.
30
+ """
31
+ try:
32
+ return cls(status.upper())
33
+ except ValueError:
34
+ raise ValueError(f"Invalid payment status: {status}")
35
+
36
+ @property
37
+ def is_final(self) -> bool:
38
+ """Check if this is a final status (completed, failed, or cancelled)."""
39
+ return self in (self.COMPLETED, self.FAILED, self.CANCELLED)
40
+
41
+ @property
42
+ def is_successful(self) -> bool:
43
+ """Check if this represents a successful payment."""
44
+ return self == self.COMPLETED
45
+
46
+ def __str__(self) -> str:
47
+ """String representation of the status."""
48
+ return self.value
49
+
50
+
51
+ class PaymentMethod(str, Enum):
52
+ """Enumeration of payment methods."""
53
+
54
+ USSD = "USSD"
55
+ MOBILE_MONEY = "MOBILE_MONEY"
56
+ BANK_TRANSFER = "BANK_TRANSFER"
57
+ CARD = "CARD"
58
+
59
+ def __str__(self) -> str:
60
+ """String representation of the payment method."""
61
+ return self.value
62
+
63
+
64
+ class PaymentProvider(str, Enum):
65
+ """Enumeration of payment providers."""
66
+
67
+ VODACOM = "VODACOM"
68
+ AIRTEL = "AIRTEL"
69
+ TIGO = "TIGO"
70
+ HALOPESA = "HALOPESA"
71
+ OTHER = "OTHER"
72
+
73
+ def __str__(self) -> str:
74
+ """String representation of the payment provider."""
75
+ return self.value
76
+
77
+
78
+ class PaymentBase(BaseModel):
79
+ """Base payment model with common fields."""
80
+
81
+ amount: int = Field(..., gt=0, description="Payment amount in smallest currency unit")
82
+ currency: str = Field("TZS", description="Payment currency code")
83
+ reference: Optional[str] = Field(None, description="Payment reference number")
84
+ status: PaymentStatus = Field(PaymentStatus.PENDING, description="Payment status")
85
+
86
+ @field_validator("currency")
87
+ def validate_currency(cls, v: str) -> str:
88
+ """Validate currency code."""
89
+ valid_currencies = ["TZS", "USD", "EUR", "KES", "UGX"]
90
+ if v.upper() not in valid_currencies:
91
+ raise ValueError(f"Invalid currency. Must be one of: {', '.join(valid_currencies)}")
92
+ return v.upper()
93
+
94
+ @field_validator("status", mode="before")
95
+ def validate_status(cls, v: str) -> PaymentStatus:
96
+ """Validate and convert payment status."""
97
+ return PaymentStatus.from_string(v)
98
+
99
+
100
+ class Payment(PaymentBase):
101
+ """Complete payment model with all fields."""
102
+
103
+ # Order relationship
104
+ order_id: str = Field(..., description="Associated order ID")
105
+
106
+ # Payment details
107
+ method: Optional[PaymentMethod] = Field(None, description="Payment method used")
108
+ provider: Optional[PaymentProvider] = Field(None, description="Payment provider")
109
+ provider_reference: Optional[str] = Field(None, description="Provider's reference number")
110
+
111
+ # Customer details
112
+ customer_phone: Optional[str] = Field(None, description="Customer phone number")
113
+ customer_email: Optional[str] = Field(None, description="Customer email address")
114
+
115
+ # Transaction details
116
+ description: Optional[str] = Field(None, description="Payment description")
117
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Additional payment metadata")
118
+
119
+ # Fees and charges
120
+ fee_amount: Optional[int] = Field(None, description="Transaction fee amount")
121
+ net_amount: Optional[int] = Field(None, description="Net amount after fees")
122
+
123
+ # Timestamps
124
+ initiated_at: Optional[datetime] = Field(None, description="Payment initiation time")
125
+ completed_at: Optional[datetime] = Field(None, description="Payment completion time")
126
+ failed_at: Optional[datetime] = Field(None, description="Payment failure time")
127
+ expires_at: Optional[datetime] = Field(None, description="Payment expiration time")
128
+
129
+ # Error information
130
+ error_code: Optional[str] = Field(None, description="Error code if payment failed")
131
+ error_message: Optional[str] = Field(None, description="Error message if payment failed")
132
+
133
+ @property
134
+ def is_pending(self) -> bool:
135
+ """Check if payment is pending."""
136
+ return self.status == PaymentStatus.PENDING
137
+
138
+ @property
139
+ def is_completed(self) -> bool:
140
+ """Check if payment is completed."""
141
+ return self.status == PaymentStatus.COMPLETED
142
+
143
+ @property
144
+ def is_failed(self) -> bool:
145
+ """Check if payment failed."""
146
+ return self.status == PaymentStatus.FAILED
147
+
148
+ @property
149
+ def is_cancelled(self) -> bool:
150
+ """Check if payment was cancelled."""
151
+ return self.status == PaymentStatus.CANCELLED
152
+
153
+ @property
154
+ def is_final(self) -> bool:
155
+ """Check if payment is in a final state."""
156
+ return self.status.is_final
157
+
158
+ @property
159
+ def formatted_amount(self) -> str:
160
+ """Get formatted amount string."""
161
+ if self.currency == "TZS":
162
+ return f"{self.amount:,} {self.currency}"
163
+ else:
164
+ amount_decimal = self.amount / 100
165
+ return f"{amount_decimal:.2f} {self.currency}"
166
+
167
+ @property
168
+ def formatted_net_amount(self) -> str:
169
+ """Get formatted net amount string."""
170
+ if self.net_amount is None:
171
+ return self.formatted_amount
172
+
173
+ if self.currency == "TZS":
174
+ return f"{self.net_amount:,} {self.currency}"
175
+ else:
176
+ net_decimal = self.net_amount / 100
177
+ return f"{net_decimal:.2f} {self.currency}"
178
+
179
+ def get_metadata_value(self, key: str, default: Any = None) -> Any:
180
+ """Get a specific value from the metadata."""
181
+ if self.metadata:
182
+ return self.metadata.get(key, default)
183
+ return default
184
+
185
+ def calculate_fee_percentage(self) -> Optional[float]:
186
+ """Calculate fee as percentage of total amount."""
187
+ if self.fee_amount is None or self.amount <= 0:
188
+ return None
189
+ return (self.fee_amount / self.amount) * 100
190
+
191
+ model_config = ConfigDict(
192
+ json_schema_extra={
193
+ "example": {
194
+ "id": "pay_677e43274d7cb",
195
+ "order_id": "ord_677e43274d7cb",
196
+ "amount": 1000,
197
+ "currency": "TZS",
198
+ "status": "COMPLETED",
199
+ "reference": "1003020496",
200
+ "method": "USSD",
201
+ "provider": "VODACOM",
202
+ "provider_reference": "MP240615001",
203
+ "customer_phone": "0652449389",
204
+ "customer_email": "jackson@gmail.com",
205
+ "description": "Payment for Order #12345",
206
+ "fee_amount": 50,
207
+ "net_amount": 950,
208
+ "initiated_at": "2025-06-15T10:00:00Z",
209
+ "completed_at": "2025-06-15T10:05:00Z",
210
+ "created_at": "2025-06-15T10:00:00Z",
211
+ "updated_at": "2025-06-15T10:05:00Z",
212
+ "metadata": {"product_id": "12345", "campaign": "summer_sale"},
213
+ }
214
+ }
215
+ )
216
+
217
+
218
+ class PaymentCreate(PaymentBase):
219
+ """Model for creating a new payment."""
220
+
221
+ order_id: str = Field(..., description="Associated order ID")
222
+ method: Optional[PaymentMethod] = Field(None, description="Preferred payment method")
223
+ provider: Optional[PaymentProvider] = Field(None, description="Preferred payment provider")
224
+ customer_phone: Optional[str] = Field(None, description="Customer phone number")
225
+ description: Optional[str] = Field(None, description="Payment description")
226
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Additional payment metadata")
227
+
228
+ @field_validator("customer_phone")
229
+ def validate_phone(cls, v: Optional[str]) -> Optional[str]:
230
+ """Validate phone number format."""
231
+ if v is not None:
232
+ # Remove any non-digit characters except +
233
+ cleaned = "".join(c for c in v if c.isdigit() or c == "+")
234
+ if len(cleaned) < 10:
235
+ raise ValueError("Phone number must be at least 10 digits")
236
+ return cleaned
237
+ return v
238
+
239
+ model_config = ConfigDict(
240
+ json_schema_extra={
241
+ "example": {
242
+ "order_id": "ord_677e43274d7cb",
243
+ "amount": 1000,
244
+ "currency": "TZS",
245
+ "method": "USSD",
246
+ "provider": "VODACOM",
247
+ "customer_phone": "0652449389",
248
+ "description": "Payment for Order #12345",
249
+ "metadata": {"product_id": "12345", "campaign": "summer_sale"},
250
+ }
251
+ }
252
+ )
253
+
254
+
255
+ class PaymentSearch(BaseModel):
256
+ """Model for searching payments."""
257
+
258
+ order_id: Optional[str] = Field(None, description="Filter by order ID")
259
+ status: Optional[PaymentStatus] = Field(None, description="Filter by payment status")
260
+ method: Optional[PaymentMethod] = Field(None, description="Filter by payment method")
261
+ provider: Optional[PaymentProvider] = Field(None, description="Filter by payment provider")
262
+ reference: Optional[str] = Field(None, description="Filter by payment reference")
263
+ customer_phone: Optional[str] = Field(None, description="Filter by customer phone")
264
+ date_from: Optional[Union[datetime, str]] = Field(None, description="Filter from date")
265
+ date_to: Optional[Union[datetime, str]] = Field(None, description="Filter to date")
266
+ limit: Optional[int] = Field(50, ge=1, le=100, description="Number of results to return")
267
+
268
+ @field_validator("date_from", mode="before")
269
+ def parse_date_from(cls, v: Union[str, datetime, None]) -> Union[datetime, None]:
270
+ """Parse string dates to datetime objects."""
271
+ if isinstance(v, str):
272
+ try:
273
+ return datetime.fromisoformat(v.replace("Z", "+00:00"))
274
+ except ValueError:
275
+ try:
276
+ return datetime.strptime(v, "%Y-%m-%d")
277
+ except ValueError:
278
+ raise ValueError(f"Invalid date format: {v}")
279
+ return v
280
+
281
+ @field_validator("date_to", mode="before")
282
+ def parse_date_to(cls, v: Union[str, datetime, None]) -> Union[datetime, None]:
283
+ """Parse string dates to datetime objects."""
284
+ if isinstance(v, str):
285
+ try:
286
+ return datetime.fromisoformat(v.replace("Z", "+00:00"))
287
+ except ValueError:
288
+ try:
289
+ return datetime.strptime(v, "%Y-%m-%d")
290
+ except ValueError:
291
+ raise ValueError(f"Invalid date format: {v}")
292
+ return v
293
+
294
+ model_config = ConfigDict(
295
+ json_schema_extra={
296
+ "example": {
297
+ "status": "COMPLETED",
298
+ "method": "USSD",
299
+ "date_from": "2025-06-01",
300
+ "date_to": "2025-06-15",
301
+ "limit": 50,
302
+ }
303
+ }
304
+ )
@@ -0,0 +1,122 @@
1
+ """Webhook-related models for the ZenoPay SDK."""
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
7
+
8
+
9
+ class WebhookPayload(BaseModel):
10
+ """Model for webhook payload from ZenoPay."""
11
+
12
+ order_id: str = Field(..., description="Order ID")
13
+ payment_status: str = Field(..., description="Payment status")
14
+ reference: Optional[str] = Field(None, description="Payment reference number")
15
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Order metadata")
16
+
17
+ @field_validator("payment_status")
18
+ def validate_payment_status(cls, v: str) -> str:
19
+ """Validate payment status."""
20
+ valid_statuses = ["PENDING", "COMPLETED", "FAILED", "CANCELLED"]
21
+ if v not in valid_statuses:
22
+ raise ValueError(f"Invalid payment status: {v}")
23
+ return v
24
+
25
+ @field_validator("metadata")
26
+ def parse_metadata(cls, v: Union[str, Dict[str, Any], None]) -> Optional[Dict[str, Any]]:
27
+ """Parse metadata from JSON string if necessary."""
28
+ if isinstance(v, str):
29
+ try:
30
+ return json.loads(v)
31
+ except (json.JSONDecodeError, TypeError):
32
+ return None
33
+ return v
34
+
35
+ @property
36
+ def is_completed(self) -> bool:
37
+ """Check if payment is completed."""
38
+ return self.payment_status == "COMPLETED"
39
+
40
+ @property
41
+ def is_failed(self) -> bool:
42
+ """Check if payment failed."""
43
+ return self.payment_status == "FAILED"
44
+
45
+ def get_metadata_value(self, key: str, default: Any = None) -> Any:
46
+ """Get a specific value from the metadata."""
47
+ if self.metadata:
48
+ return self.metadata.get(key, default)
49
+ return default
50
+
51
+ model_config = ConfigDict(
52
+ json_schema_extra={
53
+ "example": {
54
+ "order_id": "677e43274d7cb",
55
+ "payment_status": "COMPLETED",
56
+ "reference": "1003020496",
57
+ "metadata": {
58
+ "product_id": "12345",
59
+ "color": "blue",
60
+ "size": "L",
61
+ "custom_notes": "Please gift-wrap this item.",
62
+ },
63
+ }
64
+ }
65
+ )
66
+
67
+
68
+ class WebhookEvent(BaseModel):
69
+ """Model for complete webhook event information."""
70
+
71
+ payload: WebhookPayload = Field(..., description="Webhook payload data")
72
+ raw_data: str = Field(..., description="Raw webhook data received")
73
+ timestamp: Optional[str] = Field(None, description="Event timestamp")
74
+ signature: Optional[str] = Field(None, description="Webhook signature for verification")
75
+
76
+ @classmethod
77
+ def from_raw_data(cls, raw_data: str) -> "WebhookEvent":
78
+ """Create WebhookEvent from raw webhook data.
79
+
80
+ Args:
81
+ raw_data: Raw JSON string from webhook.
82
+
83
+ Returns:
84
+ WebhookEvent instance.
85
+
86
+ Raises:
87
+ ValueError: If the raw data cannot be parsed.
88
+ """
89
+ try:
90
+ payload_data = json.loads(raw_data)
91
+ payload = WebhookPayload.model_validate(payload_data)
92
+
93
+ return cls(payload=payload, raw_data=raw_data, timestamp=None, signature=None)
94
+ except (json.JSONDecodeError, ValueError) as e:
95
+ raise ValueError(f"Invalid webhook data: {e}")
96
+
97
+ def to_dict(self) -> Dict[str, Any]:
98
+ """Convert to dictionary for easy processing."""
99
+ return {
100
+ "order_id": self.payload.order_id,
101
+ "payment_status": self.payload.payment_status,
102
+ "reference": self.payload.reference,
103
+ "metadata": self.payload.metadata,
104
+ "raw_data": self.raw_data,
105
+ "timestamp": self.timestamp,
106
+ }
107
+
108
+
109
+ class WebhookResponse(BaseModel):
110
+ """Model for webhook response."""
111
+
112
+ status: str = Field("success", description="Response status")
113
+ message: str = Field("Webhook received", description="Response message")
114
+
115
+ model_config = ConfigDict(
116
+ json_schema_extra={
117
+ "example": {
118
+ "status": "success",
119
+ "message": "Webhook received and processed",
120
+ }
121
+ }
122
+ )
@@ -0,0 +1,7 @@
1
+ from elusion.zenopay.services.orders import OrderService
2
+ from elusion.zenopay.services.webhooks import WebhookService
3
+
4
+ __all__ = [
5
+ "OrderService",
6
+ "WebhookService",
7
+ ]