django-cfg 1.2.23__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.
Files changed (85) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/knowbase/tasks/archive_tasks.py +6 -6
  3. django_cfg/apps/knowbase/tasks/document_processing.py +3 -3
  4. django_cfg/apps/knowbase/tasks/external_data_tasks.py +2 -2
  5. django_cfg/apps/knowbase/tasks/maintenance.py +3 -3
  6. django_cfg/apps/payments/config/__init__.py +15 -37
  7. django_cfg/apps/payments/config/module.py +30 -122
  8. django_cfg/apps/payments/config/providers.py +22 -0
  9. django_cfg/apps/payments/config/settings.py +53 -93
  10. django_cfg/apps/payments/config/utils.py +10 -156
  11. django_cfg/apps/payments/management/__init__.py +3 -0
  12. django_cfg/apps/payments/management/commands/README.md +178 -0
  13. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  14. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  15. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  16. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  17. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  18. django_cfg/apps/payments/middleware/api_access.py +33 -0
  19. django_cfg/apps/payments/migrations/0001_initial.py +94 -1
  20. django_cfg/apps/payments/models/payments.py +110 -0
  21. django_cfg/apps/payments/services/__init__.py +7 -1
  22. django_cfg/apps/payments/services/core/balance_service.py +14 -16
  23. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  24. django_cfg/apps/payments/services/core/payment_service.py +212 -29
  25. django_cfg/apps/payments/services/core/subscription_service.py +15 -17
  26. django_cfg/apps/payments/services/internal_types.py +31 -0
  27. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  28. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  29. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  30. django_cfg/apps/payments/services/providers/__init__.py +3 -0
  31. django_cfg/apps/payments/services/providers/cryptapi.py +14 -3
  32. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  33. django_cfg/apps/payments/services/providers/registry.py +4 -0
  34. django_cfg/apps/payments/services/security/__init__.py +34 -0
  35. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  36. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  37. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  38. django_cfg/apps/payments/signals/api_key_signals.py +10 -0
  39. django_cfg/apps/payments/signals/payment_signals.py +3 -2
  40. django_cfg/apps/payments/tasks/__init__.py +12 -0
  41. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  42. django_cfg/apps/payments/utils/__init__.py +7 -4
  43. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  44. django_cfg/apps/payments/utils/config_utils.py +2 -0
  45. django_cfg/apps/payments/views/payment_views.py +40 -2
  46. django_cfg/apps/payments/views/webhook_views.py +266 -0
  47. django_cfg/apps/payments/viewsets.py +65 -0
  48. django_cfg/cli/README.md +2 -2
  49. django_cfg/cli/commands/create_project.py +1 -1
  50. django_cfg/cli/commands/info.py +1 -1
  51. django_cfg/cli/main.py +1 -1
  52. django_cfg/cli/utils.py +5 -5
  53. django_cfg/core/config.py +18 -4
  54. django_cfg/models/payments.py +546 -0
  55. django_cfg/models/tasks.py +51 -2
  56. django_cfg/modules/base.py +11 -5
  57. django_cfg/modules/django_currency/README.md +104 -269
  58. django_cfg/modules/django_currency/__init__.py +99 -41
  59. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  60. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  61. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  62. django_cfg/modules/django_currency/core/__init__.py +42 -0
  63. django_cfg/modules/django_currency/core/converter.py +169 -0
  64. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  65. django_cfg/modules/django_currency/core/models.py +54 -0
  66. django_cfg/modules/django_currency/database/__init__.py +25 -0
  67. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  68. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  69. django_cfg/modules/django_currency/utils/cache.py +92 -0
  70. django_cfg/registry/core.py +10 -0
  71. django_cfg/template_archive/__init__.py +0 -0
  72. django_cfg/template_archive/django_sample.zip +0 -0
  73. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/METADATA +10 -6
  74. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/RECORD +77 -51
  75. django_cfg/apps/agents/examples/__init__.py +0 -3
  76. django_cfg/apps/agents/examples/simple_example.py +0 -161
  77. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  78. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  79. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  80. django_cfg/modules/django_currency/cache.py +0 -430
  81. django_cfg/modules/django_currency/converter.py +0 -324
  82. django_cfg/modules/django_currency/service.py +0 -277
  83. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  84. {django_cfg-1.2.23.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  85. {django_cfg-1.2.23.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