django-cfg 1.2.22__py3-none-any.whl → 1.2.25__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
- django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
- django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
- django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
- django_cfg/apps/payments/admin/__init__.py +23 -0
- django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
- django_cfg/apps/payments/admin/balance_admin.py +434 -0
- django_cfg/apps/payments/admin/currencies_admin.py +186 -0
- django_cfg/apps/payments/admin/filters.py +259 -0
- django_cfg/apps/payments/admin/payments_admin.py +142 -0
- django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
- django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
- django_cfg/apps/payments/config/__init__.py +65 -0
- django_cfg/apps/payments/config/module.py +70 -0
- django_cfg/apps/payments/config/providers.py +115 -0
- django_cfg/apps/payments/config/settings.py +96 -0
- django_cfg/apps/payments/config/utils.py +52 -0
- django_cfg/apps/payments/decorators.py +291 -0
- django_cfg/apps/payments/management/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/README.md +178 -0
- django_cfg/apps/payments/management/commands/__init__.py +3 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
- django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
- django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
- django_cfg/apps/payments/managers/currency_manager.py +65 -14
- django_cfg/apps/payments/middleware/api_access.py +294 -0
- django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
- django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
- django_cfg/apps/payments/migrations/0001_initial.py +125 -11
- django_cfg/apps/payments/models/__init__.py +18 -0
- django_cfg/apps/payments/models/api_keys.py +2 -2
- django_cfg/apps/payments/models/balance.py +2 -2
- django_cfg/apps/payments/models/base.py +16 -0
- django_cfg/apps/payments/models/events.py +2 -2
- django_cfg/apps/payments/models/payments.py +112 -2
- django_cfg/apps/payments/models/subscriptions.py +2 -2
- django_cfg/apps/payments/services/__init__.py +64 -7
- django_cfg/apps/payments/services/billing/__init__.py +8 -0
- django_cfg/apps/payments/services/cache/__init__.py +15 -0
- django_cfg/apps/payments/services/cache/base.py +30 -0
- django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
- django_cfg/apps/payments/services/core/__init__.py +17 -0
- django_cfg/apps/payments/services/core/balance_service.py +447 -0
- django_cfg/apps/payments/services/core/fallback_service.py +432 -0
- django_cfg/apps/payments/services/core/payment_service.py +576 -0
- django_cfg/apps/payments/services/core/subscription_service.py +614 -0
- django_cfg/apps/payments/services/internal_types.py +297 -0
- django_cfg/apps/payments/services/middleware/__init__.py +8 -0
- django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
- django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
- django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
- django_cfg/apps/payments/services/providers/__init__.py +22 -0
- django_cfg/apps/payments/services/providers/base.py +137 -0
- django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
- django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
- django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
- django_cfg/apps/payments/services/providers/registry.py +103 -0
- django_cfg/apps/payments/services/security/__init__.py +34 -0
- django_cfg/apps/payments/services/security/error_handler.py +637 -0
- django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
- django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
- django_cfg/apps/payments/services/validators/__init__.py +8 -0
- django_cfg/apps/payments/signals/__init__.py +13 -0
- django_cfg/apps/payments/signals/api_key_signals.py +160 -0
- django_cfg/apps/payments/signals/payment_signals.py +128 -0
- django_cfg/apps/payments/signals/subscription_signals.py +196 -0
- django_cfg/apps/payments/tasks/__init__.py +12 -0
- django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
- django_cfg/apps/payments/urls.py +5 -5
- django_cfg/apps/payments/utils/__init__.py +45 -0
- django_cfg/apps/payments/utils/billing_utils.py +342 -0
- django_cfg/apps/payments/utils/config_utils.py +245 -0
- django_cfg/apps/payments/utils/middleware_utils.py +228 -0
- django_cfg/apps/payments/utils/validation_utils.py +94 -0
- django_cfg/apps/payments/views/payment_views.py +40 -2
- django_cfg/apps/payments/views/webhook_views.py +266 -0
- django_cfg/apps/payments/viewsets.py +65 -0
- django_cfg/apps/support/signals.py +16 -4
- django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
- django_cfg/cli/README.md +2 -2
- django_cfg/cli/commands/create_project.py +1 -1
- django_cfg/cli/commands/info.py +1 -1
- django_cfg/cli/main.py +1 -1
- django_cfg/cli/utils.py +5 -5
- django_cfg/core/config.py +18 -4
- django_cfg/models/payments.py +546 -0
- django_cfg/models/revolution.py +1 -1
- django_cfg/models/tasks.py +51 -2
- django_cfg/modules/base.py +12 -6
- django_cfg/modules/django_currency/README.md +104 -269
- django_cfg/modules/django_currency/__init__.py +99 -41
- django_cfg/modules/django_currency/clients/__init__.py +11 -0
- django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
- django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
- django_cfg/modules/django_currency/core/__init__.py +42 -0
- django_cfg/modules/django_currency/core/converter.py +169 -0
- django_cfg/modules/django_currency/core/exceptions.py +28 -0
- django_cfg/modules/django_currency/core/models.py +54 -0
- django_cfg/modules/django_currency/database/__init__.py +25 -0
- django_cfg/modules/django_currency/database/database_loader.py +507 -0
- django_cfg/modules/django_currency/utils/__init__.py +9 -0
- django_cfg/modules/django_currency/utils/cache.py +92 -0
- django_cfg/modules/django_email.py +42 -4
- django_cfg/modules/django_unfold/dashboard.py +20 -0
- django_cfg/registry/core.py +10 -0
- django_cfg/template_archive/__init__.py +0 -0
- django_cfg/template_archive/django_sample.zip +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
- django_cfg/apps/agents/examples/__init__.py +0 -3
- django_cfg/apps/agents/examples/simple_example.py +0 -161
- django_cfg/apps/knowbase/examples/__init__.py +0 -3
- django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
- django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
- django_cfg/apps/payments/services/base.py +0 -68
- django_cfg/apps/payments/services/nowpayments.py +0 -78
- django_cfg/apps/payments/services/providers.py +0 -77
- django_cfg/apps/payments/services/redis_service.py +0 -215
- django_cfg/modules/django_currency/cache.py +0 -430
- django_cfg/modules/django_currency/converter.py +0 -324
- django_cfg/modules/django_currency/service.py +0 -277
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,637 @@
|
|
1
|
+
"""
|
2
|
+
Centralized Error Handling and Recovery System.
|
3
|
+
Critical Foundation Security Component.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
import traceback
|
9
|
+
from typing import Dict, Any, Optional, Union, Type
|
10
|
+
from enum import Enum
|
11
|
+
from datetime import datetime
|
12
|
+
from pydantic import BaseModel, Field
|
13
|
+
from django.http import JsonResponse, HttpResponse
|
14
|
+
from django.utils import timezone
|
15
|
+
from django.conf import settings
|
16
|
+
from .payment_notifications import payment_notifications
|
17
|
+
from django.core.cache import cache
|
18
|
+
|
19
|
+
from ..models.events import PaymentEvent
|
20
|
+
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class ErrorSeverity(Enum):
|
25
|
+
"""Error severity levels for classification and response."""
|
26
|
+
LOW = "LOW"
|
27
|
+
MEDIUM = "MEDIUM"
|
28
|
+
HIGH = "HIGH"
|
29
|
+
CRITICAL = "CRITICAL"
|
30
|
+
|
31
|
+
|
32
|
+
class ErrorCategory(Enum):
|
33
|
+
"""Error categories for better organization and handling."""
|
34
|
+
SECURITY = "SECURITY"
|
35
|
+
PAYMENT = "PAYMENT"
|
36
|
+
PROVIDER = "PROVIDER"
|
37
|
+
VALIDATION = "VALIDATION"
|
38
|
+
SYSTEM = "SYSTEM"
|
39
|
+
NETWORK = "NETWORK"
|
40
|
+
DATABASE = "DATABASE"
|
41
|
+
|
42
|
+
|
43
|
+
class ErrorDetails(BaseModel):
|
44
|
+
"""Pydantic model for error details."""
|
45
|
+
exception_type: Optional[str] = None
|
46
|
+
exception_module: Optional[str] = None
|
47
|
+
traceback: Optional[str] = None
|
48
|
+
provider: Optional[str] = None
|
49
|
+
field: Optional[str] = None
|
50
|
+
validation_error: Optional[str] = None
|
51
|
+
ip_address: Optional[str] = None
|
52
|
+
api_key_prefix: Optional[str] = None
|
53
|
+
path: Optional[str] = None
|
54
|
+
method: Optional[str] = None
|
55
|
+
|
56
|
+
class Config:
|
57
|
+
extra = "allow" # Allow additional fields
|
58
|
+
|
59
|
+
|
60
|
+
class ErrorContext(BaseModel):
|
61
|
+
"""Pydantic model for error context."""
|
62
|
+
operation: Optional[str] = None
|
63
|
+
middleware: Optional[str] = None
|
64
|
+
provider: Optional[str] = None
|
65
|
+
user_id: Optional[str] = None
|
66
|
+
request: Optional[Dict[str, Any]] = None
|
67
|
+
system: Optional[Dict[str, Any]] = None
|
68
|
+
|
69
|
+
class Config:
|
70
|
+
extra = "allow"
|
71
|
+
|
72
|
+
|
73
|
+
class ErrorInfo(BaseModel):
|
74
|
+
"""Pydantic model for error information."""
|
75
|
+
error_code: str
|
76
|
+
message: str
|
77
|
+
category: str
|
78
|
+
severity: str
|
79
|
+
recoverable: bool = True
|
80
|
+
timestamp: datetime
|
81
|
+
details: ErrorDetails = Field(default_factory=ErrorDetails)
|
82
|
+
|
83
|
+
class Config:
|
84
|
+
json_encoders = {
|
85
|
+
datetime: lambda v: v.isoformat()
|
86
|
+
}
|
87
|
+
|
88
|
+
|
89
|
+
class RecoveryResult(BaseModel):
|
90
|
+
"""Pydantic model for recovery attempt result."""
|
91
|
+
attempted: bool = False
|
92
|
+
success: bool = False
|
93
|
+
actions: list[str] = Field(default_factory=list)
|
94
|
+
message: Optional[str] = None
|
95
|
+
error: Optional[str] = None
|
96
|
+
|
97
|
+
|
98
|
+
class ErrorHandlerResult(BaseModel):
|
99
|
+
"""Pydantic model for error handler result."""
|
100
|
+
error: ErrorInfo
|
101
|
+
context: ErrorContext
|
102
|
+
recovery: RecoveryResult
|
103
|
+
handled_at: datetime
|
104
|
+
|
105
|
+
class Config:
|
106
|
+
json_encoders = {
|
107
|
+
datetime: lambda v: v.isoformat()
|
108
|
+
}
|
109
|
+
|
110
|
+
|
111
|
+
class PaymentError(Exception):
|
112
|
+
"""Base payment system error with severity and category."""
|
113
|
+
|
114
|
+
def __init__(
|
115
|
+
self,
|
116
|
+
message: str,
|
117
|
+
category: ErrorCategory = ErrorCategory.SYSTEM,
|
118
|
+
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
|
119
|
+
error_code: str = None,
|
120
|
+
details: ErrorDetails = None,
|
121
|
+
recoverable: bool = True
|
122
|
+
):
|
123
|
+
super().__init__(message)
|
124
|
+
self.message = message
|
125
|
+
self.category = category
|
126
|
+
self.severity = severity
|
127
|
+
self.error_code = error_code or self._generate_error_code()
|
128
|
+
self.details = details or ErrorDetails()
|
129
|
+
self.recoverable = recoverable
|
130
|
+
self.timestamp = timezone.now()
|
131
|
+
|
132
|
+
def _generate_error_code(self) -> str:
|
133
|
+
"""Generate unique error code."""
|
134
|
+
import uuid
|
135
|
+
return f"{self.category.value}_{uuid.uuid4().hex[:8].upper()}"
|
136
|
+
|
137
|
+
def to_error_info(self) -> ErrorInfo:
|
138
|
+
"""Convert to Pydantic ErrorInfo model."""
|
139
|
+
return ErrorInfo(
|
140
|
+
error_code=self.error_code,
|
141
|
+
message=self.message,
|
142
|
+
category=self.category.value,
|
143
|
+
severity=self.severity.value,
|
144
|
+
recoverable=self.recoverable,
|
145
|
+
timestamp=self.timestamp,
|
146
|
+
details=self.details
|
147
|
+
)
|
148
|
+
|
149
|
+
|
150
|
+
class SecurityError(PaymentError):
|
151
|
+
"""Security-related errors."""
|
152
|
+
|
153
|
+
def __init__(self, message: str, **kwargs):
|
154
|
+
kwargs.setdefault('category', ErrorCategory.SECURITY)
|
155
|
+
kwargs.setdefault('severity', ErrorSeverity.HIGH)
|
156
|
+
kwargs.setdefault('recoverable', False)
|
157
|
+
|
158
|
+
# Convert dict details to Pydantic model
|
159
|
+
if 'details' in kwargs and isinstance(kwargs['details'], dict):
|
160
|
+
kwargs['details'] = ErrorDetails(**kwargs['details'])
|
161
|
+
|
162
|
+
super().__init__(message, **kwargs)
|
163
|
+
|
164
|
+
|
165
|
+
class ProviderError(PaymentError):
|
166
|
+
"""Payment provider errors."""
|
167
|
+
|
168
|
+
def __init__(self, message: str, provider: str = None, **kwargs):
|
169
|
+
kwargs.setdefault('category', ErrorCategory.PROVIDER)
|
170
|
+
kwargs.setdefault('severity', ErrorSeverity.HIGH)
|
171
|
+
|
172
|
+
# Handle provider in Pydantic details
|
173
|
+
details_dict = kwargs.get('details', {})
|
174
|
+
if isinstance(details_dict, dict):
|
175
|
+
if provider:
|
176
|
+
details_dict['provider'] = provider
|
177
|
+
kwargs['details'] = ErrorDetails(**details_dict)
|
178
|
+
|
179
|
+
super().__init__(message, **kwargs)
|
180
|
+
|
181
|
+
|
182
|
+
class ValidationError(PaymentError):
|
183
|
+
"""Data validation errors."""
|
184
|
+
|
185
|
+
def __init__(self, message: str, field: str = None, **kwargs):
|
186
|
+
kwargs.setdefault('category', ErrorCategory.VALIDATION)
|
187
|
+
kwargs.setdefault('severity', ErrorSeverity.MEDIUM)
|
188
|
+
|
189
|
+
# Handle field in Pydantic details
|
190
|
+
details_dict = kwargs.get('details', {})
|
191
|
+
if isinstance(details_dict, dict):
|
192
|
+
if field:
|
193
|
+
details_dict['field'] = field
|
194
|
+
kwargs['details'] = ErrorDetails(**details_dict)
|
195
|
+
|
196
|
+
super().__init__(message, **kwargs)
|
197
|
+
|
198
|
+
|
199
|
+
class CentralizedErrorHandler:
|
200
|
+
"""
|
201
|
+
Centralized error handling system with recovery mechanisms.
|
202
|
+
|
203
|
+
Foundation Security Component - CRITICAL for system stability.
|
204
|
+
"""
|
205
|
+
|
206
|
+
def __init__(self):
|
207
|
+
self.error_count_cache_timeout = 300 # 5 minutes
|
208
|
+
self.max_errors_per_minute = 100
|
209
|
+
self.notification_cooldown = 900 # 15 minutes
|
210
|
+
|
211
|
+
def handle_error(
|
212
|
+
self,
|
213
|
+
error: Union[Exception, PaymentError],
|
214
|
+
context: Dict[str, Any] = None,
|
215
|
+
request = None,
|
216
|
+
user_id: str = None
|
217
|
+
) -> ErrorHandlerResult:
|
218
|
+
"""
|
219
|
+
Handle error with comprehensive logging, alerting, and recovery.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
error: Exception or PaymentError instance
|
223
|
+
context: Additional context information
|
224
|
+
request: Django request object (optional)
|
225
|
+
user_id: User ID for tracking (optional)
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
Dict with error details and recovery information
|
229
|
+
"""
|
230
|
+
|
231
|
+
try:
|
232
|
+
# Convert exception to PaymentError if needed
|
233
|
+
if not isinstance(error, PaymentError):
|
234
|
+
payment_error = self._convert_exception_to_payment_error(error)
|
235
|
+
else:
|
236
|
+
payment_error = error
|
237
|
+
|
238
|
+
# Enrich context (convert to Pydantic)
|
239
|
+
enriched_context = self._enrich_context(context, request, user_id, payment_error)
|
240
|
+
|
241
|
+
# Log error
|
242
|
+
self._log_error(payment_error, enriched_context)
|
243
|
+
|
244
|
+
# Store error in database
|
245
|
+
self._store_error_event(payment_error, enriched_context)
|
246
|
+
|
247
|
+
# Check for error patterns and rate limits
|
248
|
+
self._check_error_patterns(payment_error, enriched_context)
|
249
|
+
|
250
|
+
# Send notifications if needed
|
251
|
+
self._send_notifications(payment_error, enriched_context)
|
252
|
+
|
253
|
+
# Attempt recovery if possible
|
254
|
+
recovery_result = self._attempt_recovery(payment_error, enriched_context)
|
255
|
+
|
256
|
+
# Return Pydantic result
|
257
|
+
return ErrorHandlerResult(
|
258
|
+
error=payment_error.to_error_info(),
|
259
|
+
context=enriched_context,
|
260
|
+
recovery=recovery_result,
|
261
|
+
handled_at=timezone.now()
|
262
|
+
)
|
263
|
+
|
264
|
+
except Exception as handler_error:
|
265
|
+
# Error in error handler - log critically
|
266
|
+
logger.critical(
|
267
|
+
f"🚨 CRITICAL: Error handler failed: {handler_error}",
|
268
|
+
exc_info=True,
|
269
|
+
extra={
|
270
|
+
'original_error': str(error),
|
271
|
+
'handler_error': str(handler_error)
|
272
|
+
}
|
273
|
+
)
|
274
|
+
|
275
|
+
# Return minimal Pydantic error info
|
276
|
+
return ErrorHandlerResult(
|
277
|
+
error=ErrorInfo(
|
278
|
+
error_code='HANDLER_FAILURE',
|
279
|
+
message='Error handling system failure',
|
280
|
+
severity=ErrorSeverity.CRITICAL.value,
|
281
|
+
category=ErrorCategory.SYSTEM.value,
|
282
|
+
timestamp=timezone.now()
|
283
|
+
),
|
284
|
+
context=ErrorContext(),
|
285
|
+
recovery=RecoveryResult(attempted=False, success=False),
|
286
|
+
handled_at=timezone.now()
|
287
|
+
)
|
288
|
+
|
289
|
+
def _convert_exception_to_payment_error(self, exception: Exception) -> PaymentError:
|
290
|
+
"""Convert standard exceptions to PaymentError instances."""
|
291
|
+
|
292
|
+
# Map common exceptions to payment errors
|
293
|
+
exception_mapping = {
|
294
|
+
PermissionError: (ErrorCategory.SECURITY, ErrorSeverity.HIGH),
|
295
|
+
ValueError: (ErrorCategory.VALIDATION, ErrorSeverity.MEDIUM),
|
296
|
+
ConnectionError: (ErrorCategory.NETWORK, ErrorSeverity.HIGH),
|
297
|
+
TimeoutError: (ErrorCategory.NETWORK, ErrorSeverity.HIGH),
|
298
|
+
}
|
299
|
+
|
300
|
+
# Get exception type mapping
|
301
|
+
exception_type = type(exception)
|
302
|
+
if exception_type in exception_mapping:
|
303
|
+
category, severity = exception_mapping[exception_type]
|
304
|
+
else:
|
305
|
+
category, severity = ErrorCategory.SYSTEM, ErrorSeverity.MEDIUM
|
306
|
+
|
307
|
+
# Extract additional details as Pydantic model
|
308
|
+
details = ErrorDetails(
|
309
|
+
exception_type=exception_type.__name__,
|
310
|
+
exception_module=exception_type.__module__,
|
311
|
+
traceback=traceback.format_exc()
|
312
|
+
)
|
313
|
+
|
314
|
+
return PaymentError(
|
315
|
+
message=str(exception),
|
316
|
+
category=category,
|
317
|
+
severity=severity,
|
318
|
+
details=details
|
319
|
+
)
|
320
|
+
|
321
|
+
def _enrich_context(
|
322
|
+
self,
|
323
|
+
context: Dict[str, Any],
|
324
|
+
request,
|
325
|
+
user_id: str,
|
326
|
+
error: PaymentError
|
327
|
+
) -> ErrorContext:
|
328
|
+
"""Enrich error context with additional information."""
|
329
|
+
|
330
|
+
# Start with provided context
|
331
|
+
enriched_dict = context.copy() if context else {}
|
332
|
+
|
333
|
+
# Add request information
|
334
|
+
request_info = None
|
335
|
+
if request:
|
336
|
+
request_info = {
|
337
|
+
'method': request.method,
|
338
|
+
'path': request.path,
|
339
|
+
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
340
|
+
'ip_address': self._get_client_ip(request),
|
341
|
+
'timestamp': timezone.now().isoformat()
|
342
|
+
}
|
343
|
+
|
344
|
+
# Determine user ID
|
345
|
+
final_user_id = user_id
|
346
|
+
if not final_user_id and request and hasattr(request, 'payment_user'):
|
347
|
+
final_user_id = str(request.payment_user.id)
|
348
|
+
|
349
|
+
# Add system information
|
350
|
+
system_info = {
|
351
|
+
'environment': getattr(settings, 'ENVIRONMENT', 'unknown'),
|
352
|
+
'debug': settings.DEBUG,
|
353
|
+
'timestamp': timezone.now().isoformat()
|
354
|
+
}
|
355
|
+
|
356
|
+
# Build Pydantic context
|
357
|
+
return ErrorContext(
|
358
|
+
operation=enriched_dict.get('operation'),
|
359
|
+
middleware=enriched_dict.get('middleware'),
|
360
|
+
provider=enriched_dict.get('provider'),
|
361
|
+
user_id=final_user_id,
|
362
|
+
request=request_info,
|
363
|
+
system=system_info,
|
364
|
+
**{k: v for k, v in enriched_dict.items()
|
365
|
+
if k not in ['operation', 'middleware', 'provider', 'user_id', 'request', 'system']}
|
366
|
+
)
|
367
|
+
|
368
|
+
def _log_error(self, error: PaymentError, context: ErrorContext):
|
369
|
+
"""Log error with appropriate level based on severity."""
|
370
|
+
|
371
|
+
log_message = f"[{error.error_code}] {error.message}"
|
372
|
+
|
373
|
+
extra_data = {
|
374
|
+
'error_code': error.error_code,
|
375
|
+
'category': error.category.value,
|
376
|
+
'severity': error.severity.value,
|
377
|
+
'context': context.dict()
|
378
|
+
}
|
379
|
+
|
380
|
+
# Log based on severity
|
381
|
+
if error.severity == ErrorSeverity.CRITICAL:
|
382
|
+
logger.critical(f"🚨 CRITICAL: {log_message}", extra=extra_data)
|
383
|
+
elif error.severity == ErrorSeverity.HIGH:
|
384
|
+
logger.error(f"❌ HIGH: {log_message}", extra=extra_data)
|
385
|
+
elif error.severity == ErrorSeverity.MEDIUM:
|
386
|
+
logger.warning(f"⚠️ MEDIUM: {log_message}", extra=extra_data)
|
387
|
+
else:
|
388
|
+
logger.info(f"ℹ️ LOW: {log_message}", extra=extra_data)
|
389
|
+
|
390
|
+
def _store_error_event(self, error: PaymentError, context: ErrorContext):
|
391
|
+
"""Store error event in database for analysis."""
|
392
|
+
|
393
|
+
try:
|
394
|
+
PaymentEvent.objects.create(
|
395
|
+
event_type=f'error_{error.category.value.lower()}',
|
396
|
+
metadata={
|
397
|
+
'error': error.to_error_info().dict(),
|
398
|
+
'context': context.dict(),
|
399
|
+
'stored_at': timezone.now().isoformat()
|
400
|
+
}
|
401
|
+
)
|
402
|
+
except Exception as e:
|
403
|
+
logger.error(f"Failed to store error event: {e}")
|
404
|
+
|
405
|
+
def _check_error_patterns(self, error: PaymentError, context: ErrorContext):
|
406
|
+
"""Check for error patterns and potential attacks."""
|
407
|
+
|
408
|
+
# Check error rate
|
409
|
+
error_rate_key = f"error_rate:{error.category.value}"
|
410
|
+
current_errors = cache.get(error_rate_key, 0)
|
411
|
+
|
412
|
+
if current_errors >= self.max_errors_per_minute:
|
413
|
+
logger.critical(
|
414
|
+
f"🚨 HIGH ERROR RATE DETECTED: {current_errors} {error.category.value} "
|
415
|
+
f"errors in last minute - possible attack or system failure"
|
416
|
+
)
|
417
|
+
|
418
|
+
# Store critical event
|
419
|
+
PaymentEvent.objects.create(
|
420
|
+
event_type='high_error_rate',
|
421
|
+
metadata={
|
422
|
+
'category': error.category.value,
|
423
|
+
'error_count': current_errors,
|
424
|
+
'threshold': self.max_errors_per_minute,
|
425
|
+
'timestamp': timezone.now().isoformat()
|
426
|
+
}
|
427
|
+
)
|
428
|
+
|
429
|
+
# Send high error rate alert
|
430
|
+
payment_notifications.send_high_error_rate_alert(
|
431
|
+
error.category.value, current_errors, self.max_errors_per_minute
|
432
|
+
)
|
433
|
+
|
434
|
+
# Increment error count
|
435
|
+
cache.set(error_rate_key, current_errors + 1, timeout=self.error_count_cache_timeout)
|
436
|
+
|
437
|
+
# Check for security patterns
|
438
|
+
if error.category == ErrorCategory.SECURITY:
|
439
|
+
self._check_security_patterns(error, context)
|
440
|
+
|
441
|
+
def _check_security_patterns(self, error: PaymentError, context: ErrorContext):
|
442
|
+
"""Check for security attack patterns."""
|
443
|
+
|
444
|
+
# Check for multiple security errors from same IP
|
445
|
+
ip_address = context.request.get('ip_address') if context.request else None
|
446
|
+
if ip_address:
|
447
|
+
security_errors_key = f"security_errors:{ip_address}"
|
448
|
+
error_count = cache.get(security_errors_key, 0)
|
449
|
+
|
450
|
+
if error_count >= 5: # 5 security errors from same IP
|
451
|
+
logger.critical(
|
452
|
+
f"🚨 SECURITY ATTACK PATTERN: {error_count} security errors "
|
453
|
+
f"from IP {ip_address} - possible coordinated attack"
|
454
|
+
)
|
455
|
+
|
456
|
+
# Store security incident
|
457
|
+
PaymentEvent.objects.create(
|
458
|
+
event_type='security_attack_pattern',
|
459
|
+
metadata={
|
460
|
+
'ip_address': ip_address,
|
461
|
+
'error_count': error_count,
|
462
|
+
'error_details': error.to_error_info().dict(),
|
463
|
+
'timestamp': timezone.now().isoformat()
|
464
|
+
}
|
465
|
+
)
|
466
|
+
|
467
|
+
# Send attack pattern alert
|
468
|
+
payment_notifications.send_attack_pattern_alert(
|
469
|
+
ip_address, error_count, error.to_error_info().dict()
|
470
|
+
)
|
471
|
+
|
472
|
+
# Increment IP error count
|
473
|
+
cache.set(security_errors_key, error_count + 1, timeout=3600) # 1 hour
|
474
|
+
|
475
|
+
def _send_notifications(self, error: PaymentError, context: ErrorContext):
|
476
|
+
"""Send notifications for critical errors."""
|
477
|
+
|
478
|
+
# Only send notifications for high severity errors
|
479
|
+
if error.severity not in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
|
480
|
+
return
|
481
|
+
|
482
|
+
# Check notification cooldown
|
483
|
+
notification_key = f"notification_sent:{error.category.value}:{error.severity.value}"
|
484
|
+
if cache.get(notification_key):
|
485
|
+
return # Already notified recently
|
486
|
+
|
487
|
+
try:
|
488
|
+
# Send notification using payment notification service
|
489
|
+
self._send_admin_notification(error, context)
|
490
|
+
|
491
|
+
# Set cooldown
|
492
|
+
cache.set(notification_key, True, timeout=self.notification_cooldown)
|
493
|
+
|
494
|
+
except Exception as e:
|
495
|
+
logger.error(f"Failed to send error notifications: {e}")
|
496
|
+
|
497
|
+
def _send_admin_notification(self, error: PaymentError, context: ErrorContext):
|
498
|
+
"""Send admin notification using payment notification service."""
|
499
|
+
|
500
|
+
# Route to appropriate notification method based on error category
|
501
|
+
if error.category == ErrorCategory.SECURITY:
|
502
|
+
if hasattr(error.details, 'api_key_prefix') or 'api_access' in context.middleware:
|
503
|
+
payment_notifications.send_api_security_breach(error.to_error_info(), context)
|
504
|
+
elif hasattr(error.details, 'validation_error'):
|
505
|
+
payment_notifications.send_webhook_validation_failure(error.to_error_info(), context)
|
506
|
+
else:
|
507
|
+
payment_notifications.send_security_alert(error.to_error_info(), context)
|
508
|
+
|
509
|
+
elif error.category == ErrorCategory.PROVIDER:
|
510
|
+
payment_notifications.send_provider_error(error.to_error_info(), context)
|
511
|
+
|
512
|
+
elif error.category == ErrorCategory.PAYMENT:
|
513
|
+
payment_notifications.send_payment_failure(error.to_error_info(), context)
|
514
|
+
|
515
|
+
else:
|
516
|
+
payment_notifications.send_system_error(error.to_error_info(), context)
|
517
|
+
|
518
|
+
def _attempt_recovery(self, error: PaymentError, context: ErrorContext) -> RecoveryResult:
|
519
|
+
"""Attempt automatic recovery based on error type."""
|
520
|
+
|
521
|
+
if not error.recoverable:
|
522
|
+
return RecoveryResult(
|
523
|
+
attempted=False,
|
524
|
+
message='Error marked as non-recoverable'
|
525
|
+
)
|
526
|
+
|
527
|
+
recovery_result = RecoveryResult(attempted=True, success=False)
|
528
|
+
|
529
|
+
try:
|
530
|
+
# Recovery based on error category
|
531
|
+
if error.category == ErrorCategory.NETWORK:
|
532
|
+
recovery_result = self._recover_network_error(error, context)
|
533
|
+
elif error.category == ErrorCategory.PROVIDER:
|
534
|
+
recovery_result = self._recover_provider_error(error, context)
|
535
|
+
elif error.category == ErrorCategory.DATABASE:
|
536
|
+
recovery_result = self._recover_database_error(error, context)
|
537
|
+
|
538
|
+
except Exception as e:
|
539
|
+
logger.error(f"Recovery attempt failed: {e}")
|
540
|
+
recovery_result.error = str(e)
|
541
|
+
|
542
|
+
return recovery_result
|
543
|
+
|
544
|
+
def _recover_network_error(self, error: PaymentError, context: ErrorContext) -> RecoveryResult:
|
545
|
+
"""Attempt recovery for network errors."""
|
546
|
+
|
547
|
+
return RecoveryResult(
|
548
|
+
attempted=True,
|
549
|
+
success=False,
|
550
|
+
actions=['retry_scheduled'],
|
551
|
+
message='Network error - retry scheduled with exponential backoff'
|
552
|
+
)
|
553
|
+
|
554
|
+
def _recover_provider_error(self, error: PaymentError, context: ErrorContext) -> RecoveryResult:
|
555
|
+
"""Attempt recovery for provider errors."""
|
556
|
+
|
557
|
+
provider = error.details.provider
|
558
|
+
if provider:
|
559
|
+
# Could implement provider fallback logic here
|
560
|
+
return RecoveryResult(
|
561
|
+
attempted=True,
|
562
|
+
success=False,
|
563
|
+
actions=['provider_fallback_considered'],
|
564
|
+
message=f'Provider {provider} error - fallback providers evaluated'
|
565
|
+
)
|
566
|
+
|
567
|
+
return RecoveryResult(
|
568
|
+
attempted=True,
|
569
|
+
success=False,
|
570
|
+
actions=[],
|
571
|
+
message='No recovery action available'
|
572
|
+
)
|
573
|
+
|
574
|
+
def _recover_database_error(self, error: PaymentError, context: ErrorContext) -> RecoveryResult:
|
575
|
+
"""Attempt recovery for database errors."""
|
576
|
+
|
577
|
+
return RecoveryResult(
|
578
|
+
attempted=True,
|
579
|
+
success=False,
|
580
|
+
actions=['connection_refresh'],
|
581
|
+
message='Database error - connection refresh attempted'
|
582
|
+
)
|
583
|
+
|
584
|
+
def _get_client_ip(self, request) -> str:
|
585
|
+
"""Extract client IP address from request."""
|
586
|
+
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
587
|
+
if x_forwarded_for:
|
588
|
+
ip = x_forwarded_for.split(',')[0].strip()
|
589
|
+
else:
|
590
|
+
ip = request.META.get('REMOTE_ADDR', '')
|
591
|
+
return ip
|
592
|
+
|
593
|
+
def get_error_statistics(self, hours: int = 24) -> Dict[str, Any]:
|
594
|
+
"""Get error statistics for monitoring dashboard."""
|
595
|
+
|
596
|
+
from datetime import timedelta
|
597
|
+
from django.db.models import Count
|
598
|
+
|
599
|
+
since = timezone.now() - timedelta(hours=hours)
|
600
|
+
|
601
|
+
# Get error events from database
|
602
|
+
error_events = PaymentEvent.objects.filter(
|
603
|
+
created_at__gte=since,
|
604
|
+
event_type__startswith='error_'
|
605
|
+
)
|
606
|
+
|
607
|
+
# Group by category and severity
|
608
|
+
stats = error_events.values('event_type').annotate(count=Count('id'))
|
609
|
+
|
610
|
+
return {
|
611
|
+
'period_hours': hours,
|
612
|
+
'total_errors': error_events.count(),
|
613
|
+
'breakdown': list(stats),
|
614
|
+
'generated_at': timezone.now().isoformat()
|
615
|
+
}
|
616
|
+
|
617
|
+
|
618
|
+
# Singleton instance for import
|
619
|
+
error_handler = CentralizedErrorHandler()
|
620
|
+
|
621
|
+
|
622
|
+
# Context manager for error handling
|
623
|
+
class error_context:
|
624
|
+
"""Context manager for automatic error handling."""
|
625
|
+
|
626
|
+
def __init__(self, operation: str, **context):
|
627
|
+
self.operation = operation
|
628
|
+
self.context = context
|
629
|
+
|
630
|
+
def __enter__(self):
|
631
|
+
return self
|
632
|
+
|
633
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
634
|
+
if exc_value:
|
635
|
+
self.context['operation'] = self.operation
|
636
|
+
error_handler.handle_error(exc_value, self.context)
|
637
|
+
return False # Don't suppress the exception
|