django-cfg 1.3.1__py3-none-any.whl → 1.3.5__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 (115) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/admin_interface/old/payments/base.html +175 -0
  3. django_cfg/apps/payments/admin_interface/old/payments/components/dev_tool_card.html +125 -0
  4. django_cfg/apps/payments/admin_interface/old/payments/components/ngrok_status_card.html +113 -0
  5. django_cfg/apps/payments/admin_interface/old/payments/components/status_card.html +35 -0
  6. django_cfg/apps/payments/admin_interface/old/payments/payment_dashboard.html +309 -0
  7. django_cfg/apps/payments/admin_interface/old/payments/payment_form.html +303 -0
  8. django_cfg/apps/payments/admin_interface/old/payments/payment_list.html +382 -0
  9. django_cfg/apps/payments/admin_interface/old/payments/webhook_dashboard.html +518 -0
  10. django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/components.css +248 -9
  11. django_cfg/apps/payments/admin_interface/old/static/payments/js/ngrok-status.js +163 -0
  12. django_cfg/apps/payments/admin_interface/serializers/__init__.py +39 -0
  13. django_cfg/apps/payments/admin_interface/serializers/payment_serializers.py +149 -0
  14. django_cfg/apps/payments/admin_interface/serializers/webhook_serializers.py +114 -0
  15. django_cfg/apps/payments/admin_interface/templates/payments/base.html +55 -90
  16. django_cfg/apps/payments/admin_interface/templates/payments/components/dialog.html +81 -0
  17. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_help_dialog.html +112 -0
  18. django_cfg/apps/payments/admin_interface/templates/payments/components/ngrok_status.html +175 -0
  19. django_cfg/apps/payments/admin_interface/templates/payments/components/status_card.html +21 -17
  20. django_cfg/apps/payments/admin_interface/templates/payments/payment_dashboard.html +123 -250
  21. django_cfg/apps/payments/admin_interface/templates/payments/payment_form.html +170 -269
  22. django_cfg/apps/payments/admin_interface/templates/payments/payment_list.html +152 -355
  23. django_cfg/apps/payments/admin_interface/templates/payments/webhook_dashboard.html +202 -551
  24. django_cfg/apps/payments/admin_interface/views/__init__.py +25 -14
  25. django_cfg/apps/payments/admin_interface/views/api/__init__.py +20 -0
  26. django_cfg/apps/payments/admin_interface/views/api/payments.py +191 -0
  27. django_cfg/apps/payments/admin_interface/views/api/stats.py +206 -0
  28. django_cfg/apps/payments/admin_interface/views/api/users.py +60 -0
  29. django_cfg/apps/payments/admin_interface/views/api/webhook_admin.py +257 -0
  30. django_cfg/apps/payments/admin_interface/views/api/webhook_public.py +70 -0
  31. django_cfg/apps/payments/admin_interface/views/base.py +114 -0
  32. django_cfg/apps/payments/admin_interface/views/dashboard.py +60 -0
  33. django_cfg/apps/payments/admin_interface/views/forms.py +94 -0
  34. django_cfg/apps/payments/config/helpers.py +2 -2
  35. django_cfg/apps/payments/management/commands/cleanup_expired_data.py +429 -0
  36. django_cfg/apps/payments/management/commands/currency_stats.py +443 -0
  37. django_cfg/apps/payments/management/commands/manage_currencies.py +9 -20
  38. django_cfg/apps/payments/management/commands/manage_providers.py +5 -5
  39. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  40. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  41. django_cfg/apps/payments/middleware/api_access.py +35 -34
  42. django_cfg/apps/payments/migrations/0001_initial.py +1 -1
  43. django_cfg/apps/payments/models/balance.py +5 -2
  44. django_cfg/apps/payments/models/managers/api_key_managers.py +6 -2
  45. django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
  46. django_cfg/apps/payments/models/managers/payment_managers.py +5 -0
  47. django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
  48. django_cfg/apps/payments/models/subscriptions.py +0 -24
  49. django_cfg/apps/payments/services/cache/__init__.py +1 -1
  50. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  51. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  52. django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
  53. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  54. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  55. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  56. django_cfg/apps/payments/services/core/balance_service.py +13 -2
  57. django_cfg/apps/payments/services/core/payment_service.py +49 -22
  58. django_cfg/apps/payments/services/integrations/ngrok_service.py +3 -3
  59. django_cfg/apps/payments/services/providers/registry.py +20 -0
  60. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  61. django_cfg/apps/payments/signals/balance_signals.py +8 -5
  62. django_cfg/apps/payments/static/payments/js/api-client.js +385 -0
  63. django_cfg/apps/payments/static/payments/js/ngrok-status.js +58 -0
  64. django_cfg/apps/payments/static/payments/js/payment-dashboard.js +50 -0
  65. django_cfg/apps/payments/static/payments/js/payment-form.js +175 -0
  66. django_cfg/apps/payments/static/payments/js/payment-list.js +95 -0
  67. django_cfg/apps/payments/static/payments/js/webhook-dashboard.js +154 -0
  68. django_cfg/apps/payments/urls.py +4 -0
  69. django_cfg/apps/payments/urls_admin.py +37 -18
  70. django_cfg/apps/payments/views/api/api_keys.py +14 -0
  71. django_cfg/apps/payments/views/api/base.py +1 -0
  72. django_cfg/apps/payments/views/api/currencies.py +2 -2
  73. django_cfg/apps/payments/views/api/payments.py +11 -5
  74. django_cfg/apps/payments/views/api/subscriptions.py +36 -31
  75. django_cfg/apps/payments/views/overview/__init__.py +40 -0
  76. django_cfg/apps/payments/views/overview/serializers.py +205 -0
  77. django_cfg/apps/payments/views/overview/services.py +439 -0
  78. django_cfg/apps/payments/views/overview/urls.py +27 -0
  79. django_cfg/apps/payments/views/overview/views.py +231 -0
  80. django_cfg/apps/payments/views/serializers/api_keys.py +20 -6
  81. django_cfg/apps/payments/views/serializers/balances.py +5 -8
  82. django_cfg/apps/payments/views/serializers/currencies.py +2 -6
  83. django_cfg/apps/payments/views/serializers/payments.py +37 -32
  84. django_cfg/apps/payments/views/serializers/subscriptions.py +4 -26
  85. django_cfg/apps/urls.py +2 -1
  86. django_cfg/core/config.py +25 -15
  87. django_cfg/core/generation.py +12 -12
  88. django_cfg/core/integration/display/startup.py +1 -1
  89. django_cfg/core/validation.py +4 -4
  90. django_cfg/management/commands/show_config.py +2 -2
  91. django_cfg/management/commands/tree.py +1 -3
  92. django_cfg/middleware/__init__.py +2 -0
  93. django_cfg/middleware/static_nocache.py +55 -0
  94. django_cfg/models/payments.py +13 -15
  95. django_cfg/models/security.py +15 -0
  96. django_cfg/modules/django_ngrok.py +6 -0
  97. django_cfg/modules/django_unfold/dashboard.py +1 -3
  98. django_cfg/utils/smart_defaults.py +51 -5
  99. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/METADATA +1 -1
  100. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/RECORD +111 -69
  101. django_cfg/apps/payments/admin_interface/templates/payments/components/dev_tool_card.html +0 -38
  102. django_cfg/apps/payments/admin_interface/views/payment_views.py +0 -259
  103. django_cfg/apps/payments/admin_interface/views/webhook_dashboard.py +0 -37
  104. django_cfg/apps/payments/services/cache/cache_service.py +0 -235
  105. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/loading_spinner.html +0 -0
  106. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/notification.html +0 -0
  107. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/components/provider_card.html +0 -0
  108. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/currency_converter.html +0 -0
  109. /django_cfg/apps/payments/admin_interface/{templates → old}/payments/payment_status.html +0 -0
  110. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/css/dashboard.css +0 -0
  111. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/components.js +0 -0
  112. /django_cfg/apps/payments/{static → admin_interface/old/static}/payments/js/utils.js +0 -0
  113. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/WHEEL +0 -0
  114. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/entry_points.txt +0 -0
  115. {django_cfg-1.3.1.dist-info → django_cfg-1.3.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,434 @@
1
+ """
2
+ Test Providers Management Command for Universal Payment System v2.0.
3
+
4
+ Test payment provider connections, configurations, and functionality.
5
+ """
6
+
7
+ from typing import List, Dict, Any, Optional
8
+ from decimal import Decimal
9
+ import time
10
+
11
+ from django.core.management.base import BaseCommand, CommandError
12
+ from django.utils import timezone
13
+
14
+ from django_cfg.modules.django_logger import get_logger
15
+ from django_cfg.apps.payments.services.providers.registry import get_provider_registry
16
+ from django_cfg.apps.payments.models import Currency, Network
17
+ from django_cfg.apps.payments.services.types.requests import PaymentCreateRequest
18
+
19
+ logger = get_logger("test_providers")
20
+
21
+
22
+ class Command(BaseCommand):
23
+ """
24
+ Test payment provider connections and functionality.
25
+
26
+ Features:
27
+ - Test provider connectivity
28
+ - Validate configurations
29
+ - Test currency support
30
+ - Check API responses
31
+ - Performance testing
32
+ """
33
+
34
+ help = 'Test payment provider connections and functionality'
35
+
36
+ def add_arguments(self, parser):
37
+ """Add command line arguments."""
38
+ parser.add_argument(
39
+ '--provider',
40
+ type=str,
41
+ help='Test specific provider only'
42
+ )
43
+
44
+ parser.add_argument(
45
+ '--test-type',
46
+ choices=['connectivity', 'currencies', 'payment', 'all'],
47
+ default='all',
48
+ help='Type of test to run (default: all)'
49
+ )
50
+
51
+ parser.add_argument(
52
+ '--timeout',
53
+ type=int,
54
+ default=30,
55
+ help='Request timeout in seconds (default: 30)'
56
+ )
57
+
58
+ parser.add_argument(
59
+ '--verbose',
60
+ action='store_true',
61
+ help='Show detailed test information'
62
+ )
63
+
64
+ parser.add_argument(
65
+ '--create-test-payment',
66
+ action='store_true',
67
+ help='Create actual test payment (use with caution!)'
68
+ )
69
+
70
+ parser.add_argument(
71
+ '--test-amount',
72
+ type=float,
73
+ default=1.0,
74
+ help='Test payment amount in USD (default: 1.0)'
75
+ )
76
+
77
+ def handle(self, *args, **options):
78
+ """Execute the command."""
79
+ try:
80
+ self.options = options
81
+ self.verbose = options['verbose']
82
+
83
+ self.show_header()
84
+
85
+ # Get provider registry
86
+ self.provider_registry = get_provider_registry()
87
+
88
+ # Initialize test results
89
+ self.test_results = {}
90
+
91
+ # Get providers to test
92
+ if options['provider']:
93
+ providers = [options['provider']]
94
+ else:
95
+ providers = self.provider_registry.get_available_providers()
96
+
97
+ if not providers:
98
+ self.stdout.write(self.style.WARNING("No providers available to test"))
99
+ return
100
+
101
+ # Run tests
102
+ for provider_name in providers:
103
+ self.test_provider(provider_name)
104
+
105
+ self.show_summary()
106
+
107
+ except Exception as e:
108
+ logger.error(f"Test providers command failed: {e}")
109
+ raise CommandError(f"Failed to test providers: {e}")
110
+
111
+ def show_header(self):
112
+ """Display command header."""
113
+ self.stdout.write(
114
+ self.style.SUCCESS("=" * 60)
115
+ )
116
+ self.stdout.write(
117
+ self.style.SUCCESS("🧪 PAYMENT PROVIDER TESTING")
118
+ )
119
+ self.stdout.write(
120
+ self.style.SUCCESS("=" * 60)
121
+ )
122
+ self.stdout.write(f"Started: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
123
+ self.stdout.write("")
124
+
125
+ def test_provider(self, provider_name: str):
126
+ """Test a specific provider."""
127
+ self.stdout.write(self.style.SUCCESS(f"🔍 TESTING PROVIDER: {provider_name.upper()}"))
128
+ self.stdout.write("-" * 50)
129
+
130
+ # Initialize provider results
131
+ self.test_results[provider_name] = {
132
+ 'connectivity': False,
133
+ 'currencies': False,
134
+ 'payment': False,
135
+ 'errors': [],
136
+ 'warnings': [],
137
+ 'performance': {}
138
+ }
139
+
140
+ try:
141
+ # Get provider instance
142
+ provider = self.provider_registry.get_provider(provider_name)
143
+ if not provider:
144
+ self.test_results[provider_name]['errors'].append("Provider not available")
145
+ self.stdout.write(self.style.ERROR(f"❌ Provider {provider_name} not available"))
146
+ return
147
+
148
+ # Run selected tests
149
+ test_type = self.options['test_type']
150
+
151
+ if test_type in ['connectivity', 'all']:
152
+ self.test_connectivity(provider_name, provider)
153
+
154
+ if test_type in ['currencies', 'all']:
155
+ self.test_currencies(provider_name, provider)
156
+
157
+ if test_type in ['payment', 'all'] and self.options['create_test_payment']:
158
+ self.test_payment_creation(provider_name, provider)
159
+
160
+ except Exception as e:
161
+ error_msg = f"Provider test failed: {e}"
162
+ self.test_results[provider_name]['errors'].append(error_msg)
163
+ logger.error(f"Error testing provider {provider_name}: {e}")
164
+ self.stdout.write(self.style.ERROR(f"❌ {error_msg}"))
165
+
166
+ self.stdout.write("")
167
+
168
+ def test_connectivity(self, provider_name: str, provider):
169
+ """Test provider connectivity."""
170
+ self.stdout.write(" 🌐 Testing connectivity...")
171
+
172
+ try:
173
+ start_time = time.time()
174
+
175
+ # Test basic connectivity (health check or similar)
176
+ if hasattr(provider, 'health_check'):
177
+ result = provider.health_check()
178
+ success = result.success if hasattr(result, 'success') else True
179
+ elif hasattr(provider, 'get_supported_currencies'):
180
+ # Fallback: try to get currencies as connectivity test
181
+ result = provider.get_supported_currencies()
182
+ success = len(result) > 0 if isinstance(result, list) else bool(result)
183
+ else:
184
+ # Last resort: assume connectivity is OK if provider exists
185
+ success = True
186
+
187
+ end_time = time.time()
188
+ response_time = (end_time - start_time) * 1000 # Convert to milliseconds
189
+
190
+ self.test_results[provider_name]['performance']['connectivity_ms'] = response_time
191
+
192
+ if success:
193
+ self.test_results[provider_name]['connectivity'] = True
194
+ self.stdout.write(f" ✅ Connectivity OK ({response_time:.0f}ms)")
195
+ else:
196
+ self.test_results[provider_name]['errors'].append("Connectivity test failed")
197
+ self.stdout.write(f" ❌ Connectivity failed")
198
+
199
+ except Exception as e:
200
+ error_msg = f"Connectivity test error: {e}"
201
+ self.test_results[provider_name]['errors'].append(error_msg)
202
+ self.stdout.write(f" ❌ Connectivity error: {e}")
203
+
204
+ def test_currencies(self, provider_name: str, provider):
205
+ """Test provider currency support."""
206
+ self.stdout.write(" 💰 Testing currency support...")
207
+
208
+ try:
209
+ start_time = time.time()
210
+
211
+ # Get supported currencies from provider
212
+ if hasattr(provider, 'get_supported_currencies'):
213
+ supported_currencies = provider.get_supported_currencies()
214
+ else:
215
+ self.test_results[provider_name]['warnings'].append("Provider doesn't support currency listing")
216
+ self.stdout.write(" ⚠️ Provider doesn't support currency listing")
217
+ return
218
+
219
+ end_time = time.time()
220
+ response_time = (end_time - start_time) * 1000
221
+
222
+ self.test_results[provider_name]['performance']['currencies_ms'] = response_time
223
+
224
+ if isinstance(supported_currencies, list) and len(supported_currencies) > 0:
225
+ self.test_results[provider_name]['currencies'] = True
226
+ currency_count = len(supported_currencies)
227
+ self.stdout.write(f" ✅ {currency_count} currencies supported ({response_time:.0f}ms)")
228
+
229
+ if self.verbose:
230
+ # Show first few currencies
231
+ sample_currencies = supported_currencies[:5]
232
+ currency_codes = [c.get('code', str(c)) for c in sample_currencies]
233
+ self.stdout.write(f" Sample: {', '.join(currency_codes)}")
234
+
235
+ # Check if our database currencies are supported
236
+ our_currencies = Currency.objects.filter(is_active=True)[:10]
237
+ supported_codes = []
238
+
239
+ if isinstance(supported_currencies[0], dict):
240
+ supported_codes = [c.get('code', '').upper() for c in supported_currencies]
241
+ else:
242
+ supported_codes = [str(c).upper() for c in supported_currencies]
243
+
244
+ matching_count = 0
245
+ for currency in our_currencies:
246
+ if currency.code.upper() in supported_codes:
247
+ matching_count += 1
248
+
249
+ if matching_count > 0:
250
+ self.stdout.write(f" ✅ {matching_count}/{our_currencies.count()} of our currencies supported")
251
+ else:
252
+ self.test_results[provider_name]['warnings'].append("No matching currencies found")
253
+ self.stdout.write(" ⚠️ No matching currencies found")
254
+
255
+ else:
256
+ self.test_results[provider_name]['errors'].append("No currencies returned")
257
+ self.stdout.write(" ❌ No currencies returned")
258
+
259
+ except Exception as e:
260
+ error_msg = f"Currency test error: {e}"
261
+ self.test_results[provider_name]['errors'].append(error_msg)
262
+ self.stdout.write(f" ❌ Currency test error: {e}")
263
+
264
+ def test_payment_creation(self, provider_name: str, provider):
265
+ """Test payment creation (creates actual test payment!)."""
266
+ self.stdout.write(" 💳 Testing payment creation...")
267
+ self.stdout.write(" ⚠️ WARNING: This creates an actual payment!")
268
+
269
+ try:
270
+ # Get a supported currency for testing
271
+ test_currency = self.get_test_currency(provider)
272
+ if not test_currency:
273
+ self.test_results[provider_name]['warnings'].append("No suitable test currency found")
274
+ self.stdout.write(" ⚠️ No suitable test currency found")
275
+ return
276
+
277
+ # Create test payment request
278
+ payment_request = PaymentCreateRequest(
279
+ amount_usd=self.options['test_amount'],
280
+ currency_code=test_currency,
281
+ description=f"Test payment from django-cfg ({timezone.now().isoformat()})",
282
+ callback_url="https://example.com/webhook/test",
283
+ success_url="https://example.com/success",
284
+ cancel_url="https://example.com/cancel"
285
+ )
286
+
287
+ start_time = time.time()
288
+
289
+ # Create payment
290
+ result = provider.create_payment(payment_request)
291
+
292
+ end_time = time.time()
293
+ response_time = (end_time - start_time) * 1000
294
+
295
+ self.test_results[provider_name]['performance']['payment_creation_ms'] = response_time
296
+
297
+ if hasattr(result, 'success') and result.success:
298
+ self.test_results[provider_name]['payment'] = True
299
+ payment_id = getattr(result, 'provider_payment_id', 'Unknown')
300
+ payment_url = getattr(result, 'payment_url', 'No URL')
301
+
302
+ self.stdout.write(f" ✅ Payment created ({response_time:.0f}ms)")
303
+ self.stdout.write(f" Payment ID: {payment_id}")
304
+
305
+ if self.verbose and payment_url != 'No URL':
306
+ self.stdout.write(f" Payment URL: {payment_url}")
307
+
308
+ # Test payment status check
309
+ if hasattr(provider, 'get_payment_status') and payment_id != 'Unknown':
310
+ try:
311
+ status_result = provider.get_payment_status(payment_id)
312
+ if hasattr(status_result, 'success') and status_result.success:
313
+ status = getattr(status_result, 'status', 'Unknown')
314
+ self.stdout.write(f" ✅ Status check OK: {status}")
315
+ else:
316
+ self.test_results[provider_name]['warnings'].append("Status check failed")
317
+ self.stdout.write(" ⚠️ Status check failed")
318
+ except Exception as e:
319
+ self.test_results[provider_name]['warnings'].append(f"Status check error: {e}")
320
+ self.stdout.write(f" ⚠️ Status check error: {e}")
321
+
322
+ else:
323
+ error = getattr(result, 'error', 'Unknown error')
324
+ self.test_results[provider_name]['errors'].append(f"Payment creation failed: {error}")
325
+ self.stdout.write(f" ❌ Payment creation failed: {error}")
326
+
327
+ except Exception as e:
328
+ error_msg = f"Payment creation error: {e}"
329
+ self.test_results[provider_name]['errors'].append(error_msg)
330
+ self.stdout.write(f" ❌ Payment creation error: {e}")
331
+
332
+ def get_test_currency(self, provider) -> Optional[str]:
333
+ """Get a suitable currency for testing."""
334
+ # Common test currencies
335
+ test_currencies = ['BTC', 'ETH', 'USDT', 'LTC', 'USD']
336
+
337
+ try:
338
+ # Get supported currencies from provider
339
+ if hasattr(provider, 'get_supported_currencies'):
340
+ supported = provider.get_supported_currencies()
341
+
342
+ if isinstance(supported, list) and len(supported) > 0:
343
+ # Extract currency codes
344
+ if isinstance(supported[0], dict):
345
+ supported_codes = [c.get('code', '').upper() for c in supported]
346
+ else:
347
+ supported_codes = [str(c).upper() for c in supported]
348
+
349
+ # Find first matching test currency
350
+ for currency in test_currencies:
351
+ if currency in supported_codes:
352
+ return currency
353
+
354
+ # Fallback: return first supported currency
355
+ if supported_codes:
356
+ return supported_codes[0]
357
+
358
+ # Last resort: try BTC
359
+ return 'BTC'
360
+
361
+ except Exception:
362
+ return 'BTC'
363
+
364
+ def show_summary(self):
365
+ """Display test summary."""
366
+ self.stdout.write(self.style.SUCCESS("📊 TEST SUMMARY"))
367
+ self.stdout.write("-" * 40)
368
+
369
+ total_providers = len(self.test_results)
370
+ successful_providers = 0
371
+ total_errors = 0
372
+ total_warnings = 0
373
+
374
+ for provider_name, results in self.test_results.items():
375
+ errors = len(results['errors'])
376
+ warnings = len(results['warnings'])
377
+
378
+ total_errors += errors
379
+ total_warnings += warnings
380
+
381
+ # Count as successful if no errors and at least one test passed
382
+ if errors == 0 and (results['connectivity'] or results['currencies']):
383
+ successful_providers += 1
384
+
385
+ # Show provider summary
386
+ status_icon = "✅" if errors == 0 else "❌"
387
+ self.stdout.write(f"{status_icon} {provider_name}:")
388
+
389
+ if results['connectivity']:
390
+ self.stdout.write(" ✅ Connectivity")
391
+ if results['currencies']:
392
+ self.stdout.write(" ✅ Currencies")
393
+ if results['payment']:
394
+ self.stdout.write(" ✅ Payment Creation")
395
+
396
+ if errors > 0:
397
+ self.stdout.write(f" ❌ {errors} error(s)")
398
+ if warnings > 0:
399
+ self.stdout.write(f" ⚠️ {warnings} warning(s)")
400
+
401
+ # Show performance metrics
402
+ if self.verbose and results['performance']:
403
+ perf = results['performance']
404
+ if 'connectivity_ms' in perf:
405
+ self.stdout.write(f" ⏱️ Connectivity: {perf['connectivity_ms']:.0f}ms")
406
+ if 'currencies_ms' in perf:
407
+ self.stdout.write(f" ⏱️ Currencies: {perf['currencies_ms']:.0f}ms")
408
+ if 'payment_creation_ms' in perf:
409
+ self.stdout.write(f" ⏱️ Payment: {perf['payment_creation_ms']:.0f}ms")
410
+
411
+ # Overall summary
412
+ self.stdout.write("")
413
+ self.stdout.write(f"Providers tested: {total_providers}")
414
+ self.stdout.write(f"Successful: {self.style.SUCCESS(successful_providers)}")
415
+ self.stdout.write(f"Failed: {self.style.ERROR(total_providers - successful_providers)}")
416
+ self.stdout.write(f"Total errors: {self.style.ERROR(total_errors)}")
417
+ self.stdout.write(f"Total warnings: {self.style.WARNING(total_warnings)}")
418
+
419
+ # Show completion time
420
+ self.stdout.write("")
421
+ self.stdout.write(f"Completed: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
422
+
423
+ # Recommendations
424
+ if total_errors > 0:
425
+ self.stdout.write("")
426
+ self.stdout.write(
427
+ self.style.WARNING("⚠️ Some providers have errors. Check configurations and network connectivity.")
428
+ )
429
+
430
+ if successful_providers == total_providers and total_errors == 0:
431
+ self.stdout.write("")
432
+ self.stdout.write(
433
+ self.style.SUCCESS("🎉 All providers tested successfully!")
434
+ )
@@ -44,8 +44,8 @@ class APIAccessMiddleware(MiddlewareMixin):
44
44
 
45
45
  # Configuration from django-cfg
46
46
  self.enabled = middleware_config['enabled']
47
- self.api_prefixes = middleware_config['api_prefixes']
48
- self.exempt_paths = middleware_config['exempt_paths']
47
+ self.protected_paths = middleware_config.get('protected_paths', [])
48
+ self.protected_patterns_raw = middleware_config.get('protected_patterns', [])
49
49
  self.cache_timeout = middleware_config['cache_timeouts']['api_key']
50
50
 
51
51
  # Default settings (can be overridden by Constance)
@@ -61,29 +61,32 @@ class APIAccessMiddleware(MiddlewareMixin):
61
61
 
62
62
  except Exception as e:
63
63
  logger.warning(f"Failed to load middleware config, using defaults: {e}")
64
- # Fallback defaults
64
+ # Fallback defaults - whitelist approach
65
65
  self.enabled = True
66
- self.api_prefixes = ['/api/']
67
- self.exempt_paths = ['/api/health/', '/admin/']
66
+ self.protected_paths = [
67
+ '/api/admin/', # Admin API endpoints
68
+ '/api/private/', # Private API endpoints
69
+ '/api/secure/', # Secure API endpoints
70
+ ]
71
+ self.protected_patterns_raw = [
72
+ r'^/api/admin/.*$', # All admin API endpoints
73
+ r'^/api/private/.*$', # All private API endpoints
74
+ r'^/api/secure/.*$', # All secure API endpoints
75
+ ]
68
76
  self.cache_timeout = 300
69
77
  self.strict_mode = False
70
78
  self.require_api_key = True
71
79
 
72
- # Compile exempt path patterns (static for now)
73
- self.exempt_patterns = [
74
- re.compile(pattern) for pattern in [
75
- r'^/api/payments/[^/]+/status/$',
76
- r'^/api/webhooks/[^/]+/$',
77
- r'^/api/payments/webhooks/(providers|health|stats)/$', # Admin webhook endpoints
78
- r'^/api/currencies/(rates|supported|convert)/$',
79
- ]
80
+ # Compile protected path patterns
81
+ self.protected_patterns = [
82
+ re.compile(pattern) for pattern in self.protected_patterns_raw
80
83
  ]
81
84
 
82
85
  logger.info(f"API Access Middleware initialized", extra={
83
86
  'enabled': self.enabled,
84
87
  'strict_mode': self.strict_mode,
85
88
  'require_api_key': self.require_api_key,
86
- 'api_prefixes': self.api_prefixes
89
+ 'protected_paths': self.protected_paths
87
90
  })
88
91
 
89
92
  def process_request(self, request: HttpRequest) -> Optional[JsonResponse]:
@@ -95,8 +98,8 @@ class APIAccessMiddleware(MiddlewareMixin):
95
98
  if not self.enabled:
96
99
  return None
97
100
 
98
- # Check if this path requires authentication
99
- if not self._requires_authentication(request.path):
101
+ # Check if this path is protected (whitelist approach)
102
+ if not self._is_protected_path(request.path):
100
103
  return None
101
104
 
102
105
  # Start timing for performance monitoring
@@ -177,26 +180,24 @@ class APIAccessMiddleware(MiddlewareMixin):
177
180
  # Graceful degradation: allow access but log the issue
178
181
  return None
179
182
 
180
- def _requires_authentication(self, path: str) -> bool:
183
+ def _is_protected_path(self, path: str) -> bool:
181
184
  """
182
- Check if the given path requires API authentication.
183
- """
184
- # Check if path starts with API prefix
185
- requires_auth = any(path.startswith(prefix) for prefix in self.api_prefixes)
186
-
187
- if not requires_auth:
188
- return False
185
+ Check if the given path is protected and requires API authentication.
189
186
 
190
- # Check exempt paths
191
- if path in self.exempt_paths:
192
- return False
187
+ Whitelist approach: only paths explicitly listed as protected require API key.
188
+ """
189
+ # Check exact protected paths
190
+ for protected_path in self.protected_paths:
191
+ if path.startswith(protected_path):
192
+ return True
193
193
 
194
- # Check exempt patterns
195
- for pattern in self.exempt_patterns:
194
+ # Check protected patterns
195
+ for pattern in self.protected_patterns:
196
196
  if pattern.match(path):
197
- return False
197
+ return True
198
198
 
199
- return True
199
+ # Path is not protected - no API key required
200
+ return False
200
201
 
201
202
  def _extract_api_key(self, request: HttpRequest) -> Optional[str]:
202
203
  """
@@ -300,7 +301,7 @@ class APIAccessMiddleware(MiddlewareMixin):
300
301
  user=api_key.user,
301
302
  status=Subscription.SubscriptionStatus.ACTIVE,
302
303
  expires_at__gt=timezone.now()
303
- ).select_related('tariff', 'endpoint_group')
304
+ ).prefetch_related('endpoint_groups')
304
305
 
305
306
  if not active_subscriptions.exists():
306
307
  return {
@@ -319,8 +320,8 @@ class APIAccessMiddleware(MiddlewareMixin):
319
320
  'allowed': True,
320
321
  'subscription_id': str(subscription.id),
321
322
  'tier': subscription.tier,
322
- 'tariff_name': subscription.tariff.name if subscription.tariff else None,
323
- 'requests_remaining': subscription.requests_remaining(),
323
+ 'tier_name': subscription.tier,
324
+ 'requests_remaining': 'unlimited', # TODO: Implement rate limiting per subscription
324
325
  'expires_at': subscription.expires_at.isoformat() if subscription.expires_at else None
325
326
  }
326
327
 
@@ -1,4 +1,4 @@
1
- # Generated by Django 5.2.6 on 2025-09-26 05:27
1
+ # Generated by Django 5.2.6 on 2025-09-27 10:37
2
2
 
3
3
  import django.core.validators
4
4
  import django.db.models.deletion
@@ -239,6 +239,9 @@ class Transaction(UUIDTimestampedModel):
239
239
 
240
240
  def save(self, *args, **kwargs):
241
241
  """Override save to ensure immutability."""
242
- if self.pk:
243
- raise ValidationError("Transactions are immutable and cannot be modified")
242
+ # Only prevent updates, not creation
243
+ if self.pk and not kwargs.get('force_insert', False):
244
+ # Check if this is actually an update (record exists in DB)
245
+ if Transaction.objects.filter(pk=self.pk).exists():
246
+ raise ValidationError("Transactions are immutable and cannot be modified")
244
247
  super().save(*args, **kwargs)
@@ -93,6 +93,10 @@ class APIKeyManager(models.Manager):
93
93
  """Get API keys expiring soon."""
94
94
  return self.get_queryset().expiring_soon(days)
95
95
 
96
+ def by_user(self, user):
97
+ """Get API keys by user."""
98
+ return self.get_queryset().by_user(user)
99
+
96
100
  # Business logic methods
97
101
  def increment_api_key_usage(self, api_key_id, ip_address=None):
98
102
  """
@@ -230,7 +234,7 @@ class APIKeyManager(models.Manager):
230
234
  logger.info(f"Created API key for user", extra={
231
235
  'api_key_id': str(api_key.id),
232
236
  'user_id': user.id,
233
- 'name': name,
237
+ 'key_name': name,
234
238
  'expires_in_days': expires_in_days
235
239
  })
236
240
 
@@ -239,7 +243,7 @@ class APIKeyManager(models.Manager):
239
243
  except Exception as e:
240
244
  logger.error(f"Failed to create API key: {e}", extra={
241
245
  'user_id': user.id,
242
- 'name': name
246
+ 'key_name': name
243
247
  })
244
248
  raise
245
249
 
@@ -4,7 +4,7 @@ Balance and transaction managers for the Universal Payment System v2.0.
4
4
  Optimized querysets and managers for balance and transaction operations.
5
5
  """
6
6
 
7
- from django.db import models
7
+ from django.db import models, transaction
8
8
  from django.utils import timezone
9
9
  from django_cfg.modules.django_logger import get_logger
10
10
 
@@ -62,7 +62,7 @@ class UserBalanceManager(models.Manager):
62
62
  # Get or create balance
63
63
  balance = self.get_or_create_for_user(user)
64
64
 
65
- with models.transaction.atomic():
65
+ with transaction.atomic():
66
66
  # Update balance
67
67
  balance.balance_usd += amount
68
68
  balance.total_deposited += amount
@@ -122,7 +122,7 @@ class UserBalanceManager(models.Manager):
122
122
  if amount > balance.balance_usd:
123
123
  raise ValueError(f"Insufficient balance: ${balance.balance_usd:.2f} < ${amount:.2f}")
124
124
 
125
- with models.transaction.atomic():
125
+ with transaction.atomic():
126
126
  # Update balance
127
127
  balance.balance_usd -= amount
128
128
  balance.total_spent += amount
@@ -244,6 +244,11 @@ class PaymentManager(models.Manager):
244
244
  """Get active payments."""
245
245
  return self.get_queryset().active()
246
246
 
247
+ # User-based methods
248
+ def by_user(self, user):
249
+ """Get payments by user."""
250
+ return self.get_queryset().by_user(user)
251
+
247
252
  # Provider-based methods
248
253
  def by_provider(self, provider):
249
254
  """Get payments by provider."""
@@ -544,7 +544,7 @@ class SubscriptionManager(models.Manager):
544
544
  else:
545
545
  subscription = subscription_id
546
546
 
547
- subscription.status = subscription.model.SubscriptionStatus.CANCELLED
547
+ subscription.status = self.model.SubscriptionStatus.CANCELLED
548
548
  subscription.save(update_fields=['status', 'updated_at'])
549
549
 
550
550
  logger.info(f"Subscription cancelled", extra={
@@ -580,7 +580,7 @@ class SubscriptionManager(models.Manager):
580
580
 
581
581
  from datetime import timedelta
582
582
 
583
- if subscription.is_expired:
583
+ if subscription.expires_at <= timezone.now():
584
584
  # If expired, start from now
585
585
  subscription.starts_at = timezone.now()
586
586
  subscription.expires_at = subscription.starts_at + timedelta(days=duration_days)
@@ -588,7 +588,7 @@ class SubscriptionManager(models.Manager):
588
588
  # If not expired, extend from current expiration
589
589
  subscription.expires_at += timedelta(days=duration_days)
590
590
 
591
- subscription.status = subscription.model.SubscriptionStatus.ACTIVE
591
+ subscription.status = self.model.SubscriptionStatus.ACTIVE
592
592
  subscription.save(update_fields=['starts_at', 'expires_at', 'status', 'updated_at'])
593
593
 
594
594
  logger.info(f"Subscription renewed", extra={