django-cfg 1.3.1__py3-none-any.whl → 1.3.3__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/payments/management/commands/cleanup_expired_data.py +419 -0
- django_cfg/apps/payments/management/commands/currency_stats.py +376 -0
- django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
- django_cfg/apps/payments/management/commands/test_providers.py +434 -0
- django_cfg/apps/payments/models/balance.py +5 -2
- django_cfg/apps/payments/models/managers/api_key_managers.py +2 -2
- django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
- django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
- django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
- django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
- django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
- django_cfg/apps/payments/services/cache_service/keys.py +49 -0
- django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
- django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
- django_cfg/apps/payments/services/core/payment_service.py +49 -22
- django_cfg/apps/payments/signals/api_key_signals.py +2 -2
- django_cfg/apps/payments/signals/balance_signals.py +1 -1
- django_cfg/utils/smart_defaults.py +10 -4
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/METADATA +1 -1
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/RECORD +24 -15
- django_cfg/apps/payments/services/cache/cache_service.py +0 -235
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.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
|
+
)
|
@@ -239,6 +239,9 @@ class Transaction(UUIDTimestampedModel):
|
|
239
239
|
|
240
240
|
def save(self, *args, **kwargs):
|
241
241
|
"""Override save to ensure immutability."""
|
242
|
-
|
243
|
-
|
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)
|
@@ -230,7 +230,7 @@ class APIKeyManager(models.Manager):
|
|
230
230
|
logger.info(f"Created API key for user", extra={
|
231
231
|
'api_key_id': str(api_key.id),
|
232
232
|
'user_id': user.id,
|
233
|
-
'
|
233
|
+
'key_name': name,
|
234
234
|
'expires_in_days': expires_in_days
|
235
235
|
})
|
236
236
|
|
@@ -239,7 +239,7 @@ class APIKeyManager(models.Manager):
|
|
239
239
|
except Exception as e:
|
240
240
|
logger.error(f"Failed to create API key: {e}", extra={
|
241
241
|
'user_id': user.id,
|
242
|
-
'
|
242
|
+
'key_name': name
|
243
243
|
})
|
244
244
|
raise
|
245
245
|
|
@@ -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
|
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
|
125
|
+
with transaction.atomic():
|
126
126
|
# Update balance
|
127
127
|
balance.balance_usd -= amount
|
128
128
|
balance.total_spent += amount
|
@@ -544,7 +544,7 @@ class SubscriptionManager(models.Manager):
|
|
544
544
|
else:
|
545
545
|
subscription = subscription_id
|
546
546
|
|
547
|
-
subscription.status =
|
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.
|
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 =
|
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={
|
@@ -0,0 +1,143 @@
|
|
1
|
+
"""
|
2
|
+
Cache services for the Universal Payment System v2.0.
|
3
|
+
|
4
|
+
Main entry point for cache functionality.
|
5
|
+
Based on proven solutions from payments_old with improvements.
|
6
|
+
ONLY for API access control - NOT payment data!
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Dict, Any
|
10
|
+
from django.core.cache import cache
|
11
|
+
from django_cfg.modules.django_logger import get_logger
|
12
|
+
|
13
|
+
from .interfaces import CacheInterface
|
14
|
+
from .simple_cache import SimpleCache
|
15
|
+
from .api_key_cache import ApiKeyCache
|
16
|
+
from .rate_limit_cache import RateLimitCache
|
17
|
+
from .keys import CacheKeys
|
18
|
+
|
19
|
+
logger = get_logger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class CacheService:
|
23
|
+
"""
|
24
|
+
Main cache service providing access to specialized caches.
|
25
|
+
|
26
|
+
Provides centralized access to different cache types.
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self):
|
30
|
+
"""Initialize cache service with specialized caches."""
|
31
|
+
self.simple_cache = SimpleCache()
|
32
|
+
self.api_key_cache = ApiKeyCache()
|
33
|
+
self.rate_limit_cache = RateLimitCache()
|
34
|
+
# Backward compatibility attributes
|
35
|
+
self.default_timeout = 300
|
36
|
+
self.key_prefix = "payments"
|
37
|
+
|
38
|
+
# Backward compatibility methods - delegate to simple_cache
|
39
|
+
def get(self, key: str):
|
40
|
+
"""Get value from cache."""
|
41
|
+
return self.simple_cache.get(key)
|
42
|
+
|
43
|
+
def set(self, key: str, value, timeout=None):
|
44
|
+
"""Set value in cache."""
|
45
|
+
return self.simple_cache.set(key, value, timeout)
|
46
|
+
|
47
|
+
def delete(self, key: str):
|
48
|
+
"""Delete value from cache."""
|
49
|
+
return self.simple_cache.delete(key)
|
50
|
+
|
51
|
+
def exists(self, key: str):
|
52
|
+
"""Check if key exists in cache."""
|
53
|
+
return self.simple_cache.exists(key)
|
54
|
+
|
55
|
+
def get_or_set(self, key: str, default, timeout=None):
|
56
|
+
"""Get value or set default if not exists."""
|
57
|
+
value = self.get(key)
|
58
|
+
if value is None:
|
59
|
+
if callable(default):
|
60
|
+
value = default()
|
61
|
+
else:
|
62
|
+
value = default
|
63
|
+
self.set(key, value, timeout)
|
64
|
+
return value
|
65
|
+
|
66
|
+
def set_many(self, data: dict, timeout=None):
|
67
|
+
"""Set multiple values."""
|
68
|
+
for key, value in data.items():
|
69
|
+
self.set(key, value, timeout)
|
70
|
+
|
71
|
+
def get_many(self, keys: list):
|
72
|
+
"""Get multiple values."""
|
73
|
+
result = {}
|
74
|
+
for key in keys:
|
75
|
+
value = self.get(key)
|
76
|
+
if value is not None:
|
77
|
+
result[key] = value
|
78
|
+
return result
|
79
|
+
|
80
|
+
def delete_many(self, keys: list):
|
81
|
+
"""Delete multiple values."""
|
82
|
+
for key in keys:
|
83
|
+
self.delete(key)
|
84
|
+
|
85
|
+
def clear(self):
|
86
|
+
"""Clear all cache (not implemented for safety)."""
|
87
|
+
# For safety, we don't implement cache.clear()
|
88
|
+
# as it would clear the entire cache backend
|
89
|
+
# Instead, we clear Django's cache which is safe for tests
|
90
|
+
from django.core.cache import cache
|
91
|
+
cache.clear()
|
92
|
+
|
93
|
+
def health_check(self) -> Dict[str, Any]:
|
94
|
+
"""Check cache health."""
|
95
|
+
try:
|
96
|
+
test_key = "health_check"
|
97
|
+
test_value = "ok"
|
98
|
+
|
99
|
+
# Test set/get/delete
|
100
|
+
self.simple_cache.set(test_key, test_value, 10)
|
101
|
+
retrieved = self.simple_cache.get(test_key)
|
102
|
+
self.simple_cache.delete(test_key)
|
103
|
+
|
104
|
+
is_healthy = retrieved == test_value
|
105
|
+
|
106
|
+
return {
|
107
|
+
'healthy': is_healthy,
|
108
|
+
'backend': cache.__class__.__name__,
|
109
|
+
'simple_cache': True,
|
110
|
+
'api_key_cache': True,
|
111
|
+
'rate_limit_cache': True
|
112
|
+
}
|
113
|
+
except Exception as e:
|
114
|
+
logger.error(f"Cache health check failed: {e}")
|
115
|
+
return {
|
116
|
+
'healthy': False,
|
117
|
+
'error': str(e),
|
118
|
+
'backend': cache.__class__.__name__
|
119
|
+
}
|
120
|
+
|
121
|
+
|
122
|
+
# Global cache service instance
|
123
|
+
_cache_service = None
|
124
|
+
|
125
|
+
|
126
|
+
def get_cache_service() -> CacheService:
|
127
|
+
"""Get global cache service instance."""
|
128
|
+
global _cache_service
|
129
|
+
if _cache_service is None:
|
130
|
+
_cache_service = CacheService()
|
131
|
+
return _cache_service
|
132
|
+
|
133
|
+
|
134
|
+
# Export main classes for backward compatibility
|
135
|
+
__all__ = [
|
136
|
+
'CacheService',
|
137
|
+
'CacheInterface',
|
138
|
+
'SimpleCache',
|
139
|
+
'ApiKeyCache',
|
140
|
+
'RateLimitCache',
|
141
|
+
'CacheKeys',
|
142
|
+
'get_cache_service'
|
143
|
+
]
|