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.
Files changed (25) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/payments/management/commands/cleanup_expired_data.py +419 -0
  3. django_cfg/apps/payments/management/commands/currency_stats.py +376 -0
  4. django_cfg/apps/payments/management/commands/process_pending_payments.py +357 -0
  5. django_cfg/apps/payments/management/commands/test_providers.py +434 -0
  6. django_cfg/apps/payments/models/balance.py +5 -2
  7. django_cfg/apps/payments/models/managers/api_key_managers.py +2 -2
  8. django_cfg/apps/payments/models/managers/balance_managers.py +3 -3
  9. django_cfg/apps/payments/models/managers/subscription_managers.py +3 -3
  10. django_cfg/apps/payments/services/cache_service/__init__.py +143 -0
  11. django_cfg/apps/payments/services/cache_service/api_key_cache.py +37 -0
  12. django_cfg/apps/payments/services/cache_service/interfaces.py +32 -0
  13. django_cfg/apps/payments/services/cache_service/keys.py +49 -0
  14. django_cfg/apps/payments/services/cache_service/rate_limit_cache.py +47 -0
  15. django_cfg/apps/payments/services/cache_service/simple_cache.py +101 -0
  16. django_cfg/apps/payments/services/core/payment_service.py +49 -22
  17. django_cfg/apps/payments/signals/api_key_signals.py +2 -2
  18. django_cfg/apps/payments/signals/balance_signals.py +1 -1
  19. django_cfg/utils/smart_defaults.py +10 -4
  20. {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/METADATA +1 -1
  21. {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/RECORD +24 -15
  22. django_cfg/apps/payments/services/cache/cache_service.py +0 -235
  23. {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/WHEEL +0 -0
  24. {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/entry_points.txt +0 -0
  25. {django_cfg-1.3.1.dist-info → django_cfg-1.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,376 @@
1
+ """
2
+ Currency Statistics Management Command for Universal Payment System v2.0.
3
+
4
+ Display comprehensive currency database statistics and health information.
5
+ """
6
+
7
+ from datetime import datetime, timedelta
8
+ from typing import List, Dict, Any
9
+ from decimal import Decimal
10
+
11
+ from django.core.management.base import BaseCommand, CommandError
12
+ from django.utils import timezone
13
+ from django.db.models import Q, Count, Avg, Sum, Max, Min
14
+ from django.contrib.humanize.templatetags.humanize import intcomma
15
+
16
+ from django_cfg.modules.django_logger import get_logger
17
+ from django_cfg.apps.payments.models import Currency, Network, ProviderCurrency, UniversalPayment
18
+ from django_cfg.apps.payments.services.providers.registry import get_provider_registry
19
+
20
+ logger = get_logger("currency_stats")
21
+
22
+
23
+ class Command(BaseCommand):
24
+ """
25
+ Display currency database statistics and health information.
26
+
27
+ Features:
28
+ - Basic and detailed statistics
29
+ - Top currencies by usage and value
30
+ - Rate freshness analysis
31
+ - Provider currency coverage
32
+ - Payment volume analysis
33
+ """
34
+
35
+ help = 'Show currency database statistics and health information'
36
+
37
+ def add_arguments(self, parser):
38
+ """Add command line arguments."""
39
+ parser.add_argument(
40
+ '--detailed',
41
+ action='store_true',
42
+ help='Show detailed statistics breakdown'
43
+ )
44
+
45
+ parser.add_argument(
46
+ '--top',
47
+ type=int,
48
+ default=10,
49
+ help='Number of top currencies to show (default: 10)'
50
+ )
51
+
52
+ parser.add_argument(
53
+ '--check-rates',
54
+ action='store_true',
55
+ help='Check for outdated exchange rates'
56
+ )
57
+
58
+ parser.add_argument(
59
+ '--provider',
60
+ type=str,
61
+ help='Show statistics for specific provider'
62
+ )
63
+
64
+ parser.add_argument(
65
+ '--export-csv',
66
+ type=str,
67
+ help='Export statistics to CSV file'
68
+ )
69
+
70
+ parser.add_argument(
71
+ '--format',
72
+ choices=['table', 'json', 'yaml'],
73
+ default='table',
74
+ help='Output format (default: table)'
75
+ )
76
+
77
+ def handle(self, *args, **options):
78
+ """Execute the command."""
79
+ try:
80
+ self.options = options
81
+ self.show_header()
82
+
83
+ if options['check_rates']:
84
+ self.check_rate_freshness()
85
+ elif options['provider']:
86
+ self.show_provider_stats(options['provider'])
87
+ else:
88
+ self.show_general_stats()
89
+
90
+ if options['detailed']:
91
+ self.show_detailed_stats()
92
+
93
+ self.show_top_currencies()
94
+
95
+ if options['export_csv']:
96
+ self.export_to_csv(options['export_csv'])
97
+
98
+ except Exception as e:
99
+ logger.error(f"Currency stats command failed: {e}")
100
+ raise CommandError(f"Failed to generate currency statistics: {e}")
101
+
102
+ def show_header(self):
103
+ """Display command header."""
104
+ self.stdout.write(
105
+ self.style.SUCCESS("=" * 60)
106
+ )
107
+ self.stdout.write(
108
+ self.style.SUCCESS("📊 CURRENCY DATABASE STATISTICS")
109
+ )
110
+ self.stdout.write(
111
+ self.style.SUCCESS("=" * 60)
112
+ )
113
+ self.stdout.write(f"Generated: {timezone.now().strftime('%Y-%m-%d %H:%M:%S UTC')}")
114
+ self.stdout.write("")
115
+
116
+ def show_general_stats(self):
117
+ """Show general currency statistics."""
118
+ # Basic counts
119
+ total_currencies = Currency.objects.count()
120
+ active_currencies = Currency.objects.filter(is_active=True).count()
121
+ crypto_currencies = Currency.objects.filter(currency_type='crypto').count()
122
+ fiat_currencies = Currency.objects.filter(currency_type='fiat').count()
123
+
124
+ # Network stats
125
+ total_networks = Network.objects.count()
126
+ active_networks = Network.objects.filter(is_active=True).count()
127
+
128
+ # Provider currency stats
129
+ total_provider_currencies = ProviderCurrency.objects.count()
130
+ active_provider_currencies = ProviderCurrency.objects.filter(is_active=True).count()
131
+
132
+ # Payment stats
133
+ total_payments = UniversalPayment.objects.count()
134
+ completed_payments = UniversalPayment.objects.filter(status='completed').count()
135
+
136
+ self.stdout.write(self.style.SUCCESS("📈 GENERAL STATISTICS"))
137
+ self.stdout.write("-" * 40)
138
+
139
+ stats = [
140
+ ("Total Currencies", total_currencies),
141
+ ("Active Currencies", active_currencies),
142
+ ("Cryptocurrency", crypto_currencies),
143
+ ("Fiat Currency", fiat_currencies),
144
+ ("Networks", f"{active_networks}/{total_networks}"),
145
+ ("Provider Currencies", f"{active_provider_currencies}/{total_provider_currencies}"),
146
+ ("Total Payments", total_payments),
147
+ ("Completed Payments", completed_payments),
148
+ ]
149
+
150
+ for label, value in stats:
151
+ self.stdout.write(f"{label:<20}: {self.style.WARNING(str(value))}")
152
+
153
+ self.stdout.write("")
154
+
155
+ def show_detailed_stats(self):
156
+ """Show detailed statistics breakdown."""
157
+ self.stdout.write(self.style.SUCCESS("🔍 DETAILED BREAKDOWN"))
158
+ self.stdout.write("-" * 40)
159
+
160
+ # Currency type breakdown
161
+ crypto_active = Currency.objects.filter(currency_type='crypto', is_active=True).count()
162
+ crypto_inactive = Currency.objects.filter(currency_type='crypto', is_active=False).count()
163
+ fiat_active = Currency.objects.filter(currency_type='fiat', is_active=True).count()
164
+ fiat_inactive = Currency.objects.filter(currency_type='fiat', is_active=False).count()
165
+
166
+ self.stdout.write("Currency Status:")
167
+ self.stdout.write(f" Crypto: {crypto_active} active, {crypto_inactive} inactive")
168
+ self.stdout.write(f" Fiat: {fiat_active} active, {fiat_inactive} inactive")
169
+
170
+ # Provider coverage
171
+ providers = get_provider_registry().get_available_providers()
172
+ self.stdout.write(f"\nProvider Coverage:")
173
+
174
+ for provider_name in providers:
175
+ provider_currencies = ProviderCurrency.objects.filter(
176
+ provider=provider_name,
177
+ is_active=True
178
+ ).count()
179
+ self.stdout.write(f" {provider_name}: {provider_currencies} currencies")
180
+
181
+ # Payment volume by currency
182
+ payment_stats = UniversalPayment.objects.filter(
183
+ status='completed'
184
+ ).values('currency__code').annotate(
185
+ count=Count('id'),
186
+ total_volume=Sum('amount_usd')
187
+ ).order_by('-total_volume')[:5]
188
+
189
+ if payment_stats:
190
+ self.stdout.write(f"\nTop Payment Currencies:")
191
+ for stat in payment_stats:
192
+ currency = stat['currency__code'] or 'Unknown'
193
+ count = stat['count']
194
+ volume = stat['total_volume'] or 0
195
+ self.stdout.write(f" {currency}: {count} payments, ${intcomma(volume)}")
196
+
197
+ self.stdout.write("")
198
+
199
+ def show_top_currencies(self):
200
+ """Show top currencies by various metrics."""
201
+ top_count = self.options['top']
202
+
203
+ self.stdout.write(self.style.SUCCESS(f"🏆 TOP {top_count} CURRENCIES"))
204
+ self.stdout.write("-" * 40)
205
+
206
+ # Top by payment count
207
+ top_by_payments = UniversalPayment.objects.filter(
208
+ status='completed'
209
+ ).values('currency__code', 'currency__name').annotate(
210
+ payment_count=Count('id')
211
+ ).order_by('-payment_count')[:top_count]
212
+
213
+ if top_by_payments:
214
+ self.stdout.write("By Payment Count:")
215
+ for i, currency in enumerate(top_by_payments, 1):
216
+ code = currency['currency__code'] or 'Unknown'
217
+ name = currency['currency__name'] or 'Unknown'
218
+ count = currency['payment_count']
219
+ self.stdout.write(f" {i:2d}. {code} ({name}): {count} payments")
220
+
221
+ # Top by volume
222
+ top_by_volume = UniversalPayment.objects.filter(
223
+ status='completed'
224
+ ).values('currency__code', 'currency__name').annotate(
225
+ total_volume=Sum('amount_usd')
226
+ ).order_by('-total_volume')[:top_count]
227
+
228
+ if top_by_volume:
229
+ self.stdout.write(f"\nBy Payment Volume:")
230
+ for i, currency in enumerate(top_by_volume, 1):
231
+ code = currency['currency__code'] or 'Unknown'
232
+ name = currency['currency__name'] or 'Unknown'
233
+ volume = currency['total_volume'] or 0
234
+ self.stdout.write(f" {i:2d}. {code} ({name}): ${intcomma(volume)}")
235
+
236
+ self.stdout.write("")
237
+
238
+ def check_rate_freshness(self):
239
+ """Check for outdated exchange rates."""
240
+ self.stdout.write(self.style.SUCCESS("🕐 RATE FRESHNESS CHECK"))
241
+ self.stdout.write("-" * 40)
242
+
243
+ now = timezone.now()
244
+ stale_threshold = now - timedelta(hours=24)
245
+ very_stale_threshold = now - timedelta(days=7)
246
+
247
+ # Check currencies with stale rates
248
+ stale_currencies = Currency.objects.filter(
249
+ updated_at__lt=stale_threshold,
250
+ is_active=True
251
+ ).order_by('updated_at')
252
+
253
+ very_stale_currencies = Currency.objects.filter(
254
+ updated_at__lt=very_stale_threshold,
255
+ is_active=True
256
+ ).order_by('updated_at')
257
+
258
+ fresh_currencies = Currency.objects.filter(
259
+ updated_at__gte=stale_threshold,
260
+ is_active=True
261
+ ).count()
262
+
263
+ self.stdout.write(f"Fresh rates (< 24h): {self.style.SUCCESS(fresh_currencies)}")
264
+ self.stdout.write(f"Stale rates (> 24h): {self.style.WARNING(stale_currencies.count())}")
265
+ self.stdout.write(f"Very stale (> 7d): {self.style.ERROR(very_stale_currencies.count())}")
266
+
267
+ if very_stale_currencies.exists():
268
+ self.stdout.write(f"\n{self.style.ERROR('⚠️ VERY STALE CURRENCIES:')}")
269
+ for currency in very_stale_currencies[:10]:
270
+ age = now - currency.updated_at
271
+ self.stdout.write(f" {currency.code}: {age.days} days old")
272
+
273
+ if stale_currencies.exists() and not very_stale_currencies.exists():
274
+ self.stdout.write(f"\n{self.style.WARNING('⚠️ STALE CURRENCIES:')}")
275
+ for currency in stale_currencies[:10]:
276
+ age = now - currency.updated_at
277
+ hours = int(age.total_seconds() / 3600)
278
+ self.stdout.write(f" {currency.code}: {hours} hours old")
279
+
280
+ self.stdout.write("")
281
+
282
+ def show_provider_stats(self, provider_name: str):
283
+ """Show statistics for specific provider."""
284
+ self.stdout.write(self.style.SUCCESS(f"🏢 PROVIDER STATISTICS: {provider_name.upper()}"))
285
+ self.stdout.write("-" * 40)
286
+
287
+ # Provider currency stats
288
+ provider_currencies = ProviderCurrency.objects.filter(provider=provider_name)
289
+ active_provider_currencies = provider_currencies.filter(is_active=True)
290
+
291
+ # Payment stats for this provider
292
+ provider_payments = UniversalPayment.objects.filter(provider=provider_name)
293
+ completed_payments = provider_payments.filter(status='completed')
294
+
295
+ # Volume stats
296
+ total_volume = completed_payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
297
+ avg_payment = completed_payments.aggregate(Avg('amount_usd'))['amount_usd__avg'] or 0
298
+
299
+ stats = [
300
+ ("Total Currencies", provider_currencies.count()),
301
+ ("Active Currencies", active_provider_currencies.count()),
302
+ ("Total Payments", provider_payments.count()),
303
+ ("Completed Payments", completed_payments.count()),
304
+ ("Total Volume", f"${intcomma(total_volume)}"),
305
+ ("Average Payment", f"${intcomma(avg_payment):.2f}"),
306
+ ]
307
+
308
+ for label, value in stats:
309
+ self.stdout.write(f"{label:<20}: {self.style.WARNING(str(value))}")
310
+
311
+ # Top currencies for this provider
312
+ top_currencies = completed_payments.values(
313
+ 'currency__code', 'currency__name'
314
+ ).annotate(
315
+ count=Count('id'),
316
+ volume=Sum('amount_usd')
317
+ ).order_by('-volume')[:5]
318
+
319
+ if top_currencies:
320
+ self.stdout.write(f"\nTop Currencies:")
321
+ for currency in top_currencies:
322
+ code = currency['currency__code'] or 'Unknown'
323
+ count = currency['count']
324
+ volume = currency['volume'] or 0
325
+ self.stdout.write(f" {code}: {count} payments, ${intcomma(volume)}")
326
+
327
+ self.stdout.write("")
328
+
329
+ def export_to_csv(self, filename: str):
330
+ """Export statistics to CSV file."""
331
+ import csv
332
+ from pathlib import Path
333
+
334
+ try:
335
+ # Collect all statistics
336
+ currencies = Currency.objects.select_related().all()
337
+
338
+ with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
339
+ writer = csv.writer(csvfile)
340
+
341
+ # Write header
342
+ writer.writerow([
343
+ 'Code', 'Name', 'Type', 'Active', 'Created', 'Updated',
344
+ 'Payment Count', 'Total Volume USD'
345
+ ])
346
+
347
+ # Write currency data
348
+ for currency in currencies:
349
+ # Get payment stats for this currency
350
+ payments = UniversalPayment.objects.filter(
351
+ currency=currency,
352
+ status='completed'
353
+ )
354
+ payment_count = payments.count()
355
+ total_volume = payments.aggregate(Sum('amount_usd'))['amount_usd__sum'] or 0
356
+
357
+ writer.writerow([
358
+ currency.code,
359
+ currency.name,
360
+ currency.currency_type,
361
+ currency.is_active,
362
+ currency.created_at.strftime('%Y-%m-%d'),
363
+ currency.updated_at.strftime('%Y-%m-%d'),
364
+ payment_count,
365
+ f"{total_volume:.2f}"
366
+ ])
367
+
368
+ self.stdout.write(
369
+ self.style.SUCCESS(f"✅ Statistics exported to: {filename}")
370
+ )
371
+
372
+ except Exception as e:
373
+ logger.error(f"Failed to export CSV: {e}")
374
+ self.stdout.write(
375
+ self.style.ERROR(f"❌ Failed to export CSV: {e}")
376
+ )