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.
Files changed (125) 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/admin/__init__.py +23 -0
  7. django_cfg/apps/payments/admin/api_keys_admin.py +347 -0
  8. django_cfg/apps/payments/admin/balance_admin.py +434 -0
  9. django_cfg/apps/payments/admin/currencies_admin.py +186 -0
  10. django_cfg/apps/payments/admin/filters.py +259 -0
  11. django_cfg/apps/payments/admin/payments_admin.py +142 -0
  12. django_cfg/apps/payments/admin/subscriptions_admin.py +227 -0
  13. django_cfg/apps/payments/admin/tariffs_admin.py +199 -0
  14. django_cfg/apps/payments/config/__init__.py +65 -0
  15. django_cfg/apps/payments/config/module.py +70 -0
  16. django_cfg/apps/payments/config/providers.py +115 -0
  17. django_cfg/apps/payments/config/settings.py +96 -0
  18. django_cfg/apps/payments/config/utils.py +52 -0
  19. django_cfg/apps/payments/decorators.py +291 -0
  20. django_cfg/apps/payments/management/__init__.py +3 -0
  21. django_cfg/apps/payments/management/commands/README.md +178 -0
  22. django_cfg/apps/payments/management/commands/__init__.py +3 -0
  23. django_cfg/apps/payments/management/commands/currency_stats.py +323 -0
  24. django_cfg/apps/payments/management/commands/populate_currencies.py +246 -0
  25. django_cfg/apps/payments/management/commands/update_currencies.py +336 -0
  26. django_cfg/apps/payments/managers/currency_manager.py +65 -14
  27. django_cfg/apps/payments/middleware/api_access.py +294 -0
  28. django_cfg/apps/payments/middleware/rate_limiting.py +216 -0
  29. django_cfg/apps/payments/middleware/usage_tracking.py +296 -0
  30. django_cfg/apps/payments/migrations/0001_initial.py +125 -11
  31. django_cfg/apps/payments/models/__init__.py +18 -0
  32. django_cfg/apps/payments/models/api_keys.py +2 -2
  33. django_cfg/apps/payments/models/balance.py +2 -2
  34. django_cfg/apps/payments/models/base.py +16 -0
  35. django_cfg/apps/payments/models/events.py +2 -2
  36. django_cfg/apps/payments/models/payments.py +112 -2
  37. django_cfg/apps/payments/models/subscriptions.py +2 -2
  38. django_cfg/apps/payments/services/__init__.py +64 -7
  39. django_cfg/apps/payments/services/billing/__init__.py +8 -0
  40. django_cfg/apps/payments/services/cache/__init__.py +15 -0
  41. django_cfg/apps/payments/services/cache/base.py +30 -0
  42. django_cfg/apps/payments/services/cache/simple_cache.py +135 -0
  43. django_cfg/apps/payments/services/core/__init__.py +17 -0
  44. django_cfg/apps/payments/services/core/balance_service.py +447 -0
  45. django_cfg/apps/payments/services/core/fallback_service.py +432 -0
  46. django_cfg/apps/payments/services/core/payment_service.py +576 -0
  47. django_cfg/apps/payments/services/core/subscription_service.py +614 -0
  48. django_cfg/apps/payments/services/internal_types.py +297 -0
  49. django_cfg/apps/payments/services/middleware/__init__.py +8 -0
  50. django_cfg/apps/payments/services/monitoring/__init__.py +22 -0
  51. django_cfg/apps/payments/services/monitoring/api_schemas.py +222 -0
  52. django_cfg/apps/payments/services/monitoring/provider_health.py +372 -0
  53. django_cfg/apps/payments/services/providers/__init__.py +22 -0
  54. django_cfg/apps/payments/services/providers/base.py +137 -0
  55. django_cfg/apps/payments/services/providers/cryptapi.py +273 -0
  56. django_cfg/apps/payments/services/providers/cryptomus.py +310 -0
  57. django_cfg/apps/payments/services/providers/nowpayments.py +293 -0
  58. django_cfg/apps/payments/services/providers/registry.py +103 -0
  59. django_cfg/apps/payments/services/security/__init__.py +34 -0
  60. django_cfg/apps/payments/services/security/error_handler.py +637 -0
  61. django_cfg/apps/payments/services/security/payment_notifications.py +342 -0
  62. django_cfg/apps/payments/services/security/webhook_validator.py +475 -0
  63. django_cfg/apps/payments/services/validators/__init__.py +8 -0
  64. django_cfg/apps/payments/signals/__init__.py +13 -0
  65. django_cfg/apps/payments/signals/api_key_signals.py +160 -0
  66. django_cfg/apps/payments/signals/payment_signals.py +128 -0
  67. django_cfg/apps/payments/signals/subscription_signals.py +196 -0
  68. django_cfg/apps/payments/tasks/__init__.py +12 -0
  69. django_cfg/apps/payments/tasks/webhook_processing.py +177 -0
  70. django_cfg/apps/payments/urls.py +5 -5
  71. django_cfg/apps/payments/utils/__init__.py +45 -0
  72. django_cfg/apps/payments/utils/billing_utils.py +342 -0
  73. django_cfg/apps/payments/utils/config_utils.py +245 -0
  74. django_cfg/apps/payments/utils/middleware_utils.py +228 -0
  75. django_cfg/apps/payments/utils/validation_utils.py +94 -0
  76. django_cfg/apps/payments/views/payment_views.py +40 -2
  77. django_cfg/apps/payments/views/webhook_views.py +266 -0
  78. django_cfg/apps/payments/viewsets.py +65 -0
  79. django_cfg/apps/support/signals.py +16 -4
  80. django_cfg/apps/support/templates/support/chat/ticket_chat.html +1 -1
  81. django_cfg/cli/README.md +2 -2
  82. django_cfg/cli/commands/create_project.py +1 -1
  83. django_cfg/cli/commands/info.py +1 -1
  84. django_cfg/cli/main.py +1 -1
  85. django_cfg/cli/utils.py +5 -5
  86. django_cfg/core/config.py +18 -4
  87. django_cfg/models/payments.py +546 -0
  88. django_cfg/models/revolution.py +1 -1
  89. django_cfg/models/tasks.py +51 -2
  90. django_cfg/modules/base.py +12 -6
  91. django_cfg/modules/django_currency/README.md +104 -269
  92. django_cfg/modules/django_currency/__init__.py +99 -41
  93. django_cfg/modules/django_currency/clients/__init__.py +11 -0
  94. django_cfg/modules/django_currency/clients/coingecko_client.py +257 -0
  95. django_cfg/modules/django_currency/clients/yfinance_client.py +246 -0
  96. django_cfg/modules/django_currency/core/__init__.py +42 -0
  97. django_cfg/modules/django_currency/core/converter.py +169 -0
  98. django_cfg/modules/django_currency/core/exceptions.py +28 -0
  99. django_cfg/modules/django_currency/core/models.py +54 -0
  100. django_cfg/modules/django_currency/database/__init__.py +25 -0
  101. django_cfg/modules/django_currency/database/database_loader.py +507 -0
  102. django_cfg/modules/django_currency/utils/__init__.py +9 -0
  103. django_cfg/modules/django_currency/utils/cache.py +92 -0
  104. django_cfg/modules/django_email.py +42 -4
  105. django_cfg/modules/django_unfold/dashboard.py +20 -0
  106. django_cfg/registry/core.py +10 -0
  107. django_cfg/template_archive/__init__.py +0 -0
  108. django_cfg/template_archive/django_sample.zip +0 -0
  109. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/METADATA +11 -6
  110. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/RECORD +113 -50
  111. django_cfg/apps/agents/examples/__init__.py +0 -3
  112. django_cfg/apps/agents/examples/simple_example.py +0 -161
  113. django_cfg/apps/knowbase/examples/__init__.py +0 -3
  114. django_cfg/apps/knowbase/examples/external_data_usage.py +0 -191
  115. django_cfg/apps/knowbase/mixins/examples/vehicle_model_example.py +0 -199
  116. django_cfg/apps/payments/services/base.py +0 -68
  117. django_cfg/apps/payments/services/nowpayments.py +0 -78
  118. django_cfg/apps/payments/services/providers.py +0 -77
  119. django_cfg/apps/payments/services/redis_service.py +0 -215
  120. django_cfg/modules/django_currency/cache.py +0 -430
  121. django_cfg/modules/django_currency/converter.py +0 -324
  122. django_cfg/modules/django_currency/service.py +0 -277
  123. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/WHEEL +0 -0
  124. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/entry_points.txt +0 -0
  125. {django_cfg-1.2.22.dist-info → django_cfg-1.2.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,228 @@
1
+ """
2
+ Utilities for middleware processing.
3
+ """
4
+
5
+ from typing import Optional, List
6
+ from django.http import HttpRequest
7
+ from django.conf import settings
8
+
9
+
10
+ def get_client_ip(request: HttpRequest) -> Optional[str]:
11
+ """
12
+ Get client IP address from request.
13
+ Handles various proxy headers and configurations.
14
+ """
15
+
16
+ # Check for forwarded headers first
17
+ forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
18
+ if forwarded_for:
19
+ # Take first IP in chain (original client)
20
+ return forwarded_for.split(',')[0].strip()
21
+
22
+ # Check for real IP header (common with nginx)
23
+ real_ip = request.META.get('HTTP_X_REAL_IP')
24
+ if real_ip:
25
+ return real_ip.strip()
26
+
27
+ # Check for Cloudflare header
28
+ cf_ip = request.META.get('HTTP_CF_CONNECTING_IP')
29
+ if cf_ip:
30
+ return cf_ip.strip()
31
+
32
+ # Fallback to remote address
33
+ remote_addr = request.META.get('REMOTE_ADDR')
34
+ if remote_addr:
35
+ return remote_addr.strip()
36
+
37
+ return None
38
+
39
+
40
+ def is_api_request(request: HttpRequest, api_prefixes: Optional[List[str]] = None) -> bool:
41
+ """
42
+ Check if request is an API request based on path prefixes.
43
+
44
+ Args:
45
+ request: Django HTTP request
46
+ api_prefixes: List of API prefixes to check (defaults to settings)
47
+ """
48
+
49
+ if api_prefixes is None:
50
+ api_prefixes = getattr(settings, 'PAYMENTS_API_PREFIXES', ['/api/'])
51
+
52
+ path = request.path
53
+ return any(path.startswith(prefix) for prefix in api_prefixes)
54
+
55
+
56
+ def extract_api_key(request: HttpRequest) -> Optional[str]:
57
+ """
58
+ Extract API key from request headers or query parameters.
59
+ Supports multiple authentication methods.
60
+
61
+ Priority:
62
+ 1. Authorization header (Bearer token)
63
+ 2. X-API-Key header
64
+ 3. Query parameter (less secure, for testing)
65
+ """
66
+
67
+ # Method 1: Authorization header (Bearer token)
68
+ auth_header = request.META.get('HTTP_AUTHORIZATION', '')
69
+ if auth_header.startswith('Bearer '):
70
+ return auth_header[7:] # Remove 'Bearer ' prefix
71
+
72
+ # Method 2: X-API-Key header
73
+ api_key_header = request.META.get('HTTP_X_API_KEY')
74
+ if api_key_header:
75
+ return api_key_header.strip()
76
+
77
+ # Method 3: Custom header variations
78
+ custom_headers = [
79
+ 'HTTP_X_API_TOKEN',
80
+ 'HTTP_APIKEY',
81
+ 'HTTP_API_TOKEN',
82
+ ]
83
+
84
+ for header in custom_headers:
85
+ value = request.META.get(header)
86
+ if value:
87
+ return value.strip()
88
+
89
+ # Method 4: Query parameter (less secure, mainly for testing)
90
+ if getattr(settings, 'PAYMENTS_ALLOW_API_KEY_IN_QUERY', False):
91
+ query_key = request.GET.get('api_key') or request.GET.get('apikey')
92
+ if query_key:
93
+ return query_key.strip()
94
+
95
+ return None
96
+
97
+
98
+ def is_exempt_path(request: HttpRequest, exempt_paths: Optional[List[str]] = None) -> bool:
99
+ """
100
+ Check if request path is exempt from API key requirements.
101
+
102
+ Args:
103
+ request: Django HTTP request
104
+ exempt_paths: List of exempt path prefixes (defaults to settings)
105
+ """
106
+
107
+ if exempt_paths is None:
108
+ exempt_paths = getattr(settings, 'PAYMENTS_EXEMPT_PATHS', [
109
+ '/admin/',
110
+ '/cfg/',
111
+ '/api/v1/api-key/validate/',
112
+ ])
113
+
114
+ path = request.path
115
+ return any(path.startswith(exempt) for exempt in exempt_paths)
116
+
117
+
118
+ def get_request_metadata(request: HttpRequest) -> dict:
119
+ """
120
+ Extract useful metadata from request for logging and analytics.
121
+ """
122
+
123
+ return {
124
+ 'method': request.method,
125
+ 'path': request.path,
126
+ 'query_string': request.META.get('QUERY_STRING', ''),
127
+ 'user_agent': request.META.get('HTTP_USER_AGENT', ''),
128
+ 'referer': request.META.get('HTTP_REFERER', ''),
129
+ 'ip_address': get_client_ip(request),
130
+ 'content_type': request.META.get('CONTENT_TYPE', ''),
131
+ 'content_length': request.META.get('CONTENT_LENGTH', 0),
132
+ 'host': request.META.get('HTTP_HOST', ''),
133
+ 'scheme': request.scheme,
134
+ 'is_secure': request.is_secure(),
135
+ }
136
+
137
+
138
+ def should_track_request_body(request: HttpRequest, max_size: int = 10000) -> bool:
139
+ """
140
+ Determine if request body should be tracked for analytics.
141
+
142
+ Args:
143
+ request: Django HTTP request
144
+ max_size: Maximum body size to track (bytes)
145
+ """
146
+
147
+ # Check content length
148
+ content_length = request.META.get('CONTENT_LENGTH')
149
+ if content_length and int(content_length) > max_size:
150
+ return False
151
+
152
+ # Don't track file uploads
153
+ content_type = request.META.get('CONTENT_TYPE', '')
154
+ if 'multipart/form-data' in content_type:
155
+ return False
156
+
157
+ # Don't track binary content
158
+ if 'application/octet-stream' in content_type:
159
+ return False
160
+
161
+ # Don't track sensitive endpoints
162
+ sensitive_paths = getattr(settings, 'PAYMENTS_SENSITIVE_PATHS', [
163
+ '/api/v1/api-key/',
164
+ '/api/v1/payment/',
165
+ '/api/v1/subscription/',
166
+ ])
167
+
168
+ path = request.path
169
+ if any(path.startswith(sensitive) for sensitive in sensitive_paths):
170
+ return False
171
+
172
+ return True
173
+
174
+
175
+ def should_track_response_body(response, max_size: int = 10000) -> bool:
176
+ """
177
+ Determine if response body should be tracked for analytics.
178
+
179
+ Args:
180
+ response: Django HTTP response
181
+ max_size: Maximum body size to track (bytes)
182
+ """
183
+
184
+ # Don't track large responses
185
+ if hasattr(response, 'content') and len(response.content) > max_size:
186
+ return False
187
+
188
+ # Only track successful JSON responses
189
+ if not (200 <= response.status_code < 300):
190
+ return False
191
+
192
+ # Check content type
193
+ content_type = response.get('Content-Type', '')
194
+ if 'application/json' not in content_type:
195
+ return False
196
+
197
+ return True
198
+
199
+
200
+ def format_error_response(error_code: str,
201
+ message: str,
202
+ status_code: int = 400,
203
+ additional_data: Optional[dict] = None) -> dict:
204
+ """
205
+ Format standardized error response for middleware.
206
+
207
+ Args:
208
+ error_code: Machine-readable error code
209
+ message: Human-readable error message
210
+ status_code: HTTP status code
211
+ additional_data: Additional error data
212
+ """
213
+
214
+ from django.utils import timezone
215
+
216
+ error_response = {
217
+ 'error': {
218
+ 'code': error_code,
219
+ 'message': message,
220
+ 'status_code': status_code,
221
+ 'timestamp': timezone.now().isoformat(),
222
+ }
223
+ }
224
+
225
+ if additional_data:
226
+ error_response['error'].update(additional_data)
227
+
228
+ return error_response
@@ -0,0 +1,94 @@
1
+ """
2
+ Validation utilities for payments module.
3
+
4
+ Basic validation functions for API keys and subscription access.
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional, Dict, Any
9
+ from django.contrib.auth import get_user_model
10
+ from django.utils import timezone
11
+
12
+ from ..models import APIKey, Subscription
13
+
14
+ User = get_user_model()
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def validate_api_key(api_key: str) -> bool:
19
+ """
20
+ Validate API key.
21
+
22
+ Args:
23
+ api_key: API key to validate
24
+
25
+ Returns:
26
+ True if valid, False otherwise
27
+ """
28
+ try:
29
+ key = APIKey.objects.select_related('user').get(
30
+ key_value=api_key,
31
+ is_active=True
32
+ )
33
+
34
+ # Check if key is expired
35
+ if key.expires_at and key.expires_at < timezone.now():
36
+ return False
37
+
38
+ return True
39
+
40
+ except APIKey.DoesNotExist:
41
+ return False
42
+ except Exception as e:
43
+ logger.error(f"Error validating API key: {e}")
44
+ return False
45
+
46
+
47
+ def check_subscription_access(user_id: int, endpoint_group: str) -> Dict[str, Any]:
48
+ """
49
+ Check subscription access for user and endpoint group.
50
+
51
+ Args:
52
+ user_id: User ID
53
+ endpoint_group: Endpoint group name
54
+
55
+ Returns:
56
+ Access check result dictionary
57
+ """
58
+ try:
59
+ subscription = Subscription.objects.select_related('endpoint_group').get(
60
+ user_id=user_id,
61
+ endpoint_group__name=endpoint_group,
62
+ status='active',
63
+ expires_at__gt=timezone.now()
64
+ )
65
+
66
+ # Check usage limits
67
+ usage_percentage = (subscription.current_usage / subscription.monthly_limit) * 100
68
+ remaining_requests = subscription.monthly_limit - subscription.current_usage
69
+
70
+ return {
71
+ 'allowed': remaining_requests > 0,
72
+ 'subscription_id': str(subscription.id),
73
+ 'remaining_requests': remaining_requests,
74
+ 'usage_percentage': usage_percentage,
75
+ 'reason': 'Active subscription' if remaining_requests > 0 else 'Usage limit exceeded'
76
+ }
77
+
78
+ except Subscription.DoesNotExist:
79
+ return {
80
+ 'allowed': False,
81
+ 'reason': 'No active subscription found',
82
+ 'subscription_id': None,
83
+ 'remaining_requests': 0,
84
+ 'usage_percentage': 0
85
+ }
86
+ except Exception as e:
87
+ logger.error(f"Error checking subscription access: {e}")
88
+ return {
89
+ 'allowed': False,
90
+ 'reason': f'Access check failed: {str(e)}',
91
+ 'subscription_id': None,
92
+ 'remaining_requests': 0,
93
+ 'usage_percentage': 0
94
+ }
@@ -46,8 +46,46 @@ class UserPaymentViewSet(viewsets.ModelViewSet):
46
46
  def check_status(self, request, user_pk=None, pk=None):
47
47
  """Check payment status via provider API."""
48
48
  payment = self.get_object()
49
- # TODO: Implement provider status check
50
- return Response({'status': payment.status})
49
+
50
+ # Import PaymentService to check status with provider
51
+ from ..services.core.payment_service import PaymentService
52
+
53
+ try:
54
+ payment_service = PaymentService()
55
+ status_result = payment_service.get_payment_status(str(payment.id))
56
+
57
+ if status_result.success:
58
+ # Update local payment status if it changed
59
+ if payment.status != status_result.status:
60
+ payment.status = status_result.status
61
+ payment.save(update_fields=['status', 'updated_at'])
62
+
63
+ return Response({
64
+ 'payment_id': str(payment.id),
65
+ 'status': status_result.status,
66
+ 'provider_status': status_result.provider_status,
67
+ 'updated': payment.status != status_result.status
68
+ })
69
+ else:
70
+ return Response({
71
+ 'payment_id': str(payment.id),
72
+ 'status': payment.status,
73
+ 'error': status_result.error_message,
74
+ 'provider_check_failed': True
75
+ }, status=status.HTTP_400_BAD_REQUEST)
76
+
77
+ except Exception as e:
78
+ # Log error but don't fail completely
79
+ import logging
80
+ logger = logging.getLogger(__name__)
81
+ logger.error(f"Payment status check failed for {payment.id}: {e}")
82
+
83
+ return Response({
84
+ 'payment_id': str(payment.id),
85
+ 'status': payment.status,
86
+ 'error': 'Status check temporarily unavailable',
87
+ 'provider_check_failed': True
88
+ })
51
89
 
52
90
  @action(detail=False, methods=['get'])
53
91
  def summary(self, request, user_pk=None):
@@ -0,0 +1,266 @@
1
+ """
2
+ Webhook processing views with signature validation.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from typing import Dict, Any
8
+
9
+ from django.http import JsonResponse, HttpResponse
10
+ from django.views.decorators.csrf import csrf_exempt
11
+ from django.views.decorators.http import require_http_methods
12
+ from django.utils.decorators import method_decorator
13
+ from rest_framework.decorators import api_view, permission_classes
14
+ from rest_framework.permissions import AllowAny
15
+ from rest_framework.response import Response
16
+ from rest_framework import status
17
+
18
+ from ..services.core.payment_service import PaymentService
19
+ from ..tasks.webhook_processing import process_webhook_with_fallback
20
+ from ..services.security.webhook_validator import webhook_validator
21
+ from ..services.security.error_handler import error_handler, SecurityError, ValidationError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @csrf_exempt
27
+ @require_http_methods(["POST"])
28
+ def webhook_handler(request, provider: str):
29
+ """
30
+ Main webhook handler with signature validation.
31
+
32
+ Accepts webhooks from payment providers and processes them
33
+ with proper validation and fallback mechanisms.
34
+ """
35
+ try:
36
+ # Parse webhook data
37
+ webhook_data = json.loads(request.body.decode('utf-8'))
38
+
39
+ # Extract request headers
40
+ request_headers = {
41
+ key: value for key, value in request.META.items()
42
+ if key.startswith('HTTP_')
43
+ }
44
+
45
+ # Generate idempotency key for deduplication
46
+ idempotency_key = _generate_idempotency_key(provider, webhook_data, request_headers)
47
+
48
+ logger.info(f"📥 Received webhook from {provider}, key: {idempotency_key}")
49
+
50
+ # Validate webhook with enhanced security
51
+ is_valid, validation_error = webhook_validator.validate_webhook(
52
+ provider=provider,
53
+ webhook_data=webhook_data,
54
+ request_headers=request_headers,
55
+ raw_body=request.body
56
+ )
57
+
58
+ if not is_valid:
59
+ security_error = SecurityError(
60
+ f"Webhook validation failed: {validation_error}",
61
+ details={'provider': provider, 'validation_error': validation_error}
62
+ )
63
+ error_handler.handle_error(security_error, {
64
+ 'provider': provider,
65
+ 'webhook_data_keys': list(webhook_data.keys()),
66
+ 'headers_count': len(request_headers)
67
+ }, request)
68
+
69
+ return JsonResponse(
70
+ {'error': 'Webhook validation failed', 'code': 'INVALID_WEBHOOK'},
71
+ status=403
72
+ )
73
+
74
+ # Process webhook (async with fallback to sync)
75
+ result = process_webhook_with_fallback(
76
+ provider=provider,
77
+ webhook_data=webhook_data,
78
+ idempotency_key=idempotency_key,
79
+ request_headers=request_headers
80
+ )
81
+
82
+ if result.get('success'):
83
+ logger.info(f"✅ Webhook processed successfully: {idempotency_key}")
84
+ return JsonResponse({
85
+ 'status': 'success',
86
+ 'idempotency_key': idempotency_key,
87
+ 'processing_mode': result.get('mode', 'unknown')
88
+ })
89
+ else:
90
+ logger.error(f"❌ Webhook processing failed: {result.get('error')}")
91
+ return JsonResponse({
92
+ 'status': 'error',
93
+ 'error': result.get('error', 'Processing failed'),
94
+ 'idempotency_key': idempotency_key
95
+ }, status=400)
96
+
97
+ except json.JSONDecodeError as e:
98
+ validation_error = ValidationError(
99
+ f"Invalid JSON in webhook from {provider}",
100
+ details={'provider': provider, 'json_error': str(e)}
101
+ )
102
+ error_result = error_handler.handle_error(validation_error, {
103
+ 'provider': provider,
104
+ 'raw_body_length': len(request.body) if request.body else 0
105
+ }, request)
106
+
107
+ return JsonResponse({
108
+ 'error': 'Invalid JSON',
109
+ 'code': validation_error.error_code
110
+ }, status=400)
111
+
112
+ except Exception as e:
113
+ # Handle unexpected errors with centralized error handler
114
+ error_result = error_handler.handle_error(e, {
115
+ 'provider': provider,
116
+ 'operation': 'webhook_processing',
117
+ 'webhook_data_available': 'webhook_data' in locals()
118
+ }, request)
119
+
120
+ return JsonResponse({
121
+ 'error': 'Internal server error',
122
+ 'code': error_result.error.error_code
123
+ }, status=500)
124
+
125
+
126
+ @api_view(['POST'])
127
+ @permission_classes([AllowAny])
128
+ def webhook_test(request):
129
+ """
130
+ Test webhook endpoint for development.
131
+
132
+ Allows testing webhook processing without requiring
133
+ actual payment provider signatures.
134
+ """
135
+ try:
136
+ provider = request.data.get('provider', 'test')
137
+ webhook_data = request.data.get('webhook_data', {})
138
+
139
+ # Add test marker
140
+ webhook_data['_test_webhook'] = True
141
+
142
+ # Generate test idempotency key
143
+ import uuid
144
+ idempotency_key = f"test_{uuid.uuid4().hex[:8]}"
145
+
146
+ logger.info(f"🧪 Processing test webhook: {provider}")
147
+
148
+ # Process with PaymentService directly (sync)
149
+ payment_service = PaymentService()
150
+ result = payment_service.process_webhook(
151
+ provider=provider,
152
+ webhook_data=webhook_data,
153
+ request_headers={'HTTP_X_TEST': 'true'}
154
+ )
155
+
156
+ return Response({
157
+ 'status': 'success',
158
+ 'test_mode': True,
159
+ 'provider': provider,
160
+ 'idempotency_key': idempotency_key,
161
+ 'result': result.dict() if hasattr(result, 'dict') else result
162
+ })
163
+
164
+ except Exception as e:
165
+ logger.error(f"❌ Test webhook error: {e}")
166
+ return Response({
167
+ 'status': 'error',
168
+ 'error': str(e),
169
+ 'test_mode': True
170
+ }, status=status.HTTP_400_BAD_REQUEST)
171
+
172
+
173
+ def _validate_webhook_signature(provider: str, webhook_data: Dict[str, Any],
174
+ request_headers: Dict[str, str]) -> bool:
175
+ """
176
+ Validate webhook signature based on provider.
177
+
178
+ Each provider has different signature validation methods.
179
+ """
180
+ try:
181
+ if provider == 'nowpayments':
182
+ return _validate_nowpayments_signature(webhook_data, request_headers)
183
+ elif provider == 'cryptapi':
184
+ return _validate_cryptapi_signature(webhook_data, request_headers)
185
+ elif provider == 'test':
186
+ return True # Allow test webhooks
187
+ else:
188
+ logger.warning(f"Unknown provider for signature validation: {provider}")
189
+ return False
190
+
191
+ except Exception as e:
192
+ logger.error(f"Signature validation error for {provider}: {e}")
193
+ return False
194
+
195
+
196
+ def _validate_nowpayments_signature(webhook_data: Dict[str, Any],
197
+ request_headers: Dict[str, str]) -> bool:
198
+ """Validate NowPayments webhook signature."""
199
+ import hmac
200
+ import hashlib
201
+ from ..utils.config_utils import get_payments_config
202
+
203
+ # Get IPN secret from config
204
+ config = get_payments_config()
205
+ if not config or not hasattr(config, 'providers') or 'nowpayments' not in config.providers:
206
+ logger.warning("NowPayments IPN secret not configured, skipping validation")
207
+ return True # Allow if not configured (development mode)
208
+
209
+ nowpayments_config = config.providers['nowpayments']
210
+ ipn_secret = getattr(nowpayments_config, 'ipn_secret', None)
211
+
212
+ if not ipn_secret:
213
+ logger.warning("NowPayments IPN secret not configured, skipping validation")
214
+ return True
215
+
216
+ # Get signature from headers
217
+ signature = request_headers.get('HTTP_X_NOWPAYMENTS_SIG')
218
+ if not signature:
219
+ logger.warning("No NowPayments signature found in headers")
220
+ return False
221
+
222
+ # Calculate expected signature
223
+ payload = json.dumps(webhook_data, separators=(',', ':'), sort_keys=True)
224
+ expected_signature = hmac.new(
225
+ ipn_secret.encode(),
226
+ payload.encode(),
227
+ hashlib.sha512
228
+ ).hexdigest()
229
+
230
+ return hmac.compare_digest(signature, expected_signature)
231
+
232
+
233
+ def _validate_cryptapi_signature(webhook_data: Dict[str, Any],
234
+ request_headers: Dict[str, str]) -> bool:
235
+ """Validate CryptAPI webhook signature."""
236
+ # CryptAPI uses different validation method
237
+ # For now, implement basic validation
238
+
239
+ # Check if required fields are present
240
+ required_fields = ['address_in', 'address_out', 'txid_in', 'value_coin', 'coin', 'confirmations']
241
+ for field in required_fields:
242
+ if field not in webhook_data:
243
+ logger.warning(f"Missing required field in CryptAPI webhook: {field}")
244
+ return False
245
+
246
+ return True
247
+
248
+
249
+ def _generate_idempotency_key(provider: str, webhook_data: Dict[str, Any],
250
+ request_headers: Dict[str, str]) -> str:
251
+ """Generate idempotency key for webhook deduplication."""
252
+ import hashlib
253
+
254
+ # Use provider + payment ID + timestamp for uniqueness
255
+ payment_id = (
256
+ webhook_data.get('payment_id') or
257
+ webhook_data.get('order_id') or
258
+ webhook_data.get('id') or
259
+ 'unknown'
260
+ )
261
+
262
+ timestamp = webhook_data.get('created_at') or webhook_data.get('timestamp')
263
+
264
+ # Create hash from key components
265
+ key_data = f"{provider}:{payment_id}:{timestamp}"
266
+ return hashlib.md5(key_data.encode()).hexdigest()[:16]
@@ -0,0 +1,65 @@
1
+ """
2
+ Payment system ViewSets router for easy integration.
3
+ """
4
+
5
+ from rest_framework.routers import DefaultRouter
6
+ from rest_framework_nested import routers
7
+
8
+ from .views import (
9
+ # Balance ViewSets
10
+ UserBalanceViewSet, TransactionViewSet,
11
+
12
+ # Payment ViewSets
13
+ UserPaymentViewSet, UniversalPaymentViewSet,
14
+
15
+ # Subscription ViewSets
16
+ UserSubscriptionViewSet, SubscriptionViewSet, EndpointGroupViewSet,
17
+
18
+ # API Key ViewSets
19
+ UserAPIKeyViewSet, APIKeyViewSet,
20
+
21
+ # Currency ViewSets
22
+ CurrencyViewSet, CurrencyNetworkViewSet,
23
+
24
+ # Tariff ViewSets
25
+ TariffViewSet, TariffEndpointGroupViewSet,
26
+ )
27
+
28
+
29
+ class PaymentSystemRouter:
30
+ """Universal router with all payment endpoints"""
31
+
32
+ def __init__(self):
33
+ self.router = DefaultRouter()
34
+ self._setup_main_routes()
35
+
36
+ def _setup_main_routes(self):
37
+ """Setup main resource routes"""
38
+ # Core payment resources
39
+ self.router.register(r'payments', UniversalPaymentViewSet, basename='payment')
40
+ self.router.register(r'balances', UserBalanceViewSet, basename='balance')
41
+ self.router.register(r'transactions', TransactionViewSet, basename='transaction')
42
+
43
+ # Subscription management
44
+ self.router.register(r'subscriptions', SubscriptionViewSet, basename='subscription')
45
+ self.router.register(r'endpoint-groups', EndpointGroupViewSet, basename='endpoint-group')
46
+
47
+ # API key management
48
+ self.router.register(r'api-keys', APIKeyViewSet, basename='api-key')
49
+
50
+ # Currency and pricing
51
+ self.router.register(r'currencies', CurrencyViewSet, basename='currency')
52
+ self.router.register(r'currency-networks', CurrencyNetworkViewSet, basename='currency-network')
53
+ self.router.register(r'tariffs', TariffViewSet, basename='tariff')
54
+ self.router.register(r'tariff-groups', TariffEndpointGroupViewSet, basename='tariff-group')
55
+
56
+ @property
57
+ def urls(self):
58
+ """Get all URLs"""
59
+ return self.router.urls
60
+
61
+
62
+ # Create default router instance
63
+ payment_router = PaymentSystemRouter()
64
+
65
+ __all__ = ['PaymentSystemRouter', 'payment_router']